diff options
20 files changed, 533 insertions, 323 deletions
diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx index 5d86836d4..d111ae145 100644 --- a/packages/aml-backoffice-ui/src/Dashboard.tsx +++ b/packages/aml-backoffice-ui/src/Dashboard.tsx @@ -1,17 +1,12 @@ -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 { useEffect, useErrorBoundary, useState } from "preact/hooks"; -import logo from "./assets/logo-2021.svg"; -import { Pages } from "./pages.js"; -import { PageEntry, Router, useCurrentLocation } from "./route.js"; -import { uiSettings } from "./settings.js"; import { TranslatedString } from "@gnu-taler/taler-util"; +import { Footer, GlobalNotificationsBanner, Header, notifyError, notifyException, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { useEffect, useErrorBoundary } from "preact/hooks"; import { useOfficer } from "./hooks/useOfficer.js"; import { getAllBooleanSettings, getLabelForSetting, useSettings } from "./hooks/useSettings.js"; +import { Pages } from "./pages.js"; +import { PageEntry, useChangeLocation, useCurrentLocation } from "./route.js"; +import { uiSettings } from "./settings.js"; function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); @@ -101,7 +96,7 @@ function LeftMenu() { "group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold", )} > - <InformationCircleIcon + {/* <InformationCircleIcon class={classNames( Pages.cases.url === currentLocation?.path ? "text-white" @@ -109,7 +104,11 @@ function LeftMenu() { "h-6 w-6 shrink-0", )} aria-hidden="true" - /> + /> */} + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /> + </svg> + Cases </a> </li> @@ -123,7 +122,11 @@ function LeftMenu() { "group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold", )} > - <UserIcon + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" /> + </svg> + + {/* <UserIcon class={classNames( Pages.officer.url === currentLocation?.path ? "text-white" @@ -131,7 +134,7 @@ function LeftMenu() { "h-6 w-6 shrink-0", )} aria-hidden="true" - /> + /> */} Account </a> </li> @@ -233,6 +236,7 @@ function Navigation(): VNode { Pages.officer, Pages.cases ] + const location = useChangeLocation(); return ( <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"> @@ -241,10 +245,13 @@ function Navigation(): VNode { <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} /> + {/* <!-- Current: "bg-indigo-700 text-white", + Default: "text-indigo-200 hover:text-white hover:bg-indigo-700" --> */} + <a href={p.url} data-selected={location == p.url} + class="data-[selected=true]:bg-indigo-700 data-[selected=true]: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"> + { p.Icon && <p.Icon />} {p.name} </a> </li> diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts index a91e7a866..5507c72dc 100644 --- a/packages/aml-backoffice-ui/src/forms/902_11e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts @@ -52,7 +52,6 @@ export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({ name: "businessEstablisher", label: "Persons" as TranslatedString, required: true, - tooltip: "hola" as TranslatedString, placeholder: "this is the placeholder" as TranslatedString, fields: [ { diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts index 167d1ac19..c212efb1a 100644 --- a/packages/aml-backoffice-ui/src/forms/902_1e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts @@ -220,7 +220,6 @@ export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({ name: "businessEstablisher", label: "Persons" as TranslatedString, required: true, - tooltip: "hola" as TranslatedString, placeholder: "this is the placeholder" as TranslatedString, fields: [ { diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts index cecd74390..7c47a8746 100644 --- a/packages/aml-backoffice-ui/src/forms/902_4e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts @@ -1,11 +1,10 @@ import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; -import { ArrowRightIcon } from "@heroicons/react/24/outline"; -import { ChevronRightIcon } from "@heroicons/react/24/solid"; import { h as create } from "preact"; import { FormState } from "../handlers/FormProvider.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; import { FlexibleForm } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; +import { ArrowRightIcon, ChevronRightIcon } from "../pages/Cases.js"; export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({ versionId: "2023-05-15", diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts index 023c1765f..6497f3949 100644 --- a/packages/aml-backoffice-ui/src/forms/simplest.ts +++ b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -7,9 +7,10 @@ import { import { FormState } from "../handlers/FormProvider.js"; import { DoubleColumnFormSection } from "../handlers/forms.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { amlStateConverter } from "../pages/CaseDetails.js"; + import { AmlExchangeBackend } from "../types.js"; import { FlexibleForm } from "./index.js"; +import { amlStateConverter } from "../pages/ShowConsolidated.js"; export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({ versionId: "2023-05-25", diff --git a/packages/aml-backoffice-ui/src/handlers/InputDate.tsx b/packages/aml-backoffice-ui/src/handlers/InputDate.tsx index 1fd81aad9..0f286e001 100644 --- a/packages/aml-backoffice-ui/src/handlers/InputDate.tsx +++ b/packages/aml-backoffice-ui/src/handlers/InputDate.tsx @@ -1,6 +1,5 @@ import { AbsoluteTime } from "@gnu-taler/taler-util"; import { InputLine, UIFormProps } from "./InputLine.js"; -import { CalendarIcon } from "@heroicons/react/24/outline"; import { VNode, h } from "preact"; import { format, parse } from "date-fns"; @@ -13,7 +12,11 @@ export function InputDate<T extends object, K extends keyof T>( type="text" after={{ type: "icon", - icon: <CalendarIcon class="h-6 w-6" />, + // icon: <CalendarIcon class="h-6 w-6" />, + icon: <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" /> + </svg> + }} converter={{ //@ts-ignore @@ -27,8 +30,8 @@ export function InputDate<T extends object, K extends keyof T>( return !v || !v.t_ms ? "" : v.t_ms === "never" - ? "never" - : format(v.t_ms, pattern); + ? "never" + : format(v.t_ms, pattern); }, }} {...props} diff --git a/packages/aml-backoffice-ui/src/pages.ts b/packages/aml-backoffice-ui/src/pages.ts index 17ede651d..109cd31d0 100644 --- a/packages/aml-backoffice-ui/src/pages.ts +++ b/packages/aml-backoffice-ui/src/pages.ts @@ -1,24 +1,24 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { AntiMoneyLaunderingForm } from "./pages/AntiMoneyLaunderingForm.js"; import { CaseDetails } from "./pages/CaseDetails.js"; -import { Cases } from "./pages/Cases.js"; +import { Cases, HomeIcon, PeopleIcon } from "./pages/Cases.js"; import { NewFormEntry } from "./pages/NewFormEntry.js"; import { Officer } from "./pages/Officer.js"; import { PageEntry, pageDefinition } from "./route.js"; -import homeLogo from "./assets/home.svg"; -import peopleLogo from "./assets/people.svg"; +// 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, + Icon: HomeIcon, }; const officer: PageEntry = { url: "#/officer", view: Officer, name: "Officer" as TranslatedString, - icon: peopleLogo, + Icon: PeopleIcon, }; const account: PageEntry<{ account: string }> = { @@ -35,17 +35,10 @@ const newFormEntry: PageEntry<{ account?: string; type?: string }> = { // icon: () => undefined, }; -const form: PageEntry<{ number?: string }> = { - url: pageDefinition("#/form/:number?"), - view: AntiMoneyLaunderingForm, - name: "Form" as TranslatedString, - // icon: () => undefined, -}; export const Pages = { cases, officer, account, - form, newFormEntry, }; diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx new file mode 100644 index 000000000..a14966cc0 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx @@ -0,0 +1,94 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { + AntiMoneyLaunderingForm as TestedComponent, +} from "./AntiMoneyLaunderingForm.js"; + +export default { + title: "aml form", +}; + +export const SimpleComment = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 0, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const Identification = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 1, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const OperationalLegalEntity = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 2, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const Foundations = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 3, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const DelcarationOfTrusts = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 4, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const InformationOnLifeInsurance = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 5, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const DeclarationOfBeneficialOwner = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 6, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const CustomerProfile = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 7, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const RiskProfile = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 8, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); + diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx index c3fb7dafe..5d2a3dffe 100644 --- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx @@ -1,3 +1,5 @@ +import { AbsoluteTime, AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { NiceForm } from "../NiceForm.js"; import { v1 as form_902_11e_v1 } from "../forms/902_11e.js"; @@ -9,30 +11,63 @@ 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 { v1 as simplest } from "../forms/simplest.js"; -import { DocumentDuplicateIcon } from "@heroicons/react/24/solid"; -import { AbsoluteTime } from "@gnu-taler/taler-util"; -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; -export function AntiMoneyLaunderingForm({ number }: { number?: string }) { - const selectedForm = Number.parseInt(number ?? "0", 10); - if (Number.isNaN(selectedForm)) { - return <div>WHAT! {number}</div>; - } +export type Justification = { + // form index in the list of forms + index: number; + // form name + name: string; + // form values + value: any; +} + +export function AntiMoneyLaunderingForm({ account, selectedForm, onSubmit }: { account: string, selectedForm: number, onSubmit: (justification: Justification, state: AmlExchangeBackend.AmlState, threshold: AmountJson) => Promise<void>; }) { + const { i18n } = useTranslationContext() const showingFrom = allForms[selectedForm].impl; - const storedValue = { + const formName = allForms[selectedForm].name + const initial = { fullName: "loggedIn_user_fullname", when: AbsoluteTime.now(), + state: AmlExchangeBackend.AmlState.pending, + threshold: Amounts.parseOrThrow("KUDOS:1000"), }; return ( <NiceForm - initial={storedValue} - form={showingFrom({ - state: AmlExchangeBackend.AmlState.pending, - threshold: Amounts.parseOrThrow("USD:10"), - })} - onUpdate={() => {}} - /> + initial={initial} + form={showingFrom(initial)} + onUpdate={() => { }} + onSubmit={(formValue) => { + if (formValue.state === undefined || formValue.threshold === undefined) return; + const st = formValue.state; + const amount = formValue.threshold; + + const justification = { + index: selectedForm, + name: formName, + value: formValue + } + + onSubmit(justification, st, amount); + }} + > + <div class="mt-6 flex items-center justify-end gap-x-6"> + <a + // type="button" + href={Pages.account.url({ account })} + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </a> + <button + type="submit" + class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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" + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> + </NiceForm> ); } @@ -41,6 +76,11 @@ export interface State { threshold: AmountJson; } +const DocumentDuplicateIcon = <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> +</svg> + + export const allForms = [ { name: "Simple comment", diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index f618a3592..1f8d6ac5e 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -2,24 +2,20 @@ import { AbsoluteTime, AmountJson, Amounts, - PaytoString, TalerError, TranslatedString, - assertUnreachable, + assertUnreachable } from "@gnu-taler/taler-util"; import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { NiceForm } from "../NiceForm.js"; -import { FlexibleForm } from "../forms/index.js"; -import { UIFormField } from "../handlers/forms.js"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; +import { ShowConsolidated } from "./ShowConsolidated.js"; -type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; +export type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; @@ -47,7 +43,7 @@ function selectSooner(a: WithTime, b: WithTime) { return AbsoluteTime.cmp(a.when, b.when); } -function getEventsFromAmlHistory( +export function getEventsFromAmlHistory( aml: AmlExchangeBackend.AmlDecisionDetail[], kyc: AmlExchangeBackend.KycDetail[], ): AmlEvent[] { @@ -113,12 +109,16 @@ export function CaseDetails({ account }: { account: string }) { href={Pages.newFormEntry.url({ account })} 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" > - New AML form + <i18n.Translate> + New AML form + </i18n.Translate> </a> <header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8"> <h1 class="text-base font-semibold leading-7 text-black"> - Case history + <i18n.Translate> + Case history + </i18n.Translate> </h1> </header> <div class="flow-root"> @@ -187,11 +187,18 @@ export function CaseDetails({ account }: { account: string }) { } case "kyc-collection": { return ( - <ArrowDownCircleIcon class="h-8 w-8 text-green-700" /> + // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" /> + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> ); } case "kyc-expiration": { - return <ClockIcon class="h-8 w-8 text-gray-700" />; + // return <ClockIcon class="h-8 w-8 text-gray-700" />; + return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> + } } })()} @@ -217,7 +224,7 @@ export function CaseDetails({ account }: { account: string }) { </ul> </div> {selected && <ShowEventDetails event={selected} />} - {selected && <ShowConsolidated history={events} until={selected} />} + {selected && <ShowConsolidated history={events} until={selected.when} />} </div> ); } @@ -226,197 +233,4 @@ function ShowEventDetails({ event }: { event: AmlEvent }): VNode { return <div>type {event.type}</div>; } -function ShowConsolidated({ - history, - until, -}: { - history: AmlEvent[]; - until: AmlEvent; -}): VNode { - const cons = getConsolidated(history, until.when); - - const form: FlexibleForm<Consolidated> = { - versionId: "1", - behavior: (form) => { - return { - aml: { - threshold: { - hidden: !form.aml - }, - since: { - hidden: !form.aml - }, - state: { - hidden: !form.aml - } - } - }; - }, - design: [ - { - title: "AML" as TranslatedString, - fields: [ - { - type: "amount", - props: { - label: "Threshold" as TranslatedString, - name: "aml.threshold", - }, - }, - { - type: "choiceHorizontal", - props: { - label: "State" as TranslatedString, - name: "aml.state", - converter: amlStateConverter, - choices: [ - { - label: "Frozen" as TranslatedString, - value: AmlExchangeBackend.AmlState.frozen, - }, - { - label: "Pending" as TranslatedString, - value: AmlExchangeBackend.AmlState.pending, - }, - { - label: "Normal" as TranslatedString, - value: AmlExchangeBackend.AmlState.normal, - }, - ], - }, - }, - ], - }, - Object.entries(cons.kyc).length > 0 - ? { - title: "KYC" as TranslatedString, - fields: Object.entries(cons.kyc).map(([key, field]) => { - const result: UIFormField = { - type: "text", - props: { - label: key as TranslatedString, - name: `kyc.${key}.value`, - help: `${field.provider} since ${field.since.t_ms === "never" - ? "never" - : format(field.since.t_ms, "dd/MM/yyyy") - }` as TranslatedString, - }, - }; - return result; - }), - } - : undefined, - ], - }; - return ( - <Fragment> - <h1 class="text-base font-semibold leading-7 text-black"> - Consolidated information after{" "} - {until.when.t_ms === "never" - ? "never" - : format(until.when.t_ms, "dd MMMM yyyy")} - </h1> - <NiceForm - key={`${String(Date.now())}`} - form={form} - initial={cons} - onUpdate={() => { }} - /> - </Fragment> - ); -} -interface Consolidated { - aml: { - state: AmlExchangeBackend.AmlState; - threshold: AmountJson; - since: AbsoluteTime; - }; - kyc: { - [field: string]: { - value: any; - provider: string; - since: AbsoluteTime; - }; - }; -} - -function getConsolidated( - history: AmlEvent[], - when: AbsoluteTime, -): Consolidated { - const initial: Consolidated = { - aml: { - state: AmlExchangeBackend.AmlState.normal, - threshold: { - currency: "ARS", - value: 1000, - fraction: 0, - }, - since: AbsoluteTime.never() - }, - kyc: {}, - }; - return history.reduce((prev, cur) => { - if (AbsoluteTime.cmp(when, cur.when) < 0) { - return prev; - } - switch (cur.type) { - case "kyc-expiration": { - cur.fields.forEach((field) => { - delete prev.kyc[field]; - }); - break; - } - case "aml-form": { - prev.aml = { - since: cur.when, - state: cur.state, - threshold: cur.threshold - } - break; - } - case "kyc-collection": { - Object.keys(cur.values).forEach((field) => { - prev.kyc[field] = { - value: (cur.values as any)[field], - provider: cur.provider, - since: cur.when, - }; - }); - break; - } - } - return prev; - }, initial); -} - -export const amlStateConverter = { - toStringUI: stringifyAmlState, - fromStringUI: parseAmlState, -}; - -function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string { - if (s === undefined) return ""; - switch (s) { - case AmlExchangeBackend.AmlState.normal: - return "normal"; - case AmlExchangeBackend.AmlState.pending: - return "pending"; - case AmlExchangeBackend.AmlState.frozen: - return "frozen"; - } -} - -function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState { - switch (s) { - case "normal": - return AmlExchangeBackend.AmlState.normal; - case "pending": - return AmlExchangeBackend.AmlState.pending; - case "frozen": - return AmlExchangeBackend.AmlState.frozen; - default: - throw Error(`unknown AML state: ${s}`); - } -} diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 624f2c985..64cacf68c 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -6,8 +6,9 @@ import { createNewForm } from "../handlers/forms.js"; import { useCases } from "../hooks/useCases.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; -import { amlStateConverter } from "./CaseDetails.js"; + import { Officer } from "./Officer.js"; +import { amlStateConverter } from "./ShowConsolidated.js"; export function Cases() { const { i18n } = useTranslationContext(); @@ -43,10 +44,14 @@ export function Cases() { <div class="sm:flex sm:items-center"> <div class="sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate> Cases + </i18n.Translate> </h1> <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> A list of all the account with the status + </i18n.Translate> </p> </div> <form.Provider @@ -166,6 +171,25 @@ export function Cases() { ); } +export const PeopleIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> +<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /> +</svg> + +export const HomeIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> +<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> + + +export const ChevronRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> +<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /> +</svg> + + +export const ArrowRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> +</svg> + + function Pagination() { return ( <nav class="flex items-center justify-between px-4 sm:px-0"> diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx index b3d04d97e..ff800ebdc 100644 --- a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx +++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx @@ -2,6 +2,7 @@ import { VNode, h } from "preact"; import { OfficerNotReady } from "../hooks/useOfficer.js"; import { CreateAccount } from "./CreateAccount.js"; import { UnlockAccount } from "./UnlockAccount.js"; +import { assertUnreachable } from "@gnu-taler/taler-util"; export function HandleAccountNotReady({ officer, @@ -24,14 +25,11 @@ export function HandleAccountNotReady({ onRemoveAccount={() => { officer.forget(); }} - onAccountUnlocked={(pwd) => { - officer.tryUnlock(pwd); + onAccountUnlocked={async (pwd) => { + await officer.tryUnlock(pwd); }} /> ); } - return <div> - some - </div> - throw Error(`unexpected account state ${(officer as any).state}`); + assertUnreachable(officer) } diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx index b291ffbee..e70536cb2 100644 --- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx +++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -1,5 +1,5 @@ import { VNode, h } from "preact"; -import { allForms } from "./AntiMoneyLaunderingForm.js"; +import { AntiMoneyLaunderingForm, allForms } from "./AntiMoneyLaunderingForm.js"; import { Pages } from "../pages.js"; import { NiceForm } from "../NiceForm.js"; import { AbsoluteTime, Amounts, TalerExchangeApi, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; @@ -31,60 +31,27 @@ export function NewFormEntry({ if (Number.isNaN(selectedForm)) { return <div>WHAT! {type}</div>; } - const showingFrom = allForms[selectedForm].impl; - const formName = allForms[selectedForm].name - const initial = { - fullName: "loggedIn_user_fullname", - when: AbsoluteTime.now(), - state: AmlExchangeBackend.AmlState.pending, - threshold: Amounts.parseOrThrow("KUDOS:1000"), - }; + const { api } = useExchangeApiContext() return ( - <NiceForm - initial={initial} - form={showingFrom(initial)} - onSubmit={(formValue) => { - if (formValue.state === undefined || formValue.threshold === undefined) return; - - const justification = { - index: selectedForm, - name: formName, - value: formValue - } + <AntiMoneyLaunderingForm + account={account} + selectedForm={selectedForm} + onSubmit={async (justification, new_state, new_threshold) => { const decision: TalerExchangeApi.AmlDecision = { justification: JSON.stringify(justification), decision_time: TalerProtocolTimestamp.now(), h_payto: account, - new_state: formValue.state, - new_threshold: Amounts.stringify(formValue.threshold), + new_state, + new_threshold: Amounts.stringify(new_threshold), officer_sig: "", kyc_requirements: undefined } - // const signature = buildDecisionSignature(officer.account.signingKey, decision); - // decision.officer_sig = signature api.addDecisionDetails(officer.account, decision); - // alert(JSON.stringify(formValue)); }} - > - <div class="mt-6 flex items-center justify-end gap-x-6"> - <a - // type="button" - href={Pages.account.url({ account })} - class="text-sm font-semibold leading-6 text-gray-900" - > - Cancel - </a> - <button - type="submit" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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" - > - Confirm - </button> - </div> - </NiceForm> + /> ); } diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx new file mode 100644 index 000000000..1a86e8e98 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx @@ -0,0 +1,60 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { addDays } from "date-fns"; +import { + ShowConsolidated as TestedComponent, +} from "./ShowConsolidated.js"; +import * as tests from "@gnu-taler/web-util/testing"; +import { getEventsFromAmlHistory } from "./CaseDetails.js"; +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; + +export default { + title: "show consolidated", +}; + +export const WithEmptyHistory = tests.createExample(TestedComponent, { + history: getEventsFromAmlHistory([],[]), + until: AbsoluteTime.now() +}); + +export const WithSomeEvents = tests.createExample(TestedComponent, { + history: getEventsFromAmlHistory([{ + decider_pub: "123", + decision_time: { t_s: 1 }, + justification: "yes", + new_state: 1, + new_threshold: "USD:10", + }],[{ + collection_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromPrettyString("1d")) + ), + expiration_time: { t_s: "never"}, + provider_section: "asd", + attributes: { + email: "sebasjm@qwe.com" + } + }]), + until: AbsoluteTime.now() +}); + + + diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx new file mode 100644 index 000000000..0efc68632 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -0,0 +1,204 @@ +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { NiceForm } from "../NiceForm.js"; +import { FlexibleForm } from "../forms/index.js"; +import { UIFormField } from "../handlers/forms.js"; +import { AmlEvent } from "./CaseDetails.js"; +import { AmlExchangeBackend } from "../types.js"; +import { AbsoluteTime, AmountJson, TranslatedString } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; + +export function ShowConsolidated({ + history, + until, +}: { + history: AmlEvent[]; + until: AbsoluteTime; +}): VNode { + const cons = getConsolidated(history, until); + + const form: FlexibleForm<Consolidated> = { + versionId: "1", + behavior: (form) => { + return { + aml: { + threshold: { + hidden: !form.aml + }, + since: { + hidden: !form.aml + }, + state: { + hidden: !form.aml + } + } + }; + }, + design: [ + { + title: "AML" as TranslatedString, + fields: [ + { + type: "amount", + props: { + label: "Threshold" as TranslatedString, + name: "aml.threshold", + }, + }, + { + type: "choiceHorizontal", + props: { + label: "State" as TranslatedString, + name: "aml.state", + converter: amlStateConverter, + choices: [ + { + label: "Frozen" as TranslatedString, + value: AmlExchangeBackend.AmlState.frozen, + }, + { + label: "Pending" as TranslatedString, + value: AmlExchangeBackend.AmlState.pending, + }, + { + label: "Normal" as TranslatedString, + value: AmlExchangeBackend.AmlState.normal, + }, + ], + }, + }, + ], + }, + Object.entries(cons.kyc).length > 0 + ? { + title: "KYC" as TranslatedString, + fields: Object.entries(cons.kyc).map(([key, field]) => { + const result: UIFormField = { + type: "text", + props: { + label: key as TranslatedString, + name: `kyc.${key}.value`, + help: `${field.provider} since ${field.since.t_ms === "never" + ? "never" + : format(field.since.t_ms, "dd/MM/yyyy") + }` as TranslatedString, + }, + }; + return result; + }), + } + : undefined, + ], + }; + return ( + <Fragment> + <h1 class="text-base font-semibold leading-7 text-black"> + Consolidated information + {until.t_ms === "never" + ? "" + : `after ${format(until.t_ms, "dd MMMM yyyy")}` } + </h1> + <NiceForm + key={`${String(Date.now())}`} + form={form} + initial={cons} + onUpdate={() => { }} + /> + </Fragment> + ); +} + +interface Consolidated { + aml: { + state: AmlExchangeBackend.AmlState; + threshold: AmountJson; + since: AbsoluteTime; + }; + kyc: { + [field: string]: { + value: any; + provider: string; + since: AbsoluteTime; + }; + }; +} + +function getConsolidated( + history: AmlEvent[], + when: AbsoluteTime, +): Consolidated { + const initial: Consolidated = { + aml: { + state: AmlExchangeBackend.AmlState.normal, + threshold: { + currency: "ARS", + value: 1000, + fraction: 0, + }, + since: AbsoluteTime.never() + }, + kyc: {}, + }; + return history.reduce((prev, cur) => { + if (AbsoluteTime.cmp(when, cur.when) < 0) { + return prev; + } + switch (cur.type) { + case "kyc-expiration": { + cur.fields.forEach((field) => { + delete prev.kyc[field]; + }); + break; + } + case "aml-form": { + prev.aml = { + since: cur.when, + state: cur.state, + threshold: cur.threshold + } + break; + } + case "kyc-collection": { + Object.keys(cur.values).forEach((field) => { + prev.kyc[field] = { + value: (cur.values as any)[field], + provider: cur.provider, + since: cur.when, + }; + }); + break; + } + } + return prev; + }, initial); +} + +export const amlStateConverter = { + toStringUI: stringifyAmlState, + fromStringUI: parseAmlState, +}; + +function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string { + if (s === undefined) return ""; + switch (s) { + case AmlExchangeBackend.AmlState.normal: + return "normal"; + case AmlExchangeBackend.AmlState.pending: + return "pending"; + case AmlExchangeBackend.AmlState.frozen: + return "frozen"; + } +} + +function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState { + switch (s) { + case "normal": + return AmlExchangeBackend.AmlState.normal; + case "pending": + return AmlExchangeBackend.AmlState.pending; + case "frozen": + return AmlExchangeBackend.AmlState.frozen; + default: + throw Error(`unknown AML state: ${s}`); + } +} diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index a6570ffcc..ba5aa7b1f 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -7,7 +7,7 @@ export function UnlockAccount({ onAccountUnlocked, onRemoveAccount, }: { - onAccountUnlocked: (password: string) => void; + onAccountUnlocked: (password: string) => Promise<void>; onRemoveAccount: () => void; }): VNode { const { i18n } = useTranslationContext() @@ -30,13 +30,10 @@ export function UnlockAccount({ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> <Form.Provider - initialValue={{ - password: "qwe", - }} + initialValue={{}} onSubmit={async (v) => { try { await onAccountUnlocked(v.password!); - notifyInfo("Account unlocked" as TranslatedString); } catch (e) { if (e instanceof UnwrapKeyError) { diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts new file mode 100644 index 000000000..e31e13a28 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts @@ -0,0 +1,2 @@ +export * as a1 from "./ShowConsolidated.stories.js"; +export * as a2 from "./AntiMoneyLaunderingForm.stories.js"; diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts index 4c3331668..9176ab5e4 100644 --- a/packages/aml-backoffice-ui/src/route.ts +++ b/packages/aml-backoffice-ui/src/route.ts @@ -49,14 +49,14 @@ export type PageEntry<T = unknown> = T extends Record<string, string> url: PageDefinition<T>; view: (props: T) => VNode; name: TranslatedString, - icon?: string, + Icon?: () => VNode, } : T extends unknown ? { url: string; view: (props: {}) => VNode; name: TranslatedString, - icon?: string, + Icon?: () => VNode, } : never; @@ -124,6 +124,16 @@ export function useCurrentLocation(pageList: Array<PageEntry<any>>) { return currentLocation; } +export function useChangeLocation() { + const [location, setLocation] = useState(window.location.hash) + useEffect(() => { + return history.listen(() => { + setLocation(window.location.hash) + }); + }, []); + return location; +} + function doestUrlMatchToRoute( url: string, route: string, diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts index 4e24967e4..3b1d0267d 100644 --- a/packages/aml-backoffice-ui/src/stories.test.ts +++ b/packages/aml-backoffice-ui/src/stories.test.ts @@ -23,7 +23,7 @@ import { parseGroupImport } from "@gnu-taler/web-util/browser"; import * as tests from "@gnu-taler/web-util/testing"; // import * as components from "./components/index.examples.js"; -// import * as pages from "./pages/index.stories.js"; +import * as pages from "./pages/index.stories.js"; import { ComponentChildren, Fragment, VNode, h as create } from "preact"; // import { BackendStateProviderTesting } from "./context/backend.js"; @@ -31,7 +31,7 @@ import { ComponentChildren, Fragment, VNode, h as create } from "preact"; setupI18n("en", { en: {} }); describe("All the examples:", () => { - const cms = parseGroupImport({}); + const cms = parseGroupImport({pages}); cms.forEach((group) => { describe(`Example for group "${group.title}:"`, () => { group.list.forEach((component) => { diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx index b6c0c1f07..5ef54309c 100644 --- a/packages/aml-backoffice-ui/src/stories.tsx +++ b/packages/aml-backoffice-ui/src/stories.tsx @@ -20,17 +20,16 @@ */ import { strings } from "./i18n/strings.js"; -// import * as pages from "./pages/index.stories.js"; +import * as pages from "./pages/index.stories.js"; // import * as components from "./components/index.examples.js"; import { renderStories } from "@gnu-taler/web-util/browser"; -// import "./scss/main.scss"; +import "./scss/main.css"; function main(): void { renderStories( - // { pages, components }, - {}, + {pages}, { strings, }, |