diff options
author | Sebastian <sebasjm@gmail.com> | 2023-11-20 12:38:16 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-11-20 12:38:16 -0300 |
commit | 6138846050563e0dca95b0b6d792776925e4c35f (patch) | |
tree | b33cd36acf4b38d3a016506d4f7fa681c83beb63 /packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | |
parent | 7ed3e78f790837479fc2bb2eb6ddc40c78ce59b5 (diff) |
new forms api
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages/CaseDetails.tsx')
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 288 |
1 files changed, 180 insertions, 108 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index 1f8d6ac5e..1cfa65926 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -2,6 +2,7 @@ import { AbsoluteTime, AmountJson, Amounts, + OperationResult, TalerError, TranslatedString, assertUnreachable @@ -14,12 +15,25 @@ import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; +import { FormMetadata, Justification, allForms, parseJustification } from "./AntiMoneyLaunderingForm.js"; +import { NiceForm } from "../NiceForm.js"; -export type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; +export type AmlEvent = AmlFormEvent | AmlFormEventError | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; title: TranslatedString; + justification: Justification; + metadata: FormMetadata; + state: AmlExchangeBackend.AmlState; + threshold: AmountJson; +}; +type AmlFormEventError = { + type: "aml-form-error"; + when: AbsoluteTime; + title: TranslatedString; + justification: undefined, + metadata: undefined, state: AmlExchangeBackend.AmlState; threshold: AmountJson; }; @@ -43,16 +57,32 @@ function selectSooner(a: WithTime, b: WithTime) { return AbsoluteTime.cmp(a.when, b.when); } +function titleForJustification(op: ReturnType<typeof parseJustification>): 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 + } + assertUnreachable(op.case) +} + export function getEventsFromAmlHistory( aml: AmlExchangeBackend.AmlDecisionDetail[], kyc: AmlExchangeBackend.KycDetail[], ): AmlEvent[] { const ae: AmlEvent[] = aml.map((a) => { + const just = parseJustification(a.justification, allForms) return { - type: "aml-form", + type: just.type === "ok" ? "aml-form" : "aml-form-error", state: a.new_state, threshold: Amounts.parseOrThrow(a.new_threshold), - title: a.justification as TranslatedString, + title: titleForJustification(just), + metadata: just.type === "ok" ? just.body.metadata : undefined, + justification: just.type === "ok" ? just.body.justification : undefined, when: { t_ms: a.decision_time.t_s === "never" @@ -81,7 +111,8 @@ export function getEventsFromAmlHistory( } export function CaseDetails({ account }: { account: string }) { - const [selected, setSelected] = useState<AmlEvent | undefined>(undefined); + const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now()); + const [showForm, setShowForm] = useState<{ justification: Justification, metadata: FormMetadata }>() const { i18n } = useTranslationContext(); const details = useCaseDetails(account) @@ -103,6 +134,25 @@ export function CaseDetails({ account }: { account: string }) { const events = getEventsFromAmlHistory(aml_history, kyc_attributes); + if (showForm !== undefined) { + return <NiceForm + 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> + + </NiceForm> + } return ( <div> <a @@ -117,116 +167,138 @@ export function CaseDetails({ account }: { account: string }) { <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 + Case history for account <span title={account}>{account.substring(0, 16)}...</span> </i18n.Translate> </h1> </header> - <div class="flow-root"> - <ul role="list"> - {events.map((e, idx) => { - const isLast = events.length - 1 === idx; - return ( - <li - class="hover:bg-gray-200 p-2 rounded cursor-pointer" - onClick={() => { - setSelected(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": { - switch (e.state) { - case AmlExchangeBackend.AmlState.normal: { - return ( - <div> - <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20"> - Normal - </span> - <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 AmlExchangeBackend.AmlState.pending: { - return ( - <div> - <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20"> - Pending - </span> - <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 AmlExchangeBackend.AmlState.frozen: { - return ( - <div> - <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20"> - Frozen - </span> - <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> - - } - } - })()} - <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> - <div> - <p class="text-sm text-gray-900">{e.title}</p> - </div> - <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> - )} + <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": + } + }} /> + {/* {selected && <ShowEventDetails event={selected} />} */} + {selected && <ShowConsolidated history={events} until={selected} />} + </div> + ); +} + +function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode { + switch (state) { + case AmlExchangeBackend.AmlState.normal: { + return ( + <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20"> + Normal + </span> + ); + } + case AmlExchangeBackend.AmlState.pending: { + return ( + <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20"> + Pending + </span> + ); + } + case AmlExchangeBackend.AmlState.frozen: { + return ( + <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20"> + Frozen + </span> + ); + } + } + 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> - </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" + ) : ( + <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}> + {format(e.when.t_ms, "dd MMM yyyy")} + </time> + )} </div> </div> - </li> - ); - })} - </ul> - </div> - {selected && <ShowEventDetails event={selected} />} - {selected && <ShowConsolidated history={events} until={selected.when} />} - </div> - ); + </div> + </div> + </li> + ); + })} + </ul> + </div> + } function ShowEventDetails({ event }: { event: AmlEvent }): VNode { |