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/aml-backoffice-ui/src/Dashboard.tsx | |
parent | 3d1ab082d4a66df08fcb468d04198c055d00b8c5 (diff) | |
download | wallet-core-4ee903eb5e639fa0e122a689cc431e8f18ca1197.tar.xz |
aml ui
Diffstat (limited to 'packages/aml-backoffice-ui/src/Dashboard.tsx')
-rw-r--r-- | packages/aml-backoffice-ui/src/Dashboard.tsx | 564 |
1 files changed, 120 insertions, 444 deletions
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> - ); -} |