diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/Dashboard.tsx')
-rw-r--r-- | packages/exchange-backoffice-ui/src/Dashboard.tsx | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/packages/exchange-backoffice-ui/src/Dashboard.tsx b/packages/exchange-backoffice-ui/src/Dashboard.tsx new file mode 100644 index 000000000..80f33954a --- /dev/null +++ b/packages/exchange-backoffice-ui/src/Dashboard.tsx @@ -0,0 +1,575 @@ +import { Dialog, Menu, Transition } from "@headlessui/react"; +import { + ChevronDownIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/20/solid"; +import { + Bars3Icon, + BellIcon, + Cog6ToothIcon, + DocumentDuplicateIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { ForwardedRef, forwardRef } from "preact/compat"; +import { useRef, useState } from "preact/hooks"; +import { v1 as form_902_11e_v1 } from "./forms/902_11e.js"; +import { v1 as form_902_12e_v1 } from "./forms/902_12e.js"; +import { v1 as form_902_13e_v1 } from "./forms/902_13e.js"; +import { v1 as form_902_15e_v1 } from "./forms/902_15e.js"; +import { v1 as form_902_1e_v1 } from "./forms/902_1e.js"; +import { v1 as form_902_4e_v1 } from "./forms/902_4e.js"; +import { v1 as form_902_5e_v1 } from "./forms/902_5e.js"; +import { v1 as form_902_9e_v1 } from "./forms/902_9e.js"; +import { Pages } from "./pages.js"; +import { Router, useCurrentLocation } from "./route.js"; + +/** + * references between forms + * + * 902.1e + * --> 902.11 (operational legal entity or partnership) + * --> 902.12 (a foundation) + * --> 902.13 (a trust) + * --> 902.15 (life insurance policy) + * --> 902.9 (all other cases) + * --> 902.5 (cash transaction with no customer profile) + * --> 902.4 (risk profile) + * + * 902.11 + * --> 902.9 (beneficial owner in fiduciary holding assets) + * + * 902.12 + * + * 902.13 + * + * 902.15 + * + * 902.9 + * + * 902.5 + * + * 902.4 + */ + +export const allForms = [ + { + name: "Identification form (902.1e)", + icon: DocumentDuplicateIcon, + impl: form_902_1e_v1, + }, + { + name: "Operational legal entity or partnership (902.11e)", + icon: DocumentDuplicateIcon, + impl: form_902_11e_v1, + }, + { + name: "Foundations (902.12e)", + icon: DocumentDuplicateIcon, + impl: form_902_12e_v1, + }, + { + name: "Declaration for trusts (902.13e)", + icon: DocumentDuplicateIcon, + impl: form_902_13e_v1, + }, + { + name: "Information on life insurance policies (902.15e)", + icon: DocumentDuplicateIcon, + impl: form_902_15e_v1, + }, + { + name: "Declaration of beneficial owner (902.9e)", + icon: DocumentDuplicateIcon, + impl: form_902_9e_v1, + }, + { + name: "Customer profile (902.5e)", + icon: DocumentDuplicateIcon, + impl: form_902_5e_v1, + }, + { + name: "Risk profile (902.4e)", + icon: DocumentDuplicateIcon, + impl: form_902_4e_v1, + }, +]; +const teams = [ + { id: 1, name: "Heroicons", href: "#", initial: "H", current: false }, + { id: 2, name: "Tailwind Labs", href: "#", initial: "T", current: false }, + { id: 3, name: "Workcation", href: "#", initial: "W", current: false }, +]; +const userNavigation = [ + { name: "Your profile", href: "#" }, + { name: "Sign out", href: "#" }, +]; + +function classNames(...classes: string[]) { + return classes.filter(Boolean).join(" "); +} + +/** + * mapping route to view + * not found (error page) + * nested, index element, relative routes + * link interception + * form POST interception, call action + * fromData => Object.fromEntries + * segments in the URL + * navigationState: idle, submitting, loading + * form GET interception: does a navigateTo + * form GET Sync: + * 1.- back after submit: useEffect to sync URL to form + * 2.- refresh after submit: input default value + * useSubmit for form submission onChange, history replace + * + * post form without redirect + * + * + * @param param0 + * @returns + */ + +const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; +const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; + +const versionText = VERSION + ? GIT_HASH + ? `Version ${VERSION} (${GIT_HASH.substring(0, 8)})` + : VERSION + : ""; + +/** + * TO BE FIXED: + * + * 1.- when the form change to other form and both form share the same structure + * the same input component may be rendered in the same place, + * since input are uncontrolled the are not re-rendered and since they are + * uncontrolled it will keep the value of the previous form. + * One solutions could be to remove the form when unloading and when the new + * form load it will start without previous vdom, preventing the cache + * to create this behavior. + * Other solutions could be using IDs in the fields that are constructed + * with the ID of the form, so two fields of different form will need to re-render + * cleaning up the state of the previous form. + * + * 2.- currently the design prop and the behavior prop of the flexible form + * are two side of the same coin. From the design point of view, it is important + * to design the form in a list-of-field manner and there may be additional + * content that is not directly mapped to the form structure (object) + * So maybe we want to change the current shape so the computation of the state + * of the form is in a field level, but this computation required the field value and + * the whole form values and state (since one field may be disabled/hidden) because + * of the value of other field. + * + * 3.- given the previous requirement, maybe the name of the field of the form could be + * a function (P: F -> V) where F is the form (or parent object) and V is the type of the + * property. That will help with the typing of the forms props + * + * 4.- tooltip are not placed correctly: the arrow should point the question mark + * and the text area should be bigger + * + * 5.- date field should have the calendar icon clickable so the user can select date without + * writing text with the correct format + */ + +export function Dashboard({ + children, +}: { + children?: ComponentChildren; +}): VNode { + const [sidebarOpen, setSidebarOpen] = useState(false); + + const logRef = useRef<HTMLPreElement>(null); + function showFormOnSidebar(v: any) { + if (!logRef.current) return; + logRef.current.innerHTML = JSON.stringify(v, undefined, 1); + } + + const Nav = forwardRef(NavigationBar); + return ( + <Fragment> + <Nav ref={logRef} isOpen={sidebarOpen} setOpen={setSidebarOpen} /> + <div class="lg:pl-72"> + <TopBar + onOpenSidebar={() => { + setSidebarOpen(true); + }} + /> + <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>; + }} + /> + </div> + </main> + + <Footer /> + </div> + </Fragment> + ); +} + +const pageList = Object.values(Pages); + +function NavigationBar( + { isOpen, setOpen }: { isOpen: boolean; setOpen: (v: boolean) => void }, + logRef: ForwardedRef<HTMLPreElement>, +) { + const currentLocation = useCurrentLocation(pageList); + 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> + + <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> + <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"> + <img + class="h-8 w-auto" + src="https://tailwindui.com/img/logos/mark.svg?color=white" + alt="Your Company" + /> + </div> + <nav class="flex flex-1 flex-col"> + <ul role="list" class="flex flex-1 flex-col gap-y-7"> + <li> + <ul role="list" class="-mx-2 space-y-1"> + {allForms.map((item, idx) => { + const url = Pages.form.url({ number: String(idx) }); + return ( + <li key={item.name}> + <a + href={url} + class={classNames( + 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", + )} + > + <item.icon + class={classNames( + url === currentLocation?.path + ? "text-white" + : "text-indigo-200 group-hover:text-white", + "h-6 w-6 shrink-0", + )} + aria-hidden="true" + /> + {item.name} + </a> + </li> + ); + })} + </ul> + </li> + {/* <li> + <div class="text-xs font-semibold leading-6 text-indigo-200"> + Your teams + </div> + <ul role="list" class="-mx-2 mt-2 space-y-1"> + {teams.map((team) => ( + <li key={team.name}> + <a + href={team.href} + class={classNames( + team.current + ? "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"> + {team.initial} + </span> + <span class="truncate">{team.name}</span> + </a> + </li> + ))} + </ul> + </li> */} + <li class="mt-auto"> + <a + href={Pages.settings.url} + 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" + > + <Cog6ToothIcon + class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" + aria-hidden="true" + /> + Settings + </a> + </li> + </ul> + </nav> + </div> + </Dialog.Panel> + </Transition.Child> + </div> + </Dialog> + </Transition.Root> + + <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col"> + <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"> + <img + class="h-8 w-auto" + src="https://tailwindui.com/img/logos/mark.svg?color=white" + alt="Your Company" + /> + </div> + <nav class="flex flex-1 flex-col"> + <ul role="list" class="flex flex-1 flex-col gap-y-7"> + <li> + <ul role="list" class="-mx-2 space-y-1"> + {allForms.map((item, idx) => { + const url = Pages.form.url({ number: String(idx) }); + return ( + <li key={item.name}> + <a + href={url} + class={classNames( + 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", + )} + > + <item.icon + class={classNames( + url === currentLocation?.path + ? "text-white" + : "text-indigo-200 group-hover:text-white", + "h-6 w-6 shrink-0", + )} + aria-hidden="true" + /> + {item.name} + </a> + </li> + ); + })} + </ul> + </li> + {/* <li> + <div class="text-xs font-semibold leading-6 text-indigo-200"> + Your teams + </div> + <ul role="list" class="-mx-2 mt-2 space-y-1"> + {teams.map((team) => ( + <li key={team.name}> + <a + href={team.href} + class={classNames( + team.current + ? "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"> + {team.initial} + </span> + <span class="truncate">{team.name}</span> + </a> + </li> + ))} + </ul> + </li> */} + <li class="mt-auto"> + <a + href={Pages.settings.url} + 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" + > + <Cog6ToothIcon + class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" + aria-hidden="true" + /> + Settings + </a> + </li> + </ul> + </nav> + <div class="text-white text-sm"> + <pre ref={logRef}></pre> + </div> + </div> + </div> + </Fragment> + ); +} + +function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) { + 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> + + {/* Separator */} + <div class="h-6 w-px bg-gray-900/10 lg:hidden" aria-hidden="true" /> + + <div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6"> + <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> + + {/* Separator */} + <div + class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10" + aria-hidden="true" + /> + + {/* Profile dropdown */} + <Menu + as="div" + /* @ts-ignore */ + 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" + > + Tom Cook + </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-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none"> + {userNavigation.map((item) => ( + <Menu.Item key={item.name}> + {({ active }: { active: boolean }) => ( + <a + href={item.href} + class={classNames( + active ? "bg-gray-50" : "", + "block px-3 py-1 text-sm leading-6 text-gray-900", + )} + > + {item.name} + </a> + )} + </Menu.Item> + ))} + </Menu.Items> + </Transition> + </Menu> + </div> + </div> + </div> + ); +} + +function Footer() { + return ( + <footer class="bg-white"> + <div class="mx-auto px-4 py-2 md:flex md:items-center md:justify-between lg:px-8"> + <div class="mt-8 md:order-1 md:mt-0"> + <p class="text-center text-xs leading-5 text-gray-500"> + Copyright © 2014—2023 Taler Systems SA. + {versionText} + </p> + </div> + </div> + </footer> + ); +} |