aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages/CaseDetails.tsx')
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx288
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 {