aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx173
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx25
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx68
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx3
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx3
-rw-r--r--packages/web-util/src/forms/forms.ts2
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);
}),
);
}