diff options
Diffstat (limited to 'packages')
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 173 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 25 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 68 | ||||
-rw-r--r-- | packages/bank-ui/src/pages/PaytoWireTransferForm.tsx | 3 | ||||
-rw-r--r-- | packages/bank-ui/src/pages/WalletWithdrawForm.tsx | 3 | ||||
-rw-r--r-- | packages/web-util/src/forms/forms.ts | 2 |
6 files changed, 205 insertions, 69 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index d42e1f2c6..b26e6f430 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -32,22 +32,25 @@ import { codecOptional, } from "@gnu-taler/taler-util"; import { + Attention, DefaultForm, - ErrorLoading, FormMetadata, InternationalizationAPI, Loading, - useTranslationContext, + ShowInputErrorLabel, + Time, + useExchangeApiContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { VNode, h } from "preact"; +import { format, formatDuration, intervalToDuration } from "date-fns"; +import { Fragment, Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { privatePages } from "../Routing.js"; +import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { useUiFormsContext } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; import { useAccountInformation } from "../hooks/account.js"; +import { useAccountDecisions } from "../hooks/decisions.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; -import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; export type AmlEvent = | AmlFormEvent @@ -175,10 +178,12 @@ export function CaseDetails({ account }: { account: string }) { const { i18n } = useTranslationContext(); const details = useAccountInformation(account); + const history = useAccountDecisions(account); + const { forms } = useUiFormsContext(); const allForms = [...forms, ...preloadedForms(i18n)]; - if (!details) { + if (!details || !history) { return <Loading />; } if (details instanceof TalerError) { @@ -195,8 +200,22 @@ export function CaseDetails({ account }: { account: string }) { assertUnreachable(details); } } + if (history instanceof TalerError) { + return <ErrorLoadingWithDebug error={history} />; + } + if (history.type === "fail") { + switch (history.case) { + // case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + case HttpStatusCode.Conflict: + return <div />; + default: + assertUnreachable(history); + } + } const { details: accountDetails } = details.body; - + const activeDesicion = history.body.find(d => d.is_active) const events = getEventsFromAmlHistory( accountDetails, @@ -227,10 +246,25 @@ export function CaseDetails({ account }: { account: string }) { return ( <div> <a - href={privatePages.caseNew.url({ cid: account })} + // href={privatePages.caseNew.url({ cid: account })} + href="#" + 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" + > + <i18n.Translate>Freeze account</i18n.Translate> + </a> + <a + // href={privatePages.caseNew.url({ cid: account })} + href="#" + 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" + > + <i18n.Translate>New threshold</i18n.Translate> + </a> + <a + // href={privatePages.caseNew.url({ cid: account })} + href="#" 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" > - <i18n.Translate>New AML form</i18n.Translate> + <i18n.Translate>Ask more information</i18n.Translate> </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"> @@ -241,6 +275,10 @@ export function CaseDetails({ account }: { account: string }) { </i18n.Translate> </h1> </header> + {!activeDesicion ? <Attention title={i18n.str`No active decision found`} type="warning" /> : <Fragment> + {!activeDesicion.to_investigate ? undefined : <Attention title={i18n.str`Requires investigation`} type="warning" />} + <ShowActiveDecision decision={activeDesicion} /> + </Fragment>} <ShowTimeline history={events} onSelect={(e) => { @@ -265,6 +303,59 @@ export function CaseDetails({ account }: { account: string }) { ); } +function ShowActiveDecision({ decision }: { decision: TalerExchangeApi.AmlDecision }): VNode { + const { i18n } = useTranslationContext(); + const { config } = useExchangeApiContext() + + return <Fragment> + <h1 class="mt-4 text-base font-semibold leading-6 text-black"> + <i18n.Translate>Current active rules</i18n.Translate> + </h1> + + <div class="sm:col-span-5"> + <label + for="amount" + class="block text-sm font-medium leading-6 text-gray-900" + > + <b>Expiration time</b> + <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> + + <div + class="p-4 disabled:bg-gray-200 rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6" + > + + <Time format="dd/MM/yyyy HH:mm:ss" timestamp={AbsoluteTime.fromProtocolTimestamp(decision.limits.expiration_time)} /> + </div> + </div> + + + </label> + </div> + {decision.limits.rules.map(r => { + const bySpec = Amounts.stringifyValueWithSpec(Amounts.parseOrThrow(r.threshold), config.currency_specification) + return <div class="sm:col-span-5"> + <label + for="amount" + class="block text-sm font-medium leading-6 text-gray-900" + > + {r.operation_type} + <InputAmount + name="minCashout" + left + currency={bySpec.currency} + value={bySpec.normal} + onChange={undefined} + /> + </label> + <p class="mt-2 text-sm text-gray-500"> + over {r.timeframe.d_us === "forever" ? "" : formatDuration(intervalToDuration({ start: 0, end: r.timeframe.d_us / 1000 }))} + </p> + </div> + })} + + </Fragment> +} + function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode { switch (state) { case TalerExchangeApi.AmlState.normal: { @@ -392,7 +483,7 @@ function ShowTimeline({ "never" ) : ( <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}> - {format(e.when.t_ms, "dd MMM yyyy")} + {format(e.when.t_ms, "dd MMM yyyy HH:mm:ss")} </time> )} </div> @@ -407,6 +498,66 @@ function ShowTimeline({ ); } +function InputAmount( + { + currency, + name, + value, + left, + onChange, + }: { + currency: string; + name: string; + left?: boolean | undefined; + value: string | undefined; + onChange?: (s: string) => void; + }, + ref: Ref<HTMLInputElement>, +): VNode { + const FRAC_SEPARATOR = "," + const { config } = useExchangeApiContext(); + return ( + <div class="mt-2"> + <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> + <div class="pointer-events-none inset-y-0 flex items-center px-3"> + <span class="text-gray-500 sm:text-sm">{currency}</span> + </div> + <input + type="number" + data-left={left} + class="disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6" + placeholder="0.00" + aria-describedby="price-currency" + ref={ref} + name={name} + id={name} + autocomplete="off" + value={value ?? ""} + disabled={!onChange} + onInput={(e) => { + if (!onChange) return; + const l = e.currentTarget.value.length; + const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR); + if ( + sep_pos !== -1 && + l - sep_pos - 1 > + config.currency_specification.num_fractional_input_digits + ) { + e.currentTarget.value = e.currentTarget.value.substring( + 0, + sep_pos + + config.currency_specification.num_fractional_input_digits + + 1, + ); + } + onChange(e.currentTarget.value); + }} + /> + </div> + </div> + ); +} + export type Justification<T = Record<string, unknown>> = { // form values value: T; diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index e468d80ad..9a569cd60 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -148,12 +148,6 @@ export function CasesUI({ > <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 w-40" - > - <i18n.Translate>Threshold</i18n.Translate> - </th> </tr> </thead> <tbody class="divide-y divide-gray-200 bg-white"> @@ -172,11 +166,8 @@ export function CasesUI({ </a> </div> </td> - <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500"> - {r.rowid} - </td> <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> - ??? + {r.to_investigate ? <ToInvestigateIcon /> : undefined} </td> </tr> ); @@ -191,6 +182,12 @@ export function CasesUI({ </div> ); } +function ToInvestigateIcon(): VNode { + return <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /> + </svg> +} + export function Cases() { // const [stateFilter, setStateFilter] = useState( @@ -257,10 +254,10 @@ export function Cases() { records={list.body} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} - // filter={stateFilter} - // onChangeFilter={(d) => { - // setStateFilter(d); - // }} + // filter={stateFilter} + // onChangeFilter={(d) => { + // setStateFilter(d); + // }} /> ); } diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx index 2fbbefe0c..cea8157b0 100644 --- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -30,6 +30,26 @@ import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { AmlEvent } from "./CaseDetails.js"; +/** + * the exchange doesn't hava a consistent api + * https://bugs.gnunet.org/view.php?id=9142 + * + * @param data + * @returns + */ +function fixProvidedInfo(data: object): object { + return Object.entries(data).reduce((prev, [key,value]) => { + prev[key] = value + if (typeof value === "object" && value["value"]) { + const v = value["value"] + if (typeof v === "object" && v["text"]) { + prev[key].value = v["text"] + } + } + return prev + }, {} as any) +} + export function ShowConsolidated({ history, until, @@ -41,54 +61,24 @@ export function ShowConsolidated({ const cons = getConsolidated(history, until); + const fixed = fixProvidedInfo(cons.kyc); + console.log("fixed", fixed) const form: FormConfiguration = { type: "double-column", design: [ - { - title: i18n.str`AML`, - fields: [ - { - type: "amount", - id: ".aml.threshold" as UIHandlerId, - currency: "NETZBON", - label: i18n.str`Threshold`, - name: "aml.threshold", - }, - { - type: "choiceHorizontal", - label: i18n.str`State`, - name: "aml.state", - id: ".aml.state" as UIHandlerId, - choices: [ - { - label: i18n.str`Frozen`, - value: "frozen", - }, - { - label: i18n.str`Pending`, - value: "pending", - }, - { - label: i18n.str`Normal`, - value: "normal", - }, - ], - }, - ], - }, - Object.entries(cons.kyc).length > 0 + Object.entries(fixed).length > 0 ? { - title: i18n.str`KYC`, - fields: Object.entries(cons.kyc).map(([key, field]) => { + title: i18n.str`KYC collected info`, + fields: Object.entries(fixed).map(([key, field]) => { const result: UIFormElementConfig = { type: "text", label: key as TranslatedString, id: `kyc.${key}.value` as UIHandlerId, name: `kyc.${key}.value`, - help: `${field.provider} since ${ + help: `At ${ field.since.t_ms === "never" ? "never" - : format(field.since.t_ms, "dd/MM/yyyy") + : format(field.since.t_ms, "dd/MM/yyyy HH:mm:ss") }` as TranslatedString, }; return result; @@ -99,12 +89,12 @@ export function ShowConsolidated({ }; return ( <Fragment> - <h1 class="text-base font-semibold leading-7 text-black"> + {/* <h1 class="text-base font-semibold leading-7 text-black"> Consolidated information{" "} {until.t_ms === "never" ? "" : `after ${format(until.t_ms, "dd MMMM yyyy")}`} - </h1> + </h1> */} <DefaultForm key={`${String(Date.now())}`} form={form as any} diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx index 0fb8c0ac1..852cfe816 100644 --- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -725,11 +725,9 @@ export function InputAmount( currency, name, value, - error, left, onChange, }: { - error?: string; currency: string; name: string; left?: boolean | undefined; @@ -777,7 +775,6 @@ export function InputAmount( }} /> </div> - <ShowInputErrorLabel message={error} isDirty={value !== undefined} /> </div> ); } diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx index 3a7b30a09..908412f2b 100644 --- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx @@ -29,6 +29,7 @@ import { Attention, LocalNotificationBanner, notifyError, + ShowInputErrorLabel, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -283,10 +284,10 @@ function OldWithdrawalForm({ onChange={(v) => { setAmountStr(v); }} - error={errors?.amount} ref={focus ? doAutoFocus : undefined} /> </div> + <ShowInputErrorLabel message={errors?.amount} isDirty={amountStr !== undefined} /> </div> <p class="mt-2 text-sm text-gray-500"> <i18n.Translate> diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts index 4c5050830..fc9a85f29 100644 --- a/packages/web-util/src/forms/forms.ts +++ b/packages/web-util/src/forms/forms.ts @@ -117,7 +117,7 @@ export function RenderAllFieldsByUiConfig({ const Component = UIFormConfiguration[ field.type ] as FieldComponentFunction<any>; - return Component(field.properties); + return Component(field); }), ); } |