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 | |
parent | 7ed3e78f790837479fc2bb2eb6ddc40c78ce59b5 (diff) | |
download | wallet-core-6138846050563e0dca95b0b6d792776925e4c35f.tar.xz |
new forms api
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages')
8 files changed, 633 insertions, 425 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx index a14966cc0..0b055f682 100644 --- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx @@ -30,65 +30,75 @@ export default { export const SimpleComment = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 0, + formId: "simple_comment", onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); + export const Identification = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 1, + formId: "902.1e", onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); + export const OperationalLegalEntity = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 2, + formId: "902.11e", + onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); export const Foundations = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 3, + formId: "902.12e", + onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); export const DelcarationOfTrusts = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 4, + formId: "902.13e", + onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); + export const InformationOnLifeInsurance = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 5, + formId: "902.15e", + onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); export const DeclarationOfBeneficialOwner = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 6, + formId: "902.9e", + onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); export const CustomerProfile = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 7, + formId: "902.5e", + onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); export const RiskProfile = tests.createExample(TestedComponent, { account: "the_account", - selectedForm: 8, + formId: "902.4e", + onSubmit: async (justification, newState, newThreshold) => { - alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2)) } }); diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx index faf9671bb..d1fb3b895 100644 --- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx @@ -1,5 +1,5 @@ -import { AbsoluteTime, AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { AbsoluteTime, AmountJson, Amounts, Codec, OperationResult, buildCodecForObject, codecForNumber, codecForString, codecOptional } from "@gnu-taler/taler-util"; +import { FlexibleForm, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { NiceForm } from "../NiceForm.js"; import { v1 as form_902_11e_v1 } from "../forms/902_11e.js"; @@ -10,29 +10,21 @@ import { v1 as form_902_1e_v1 } from "../forms/902_1e.js"; import { v1 as form_902_4e_v1 } from "../forms/902_4e.js"; import { v1 as form_902_5e_v1 } from "../forms/902_5e.js"; import { v1 as form_902_9e_v1 } from "../forms/902_9e.js"; -import { v1 as simplest } from "../forms/simplest.js"; +import { Simplest, v1 as simplest } from "../forms/simplest.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; import { useExchangeApiContext } from "../context/config.js"; -export type Justification = { - // form index in the list of forms - index: number; - // form name - name: string; - // form values - value: any; -} - -export function AntiMoneyLaunderingForm({ account, selectedForm, onSubmit }: { account: string, selectedForm: number, onSubmit: (justification: Justification, state: AmlExchangeBackend.AmlState, threshold: AmountJson) => Promise<void>; }) { +export function AntiMoneyLaunderingForm({ account, formId, onSubmit }: { account: string, formId: string, onSubmit: (justification: Justification, state: AmlExchangeBackend.AmlState, threshold: AmountJson) => Promise<void>; }) { const { i18n } = useTranslationContext() - const showingFrom = allForms[selectedForm].impl; - const formName = allForms[selectedForm].name + const theForm = allForms.find((v) => v.id === formId) + if (!theForm) { + return <div>form with id {formId} not found</div> + } const { config } = useExchangeApiContext() const initial = { - fullName: "loggedIn_user_fullname", when: AbsoluteTime.now(), state: AmlExchangeBackend.AmlState.pending, threshold: Amounts.zeroOfCurrency(config.currency), @@ -40,16 +32,17 @@ export function AntiMoneyLaunderingForm({ account, selectedForm, onSubmit }: { a return ( <NiceForm initial={initial} - form={showingFrom(initial)} + form={theForm.impl(initial)} onUpdate={() => { }} onSubmit={(formValue) => { if (formValue.state === undefined || formValue.threshold === undefined) return; const st = formValue.state; const amount = formValue.threshold; - const justification = { - index: selectedForm, - name: formName, + const justification: Justification = { + id: theForm.id, + label: theForm.label, + version: theForm.version, value: formValue } @@ -75,7 +68,7 @@ export function AntiMoneyLaunderingForm({ account, selectedForm, onSubmit }: { a ); } -export interface State { +export interface BaseForm { state: AmlExchangeBackend.AmlState; threshold: AmountJson; } @@ -85,49 +78,146 @@ const DocumentDuplicateIcon = <svg xmlns="http://www.w3.org/2000/svg" fill="none </svg> -export const allForms = [ +export type FormMetadata = { + label: string, + id: string, + version: number, + icon: h.JSX.Element, + impl: (current: BaseForm) => FlexibleForm<BaseForm> +} + +export type Justification<T = any> = { + // form values + value: T; +} & Omit<Omit<FormMetadata, "icon">, "impl"> + +export function stringifyJustification(j: Justification): string { + return JSON.stringify(j) +} + + +type SimpleFormMetadata = { + version?: number, + id?: string, +} + +export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> => + buildCodecForObject<SimpleFormMetadata>() + .property("id", codecOptional(codecForString())) + .property("version", codecOptional(codecForNumber())) + .build("SimpleFormMetadata"); + +type ParseJustificationFail = + "not-json" | + "id-not-found" | + "form-not-found" | + "version-not-found"; + +export function parseJustification(s: string, listOfAllKnownForms: FormMetadata[]): OperationResult<{ justification: Justification, metadata: FormMetadata }, ParseJustificationFail> { + try { + const justification = JSON.parse(s) + const info = codecForSimpleFormMetadata().decode(justification) + if (!info.id) { + return { + type: "fail", + case: "id-not-found", + detail: {} as any + } + } + if (!info.version) { + return { + type: "fail", + case: "version-not-found", + detail: {} as any + } + } + const found = listOfAllKnownForms.find((f) => { + return f.id === info.id && f.version === info.version + }) + if (!found) { + return { + type: "fail", + case: "form-not-found", + detail: {} as any + } + } + return { + type: "ok", + body: { + justification, metadata: found + } + } + } catch (e) { + return { + type: "fail", + case: "not-json", + detail: {} as any + } + } + +} + +export const allForms: Array<FormMetadata> = [ { - name: "Simple comment", + label: "Simple comment", + id: "simple_comment", + version: 1, icon: DocumentDuplicateIcon, impl: simplest, }, { - name: "Identification form (902.1e)", + label: "Identification form", + id: "902.1e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_1e_v1, }, { - name: "Operational legal entity or partnership (902.11e)", + label: "Operational legal entity or partnership", + id: "902.11e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_11e_v1, }, { - name: "Foundations (902.12e)", + label: "Foundations", + id: "902.12e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_12e_v1, }, { - name: "Declaration for trusts (902.13e)", + label: "Declaration for trusts", + id: "902.13e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_13e_v1, }, { - name: "Information on life insurance policies (902.15e)", + label: "Information on life insurance policies", + id: "902.15e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_15e_v1, }, { - name: "Declaration of beneficial owner (902.9e)", + label: "Declaration of beneficial owner", + id: "902.9e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_9e_v1, }, { - name: "Customer profile (902.5e)", + label: "Customer profile", + id: "902.5e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_5e_v1, }, { - name: "Risk profile (902.4e)", + label: "Risk profile", + id: "902.4e", + version: 1, icon: DocumentDuplicateIcon, impl: form_902_4e_v1, }, 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 { diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx new file mode 100644 index 000000000..0355d5a31 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx @@ -0,0 +1,42 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { AmlExchangeBackend } from "../types.js"; +import { + CasesUI as TestedComponent, +} from "./Cases.js"; +import { AmountString } from "@gnu-taler/taler-util"; + +export default { + title: "cases", +}; + +export const OneRow = tests.createExample(TestedComponent, { + filter: AmlExchangeBackend.AmlState.normal, + onChangeFilter: () => null, + records: [{ + current_state: AmlExchangeBackend.AmlState.normal, + h_payto: "QWEQWEQWEQWE", + rowid: 1, + threshold: "USD:1" as AmountString + }] +}); diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 64cacf68c..32e162e5b 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -1,4 +1,4 @@ -import { TalerError, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; +import { TalerError, TalerExchangeApi, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -10,13 +10,152 @@ import { AmlExchangeBackend } from "../types.js"; import { Officer } from "./Officer.js"; import { amlStateConverter } from "./ShowConsolidated.js"; -export function Cases() { +export function CasesUI({ records, filter, onChangeFilter, onFirstPage, onNext }: { onFirstPage?: () => void, onNext?: () => void, filter: AmlExchangeBackend.AmlState, onChangeFilter: (f: AmlExchangeBackend.AmlState) => void, records: TalerExchangeApi.AmlRecord[] }): VNode { const { i18n } = useTranslationContext(); const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>(); - const initial = AmlExchangeBackend.AmlState.pending; - const [stateFilter, setStateFilter] = useState(initial); + return <div> + <div class="sm:flex sm:items-center"> + <div class="px-2 sm:flex-auto"> + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate> + Cases + </i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> + A list of all the account with the status + </i18n.Translate> + </p> + </div> + <div class="px-2"> + <form.Provider + initialValue={{ state: filter }} + onUpdate={(v) => { + onChangeFilter(v.state ?? filter); + }} + onSubmit={(v) => { }} + > + <form.InputChoiceHorizontal + name="state" + label={i18n.str`Filter`} + converter={amlStateConverter} + choices={[ + { + label: "Pending" as TranslatedString, + value: AmlExchangeBackend.AmlState.pending, + }, + { + label: "Frozen" as TranslatedString, + value: AmlExchangeBackend.AmlState.frozen, + }, + { + label: "Normal" as TranslatedString, + value: AmlExchangeBackend.AmlState.normal, + }, + ]} + /> + + </form.Provider> + </div> + </div> + <div class="mt-8 flow-root"> + <div class="overflow-x-auto"> + {!records.length ? ( + <div>empty result </div> + ) : ( + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + <table class="min-w-full divide-y divide-gray-300"> + <thead> + <tr> + <th + scope="col" + class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate> + Account Id + </i18n.Translate> + </th> + <th + scope="col" + class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate> + Status + </i18n.Translate> + </th> + <th + scope="col" + class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate> + Threshold + </i18n.Translate> + </th> + </tr> + </thead> + <tbody class="divide-y divide-gray-200 bg-white"> + {records.map((r) => { + return ( + <tr class="hover:bg-gray-100 "> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> + <div class="text-gray-900"> + <a + href={Pages.account.url({ account: r.h_payto })} + class="text-indigo-600 hover:text-indigo-900" + > + {r.h_payto.substring(0, 16)}... + </a> + </div> + </td> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500"> + {((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> + ); + } + } + })(r.current_state)} + </td> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> + {r.threshold} + </td> + </tr> + ); + })} + </tbody> + </table> + <Pagination onFirstPage={onFirstPage} onNext={onNext} /> + </div> + )} + </div> + </div> + </div> + +} + + +export function Cases() { + const [stateFilter, setStateFilter] = useState(AmlExchangeBackend.AmlState.pending); const list = useCases(stateFilter); @@ -38,150 +177,26 @@ export function Cases() { const { records } = list.data.body - return ( - <div> - <div class="px-4 sm:px-6 lg:px-8"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate> - Cases - </i18n.Translate> - </h1> - <p class="mt-2 text-sm text-gray-700"> - <i18n.Translate> - A list of all the account with the status - </i18n.Translate> - </p> - </div> - <form.Provider - initialValue={{ state: stateFilter }} - onUpdate={(v) => { - setStateFilter(v.state ?? initial); - }} - onSubmit={(v) => { }} - > - <form.InputChoiceHorizontal - name="state" - label={"Filter" as TranslatedString} - converter={amlStateConverter} - choices={[ - { - label: "Pending" as TranslatedString, - value: AmlExchangeBackend.AmlState.pending, - }, - { - label: "Frozen" as TranslatedString, - value: AmlExchangeBackend.AmlState.frozen, - }, - { - label: "Normal" as TranslatedString, - value: AmlExchangeBackend.AmlState.normal, - }, - ]} - /> - </form.Provider> - </div> - <div class="mt-8 flow-root"> - <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> - {!records.length ? ( - <div>empty result </div> - ) : ( - <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> - <Pagination /> - <table class="min-w-full divide-y divide-gray-300"> - <thead> - <tr> - <th - scope="col" - class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900" - > - Account Id - </th> - <th - scope="col" - class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900" - > - Status - </th> - <th - scope="col" - class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900" - > - Threshold - </th> - </tr> - </thead> - <tbody class="divide-y divide-gray-200 bg-white"> - {records.map((r) => { - return ( - <tr class="hover:bg-gray-100 "> - <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> - <div class="text-gray-900"> - <a - href={Pages.account.url({ account: r.h_payto })} - class="text-indigo-600 hover:text-indigo-900" - > - {r.h_payto} - </a> - </div> - </td> - <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500"> - {((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> - ); - } - } - })(r.current_state)} - </td> - <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> - {r.threshold} - </td> - </tr> - ); - })} - </tbody> - </table> - <Pagination /> - </div> - )} - </div> - </div> - </div> - </div> - ); + return <CasesUI + records={records} + onFirstPage={list.pagination && !list.pagination.isFirstPage ? list.pagination.reset : undefined} + onNext={list.pagination && !list.pagination.isLastPage ? list.pagination.loadMore : undefined} + filter={stateFilter} + onChangeFilter={setStateFilter} + /> } export const PeopleIcon = () => <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="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /> + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /> </svg> export const HomeIcon = () => <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="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> + <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> </svg> export const ChevronRightIcon = () => <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="M8.25 4.5l7.5 7.5-7.5 7.5" /> + <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /> </svg> @@ -190,92 +205,27 @@ export const ArrowRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill </svg> -function Pagination() { +function Pagination({ onFirstPage, onNext }: { onFirstPage?: () => void, onNext?: () => void, }) { + const { i18n } = useTranslationContext() return ( - <nav class="flex items-center justify-between px-4 sm:px-0"> - <div class="-mt-px flex w-0 flex-1"> - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent pr-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" - > - <svg - class="mr-3 h-5 w-5 text-gray-400" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - > - <path - fill-rule="evenodd" - d="M18 10a.75.75 0 01-.75.75H4.66l2.1 1.95a.75.75 0 11-1.02 1.1l-3.5-3.25a.75.75 0 010-1.1l3.5-3.25a.75.75 0 111.02 1.1l-2.1 1.95h12.59A.75.75 0 0118 10z" - clip-rule="evenodd" - /> - </svg> - Previous - </a> - </div> - <div class="hidden md:-mt-px md:flex"> - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" + <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination"> + <div class="flex flex-1 justify-between sm:justify-end"> + <button + class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onFirstPage} + onClick={onFirstPage} > - 1 - </a> - {/* <!-- Current: "border-indigo-500 text-indigo-600", Default: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" --> */} - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500" - aria-current="page" + <i18n.Translate>First page</i18n.Translate> + </button> + <button + class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onNext} + onClick={onNext} > - 2 - </a> - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" - > - 3 - </a> - <span class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500"> - ... - </span> - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" - > - 8 - </a> - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" - > - 9 - </a> - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" - > - 10 - </a> - </div> - <div class="-mt-px flex w-0 flex-1 justify-end"> - <a - href="#" - class="inline-flex items-center border-t-2 border-transparent pl-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" - > - Next - <svg - class="ml-3 h-5 w-5 text-gray-400" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - > - <path - fill-rule="evenodd" - d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" - clip-rule="evenodd" - /> - </svg> - </a> + <i18n.Translate>Next</i18n.Translate> + </button> </div> </nav> - ); + + ) } diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx index 95b1f35c4..214c17648 100644 --- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx +++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -1,13 +1,11 @@ +import { Amounts, TalerExchangeApi, TalerProtocolTimestamp, TranslatedString } from "@gnu-taler/taler-util"; +import { LocalNotificationBanner, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { AntiMoneyLaunderingForm, allForms } from "./AntiMoneyLaunderingForm.js"; -import { Pages } from "../pages.js"; -import { NiceForm } from "../NiceForm.js"; -import { AbsoluteTime, Amounts, TalerExchangeApi, TalerProtocolTimestamp, TranslatedString } from "@gnu-taler/taler-util"; -import { AmlExchangeBackend } from "../types.js"; +import { useExchangeApiContext } from "../context/config.js"; import { useOfficer } from "../hooks/useOfficer.js"; +import { Pages } from "../pages.js"; +import { AntiMoneyLaunderingForm, allForms } from "./AntiMoneyLaunderingForm.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -import { useExchangeApiContext } from "../context/config.js"; -import { LocalNotificationBanner, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; export function NewFormEntry({ account, @@ -31,19 +29,13 @@ export function NewFormEntry({ return <HandleAccountNotReady officer={officer} />; } - const selectedForm = Number.parseInt(type ?? "0", 10); - if (Number.isNaN(selectedForm)) { - return <div>WHAT! {type}</div>; - } - - return ( <Fragment> <LocalNotificationBanner notification={notification} /> <AntiMoneyLaunderingForm account={account} - selectedForm={selectedForm} + formId={type} onSubmit={async (justification, new_state, new_threshold) => { const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> = { @@ -56,27 +48,29 @@ export function NewFormEntry({ } await handleError(async () => { const resp = await api.addDecisionDetails(officer.account, decision); - if (resp.type === "fail") { - switch (resp.case) { - case "unauthorized": return notify({ - type: "error", - title: i18n.str`Wrong credentials for "${officer.account}"`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "officer-or-account-not-found": return notify({ - type: "error", - title: i18n.str`Officer or account not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "officer-disabled-or-recent-decision": return notify({ - type: "error", - title: i18n.str`Officer disabled or more recent decision was already submitted.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - } + if (resp.type === "ok") { + window.location.href = Pages.cases.url; + return; + } + switch (resp.case) { + case "unauthorized": return notify({ + type: "error", + title: i18n.str`Wrong credentials for "${officer.account}"`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "officer-or-account-not-found": return notify({ + type: "error", + title: i18n.str`Officer or account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "officer-disabled-or-recent-decision": return notify({ + type: "error", + title: i18n.str`Officer disabled or more recent decision was already submitted.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) } }) }} @@ -92,10 +86,10 @@ function SelectForm({ account }: { account: string }) { {allForms.map((form, idx) => { return ( <a - href={Pages.newFormEntry.url({ account, type: String(idx) })} + href={Pages.newFormEntry.url({ account, type: form.id })} class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600" > - {form.name} + {form.label} </a> ); })} diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx index 1a86e8e98..dc073a5f5 100644 --- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx @@ -32,25 +32,74 @@ export default { }; export const WithEmptyHistory = tests.createExample(TestedComponent, { - history: getEventsFromAmlHistory([],[]), + history: getEventsFromAmlHistory([], []), until: AbsoluteTime.now() }); export const WithSomeEvents = tests.createExample(TestedComponent, { - history: getEventsFromAmlHistory([{ - decider_pub: "123", - decision_time: { t_s: 1 }, - justification: "yes", - new_state: 1, - new_threshold: "USD:10", - }],[{ + history: getEventsFromAmlHistory([ + { + "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG", + "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}", + "new_threshold": "STATER:0", + "new_state": 1, + "decision_time": { + "t_s": 1700208199 + } + }, + { + "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG", + "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}", + "new_threshold": "STATER:0", + "new_state": 1, + "decision_time": { + "t_s": 1700208211 + } + }, + { + "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG", + "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}", + "new_threshold": "STATER:0", + "new_state": 1, + "decision_time": { + "t_s": 1700208220 + } + }, + { + "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG", + "justification": "{\"index\":4,\"name\":\"Declaration for trusts (902.13e)\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700208362854},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"contractingPartner\":\"f\",\"knownAs\":\"a\",\"trust\":{\"name\":\"b\",\"type\":\"discretionary\",\"revocability\":\"irrevocable\"}}}", + "new_threshold": "STATER:0", + "new_state": 1, + "decision_time": { + "t_s": 1700208385 + } + }, + { + "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG", + "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488420810},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"qwe\"}}", + "new_threshold": "STATER:0", + "new_state": 1, + "decision_time": { + "t_s": 1700488423 + } + }, + { + "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG", + "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488671251},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"asd asd asd \"}}", + "new_threshold": "STATER:0", + "new_state": 1, + "decision_time": { + "t_s": 1700488677 + } + } + ], [{ collection_time: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromPrettyString("1d")) ), - expiration_time: { t_s: "never"}, + expiration_time: { t_s: "never" }, provider_section: "asd", attributes: { - email: "sebasjm@qwe.com" + email: "sebasjm@qwdde.com" } }]), until: AbsoluteTime.now() diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts index e31e13a28..afe73227a 100644 --- a/packages/aml-backoffice-ui/src/pages/index.stories.ts +++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts @@ -1,2 +1,3 @@ export * as a1 from "./ShowConsolidated.stories.js"; export * as a2 from "./AntiMoneyLaunderingForm.stories.js"; +export * as a3 from "./Cases.stories.js"; |