diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx')
-rw-r--r-- | packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx b/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx new file mode 100644 index 000000000..8b9b01ae6 --- /dev/null +++ b/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx @@ -0,0 +1,457 @@ +import { Fragment, VNode, h } from "preact"; +import { + AmlDecisionDetail, + AmlDecisionDetails, + AmlState, + KycDetail, +} from "../types.js"; +import { + AbsoluteTime, + AmountJson, + Amounts, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid"; +import { useState } from "preact/hooks"; +import { NiceForm } from "../NiceForm.js"; +import { FlexibleForm } from "../forms/index.js"; +import { UIFormField } from "../handlers/forms.js"; +import { Pages } from "../pages.js"; + +const response: AmlDecisionDetails = { + aml_history: [ + { + justification: "Lack of documentation", + decider_pub: "ASDASDASD", + decision_time: { + t_s: Date.now() / 1000, + }, + new_state: 2, + new_threshold: "USD:0", + }, + { + justification: "Doing a transfer of high amount", + decider_pub: "ASDASDASD", + decision_time: { + t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6, + }, + new_state: 1, + new_threshold: "USD:2000", + }, + { + justification: "Account is known to the system", + decider_pub: "ASDASDASD", + decision_time: { + t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9, + }, + new_state: 0, + new_threshold: "USD:100", + }, + ], + kyc_attributes: [ + { + collection_time: { + t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8, + }, + expiration_time: { + t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4, + }, + provider_section: "asdasd", + attributes: { + name: "Sebastian", + }, + }, + { + collection_time: { + t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5, + }, + expiration_time: { + t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2, + }, + provider_section: "asdasd", + attributes: { + creditCard: "12312312312", + }, + }, + ], +}; +type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; +type AmlFormEvent = { + type: "aml-form"; + when: AbsoluteTime; + title: TranslatedString; + state: AmlState; + threshold: AmountJson; +}; +type KycCollectionEvent = { + type: "kyc-collection"; + when: AbsoluteTime; + title: TranslatedString; + values: object; + provider: string; +}; +type KycExpirationEvent = { + type: "kyc-expiration"; + when: AbsoluteTime; + title: TranslatedString; + fields: string[]; +}; + +type WithTime = { when: AbsoluteTime }; + +function selectSooner(a: WithTime, b: WithTime) { + return AbsoluteTime.cmp(a.when, b.when); +} + +function getEventsFromAmlHistory( + aml: AmlDecisionDetail[], + kyc: KycDetail[], +): AmlEvent[] { + const ae: AmlEvent[] = aml.map((a) => { + return { + type: "aml-form", + state: a.new_state, + threshold: Amounts.parseOrThrow(a.new_threshold), + title: a.justification as TranslatedString, + when: { + t_ms: + a.decision_time.t_s === "never" + ? "never" + : a.decision_time.t_s * 1000, + }, + } as AmlEvent; + }); + const ke = kyc.reduce((prev, k) => { + prev.push({ + type: "kyc-collection", + title: "collection" as TranslatedString, + when: { + t_ms: + k.collection_time.t_s === "never" + ? "never" + : k.collection_time.t_s * 1000, + }, + values: !k.attributes ? {} : k.attributes, + provider: k.provider_section, + }); + prev.push({ + type: "kyc-expiration", + title: "expired" as TranslatedString, + when: { + t_ms: + k.expiration_time.t_s === "never" + ? "never" + : k.expiration_time.t_s * 1000, + }, + fields: !k.attributes ? [] : Object.keys(k.attributes), + }); + return prev; + }, [] as AmlEvent[]); + return ae.concat(ke).sort(selectSooner); +} + +export function AccountDetails({ account }: { account?: string }) { + const events = getEventsFromAmlHistory( + response.aml_history, + response.kyc_attributes, + ); + console.log("DETAILS", events, events[events.length - 1 - 2]); + const [selected, setSelected] = useState<AmlEvent>( + events[events.length - 1 - 2], + ); + return ( + <div> + <a + 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 + </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 + </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 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 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 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" /> + ); + } + case "kyc-expiration": { + return <ClockIcon class="h-8 w-8 text-gray-700" />; + } + } + })()} + <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> + )} + </div> + </div> + </div> + </div> + </li> + ); + })} + </ul> + </div> + {selected && <ShowEventDetails event={selected} />} + {selected && <ShowConsolidated history={events} until={selected} />} + </div> + ); +} + +function ShowEventDetails({ event }: { event: AmlEvent }): VNode { + return <div>type {event.type}</div>; +} + +function ShowConsolidated({ + history, + until, +}: { + history: AmlEvent[]; + until: AmlEvent; +}): VNode { + console.log("UNTIL", until); + const cons = getConsolidated(history, until.when); + + const form: FlexibleForm<Consolidated> = { + versionId: "1", + behavior: (form) => { + return {}; + }, + 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: AmlState.frozen, + }, + { + label: "Pending" as TranslatedString, + value: AmlState.pending, + }, + { + label: "Normal" as TranslatedString, + value: 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?: 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: { + 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.threshold = cur.threshold; + prev.aml.state = cur.state; + prev.aml.since = cur.when; + 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: AmlState | undefined): string { + if (s === undefined) return ""; + switch (s) { + case AmlState.normal: + return "normal"; + case AmlState.pending: + return "pending"; + case AmlState.frozen: + return "frozen"; + } +} + +function parseAmlState(s: string | undefined): AmlState { + switch (s) { + case "normal": + return AmlState.normal; + case "pending": + return AmlState.pending; + case "frozen": + return AmlState.frozen; + default: + throw Error(`unknown AML state: ${s}`); + } +} |