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.tsx447
1 files changed, 447 insertions, 0 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
new file mode 100644
index 000000000..e5fb8eaba
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -0,0 +1,447 @@
+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: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
+ values: !k.attributes ? {} : k.attributes,
+ provider: k.provider_section,
+ });
+ prev.push({
+ type: "kyc-expiration",
+ title: "expired" as TranslatedString,
+ when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
+ fields: !k.attributes ? [] : Object.keys(k.attributes),
+ });
+ return prev;
+ }, [] as AmlEvent[]);
+ return ae.concat(ke).sort(selectSooner);
+}
+
+export function CaseDetails({ 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}`);
+ }
+}