diff options
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages/CaseDetails.tsx')
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 358 |
1 files changed, 215 insertions, 143 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index 0875f047b..a91f3d107 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -1,3 +1,18 @@ +/* + 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 { AbsoluteTime, AmountJson, @@ -5,27 +20,39 @@ import { HttpStatusCode, TalerError, TranslatedString, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; -import { DefaultForm, ErrorLoading, InternationalizationAPI, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + DefaultForm, + ErrorLoading, + InternationalizationAPI, + Loading, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { Fragment, VNode, h } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { FormMetadata } from "../forms/declaration.js"; +import { BaseForm, FormMetadata, uiForms } from "../forms/declaration.js"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { Pages } from "../pages.js"; -import { Justification, parseJustification } from "./AntiMoneyLaunderingForm.js"; -import { ShowConsolidated } from "./ShowConsolidated.js"; import { AmlExchangeBackend } from "../utils/types.js"; -import { uiForms } from "../forms/declaration.js"; +import { + Justification, + parseJustification, +} from "./AntiMoneyLaunderingForm.js"; +import { ShowConsolidated } from "./ShowConsolidated.js"; -export type AmlEvent = AmlFormEvent | AmlFormEventError | KycCollectionEvent | KycExpirationEvent; +export type AmlEvent = + | AmlFormEvent + | AmlFormEventError + | KycCollectionEvent + | KycExpirationEvent; type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; title: TranslatedString; justification: Justification; - metadata: FormMetadata<any>; + metadata: FormMetadata<BaseForm>; state: AmlExchangeBackend.AmlState; threshold: AmountJson; }; @@ -33,8 +60,8 @@ type AmlFormEventError = { type: "aml-form-error"; when: AbsoluteTime; title: TranslatedString; - justification: undefined, - metadata: undefined, + justification: undefined; + metadata: undefined; state: AmlExchangeBackend.AmlState; threshold: AmountJson; }; @@ -58,17 +85,24 @@ function selectSooner(a: WithTime, b: WithTime) { return AbsoluteTime.cmp(a.when, b.when); } -function titleForJustification(op: ReturnType<typeof parseJustification>, i18n: InternationalizationAPI): TranslatedString { +function titleForJustification( + op: ReturnType<typeof parseJustification>, + i18n: InternationalizationAPI, +): TranslatedString { if (op.type === "ok") { return op.body.justification.label as TranslatedString; } switch (op.case) { - case "not-json": return "error: the justification is not a form" as TranslatedString - case "id-not-found": return "error: justification form's id not found" as TranslatedString - case "version-not-found": return "error: justification form's version not found" as TranslatedString - case "form-not-found": return `error: justification form not found` as TranslatedString + case "not-json": + return i18n.str`error: the justification is not a form`; + case "id-not-found": + return i18n.str`error: justification form's id not found`; + case "version-not-found": + return i18n.str`error: justification form's version not found`; + case "form-not-found": + return i18n.str`error: justification form not found`; default: { - assertUnreachable(op.case) + assertUnreachable(op.case); } } } @@ -79,8 +113,7 @@ export function getEventsFromAmlHistory( i18n: InternationalizationAPI, ): AmlEvent[] { const ae: AmlEvent[] = aml.map((a) => { - - const just = parseJustification(a.justification, uiForms.forms(i18n)) + const just = parseJustification(a.justification, uiForms.forms(i18n)); return { type: just.type === "ok" ? "aml-form" : "aml-form-error", state: a.new_state, @@ -117,47 +150,53 @@ export function getEventsFromAmlHistory( export function CaseDetails({ account }: { account: string }) { const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now()); - const [showForm, setShowForm] = useState<{ justification: Justification, metadata: FormMetadata<any> }>() + const [showForm, setShowForm] = useState<{ + justification: Justification; + metadata: FormMetadata<BaseForm>; + }>(); const { i18n } = useTranslationContext(); - const details = useCaseDetails(account) + const details = useCaseDetails(account); if (!details) { - return <Loading /> + return <Loading />; } if (details instanceof TalerError) { - return <ErrorLoading error={details} /> + return <ErrorLoading error={details} />; } if (details.type === "fail") { switch (details.case) { case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: - case HttpStatusCode.Conflict: return <div /> - default: assertUnreachable(details) + case HttpStatusCode.Conflict: + return <div />; + default: + assertUnreachable(details); } } - const { aml_history, kyc_attributes } = details.body + const { aml_history, kyc_attributes } = details.body; const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n); if (showForm !== undefined) { - return <DefaultForm - readOnly={true} - initial={showForm.justification.value} - form={showForm.metadata.impl(showForm.justification.value)} - > - <div class="mt-6 flex items-center justify-end gap-x-6"> - <button - class="text-sm font-semibold leading-6 text-gray-900" - onClick={() => { - setShowForm(undefined) - }} - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - </div> - - </DefaultForm> + return ( + <DefaultForm + readOnly={true} + initial={showForm.justification.value} + form={showForm.metadata.impl(showForm.justification.value)} + > + <div class="mt-6 flex items-center justify-end gap-x-6"> + <button + class="text-sm font-semibold leading-6 text-gray-900" + onClick={() => { + setShowForm(undefined); + }} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + </div> + </DefaultForm> + ); } return ( <div> @@ -165,40 +204,46 @@ 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" > - <i18n.Translate> - New AML form - </i18n.Translate> + <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"> <i18n.Translate> - Case history for account <span title={account}>{account.substring(0, 16)}...</span> + Case history for account{" "} + <span title={account}>{account.substring(0, 16)}...</span> </i18n.Translate> </h1> </header> - <ShowTimeline history={events} onSelect={(e) => { - switch (e.type) { - case "aml-form": { - const { justification, metadata } = e - setShowForm({ justification, metadata }) - break; - } - case "kyc-collection": - case "kyc-expiration": { - setSelected(e.when); - break; + <ShowTimeline + history={events} + onSelect={(e) => { + switch (e.type) { + case "aml-form": { + const { justification, metadata } = e; + setShowForm({ justification, metadata }); + break; + } + case "kyc-collection": + case "kyc-expiration": { + setSelected(e.when); + break; + } + case "aml-form-error": } - case "aml-form-error": - } - }} /> + }} + /> {/* {selected && <ShowEventDetails event={selected} />} */} {selected && <ShowConsolidated history={events} until={selected} />} </div> ); } -function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode { +function AmlStateBadge({ + state, +}: { + state: AmlExchangeBackend.AmlState; +}): VNode { switch (state) { case AmlExchangeBackend.AmlState.normal: { return ( @@ -222,93 +267,120 @@ function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode ); } } - assertUnreachable(state) + assertUnreachable(state); } -function ShowTimeline({ history, onSelect }: { onSelect: (e: AmlEvent) => void, history: AmlEvent[] }): VNode { - return <div class="flow-root"> - <ul role="list"> - {history.map((e, idx) => { - const isLast = history.length - 1 === idx; - return ( - <li - data-ok={e.type !== "aml-form-error"} - class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer" - onClick={() => { - onSelect(e); - }} - > - <div class="relative pb-6"> - {!isLast ? ( - <span - class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200" - aria-hidden="true" - ></span> - ) : undefined} - <div class="relative flex space-x-3"> - {(() => { - switch (e.type) { - case "aml-form-error": - case "aml-form": { - return <div> - <AmlStateBadge state={e.state} /> - <span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 "> - {e.threshold.currency}{" "} - {Amounts.stringifyValue(e.threshold)} - </span> - </div> - } - case "kyc-collection": { - return ( - // <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 <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> - +function ShowTimeline({ + history, + onSelect, +}: { + onSelect: (e: AmlEvent) => void; + history: AmlEvent[]; +}): VNode { + return ( + <div class="flow-root"> + <ul role="list"> + {history.map((e, idx) => { + const isLast = history.length - 1 === idx; + return ( + <li + key={idx} + data-ok={e.type !== "aml-form-error"} + class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer" + onClick={() => { + onSelect(e); + }} + > + <div class="relative pb-6"> + {!isLast ? ( + <span + class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200" + aria-hidden="true" + ></span> + ) : undefined} + <div class="relative flex space-x-3"> + {(() => { + switch (e.type) { + case "aml-form-error": + case "aml-form": { + return ( + <div> + <AmlStateBadge state={e.state} /> + <span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 "> + {e.threshold.currency}{" "} + {Amounts.stringifyValue(e.threshold)} + </span> + </div> + ); + } + case "kyc-collection": { + return ( + // <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 ( + <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> + ); + } } - } - assertUnreachable(e) - })()} - <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> - {e.type === "aml-form" ? - <span - // href={Pages.newFormEntry.url({ account })} - class="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" - > - {e.title} - </span> - : - <p class="text-sm text-gray-900">{e.title}</p> - } - <div class="whitespace-nowrap text-right text-sm text-gray-500"> - {e.when.t_ms === "never" ? ( - "never" + assertUnreachable(e); + })()} + <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> + {e.type === "aml-form" ? ( + <span + // href={Pages.newFormEntry.url({ account })} + class="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" + > + {e.title} + </span> ) : ( - <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}> - {format(e.when.t_ms, "dd MMM yyyy")} - </time> + <p class="text-sm text-gray-900">{e.title}</p> )} + <div class="whitespace-nowrap text-right text-sm text-gray-500"> + {e.when.t_ms === "never" ? ( + "never" + ) : ( + <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}> + {format(e.when.t_ms, "dd MMM yyyy")} + </time> + )} + </div> </div> </div> </div> - </div> - </li> - ); - })} - </ul> - </div> - -} - -function ShowEventDetails({ event }: { event: AmlEvent }): VNode { - return <div>type {event.type}</div>; + </li> + ); + })} + </ul> + </div> + ); } - - |