diff options
author | Sebastian <sebasjm@gmail.com> | 2023-11-06 11:54:45 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-11-06 11:54:45 -0300 |
commit | 4ee903eb5e639fa0e122a689cc431e8f18ca1197 (patch) | |
tree | d2cf28bcddb03f650fe7913ff2db8e48145740c4 /packages | |
parent | 3d1ab082d4a66df08fcb468d04198c055d00b8c5 (diff) |
aml ui
Diffstat (limited to 'packages')
19 files changed, 306 insertions, 581 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx index 0e29279ff..52c86c273 100644 --- a/packages/aml-backoffice-ui/src/App.tsx +++ b/packages/aml-backoffice-ui/src/App.tsx @@ -1,9 +1,14 @@ import { TranslationProvider } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { ExchangeAmlFrame, Main } from "./Dashboard.js"; +import { ExchangeAmlFrame } from "./Dashboard.js"; import "./scss/main.css"; import { ExchangeApiProvider } from "./context/config.js"; import { getInitialBackendBaseURL } from "./hooks/useBackend.js"; +import { Router } from "./route.js"; +import { Pages } from "./pages.js"; + +const pageList = Object.values(Pages); + export function App(): VNode { const baseUrl = getInitialBackendBaseURL(); @@ -11,7 +16,13 @@ export function App(): VNode { <TranslationProvider source={{}}> <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}> <ExchangeAmlFrame> - <Main /> + <Router + pageList={pageList} + onNotFound={() => { + window.location.href = Pages.cases.url + return <div>not found</div>; + }} + /> </ExchangeAmlFrame> </ExchangeApiProvider> </TranslationProvider> diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx index bd8a48c45..5d86836d4 100644 --- a/packages/aml-backoffice-ui/src/Dashboard.tsx +++ b/packages/aml-backoffice-ui/src/Dashboard.tsx @@ -1,13 +1,17 @@ -import { useNotifications } from "@gnu-taler/web-util/browser"; +import { Footer, GlobalNotificationsBanner, Header, LangSelector, notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Dialog, Transition } from "@headlessui/react"; import { UserIcon, XCircleIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { InformationCircleIcon } from "@heroicons/react/24/solid"; import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useErrorBoundary, useState } from "preact/hooks"; import logo from "./assets/logo-2021.svg"; import { Pages } from "./pages.js"; -import { Router, useCurrentLocation } from "./route.js"; +import { PageEntry, Router, useCurrentLocation } from "./route.js"; +import { uiSettings } from "./settings.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; +import { useOfficer } from "./hooks/useOfficer.js"; +import { getAllBooleanSettings, getLabelForSetting, useSettings } from "./hooks/useSettings.js"; function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); @@ -78,6 +82,7 @@ const versionText = VERSION * writing text with the correct format */ +const pageList = Object.values(Pages); function LeftMenu() { const currentLocation = useCurrentLocation(pageList); @@ -88,9 +93,9 @@ function LeftMenu() { <ul role="list" class="-mx-2 space-y-1"> <li> <a - href={Pages.info.url} + href={Pages.cases.url} class={classNames( - Pages.info.url === currentLocation?.path + Pages.cases.url === currentLocation?.path ? "bg-indigo-700 text-white" : "text-indigo-200 hover:text-white hover:bg-indigo-700", "group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold", @@ -98,7 +103,7 @@ function LeftMenu() { > <InformationCircleIcon class={classNames( - Pages.info.url === currentLocation?.path + Pages.cases.url === currentLocation?.path ? "text-white" : "text-indigo-200 group-hover:text-white", "h-6 w-6 shrink-0", @@ -132,472 +137,143 @@ function LeftMenu() { </li> </ul> </li> - {/* <li> - <div class="text-xs font-semibold leading-6 text-indigo-200"> - Info - </div> - <ul role="list" class="-mx-2 mt-2 space-y-1"> - <li> - <a - href={Pages.info.url} - class={classNames( - Pages.info.url === currentLocation?.path - ? "bg-indigo-700 text-white" - : "text-indigo-200 hover:text-white hover:bg-indigo-700", - "group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold", - )} - > - <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border border-indigo-400 bg-indigo-500 text-[0.625rem] font-medium text-white"> - asd - </span> - <span class="truncate">qwe</span> - </a> - </li> - </ul> - </li> */} - {/* <li class="mt-auto"> - <a - href={Pages.settings.url} - class={classNames( - Pages.settings.url === currentLocation?.path - ? "bg-indigo-700 text-white" - : "text-indigo-200 hover:text-white hover:bg-indigo-700", - "group -mx-2 flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold", - )} - > - <Cog6ToothIcon - class={classNames( - Pages.officer.url === currentLocation?.path - ? "text-white" - : "text-indigo-200 group-hover:text-white", - "h-6 w-6 shrink-0", - )} - aria-hidden="true" - /> - Settings - </a> - </li> */} </ul> </nav> ); } + export function ExchangeAmlFrame({ children, }: { children?: ComponentChildren; }): VNode { - const [sidebarOpen, setSidebarOpen] = useState(false); + const { i18n } = useTranslationContext(); - return ( - <Fragment> - <NavigationBar isOpen={sidebarOpen} setOpen={setSidebarOpen}> - <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-indigo-600 px-6 pb-4"> - <div class="flex h-16 shrink-0 items-center"> - <header class="flex items-center justify-between border-b border-white/5 "> - <h1 class="text-base font-semibold leading-7 text-white"> - Exchange AML Backoffice - </h1> - </header> - </div> - <LeftMenu /> - <Footer /> - </div> - </NavigationBar> - <div class="lg:pl-72"> - <TopBar - onOpenSidebar={() => { - setSidebarOpen(true); - }} - /> - <Notifications /> - {children} - </div> - </Fragment> - ); -} + const [error, resetError] = useErrorBoundary(); + + useEffect(() => { + if (error) { + if (error instanceof Error) { + notifyException(i18n.str`Internal error, please report.`, error) + } else { + notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString) + } + resetError() + } + }, [error]) -export function Main(): VNode { - return <main class="py-10 px-4 sm:px-6 lg:px-8"> - <div class="mx-auto max-w-3xl"> - <Router - pageList={pageList} - onNotFound={() => { - return <div>not found</div>; + const officer = useOfficer(); + const [settings, updateSettings] = useSettings(); + + return (<div class="min-h-full flex flex-col m-0 bg-slate-200" style="min-height: 100vh;"> + <div class="bg-indigo-600 pb-32"> + <Header + title="Exchange" + iconLinkURL={uiSettings.backendBaseURL ?? "#"} + onLogout={officer.state !== "ready" ? undefined : () => { + officer.lock() }} - /> + sites={[]} + supportedLangs={["en", "es", "de"]} + > + <li> + <div class="text-xs font-semibold leading-6 text-gray-400"> + <i18n.Translate>Preferences</i18n.Translate> + </div> + <ul role="list" class="space-y-1"> + {getAllBooleanSettings().map(set => { + const isOn: boolean = !!settings[set] + return <li class="mt-2 pl-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + {getLabelForSetting(set, i18n)} + </span> + </span> + <button type="button" data-enabled={isOn} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + + onClick={() => { updateSettings(set, !isOn); }}> + <span aria-hidden="true" data-enabled={isOn} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> + </li> + })} + </ul> + </li> + </Header> </div> - </main> -} -const pageList = Object.values(Pages); + <GlobalNotificationsBanner /> -function NavigationBar({ - isOpen, - setOpen, - children, -}: { - isOpen: boolean; - setOpen: (v: boolean) => void; - children: ComponentChildren; -}) { - return ( - <Fragment> - <Transition.Root show={isOpen} as={Fragment}> - <Dialog - as="div" - /* @ts-ignore */ - class="relative z-50 lg:hidden" - onClose={setOpen} - > - <Transition.Child - as={Fragment} - enter="transition-opacity ease-linear duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="transition-opacity ease-linear duration-300" - leaveFrom="opacity-100" - leaveTo="opacity-0" - > - <div class="fixed inset-0 bg-gray-900/80" /> - </Transition.Child> + <main class="-mt-32 flex grow "> + {officer.state !== "ready" ? undefined : + <Navigation /> + } + <div class="flex mx-auto my-4"> + <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6"> + {children} + </div> + </div> - <div class="fixed inset-0 flex"> - <Transition.Child - as={Fragment} - enter="transition ease-in-out duration-300 transform" - enterFrom="-translate-x-full" - enterTo="translate-x-0" - leave="transition ease-in-out duration-300 transform" - leaveFrom="translate-x-0" - leaveTo="-translate-x-full" - > - <Dialog.Panel class="relative mr-16 flex w-full max-w-xs flex-1"> - <Transition.Child - as={Fragment} - enter="ease-in-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in-out duration-300" - leaveFrom="opacity-100" - leaveTo="opacity-0" - > - <div class="absolute left-full top-0 flex w-16 justify-center pt-5"> - <button - type="button" - class="-m-2.5 p-2.5" - onClick={() => setOpen(false)} - > - <span class="sr-only">Close sidebar</span> - <XMarkIcon - class="h-6 w-6 text-white" - aria-hidden="true" - /> - </button> - </div> - </Transition.Child> - {children} - </Dialog.Panel> - </Transition.Child> - </div> - </Dialog> - </Transition.Root> + </main> - <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col"> - {children} - </div> - </Fragment> + <Footer + testingUrl={localStorage.getItem("exchange-base-url") ?? undefined} + GIT_HASH={GIT_HASH} + VERSION={VERSION} + /> + </div> ); } -function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) { +function Navigation(): VNode { + const { i18n } = useTranslationContext() + const pageList: Array<PageEntry> = [ + Pages.officer, + Pages.cases + ] return ( - <div class="relative flex h-16 justify-between"> - <div class="relative z-10 flex p-2 lg:hidden"> - <button - type="button" - onClick={() => { - onOpenSidebar(); - }} - class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-900" - aria-controls="mobile-menu" - aria-expanded="false" - > - <span class="sr-only">Open menu</span> - <svg - class="block h-6 w-6" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - aria-hidden="true" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" - /> - </svg> - <svg - class="hidden h-6 w-6" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - aria-hidden="true" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M6 18L18 6M6 6l12 12" - /> - </svg> - </button> - </div> - <div class="relative z-0 flex flex-1 items-center justify-center px-2 sm:absolute sm:inset-0"> - <div class="w-full sm:max-w-xs flex flex-1 items-center justify-center"> - <img - class="h-8 w-auto" - src={logo} - alt="Taler" - style={{ height: 35, margin: 10 }} - /> - </div> - </div> - {/* <div class="relative z-10 flex items-center lg:hidden">dd</div> */} - </div> - ); -} + <div class="flex gap-y-5 w-48 bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip"> -// return ( -// <div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8"> -// <button -// type="button" -// class="-m-2.5 p-2.5 text-gray-700 lg:hidden" -// onClick={onOpenSidebar} -// > -// <span class="sr-only">Open sidebar</span> -// <Bars3Icon class="h-6 w-6" aria-hidden="true" /> -// </button> + <nav class="flex flex-1 flex-col mx-4 mt-4 mb-2"> + <ul role="list" class="flex flex-1 flex-col gap-y-7"> + <li> + <ul role="list" class="-mx-2 space-y-1"> + {pageList.map(p => { + return <li> + {/* <!-- Current: "bg-indigo-700 text-white", Default: "text-indigo-200 hover:text-white hover:bg-indigo-700" --> */} + <a href="#" class="bg-indigo-700 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> + <img src={p.icon} /> + {p.name} + </a> + </li> -// {/* Separator */} -// <div class="h-6 w-px bg-gray-900/10 lg:hidden" aria-hidden="true" /> + })} + {/* <li> + <a href="#" class="text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> -// <div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6"> -// <div class="relative flex flex-1" /> -// {/* <form class="relative flex flex-1" action="#" method="GET"> -// <label htmlFor="search-field" class="sr-only"> -// Search -// </label> -// <MagnifyingGlassIcon -// class="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-gray-400" -// aria-hidden="true" -// /> -// <input -// id="search-field" -// class="block h-full w-full border-0 py-0 pl-8 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm" -// placeholder="Search..." -// type="search" -// name="search" -// /> -// </form> */} -// <div class="flex items-center gap-x-4 lg:gap-x-6"> -// {/* <button -// type="button" -// class="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500" -// > -// <span class="sr-only">View notifications</span> -// <BellIcon class="h-6 w-6" aria-hidden="true" /> -// </button> */} + <i18n.Translate>Officer</i18n.Translate> + </a> + </li> */} + </ul> + </li> -// {/* Separator */} -// <div -// class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10" -// aria-hidden="true" -// /> + {/* <li class="mt-auto "> + <a href="#" class="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-indigo-200 hover:bg-indigo-700 hover:text-white"> + <svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> + <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> + </svg> + Settings + </a> + </li> */} -// {/* {officerName === undefined ? ( -// <div /> -// ) : ( -// <Menu -// as="div" -// class="relative" -// > -// <Menu.Button class="-m-1.5 flex items-center p-1.5"> -// <span class="sr-only">Open user menu</span> -// <img -// class="h-8 w-8 rounded-full bg-gray-50" -// src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" -// alt="" -// /> -// <span class="hidden lg:flex lg:items-center"> -// <span -// class="ml-4 text-sm font-semibold leading-6 text-gray-900" -// aria-hidden="true" -// > -// {officerName} -// </span> -// <ChevronDownIcon -// class="ml-2 h-5 w-5 text-gray-400" -// aria-hidden="true" -// /> -// </span> -// </Menu.Button> -// <Transition -// as={Fragment} -// enter="transition ease-out duration-100" -// enterFrom="transform opacity-0 scale-95" -// enterTo="transform opacity-100 scale-100" -// leave="transition ease-in duration-75" -// leaveFrom="transform opacity-100 scale-100" -// leaveTo="transform opacity-0 scale-95" -// > -// <Menu.Items class="absolute right-0 z-10 mt-2.5 w-48 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none"> -// <Menu.Item> -// {({ active }: { active: boolean }) => ( -// <a -// onClick={() => { -// officer.reset(); -// password.reset(); -// }} -// class={classNames( -// active ? "bg-gray-50" : "", -// "block px-3 py-1 text-sm leading-6 text-gray-900", -// )} -// > -// Forget account -// </a> -// )} -// </Menu.Item> -// </Menu.Items> -// </Transition> -// </Menu> -// )} */} -// </div> -// </div> -// </div> -// ); -// } + </ul> + </nav> + </div> + ) -function Footer() { - return ( - <footer class="absolute bottom-4"> - <div class="mt-8 md:order-1 md:mt-0"> - <p class="text-xs leading-5 text-gray-300"> - Taler Systems SA. {versionText} - </p> - </div> - </footer> - ); } -function Notifications() { - const ns = useNotifications(); - - // useEffect(() => { - // if (ns.length) { - // // remove notifications after some timeout - // } - // }, []); - { - /* <!-- Global notification live region, render this permanently at the end of the document --> */ - } - return ( - <div - aria-live="assertive" - class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50" - > - <div class="flex w-full flex-col items-center space-y-4 sm:items-end "> - {/* <!-- - Notification panel, dynamically insert this into the live region when it needs to be displayed - Entering: "transform ease-out duration-300 transition" - From: "translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2" - To: "translate-y-0 opacity-100 sm:translate-x-0" - Leaving: "transition ease-in duration-100" - From: "opacity-100" - To: "opacity-0" ---> */} - {ns.map(({ message, remove }) => { - switch (message.type) { - case "error": { - return ( - <div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 "> - <div class="p-4 "> - <div class="flex items-start "> - <div class="flex-shrink-0"> - <XCircleIcon class="h-6 w-6 text-red-400" /> - </div> - <div class="ml-3 w-0 flex-1 pt-0.5"> - <p class="text-sm font-medium text-gray-900"> - {message.title} - </p> - {message.description && ( - <p class="mt-1 text-sm text-gray-500"> - {message.description} - </p> - )} - </div> - <div class="ml-4 flex flex-shrink-0"> - <button - type="button" - onClick={remove} - class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" - > - <span class="sr-only">Close</span> - <svg - class="h-5 w-5" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - > - <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> - </svg> - </button> - </div> - </div> - </div> - </div> - ); - } - case "info": { - return ( - <div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 "> - <div class="p-4 "> - <div class="flex items-start "> - <div class="flex-shrink-0"> - <CheckCircleIcon class="h-6 w-6 text-green-400" /> - </div> - <div class="ml-3 w-0 flex-1 pt-0.5"> - <p class="text-sm font-medium text-gray-900"> - {message.title} - </p> - </div> - <div class="ml-4 flex flex-shrink-0"> - <button - type="button" - onClick={remove} - class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" - > - <span class="sr-only">Close</span> - <svg - class="h-5 w-5" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - > - <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> - </svg> - </button> - </div> - </div> - </div> - </div> - ); - } - } - })} - </div> - </div> - ); -} diff --git a/packages/aml-backoffice-ui/src/assets/home.svg b/packages/aml-backoffice-ui/src/assets/home.svg new file mode 100644 index 000000000..35f340162 --- /dev/null +++ b/packages/aml-backoffice-ui/src/assets/home.svg @@ -0,0 +1,3 @@ +<svg class="h-6 w-6 shrink-0 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> +</svg>
\ No newline at end of file diff --git a/packages/aml-backoffice-ui/src/assets/people.svg b/packages/aml-backoffice-ui/src/assets/people.svg new file mode 100644 index 000000000..1dc878b81 --- /dev/null +++ b/packages/aml-backoffice-ui/src/assets/people.svg @@ -0,0 +1,3 @@ +<svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> +</svg>
\ No newline at end of file diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts index 2a133f46d..c4edd9207 100644 --- a/packages/aml-backoffice-ui/src/hooks/useCases.ts +++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts @@ -5,7 +5,7 @@ import { } from "@gnu-taler/web-util/browser"; import { AmlExchangeBackend } from "../types.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AmountString, OfficerAccount, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; +import { AmountString, OfficerAccount, OperationFail, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook } from "swr"; import { useExchangeApiContext } from "../context/config.js"; import { useOfficer } from "./useOfficer.js"; @@ -70,6 +70,15 @@ export function useCases(state: AmlExchangeBackend.AmlState) { }; // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts; + if (!session) { + return { + data: { + type: "fail", + case: "unauthorized", + detail: {} + } as OperationFail<never> + } + } if (data) { if (data.type === "fail") { return { data } diff --git a/packages/aml-backoffice-ui/src/hooks/useSettings.ts b/packages/aml-backoffice-ui/src/hooks/useSettings.ts new file mode 100644 index 000000000..52f6f1614 --- /dev/null +++ b/packages/aml-backoffice-ui/src/hooks/useSettings.ts @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + Codec, + TranslatedString, + buildCodecForObject, + codecForBoolean, + codecForNumber, + codecForString, + codecOptional +} from "@gnu-taler/taler-util"; +import { buildStorageKey, useLocalStorage, useTranslationContext } from "@gnu-taler/web-util/browser"; + +interface Settings { + allowInsecurePassword: boolean; +} + +export function getAllBooleanSettings(): Array<keyof Settings> { + return ["allowInsecurePassword"] +} + +export function getLabelForSetting(k: keyof Settings, i18n: ReturnType<typeof useTranslationContext>["i18n"]): TranslatedString { + switch (k) { + case "allowInsecurePassword": return i18n.str`Allow Insecure password` + } +} + +export const codecForSettings = (): Codec<Settings> => + buildCodecForObject<Settings>() + .property("allowInsecurePassword", (codecForBoolean())) + .build("Settings"); + +const defaultSettings: Settings = { + allowInsecurePassword: false, +}; + +const EXCHANGE_SETTINGS_KEY = buildStorageKey( + "exchange-settings", + codecForSettings(), +); + +export function useSettings(): [ + Readonly<Settings>, + <T extends keyof Settings>(key: T, value: Settings[T]) => void, +] { + const { value, update } = useLocalStorage( + EXCHANGE_SETTINGS_KEY, + defaultSettings, + ); + + function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { + const newValue = { ...value, [k]: v }; + update(newValue); + } + return [value, updateField]; +} diff --git a/packages/aml-backoffice-ui/src/pages.ts b/packages/aml-backoffice-ui/src/pages.ts index e4e16f05f..17ede651d 100644 --- a/packages/aml-backoffice-ui/src/pages.ts +++ b/packages/aml-backoffice-ui/src/pages.ts @@ -1,55 +1,51 @@ -import { Home } from "./pages/Home.js"; -import { Settings } from "./pages/Settings.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; import { AntiMoneyLaunderingForm } from "./pages/AntiMoneyLaunderingForm.js"; -import { Welcome } from "./pages/Welcome.js"; -import { PageEntry, pageDefinition } from "./route.js"; -import { Officer } from "./pages/Officer.js"; -import { Cases } from "./pages/Cases.js"; import { CaseDetails } from "./pages/CaseDetails.js"; +import { Cases } from "./pages/Cases.js"; import { NewFormEntry } from "./pages/NewFormEntry.js"; - -const home: PageEntry = { - url: "#/", - view: Home, -}; +import { Officer } from "./pages/Officer.js"; +import { PageEntry, pageDefinition } from "./route.js"; +import homeLogo from "./assets/home.svg"; +import peopleLogo from "./assets/people.svg"; const cases: PageEntry = { url: "#/cases", view: Cases, + name: "Cases" as TranslatedString, + icon: homeLogo, +}; + +const officer: PageEntry = { + url: "#/officer", + view: Officer, + name: "Officer" as TranslatedString, + icon: peopleLogo, }; + const account: PageEntry<{ account: string }> = { url: pageDefinition("#/account/:account"), view: CaseDetails, + name: "Account" as TranslatedString, + // icon: () => undefined, }; const newFormEntry: PageEntry<{ account?: string; type?: string }> = { url: pageDefinition("#/account/:account/new/:type?"), view: NewFormEntry, + name: "New Form" as TranslatedString, + // icon: () => undefined, }; -const settings: PageEntry = { - url: "#/settings", - view: Settings, -}; -const officer: PageEntry = { - url: "#/officer", - view: Officer, -}; -const welcome: PageEntry<{ asd?: string; name?: string }> = { - url: pageDefinition("#/welcome/:name?"), - view: Welcome, -}; const form: PageEntry<{ number?: string }> = { url: pageDefinition("#/form/:number?"), view: AntiMoneyLaunderingForm, + name: "Form" as TranslatedString, + // icon: () => undefined, }; export const Pages = { - home, - info: cases, + cases, officer, - details: account, - settings, - welcome, + account, form, newFormEntry, }; diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 5f79db71e..624f2c985 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -4,17 +4,16 @@ import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { createNewForm } from "../handlers/forms.js"; import { useCases } from "../hooks/useCases.js"; -import { useOfficer } from "../hooks/useOfficer.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; import { amlStateConverter } from "./CaseDetails.js"; +import { Officer } from "./Officer.js"; export function Cases() { const { i18n } = useTranslationContext(); const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>(); - const initial = AmlExchangeBackend.AmlState.pending; const [stateFilter, setStateFilter] = useState(initial); @@ -23,16 +22,15 @@ export function Cases() { if (!list) { return <Loading /> } - if (list instanceof TalerError) { return <ErrorLoading error={list} /> } if (list.data.type === "fail") { switch (list.data.case) { - case "unauthorized": - case "officer-not-found": - case "officer-disabled": return <div /> + case "unauthorized": return <Officer /> + case "officer-not-found": return <Officer /> + case "officer-disabled": return <Officer /> default: assertUnreachable(list.data) } } @@ -116,7 +114,7 @@ export function Cases() { <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> <div class="text-gray-900"> <a - href={Pages.details.url({ account: r.h_payto })} + href={Pages.account.url({ account: r.h_payto })} class="text-indigo-600 hover:text-indigo-900" > {r.h_payto} diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx index 5dcb8b21d..9b8c3c046 100644 --- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -5,6 +5,7 @@ import { } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { createNewForm } from "../handlers/forms.js"; +import { useSettings } from "../hooks/useSettings.js"; export function CreateAccount({ onNewAccount, @@ -16,12 +17,13 @@ export function CreateAccount({ password: string; repeat: string; }>(); + const [settings] = useSettings() return ( <div class="flex min-h-full flex-col "> <div class="sm:mx-auto sm:w-full sm:max-w-md"> <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> - Create account + <i18n.Translate>Create account</i18n.Translate> </h2> </div> @@ -33,29 +35,29 @@ export function CreateAccount({ password: { error: !v.password ? i18n.str`required` - : v.password.length < 8 - ? i18n.str`should have at least 8 characters` - : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/) - ? i18n.str`should have lowercase and uppercase characters` - : !v.password.match(/\d/) - ? i18n.str`should have numbers` - : !v.password.match(/[^a-zA-Z\d]/) - ? i18n.str`should have at least one character which is not a number or letter` - : undefined, + : settings.allowInsecurePassword + ? undefined + : v.password.length < 8 + ? i18n.str`should have at least 8 characters` + : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/) + ? i18n.str`should have lowercase and uppercase characters` + : !v.password.match(/\d/) + ? i18n.str`should have numbers` + : !v.password.match(/[^a-zA-Z\d]/) + ? i18n.str`should have at least one character which is not a number or letter` + : undefined, }, repeat: { error: !v.repeat ? i18n.str`required` : v.repeat !== v.password - ? i18n.str`doesn't match` - : undefined, + ? i18n.str`doesn't match` + : undefined, }, }; }} onSubmit={async (v, s) => { - console.log(v, s); const error = s?.password?.error ?? s?.repeat?.error; - console.log(error); if (error) { notifyError( "Can't create account" as TranslatedString, @@ -72,7 +74,9 @@ export function CreateAccount({ name="password" type="password" help={ - "lower and upper case letters, number and special character" as TranslatedString + settings.allowInsecurePassword + ? i18n.str`short password are insecure, turn off insecure password in settings` + : i18n.str`lower and upper case letters, number and special character` } required /> @@ -91,7 +95,7 @@ export function CreateAccount({ type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > - Create + <i18n.Translate>Create</i18n.Translate> </button> </div> </Form.Provider> diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx index 05fd0a019..b3d04d97e 100644 --- a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx +++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx @@ -30,5 +30,8 @@ export function HandleAccountNotReady({ /> ); } + return <div> + some + </div> throw Error(`unexpected account state ${(officer as any).state}`); } diff --git a/packages/aml-backoffice-ui/src/pages/Home.tsx b/packages/aml-backoffice-ui/src/pages/Home.tsx deleted file mode 100644 index 838032d63..000000000 --- a/packages/aml-backoffice-ui/src/pages/Home.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { h } from "preact"; - -export function Home() { - return <div>Home</div>; -} diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx index 4af34805a..abada3725 100644 --- a/packages/aml-backoffice-ui/src/pages/Officer.tsx +++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx @@ -1,9 +1,11 @@ import { Fragment, h } from "preact"; import { useOfficer } from "../hooks/useOfficer.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; export function Officer() { const officer = useOfficer(); + const { i18n } = useTranslationContext() if (officer.state !== "ready") { return <HandleAccountNotReady officer={officer} />; } @@ -11,7 +13,7 @@ export function Officer() { return ( <div> <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 "> - Public key + <i18n.Translate>Public key</i18n.Translate> </h1> <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg"> <p class="mt-6 font-mono break-all">{officer.account.id}</p> @@ -25,7 +27,7 @@ export function Officer() { rel="noreferrer" class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > - Request account activation + <i18n.Translate>Request account activation</i18n.Translate> </a> </p> <p> @@ -36,7 +38,7 @@ export function Officer() { }} class="m-4 block rounded-md border-0 bg-gray-200 px-3 py-2 text-center text-sm text-black shadow-sm " > - Lock account + <i18n.Translate>Lock account</i18n.Translate> </button> </p> <p> @@ -47,7 +49,7 @@ export function Officer() { }} class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " > - Remove account + <i18n.Translate>Forget account</i18n.Translate> </button> </p> </div> diff --git a/packages/aml-backoffice-ui/src/pages/Settings.tsx b/packages/aml-backoffice-ui/src/pages/Settings.tsx deleted file mode 100644 index ccff3b210..000000000 --- a/packages/aml-backoffice-ui/src/pages/Settings.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { h } from "preact"; - -export function Settings() { - return <div>Settings</div>; -} diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index 83d8767fb..a6570ffcc 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -1,5 +1,5 @@ import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util"; -import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser"; +import { notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { createNewForm } from "../handlers/forms.js"; @@ -10,6 +10,7 @@ export function UnlockAccount({ onAccountUnlocked: (password: string) => void; onRemoveAccount: () => void; }): VNode { + const { i18n } = useTranslationContext() const Form = createNewForm<{ password: string; }>(); @@ -17,12 +18,12 @@ export function UnlockAccount({ return ( <div class="flex min-h-full flex-col "> <div class="sm:mx-auto sm:w-full sm:max-w-md"> - <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> - Account locked - </h2> + <h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> + <i18n.Translate>Account locked</i18n.Translate> + </h1> <p class="mt-6 text-lg leading-8 text-gray-600"> - Your account is normally locked anytime you reload. To unlock type - your password again. + <i18n.Translate>Your account is normally locked anytime you reload. To unlock type + your password again.</i18n.Translate> </p> </div> @@ -30,7 +31,7 @@ export function UnlockAccount({ <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> <Form.Provider initialValue={{ - password: "welcometo.5146", + password: "qwe", }} onSubmit={async (v) => { try { @@ -63,7 +64,7 @@ export function UnlockAccount({ type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > - Unlock + <i18n.Translate>Unlock</i18n.Translate> </button> </div> </Form.Provider> @@ -75,7 +76,7 @@ export function UnlockAccount({ }} class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " > - Remove account + <i18n.Translate>Forget account</i18n.Translate> </button> </div> </div> diff --git a/packages/aml-backoffice-ui/src/pages/Welcome.tsx b/packages/aml-backoffice-ui/src/pages/Welcome.tsx deleted file mode 100644 index 433fbcf59..000000000 --- a/packages/aml-backoffice-ui/src/pages/Welcome.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { h } from "preact"; - -export function Welcome({ name, asd }: { asd?: string; name?: string }) { - return ( - <div> - {asd} Hello {name} - </div> - ); -} diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts index d54f9be83..4c3331668 100644 --- a/packages/aml-backoffice-ui/src/route.ts +++ b/packages/aml-backoffice-ui/src/route.ts @@ -1,3 +1,4 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; import { createHashHistory } from "history"; import { h as create, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -45,14 +46,18 @@ export function pageDefinition<T extends Record<string, string>>( export type PageEntry<T = unknown> = T extends Record<string, string> ? { - url: PageDefinition<T>; - view: (props: T) => VNode; - } + url: PageDefinition<T>; + view: (props: T) => VNode; + name: TranslatedString, + icon?: string, + } : T extends unknown ? { - url: string; - view: (props: {}) => VNode; - } + url: string; + view: (props: {}) => VNode; + name: TranslatedString, + icon?: string, + } : never; export function Router({ diff --git a/packages/aml-backoffice-ui/src/settings.ts b/packages/aml-backoffice-ui/src/settings.ts index 2897874a2..9c65837c3 100644 --- a/packages/aml-backoffice-ui/src/settings.ts +++ b/packages/aml-backoffice-ui/src/settings.ts @@ -24,7 +24,7 @@ export interface UiSettings { * Global settings for the UI. */ const defaultSettings: UiSettings = { - backendBaseURL: "https://exchange.demo.taler.net/", + backendBaseURL: "http://exchange.taler.test:1180/", allowRegistrations: true, uiName: "Taler Bank", }; diff --git a/packages/aml-backoffice-ui/src/utils/Loading.tsx b/packages/aml-backoffice-ui/src/utils/Loading.tsx deleted file mode 100644 index 7cbdad681..000000000 --- a/packages/aml-backoffice-ui/src/utils/Loading.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { h, VNode } from "preact"; - -export function Loading(): VNode { - return ( - <div - class="columns is-centered is-vcentered" - style={{ - height: "calc(100% - 3rem)", - position: "absolute", - width: "100%", - }} - > - <Spinner /> - </div> - ); -} - -export function Spinner(): VNode { - return ( - <div class="lds-ring"> - <div /> - <div /> - <div /> - <div /> - </div> - ); -} diff --git a/packages/aml-backoffice-ui/tailwind.config.js b/packages/aml-backoffice-ui/tailwind.config.js index 01f058b2e..ec51dfbb8 100644 --- a/packages/aml-backoffice-ui/tailwind.config.js +++ b/packages/aml-backoffice-ui/tailwind.config.js @@ -1,6 +1,12 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./src/**/*.{html,tsx}"], + content: { + relative: true, + files: [ + "./src/**/*.{html,tsx}", + "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}" + ], + }, theme: { extend: {}, }, |