aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-11-20 12:38:16 -0300
committerSebastian <sebasjm@gmail.com>2023-11-20 12:38:16 -0300
commit6138846050563e0dca95b0b6d792776925e4c35f (patch)
treeb33cd36acf4b38d3a016506d4f7fa681c83beb63
parent7ed3e78f790837479fc2bb2eb6ddc40c78ce59b5 (diff)
downloadwallet-core-6138846050563e0dca95b0b6d792776925e4c35f.tar.xz
new forms api
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx22
-rw-r--r--packages/aml-backoffice-ui/src/Dashboard.tsx27
-rw-r--r--packages/aml-backoffice-ui/src/NiceForm.tsx3
-rw-r--r--packages/aml-backoffice-ui/src/context/config.ts8
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_11e.ts18
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_12e.ts20
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_13e.ts32
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_15e.ts22
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_1e.ts49
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_4e.ts69
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_5e.ts27
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_9e.ts22
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts23
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Calendar.tsx116
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Dialog.tsx15
-rw-r--r--packages/aml-backoffice-ui/src/handlers/FormProvider.tsx25
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputArray.tsx35
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx1
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx1
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputDate.tsx81
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputFile.tsx88
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputLine.tsx36
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx14
-rw-r--r--packages/aml-backoffice-ui/src/handlers/useField.ts5
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts25
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx46
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx152
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx288
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx42
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx392
-rw-r--r--packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx68
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx69
-rw-r--r--packages/aml-backoffice-ui/src/pages/index.stories.ts1
-rw-r--r--packages/aml-backoffice-ui/src/route.ts24
-rw-r--r--packages/aml-backoffice-ui/src/stories.test.ts21
-rw-r--r--packages/aml-backoffice-ui/src/stories.tsx26
-rw-r--r--packages/web-util/src/forms/DefaultForm.tsx1
-rw-r--r--packages/web-util/src/stories.tsx26
38 files changed, 1137 insertions, 803 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
index 52c86c273..d461934c0 100644
--- a/packages/aml-backoffice-ui/src/App.tsx
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -4,7 +4,7 @@ import { ExchangeAmlFrame } from "./Dashboard.js";
import "./scss/main.css";
import { ExchangeApiProvider } from "./context/config.js";
import { getInitialBackendBaseURL } from "./hooks/useBackend.js";
-import { Router } from "./route.js";
+import { HashPathProvider, Router } from "./route.js";
import { Pages } from "./pages.js";
const pageList = Object.values(Pages);
@@ -15,15 +15,17 @@ export function App(): VNode {
return (
<TranslationProvider source={{}}>
<ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}>
- <ExchangeAmlFrame>
- <Router
- pageList={pageList}
- onNotFound={() => {
- window.location.href = Pages.cases.url
- return <div>not found</div>;
- }}
- />
- </ExchangeAmlFrame>
+ <HashPathProvider>
+ <ExchangeAmlFrame>
+ <Router
+ pageList={pageList}
+ onNotFound={() => {
+ window.location.href = Pages.cases.url
+ return <div>not found</div>;
+ }}
+ />
+ </ExchangeAmlFrame>
+ </HashPathProvider>
</ExchangeApiProvider>
</TranslationProvider>
);
diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx
index b813f83d5..2d75de660 100644
--- a/packages/aml-backoffice-ui/src/Dashboard.tsx
+++ b/packages/aml-backoffice-ui/src/Dashboard.tsx
@@ -5,7 +5,7 @@ import { useEffect, useErrorBoundary } from "preact/hooks";
import { useOfficer } from "./hooks/useOfficer.js";
import { getAllBooleanSettings, getLabelForSetting, useSettings } from "./hooks/useSettings.js";
import { Pages } from "./pages.js";
-import { PageEntry, useChangeLocation, useCurrentLocation } from "./route.js";
+import { PageEntry, useChangeLocation } from "./route.js";
import { uiSettings } from "./settings.js";
function classNames(...classes: string[]) {
@@ -73,8 +73,17 @@ const versionText = VERSION
* 4.- tooltip are not placed correctly: the arrow should point the question mark
* and the text area should be bigger
*
- * 5.- date field should have the calendar icon clickable so the user can select date without
- * writing text with the correct format
+ */
+
+/**
+ * check this fields
+ *
+ * Signature of Contracting partner, 902_9e
+ * Currency and amount of deposited assets, 902_5e
+ * Signature on declaration of trust, 902.13e
+ * also fundations
+ * also life insurance
+ *
*/
export function ExchangeAmlFrame({
@@ -140,17 +149,17 @@ export function ExchangeAmlFrame({
<GlobalNotificationsBanner />
- <main class="-mt-32 flex grow ">
+ <div class="-mt-32 flex grow ">
{officer.state !== "ready" ? undefined :
<Navigation />
}
<div class="flex mx-auto my-4">
- <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
+ <main class="rounded-lg bg-white px-5 py-6 shadow">
{children}
- </div>
+ </main>
</div>
- </main>
+ </div>
<Footer
testingUrl={localStorage.getItem("exchange-base-url") ?? undefined}
@@ -169,7 +178,7 @@ function Navigation(): VNode {
]
const location = useChangeLocation();
return (
- <div class="hidden sm:block w-48 min-w-min bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip">
+ <div class="hidden sm:block min-w-min bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip">
<nav class="flex flex-1 flex-col mx-4 mt-4 mb-2">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
@@ -179,7 +188,7 @@ function Navigation(): VNode {
return <li>
<a href={p.url} data-selected={location == p.url}
- class="data-[selected=true]:bg-indigo-700 data-[selected=true]:text-white text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+ class="data-[selected=true]:bg-indigo-700 pr-4 data-[selected=true]:text-white text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
{p.Icon && <p.Icon />}
<span class="hidden md:inline">
{p.name}
diff --git a/packages/aml-backoffice-ui/src/NiceForm.tsx b/packages/aml-backoffice-ui/src/NiceForm.tsx
index 4fc0ea89f..a78036a6b 100644
--- a/packages/aml-backoffice-ui/src/NiceForm.tsx
+++ b/packages/aml-backoffice-ui/src/NiceForm.tsx
@@ -10,11 +10,13 @@ export function NiceForm<T extends object>({
form,
onSubmit,
children,
+ readOnly,
}: {
children?: ComponentChildren;
initial: Partial<T>;
onSubmit?: (v: Partial<T>) => void;
form: FlexibleForm<T>;
+ readOnly?: boolean;
onUpdate?: (d: Partial<T>) => void;
}) {
return (
@@ -22,6 +24,7 @@ export function NiceForm<T extends object>({
initialValue={initial}
onUpdate={onUpdate}
onSubmit={onSubmit}
+ readOnly={readOnly}
computeFormState={form.behavior}
>
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts
index 2866717de..2df7ff40d 100644
--- a/packages/aml-backoffice-ui/src/context/config.ts
+++ b/packages/aml-backoffice-ui/src/context/config.ts
@@ -35,6 +35,14 @@ const Context = createContext<Type>(undefined as any);
export const useExchangeApiContext = (): Type => useContext(Context);
+export function ExchangeApiContextTesting({ config, children }: { config: TalerExchangeApi.ExchangeVersionResponse, children?: ComponentChildren; }): VNode {
+ return h(Context.Provider, {
+ value: { url: new URL("http://testing"), config, api: null as any },
+ children
+ }
+ )
+}
+
export type ConfigResult = undefined
| { type: "ok", config: TalerExchangeApi.ExchangeVersionResponse }
| { type: "incompatible", result: TalerExchangeApi.ExchangeVersionResponse, supported: string }
diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts
index 5507c72dc..a604b560e 100644
--- a/packages/aml-backoffice-ui/src/forms/902_11e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts
@@ -1,10 +1,10 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm } from "./index.js";
import { Simplest, resolutionSection } from "./simplest.js";
-export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_11.Form> => ({
versionId: "2023-05-15",
design: [
{
@@ -103,15 +103,6 @@ export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
],
},
},
- {
- type: "date",
- props: {
- name: "when",
- pattern: "dd/MM/yyyy",
- label: "Date" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
],
},
resolutionSection(current),
@@ -125,9 +116,6 @@ export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
v.declares !== "controlling-in-other-ways" &&
v.declares !== "managing-director",
},
- when: {
- disabled: true,
- },
};
},
});
@@ -138,7 +126,7 @@ namespace Form902_11 {
firstName: string;
address: string;
}
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
contractingPartner: string;
declares: "25-or-more" | "controlling-in-other-ways" | "managing-director";
person: Person[];
diff --git a/packages/aml-backoffice-ui/src/forms/902_12e.ts b/packages/aml-backoffice-ui/src/forms/902_12e.ts
index ea95b494b..12e885e8f 100644
--- a/packages/aml-backoffice-ui/src/forms/902_12e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_12e.ts
@@ -1,10 +1,10 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm } from "./index.js";
-import { Simplest, resolutionSection } from "./simplest.js";
+import { resolutionSection } from "./simplest.js";
-export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
versionId: "2023-05-15",
design: [
{
@@ -347,15 +347,6 @@ export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
},
},
{
- type: "date",
- props: {
- name: "when",
- pattern: "dd/MM/yyyy",
- label: "Date" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
- {
type: "text",
props: {
name: "signature",
@@ -388,9 +379,6 @@ export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
};
}),
},
- when: {
- disabled: true,
- },
};
},
});
@@ -421,7 +409,7 @@ namespace Form902_12 {
type Founder = WithRevoke<WithDeath<Person>>;
type Beneficiary = WithClaim<Person>;
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
contractingPartner: string;
knownAs: string;
boardMember: string;
diff --git a/packages/aml-backoffice-ui/src/forms/902_13e.ts b/packages/aml-backoffice-ui/src/forms/902_13e.ts
index 666cf35d4..f03364de0 100644
--- a/packages/aml-backoffice-ui/src/forms/902_13e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_13e.ts
@@ -1,10 +1,10 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm, } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm } from "./index.js";
-import { Simplest, resolutionSection } from "./simplest.js";
+import { resolutionSection } from "./simplest.js";
-export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
versionId: "2023-05-15",
design: [
{
@@ -104,7 +104,7 @@ export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
name: "dateOfBirth",
label: "Date of birth" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -120,7 +120,8 @@ export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
name: "dateOfDeath",
label: "Date of death" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
+ help: "if deceased'" as TranslatedString,
},
},
{
@@ -182,7 +183,7 @@ export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
name: "dateOfBirth",
label: "Date of birth" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -198,7 +199,8 @@ export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
name: "dateOfDeath",
label: "Date of death" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
+ help: "if deceased." as TranslatedString,
+ // help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
},
},
],
@@ -241,7 +243,7 @@ export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
name: "dateOfBirth",
label: "Date of birth" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -424,15 +426,6 @@ export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
},
},
{
- type: "date",
- props: {
- name: "when",
- label: "Date" as TranslatedString,
- pattern: "dd/MM/yyyy",
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
- {
type: "text",
props: {
name: "signature",
@@ -474,9 +467,6 @@ export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
};
}),
},
- when: {
- disabled: true,
- },
};
},
});
@@ -507,7 +497,7 @@ namespace Form902_13 {
type Founder = WithRevoke<WithDeath<Person>>;
type Beneficiary = WithClaim<Person>;
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
contractingPartner: string;
knownAs: string;
boardMember: string;
diff --git a/packages/aml-backoffice-ui/src/forms/902_15e.ts b/packages/aml-backoffice-ui/src/forms/902_15e.ts
index 502cee8e5..b9796add8 100644
--- a/packages/aml-backoffice-ui/src/forms/902_15e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_15e.ts
@@ -1,10 +1,10 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm } from "./index.js";
import { Simplest, resolutionSection } from "./simplest.js";
-export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_15.Form> => ({
versionId: "2023-05-15",
design: [
{
@@ -74,7 +74,7 @@ export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
name: "holder.dateOfBirth",
label: "Date of birth" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -115,7 +115,7 @@ export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
name: "premiumPayer.dateOfBirth",
label: "Date of birth" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -136,15 +136,6 @@ export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
},
},
{
- type: "date",
- props: {
- name: "when",
- pattern: "dd/MM/yyyy",
- label: "Date" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
- {
type: "text",
props: {
name: "signature",
@@ -166,9 +157,6 @@ export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
v: Partial<Form902_15.Form>,
): FormState<Form902_15.Form> {
return {
- when: {
- disabled: true,
- },
};
},
});
@@ -181,7 +169,7 @@ namespace Form902_15 {
nationality: string;
}
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
contractingPartner: string;
contractualRelationship: string;
insurancePolicy: string;
diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts
index c212efb1a..2cd16b840 100644
--- a/packages/aml-backoffice-ui/src/forms/902_1e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts
@@ -1,36 +1,13 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm, languageList } from "./index.js";
-import { Simplest, resolutionSection } from "./simplest.js";
+import { resolutionSection } from "./simplest.js";
-export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
versionId: "2023-05-15",
design: [
{
- title: "This form was completed by" as TranslatedString,
- description:
- "The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold." as TranslatedString,
- fields: [
- {
- type: "text",
- props: {
- name: "fullName",
- label: "Full name" as TranslatedString,
- },
- },
- {
- type: "date",
- props: {
- name: "when",
- pattern: "dd/MM/yyyy",
- label: "Date" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
- ],
- },
- {
title: "Information on customer" as TranslatedString,
description:
"The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer." as TranslatedString,
@@ -89,6 +66,7 @@ export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
name: "naturalCustomer.dateOfBirth",
label: "Date of birth" as TranslatedString,
required: true,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -244,7 +222,7 @@ export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
name: "dateOfBirth",
label: "Date of birth" as TranslatedString,
required: true,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -321,7 +299,7 @@ export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
name: "acceptance.when",
pattern: "dd/MM/yyyy",
label: "Date (conclusion of contract)" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -520,9 +498,6 @@ export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
fullName: {
disabled: true,
},
- when: {
- disabled: true,
- },
businessEstablisher: {
elements: (v.businessEstablisher ?? []).map((be) => {
return {
@@ -659,11 +634,11 @@ namespace Form902_1 {
interface BeneficialOwner {
establishment:
- | "natural-person"
- | "foundation"
- | "trust"
- | "insurance-wrapper"
- | "other";
+ | "natural-person"
+ | "foundation"
+ | "trust"
+ | "insurance-wrapper"
+ | "other";
}
interface CashTransactions {
@@ -672,7 +647,7 @@ namespace Form902_1 {
purpose: string;
}
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
fullName: string;
customerType: "natural" | "legal";
naturalCustomer: NaturalCustomer;
diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts
index 7c47a8746..041f08c98 100644
--- a/packages/aml-backoffice-ui/src/forms/902_4e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts
@@ -1,12 +1,12 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import { h as create } from "preact";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm } from "./index.js";
import { Simplest, resolutionSection } from "./simplest.js";
import { ArrowRightIcon, ChevronRightIcon } from "../pages/Cases.js";
-export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
versionId: "2023-05-15",
design: [
{
@@ -33,27 +33,6 @@ export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
],
},
{
- title: "This form was completed by" as TranslatedString,
- fields: [
- {
- type: "text",
- props: {
- label: "Full name" as TranslatedString,
- name: "fullName",
- },
- },
- {
- type: "date",
- props: {
- name: "when",
- pattern: "dd/MM/yyyy",
- label: "Date" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
- ],
- },
- {
title:
"Evaluation of politically exposed persons (PEP-Check)" as TranslatedString,
fields: [
@@ -69,8 +48,8 @@ export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
type: "choiceStacked",
props: {
label: "Foreign PEP" as TranslatedString,
- tooltip:
- "Definition see Art. 7 lit. g numeral 1 SRO Regulations" as TranslatedString,
+ // tooltip:
+ // "Definition see Art. 7 lit. g numeral 1 SRO Regulations" as TranslatedString,
help: "Is the customer, the beneficial owner or the controlling person or authorized representative a foreign PEP or closely related to such a person?" as TranslatedString,
name: "pep.foreign",
choices: [
@@ -92,8 +71,8 @@ export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
props: {
label:
"Domestic PEP and PEP of International Organizations" as TranslatedString,
- tooltip:
- "Definition see Art. 7 lit. g numeral 2 and 3 SRO Regulations " as TranslatedString,
+ // tooltip:
+ // "Definition see Art. 7 lit. g numeral 2 and 3 SRO Regulations " as TranslatedString,
help: "Is the customer, the beneficial owner or the controlling person or authorized representative a domestic PEP or PEP in International Organizations or closely related to such a person?" as TranslatedString,
name: "pep.domestic",
choices: [
@@ -123,7 +102,7 @@ export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
"The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
name: "pep.when",
pattern: "dd/MM/yyyy",
- placeholder: "dd/MM/yyyy" as TranslatedString,
+ // placeholder: "dd/MM/yyyy" as TranslatedString,
},
},
],
@@ -167,7 +146,7 @@ export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
"The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
name: "highRisk.when",
pattern: "dd/MM/yyyy",
- placeholder: "dd/MM/yyyy" as TranslatedString,
+ // placeholder: "dd/MM/yyyy" as TranslatedString,
},
},
],
@@ -613,7 +592,7 @@ export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
"The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
name: "evaluation.when",
pattern: "dd/MM/yyyy",
- placeholder: "dd/MM/yyyy" as TranslatedString,
+ // placeholder: "dd/MM/yyyy" as TranslatedString,
},
},
],
@@ -742,15 +721,12 @@ export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
v: Partial<Form902_4.Form>,
): FormState<Form902_4.Form> {
return {
- when: {
- disabled: true,
- },
};
},
});
namespace Form902_4 {
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
customer: string;
fullName: string;
pep: {
@@ -778,24 +754,24 @@ namespace Form902_4 {
industry: {
nature: "customer" | "owner";
risk:
- | "low"
- | "medium-cash"
- | "medium-unknown"
- | "high-restricted"
- | "high-unknown";
+ | "low"
+ | "medium-cash"
+ | "medium-unknown"
+ | "high-restricted"
+ | "high-unknown";
};
contact: {
risk: "low" | "medium" | "high";
};
product: {
risk:
- | "low"
- | "medium"
- | "high-offshore"
- | "high-structure"
- | "high-accounts"
- | "high-service"
- | "high-freq-tx";
+ | "low"
+ | "medium"
+ | "high-offshore"
+ | "high-structure"
+ | "high-accounts"
+ | "high-service"
+ | "high-freq-tx";
};
custom: {
definition: string;
@@ -805,7 +781,6 @@ namespace Form902_4 {
justification: string;
risk: "with" | "without";
};
- when: AbsoluteTime;
};
criteria: {
additional: string;
diff --git a/packages/aml-backoffice-ui/src/forms/902_5e.ts b/packages/aml-backoffice-ui/src/forms/902_5e.ts
index 501a3b23c..c3948e1c7 100644
--- a/packages/aml-backoffice-ui/src/forms/902_5e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_5e.ts
@@ -1,10 +1,10 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm, currencyList } from "./index.js";
-import { Simplest, resolutionSection } from "./simplest.js";
+import { resolutionSection } from "./simplest.js";
-export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_5.Form> => ({
versionId: "2023-05-15",
design: [
{
@@ -20,22 +20,6 @@ export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
help: "Pursuant Identification Form (VQF doc. No. 902.1) numeral 1" as TranslatedString,
},
},
- {
- type: "text",
- props: {
- name: "fullName",
- label: "Full name" as TranslatedString,
- },
- },
- {
- type: "date",
- props: {
- name: "when",
- pattern: "dd/MM/yyyy",
- label: "Date" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
],
},
{
@@ -232,9 +216,6 @@ export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
v: Partial<Form902_5.Form>,
): FormState<Form902_5.Form> {
return {
- when: {
- disabled: true,
- },
originOfAssets: {
categoryOther: {
hidden: v.originOfAssets?.category !== "other",
@@ -245,7 +226,7 @@ export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
});
namespace Form902_5 {
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
customer: string;
fullName: string;
businessActivity: string;
diff --git a/packages/aml-backoffice-ui/src/forms/902_9e.ts b/packages/aml-backoffice-ui/src/forms/902_9e.ts
index 04f0a1572..a5753d5d0 100644
--- a/packages/aml-backoffice-ui/src/forms/902_9e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_9e.ts
@@ -1,10 +1,10 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { FlexibleForm } from "./index.js";
-import { Simplest, resolutionSection } from "./simplest.js";
+import { resolutionSection } from "./simplest.js";
-export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Form902_9.Form> => ({
versionId: "2023-05-15",
design: [
{
@@ -19,15 +19,6 @@ export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
},
},
{
- type: "date",
- props: {
- name: "when",
- pattern: "dd/MM/yyyy",
- label: "Date" as TranslatedString,
- help: "format 'dd/MM/yyyy'" as TranslatedString,
- },
- },
- {
type: "caption",
props: {
label:
@@ -61,7 +52,7 @@ export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
name: "dateOfBirth",
label: "Date of birth" as TranslatedString,
pattern: "dd/MM/yyyy",
- help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: "format 'dd/MM/yyyy'" as TranslatedString,
},
},
{
@@ -110,9 +101,6 @@ export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
v: Partial<Form902_9.Form>,
): FormState<Form902_9.Form> {
return {
- when: {
- disabled: true,
- },
};
},
});
@@ -125,7 +113,7 @@ namespace Form902_9 {
nationality: string;
address: string;
}
- export interface Form extends Simplest.WithResolution {
+ export interface Form extends BaseForm {
contractingPartner: string;
persons: Person;
signature: string;
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index 6497f3949..9b462e1c5 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -6,13 +6,13 @@ import {
} from "@gnu-taler/taler-util";
import { FormState } from "../handlers/FormProvider.js";
import { DoubleColumnFormSection } from "../handlers/forms.js";
-import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
import { AmlExchangeBackend } from "../types.js";
import { FlexibleForm } from "./index.js";
import { amlStateConverter } from "../pages/ShowConsolidated.js";
-export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
+export const v1 = (current: BaseForm): FlexibleForm<Simplest.Form> => ({
versionId: "2023-05-25",
design: [
{
@@ -33,9 +33,6 @@ export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
v: Partial<Simplest.Form>,
): FormState<Simplest.Form> {
return {
- when: {
- disabled: true,
- },
threshold: {
disabled: v.state === AmlExchangeBackend.AmlState.frozen,
},
@@ -44,17 +41,12 @@ export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
});
export namespace Simplest {
- export interface WithResolution {
- when: AbsoluteTime;
- threshold: AmountJson;
- state: AmlExchangeBackend.AmlState;
- }
- export interface Form extends WithResolution {
+ export interface Form extends BaseForm {
comment: string;
}
}
-export function resolutionSection(current: State): DoubleColumnFormSection {
+export function resolutionSection(current: BaseForm): DoubleColumnFormSection {
return {
title: "Resolution" as TranslatedString,
description: `Current state is ${amlStateConverter.toStringUI(
@@ -64,13 +56,6 @@ export function resolutionSection(current: State): DoubleColumnFormSection {
)}` as TranslatedString,
fields: [
{
- type: "date",
- props: {
- name: "when",
- label: "Decision Time" as TranslatedString,
- },
- },
- {
type: "choiceHorizontal",
props: {
name: "state",
diff --git a/packages/aml-backoffice-ui/src/handlers/Calendar.tsx b/packages/aml-backoffice-ui/src/handlers/Calendar.tsx
new file mode 100644
index 000000000..9da6e1757
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/Calendar.tsx
@@ -0,0 +1,116 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util"
+import { useTranslationContext } from "@gnu-taler/web-util/browser"
+import { add as dateAdd, sub as dateSub, eachDayOfInterval, endOfMonth, endOfWeek, format, getMonth, getYear, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek } from "date-fns"
+import { VNode, h } from "preact"
+import { useState } from "preact/hooks"
+
+export function Calendar({ value, onChange }: { value: AbsoluteTime | undefined, onChange: (v: AbsoluteTime) => void }): VNode {
+ const today = startOfDay(new Date())
+ const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value))
+ const [showingDate, setShowingDate] = useState(selected)
+ const month = getMonth(showingDate)
+ const year = getYear(showingDate)
+
+ const start = startOfWeek(startOfMonth(showingDate));
+ const end = endOfWeek(endOfMonth(showingDate));
+ const daysInMonth = eachDayOfInterval({ start, end });
+ const { i18n } = useTranslationContext()
+ const monthNames = [
+ i18n.str`January`,
+ i18n.str`February`,
+ i18n.str`March`,
+ i18n.str`April`,
+ i18n.str`May`,
+ i18n.str`June`,
+ i18n.str`July`,
+ i18n.str`August`,
+ i18n.str`September`,
+ i18n.str`October`,
+ i18n.str`November`,
+ i18n.str`December`,
+ ]
+ return <div class="text-center p-2">
+ <div class="flex items-center text-gray-900">
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateSub(showingDate, { years: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Previous year`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ <div class="flex-auto text-sm font-semibold">{year}</div>
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateAdd(showingDate, { years: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Next year`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ </div>
+ <div class="mt-4 flex items-center text-gray-900">
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateSub(showingDate, { months: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Previous month`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ <div class="flex-auto text-sm font-semibold">{monthNames[month]}</div>
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 rounded-sm "
+ onClick={() => {
+ setShowingDate(dateAdd(showingDate, { months: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Next month`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ </div>
+ <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
+ <div>M</div>
+ <div>T</div>
+ <div>W</div>
+ <div>T</div>
+ <div>F</div>
+ <div>S</div>
+ <div>S</div>
+ </div>
+ <div class="isolate mt-2 grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm shadow ring-1 ring-gray-200">
+ {daysInMonth.map(current => (
+ <button type="button"
+ data-month={isSameMonth(current, showingDate)}
+ data-today={isSameDay(current, today)}
+ data-selected={isSameDay(current, selected)}
+ onClick={() => {
+ onChange(AbsoluteTime.fromStampMs(current.getTime()))
+ }}
+ class="text-gray-400 hover:bg-gray-700 focus:z-10 py-1.5
+ data-[month=false]:bg-gray-100 data-[month=true]:bg-white
+ data-[today=true]:font-semibold
+ data-[month=true]:text-gray-900
+ data-[today=true]:bg-red-300 data-[today=true]:hover:bg-red-200
+ data-[month=true]:hover:bg-gray-200
+ data-[selected=true]:!bg-blue-400 data-[selected=true]:hover:!bg-blue-300 ">
+ <time dateTime={format(current, "yyyy-MM-dd")}
+ class="mx-auto flex h-7 w-7 items-center justify-center rounded-full">
+ {format(current, "dd")}
+ </time>
+ </button>
+ ))}
+ </div>
+ </div>
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/Dialog.tsx b/packages/aml-backoffice-ui/src/handlers/Dialog.tsx
new file mode 100644
index 000000000..f9899e94e
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/Dialog.tsx
@@ -0,0 +1,15 @@
+import { ComponentChildren, VNode, h } from "preact";
+
+export function Dialog({ children, onClose }: { onClose?: () => void; children: ComponentChildren }): VNode {
+ return <div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true" onClick={onClose}>
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
+
+ <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
+ <div class="flex min-h-full items-center justify-center p-4 text-center sm:items-center sm:p-0">
+ <div class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ {children}
+ </div>
+ </div>
+ </div>
+ </div>
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx b/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
index 3da2a4f07..310954bd0 100644
--- a/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
@@ -7,14 +7,13 @@ import { ComponentChildren, VNode, createContext, h } from "preact";
import {
MutableRef,
StateUpdater,
- useEffect,
- useRef,
- useState,
+ useState
} from "preact/hooks";
export interface FormType<T> {
value: MutableRef<Partial<T>>;
initialValue?: Partial<T>;
+ readOnly?: boolean;
onUpdate?: StateUpdater<T>;
computeFormState?: (v: T) => FormState<T>;
}
@@ -24,14 +23,14 @@ export const FormContext = createContext<FormType<any>>({});
export type FormState<T> = {
[field in keyof T]?: T[field] extends AbsoluteTime
- ? Partial<InputFieldState>
- : T[field] extends AmountJson
- ? Partial<InputFieldState>
- : T[field] extends Array<infer P>
- ? Partial<InputArrayFieldState<P>>
- : T[field] extends (object | undefined)
- ? FormState<T[field]>
- : Partial<InputFieldState>;
+ ? Partial<InputFieldState>
+ : T[field] extends AmountJson
+ ? Partial<InputFieldState>
+ : T[field] extends Array<infer P>
+ ? Partial<InputArrayFieldState<P>>
+ : T[field] extends (object | undefined)
+ ? FormState<T[field]>
+ : Partial<InputFieldState>;
};
export interface InputFieldState {
@@ -55,11 +54,13 @@ export function FormProvider<T>({
onUpdate: notify,
onSubmit,
computeFormState,
+ readOnly,
}: {
initialValue?: Partial<T>;
onUpdate?: (v: Partial<T>) => void;
onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
computeFormState?: (v: Partial<T>) => FormState<T>;
+ readOnly?: boolean;
children: ComponentChildren;
}): VNode {
// const value = useRef(initialValue ?? {});
@@ -79,7 +80,7 @@ export function FormProvider<T>({
};
return (
<FormContext.Provider
- value={{ initialValue, value, onUpdate, computeFormState }}
+ value={{ initialValue, value, onUpdate, computeFormState, readOnly }}
>
<form
onSubmit={(e) => {
diff --git a/packages/aml-backoffice-ui/src/handlers/InputArray.tsx b/packages/aml-backoffice-ui/src/handlers/InputArray.tsx
index 00379bed6..d229b35de 100644
--- a/packages/aml-backoffice-ui/src/handlers/InputArray.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/InputArray.tsx
@@ -107,22 +107,24 @@ export function InputArray<T extends object, K extends keyof T>(
/>
);
})}
- <div class="pt-2">
- <Option
- label={"Add..." as TranslatedString}
- isSelected={selectedIndex === list.length}
- isLast
- isFirst
- disabled={
- selectedIndex !== undefined && selectedIndex !== list.length
- }
- onClick={() => {
- setSelected(
- selectedIndex === list.length ? undefined : list.length,
- );
- }}
- />
- </div>
+ {!state.disabled &&
+ <div class="pt-2">
+ <Option
+ label={"Add..." as TranslatedString}
+ isSelected={selectedIndex === list.length}
+ isLast
+ isFirst
+ disabled={
+ selectedIndex !== undefined && selectedIndex !== list.length
+ }
+ onClick={() => {
+ setSelected(
+ selectedIndex === list.length ? undefined : list.length,
+ );
+ }}
+ />
+ </div>
+ }
</div>
{selectedIndex !== undefined && (
/**
@@ -131,6 +133,7 @@ export function InputArray<T extends object, K extends keyof T>(
*/
<FormProvider
initialValue={selected}
+ readOnly={state.disabled}
computeFormState={(v) => {
// current state is ignored
// the state is defined by the parent form
diff --git a/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx b/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
index fdee35447..a5f263615 100644
--- a/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
@@ -61,6 +61,7 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>(
return (
<button
type="button"
+ disabled={state.disabled}
class={clazz}
onClick={(e) => {
onChange(
diff --git a/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx b/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx
index c37984368..29c596994 100644
--- a/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx
@@ -60,6 +60,7 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
type="radio"
name="server-size"
// defaultValue={choice.value}
+ disabled={state.disabled}
value={
(!converter
? (choice.value as string)
diff --git a/packages/aml-backoffice-ui/src/handlers/InputDate.tsx b/packages/aml-backoffice-ui/src/handlers/InputDate.tsx
index 0f286e001..7fcc16b33 100644
--- a/packages/aml-backoffice-ui/src/handlers/InputDate.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/InputDate.tsx
@@ -1,40 +1,65 @@
import { AbsoluteTime } from "@gnu-taler/taler-util";
import { InputLine, UIFormProps } from "./InputLine.js";
-import { VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { format, parse } from "date-fns";
+import { Dialog } from "./Dialog.js";
+import { Calendar } from "./Calendar.js";
+import { useState } from "preact/hooks";
+import { useField } from "./useField.js";
export function InputDate<T extends object, K extends keyof T>(
props: { pattern?: string } & UIFormProps<T, K>,
): VNode {
const pattern = props.pattern ?? "dd/MM/yyyy";
+ const [open, setOpen] = useState(false)
+ const { value, onChange } = useField<T, K>(props.name);
return (
- <InputLine<T, K>
- type="text"
- after={{
- type: "icon",
- // icon: <CalendarIcon class="h-6 w-6" />,
- icon: <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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
- </svg>
+ <Fragment>
- }}
- converter={{
- //@ts-ignore
- fromStringUI: (v): AbsoluteTime => {
- if (!v) return AbsoluteTime.never();
- const t_ms = parse(v, pattern, Date.now()).getTime();
- return AbsoluteTime.fromMilliseconds(t_ms);
- },
- //@ts-ignore
- toStringUI: (v: AbsoluteTime) => {
- return !v || !v.t_ms
- ? ""
- : v.t_ms === "never"
- ? "never"
- : format(v.t_ms, pattern);
- },
- }}
- {...props}
- />
+ <InputLine<T, K>
+ type="text"
+ after={{
+ type: "button",
+ onClick: () => {
+ setOpen(true)
+ },
+ // icon: <CalendarIcon class="h-6 w-6" />,
+ children: (
+ <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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
+ </svg>)
+ }}
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): AbsoluteTime | undefined => {
+ if (!v) return undefined;
+ try {
+ const t_ms = parse(v, pattern, Date.now()).getTime();
+ return AbsoluteTime.fromMilliseconds(t_ms);
+ } catch (e) {
+ return undefined;
+ }
+ },
+ //@ts-ignore
+ toStringUI: (v: AbsoluteTime | undefined) => {
+ return !v || !v.t_ms
+ ? undefined
+ : v.t_ms === "never"
+ ? "never"
+ : format(v.t_ms, pattern);
+ },
+ }}
+ {...props}
+ />
+ {open &&
+ <Dialog onClose={() => setOpen(false)}>
+ <Calendar value={value as AbsoluteTime ?? AbsoluteTime.now()}
+ onChange={(v) => {
+ onChange(v as any)
+ setOpen(false)
+ }} />
+ </Dialog>
+ }
+ </Fragment>
);
}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputFile.tsx b/packages/aml-backoffice-ui/src/handlers/InputFile.tsx
index 0d89a98a3..d9af03f86 100644
--- a/packages/aml-backoffice-ui/src/handlers/InputFile.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/InputFile.tsx
@@ -42,40 +42,42 @@ export function InputFile<T extends object, K extends keyof T>(
clip-rule="evenodd"
/>
</svg>
- <div class="my-2 flex text-sm leading-6 text-gray-600">
- <label
- for="file-upload"
- class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
- >
- <span>Upload a file</span>
- <input
- id="file-upload"
- name="file-upload"
- type="file"
- class="sr-only"
- accept={accept}
- onChange={(e) => {
- const f: FileList | null = e.currentTarget.files;
- if (!f || f.length != 1) {
- return onChange(undefined!);
- }
- if (f[0].size > maxBites) {
- return onChange(undefined!);
- }
- return f[0].arrayBuffer().then((b) => {
- const b64 = window.btoa(
- new Uint8Array(b).reduce(
- (data, byte) => data + String.fromCharCode(byte),
- "",
- ),
- );
- return onChange(`data:${f[0].type};base64,${b64}` as any);
- });
- }}
- />
- </label>
- {/* <p class="pl-1">or drag and drop</p> */}
- </div>
+ {!state.disabled &&
+ <div class="my-2 flex text-sm leading-6 text-gray-600">
+ <label
+ for="file-upload"
+ class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
+ >
+ <span>Upload a file</span>
+ <input
+ id="file-upload"
+ name="file-upload"
+ type="file"
+ class="sr-only"
+ accept={accept}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(undefined!);
+ }
+ if (f[0].size > maxBites) {
+ return onChange(undefined!);
+ }
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = window.btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ </label>
+ {/* <p class="pl-1">or drag and drop</p> */}
+ </div>
+ }
</div>
</div>
) : (
@@ -85,14 +87,16 @@ export function InputFile<T extends object, K extends keyof T>(
class=" h-24 w-full object-cover relative"
/>
- <div
- class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
- onClick={() => {
- onChange(undefined!);
- }}
- >
- Clear
- </div>
+ {!state.disabled &&
+ <div
+ class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
+ onClick={() => {
+ onChange(undefined!);
+ }}
+ >
+ Clear
+ </div>
+ }
</div>
)}
{help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputLine.tsx b/packages/aml-backoffice-ui/src/handlers/InputLine.tsx
index 9448ef5e4..f6c709d94 100644
--- a/packages/aml-backoffice-ui/src/handlers/InputLine.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/InputLine.tsx
@@ -1,6 +1,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useField } from "./useField.js";
+import { useEffect, useState } from "preact/hooks";
export interface IconAddon {
type: "icon";
@@ -80,7 +81,7 @@ export function LabelWithTooltipMaybeRequired({
{Label}
<span class="relative flex items-center group pl-2">
{TooltipIcon}
- <div class="absolute bottom-0 flex flex-col items-center hidden mb-6 group-hover:flex">
+ <div class="absolute bottom-0 flex flex-col items-center mb-6 group-hover:flex">
<span class="relative z-10 p-2 text-xs leading-none text-white whitespace-no-wrap bg-black shadow-lg">
{tooltip}
</span>
@@ -110,8 +111,9 @@ function InputWrapper<T extends object, K extends keyof T>({
after,
help,
error,
+ disabled,
required,
-}: { error?: string; children: ComponentChildren } & UIFormProps<T, K>): VNode {
+}: { error?: string; disabled: boolean, children: ComponentChildren } & UIFormProps<T, K>): VNode {
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -132,6 +134,7 @@ function InputWrapper<T extends object, K extends keyof T>({
) : before.type === "button" ? (
<button
type="button"
+ disabled={disabled}
onClick={before.onClick}
class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
@@ -153,6 +156,7 @@ function InputWrapper<T extends object, K extends keyof T>({
) : after.type === "button" ? (
<button
type="button"
+ disabled={disabled}
onClick={after.onClick}
class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
@@ -189,6 +193,21 @@ export function InputLine<T extends object, K extends keyof T>(
const { name, placeholder, before, after, converter, type } = props;
const { value, onChange, state, isDirty } = useField<T, K>(name);
+ const [text, setText] = useState("")
+ const fromString: (s: string) => any =
+ converter?.fromStringUI ?? defaultFromString;
+ const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
+
+ useEffect(() => {
+ const newValue = toString(value)
+ if (newValue) {
+
+ setText(newValue)
+ } else {
+ console.log("invalid")
+ }
+ }, [value])
+
if (state.hidden) return <div />;
let clazz =
@@ -233,14 +252,12 @@ export function InputLine<T extends object, K extends keyof T>(
clazz +=
" text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600";
}
- const fromString: (s: string) => any =
- converter?.fromStringUI ?? defaultFromString;
- const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
if (type === "text-area") {
return (
<InputWrapper<T, K>
{...props}
+ disabled={state.disabled}
error={showError ? state.error : undefined}
>
<textarea
@@ -262,15 +279,18 @@ export function InputLine<T extends object, K extends keyof T>(
}
return (
- <InputWrapper<T, K> {...props} error={showError ? state.error : undefined}>
+ <InputWrapper<T, K> {...props} disabled={state.disabled} error={showError ? state.error : undefined}>
<input
name={String(name)}
type={type}
onChange={(e) => {
- onChange(fromString(e.currentTarget.value));
+ setText(e.currentTarget.value)
}}
placeholder={placeholder ? placeholder : undefined}
- value={toString(value) ?? ""}
+ value={text}
+ onBlur={() => {
+ onChange(fromString(text));
+ }}
// defaultValue={toString(value)}
disabled={state.disabled}
aria-invalid={showError}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx b/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx
index 837744827..6e6186a88 100644
--- a/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx
@@ -13,7 +13,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
): VNode {
const { name, label, choices, placeholder, tooltip, required, unique, max } =
props;
- const { value, onChange } = useField<T, K>(name);
+ const { value, onChange, state } = useField<T, K>(name);
const [filter, setFilter] = useState<string | undefined>(undefined);
const regex = new RegExp(`.*${filter}.*`, "i");
@@ -26,8 +26,8 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
filter === undefined
? undefined
: choices.filter((v) => {
- return regex.test(v.label);
- });
+ return regex.test(v.label);
+ });
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -41,6 +41,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
{choiceMap[v]}
<button
type="button"
+ disabled={state.disabled}
onClick={() => {
const newValue = [...list];
newValue.splice(idx, 1);
@@ -62,7 +63,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
);
})}
- <div class="relative mt-2">
+ {!state.disabled && <div class="relative mt-2">
<input
id="combobox"
type="text"
@@ -78,6 +79,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
/>
<button
type="button"
+ disabled={state.disabled}
onClick={() => {
setFilter(filter === undefined ? "" : undefined);
}}
@@ -122,7 +124,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
onChange(newValue as T[K]);
}}
- // tabindex="-1"
+ // tabindex="-1"
>
{/* <!-- Selected: "font-semibold" --> */}
<span class="block truncate">{v.label}</span>
@@ -145,7 +147,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
{/* <!-- More items... --> */}
</ul>
)}
- </div>
+ </div>}
</div>
);
}
diff --git a/packages/aml-backoffice-ui/src/handlers/useField.ts b/packages/aml-backoffice-ui/src/handlers/useField.ts
index bf94d2f5d..7eec5c5f8 100644
--- a/packages/aml-backoffice-ui/src/handlers/useField.ts
+++ b/packages/aml-backoffice-ui/src/handlers/useField.ts
@@ -16,6 +16,7 @@ export function useField<T extends object, K extends keyof T>(
value: formValue,
computeFormState,
onUpdate: notifyUpdate,
+ readOnly: readOnlyForm,
} = useContext(FormContext);
type P = typeof name;
@@ -30,8 +31,8 @@ export function useField<T extends object, K extends keyof T>(
//compute default state
const state = {
- disabled: fieldState.disabled ?? false,
- readonly: fieldState.readonly ?? false,
+ disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
+ readonly: readOnlyForm ? true : (fieldState.readonly ?? false),
hidden: fieldState.hidden ?? false,
error: fieldState.error,
elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
index c4edd9207..81ca2755a 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -12,7 +12,6 @@ import { useOfficer } from "./useOfficer.js";
const useSWR = _useSWR as unknown as SWRHook;
const PAGE_SIZE = 10;
-const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
/**
* FIXME: mutate result when balance change (transaction )
* @param account
@@ -24,11 +23,11 @@ export function useCases(state: AmlExchangeBackend.AmlState) {
const session = officer.state === "ready" ? officer.account : undefined;
const { api } = useExchangeApiContext();
- const [offset, setOffet] = useState<string>();
+ const [offset, setOffset] = useState<string>();
async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) {
return await api.getDecisionsByState(officer, state, {
- order: "asc", offset, limit: MAX_RESULT_SIZE
+ order: "asc", offset, limit: PAGE_SIZE + 1
})
}
@@ -51,7 +50,7 @@ export function useCases(state: AmlExchangeBackend.AmlState) {
// if the query returns less that we ask, then we have reach the end or beginning
const isLastPage =
- data && data.type === "ok" && data.body.records.length < PAGE_SIZE;
+ data && data.type === "ok" && data.body.records.length <= PAGE_SIZE;
const isFirstPage = !offset;
const pagination = {
@@ -60,12 +59,10 @@ export function useCases(state: AmlExchangeBackend.AmlState) {
loadMore: () => {
if (isLastPage || data?.type !== "ok") return;
const list = data.body.records
- if (list.length < MAX_RESULT_SIZE) {
- // setOffset(list[list.length-1].account_name);
- }
+ setOffset(String(list[list.length - 1].rowid));
},
- loadMorePrev: () => {
- null;
+ reset: () => {
+ setOffset(undefined)
},
};
@@ -79,11 +76,13 @@ export function useCases(state: AmlExchangeBackend.AmlState) {
} as OperationFail<never>
}
}
+
if (data) {
if (data.type === "fail") {
return { data }
}
- return { data, pagination }
+ const records = isLastPage ? data.body.records : removeLastElement(data.body.records)
+ return { data: { type: "ok" as const, body: { records } }, pagination }
}
if (error) {
return error;
@@ -137,3 +136,9 @@ const example1: TalerExchangeApi.AmlRecords = {
};
+function removeLastElement<T>(list: Array<T>): Array<T> {
+ if (list.length === 0) {
+ return list;
+ }
+ return list.slice(0, -1)
+} \ No newline at end of file
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";
diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts
index 091b92d5c..f515a590a 100644
--- a/packages/aml-backoffice-ui/src/route.ts
+++ b/packages/aml-backoffice-ui/src/route.ts
@@ -1,8 +1,20 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { createHashHistory } from "history";
-import { h as create, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
-const history = createHashHistory();
+import { ComponentChildren, h as create, createContext, VNode } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+type ContextType = {
+ onChange: (listener: () => void) => VoidFunction
+}
+const nullChangeListener = { onChange: () => () => { } }
+const Context = createContext<ContextType>(nullChangeListener);
+
+export const usePathChangeContext = (): ContextType => useContext(Context);
+
+export function HashPathProvider({ children }: { children: ComponentChildren }): VNode {
+ const history = createHashHistory();
+ return create(Context.Provider, { value: { onChange: history.listen }, children }, children)
+}
type PageDefinition<DynamicPart extends Record<string, string>> = {
pattern: string;
@@ -81,8 +93,9 @@ type Location = {
};
export function useCurrentLocation(pageList: Array<PageEntry<any>>): Location | undefined {
const [currentLocation, setCurrentLocation] = useState<Location | null | undefined>(null);
+ const path = usePathChangeContext();
useEffect(() => {
- return history.listen(() => {
+ return path.onChange(() => {
const result = doSync(window.location.hash, new URLSearchParams(window.location.search), pageList);
setCurrentLocation(result);
});
@@ -95,8 +108,9 @@ export function useCurrentLocation(pageList: Array<PageEntry<any>>): Location |
export function useChangeLocation() {
const [location, setLocation] = useState(window.location.hash)
+ const path = usePathChangeContext()
useEffect(() => {
- return history.listen(() => {
+ return path.onChange(() => {
setLocation(window.location.hash)
});
}, []);
diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts
index 3b1d0267d..eca66cb18 100644
--- a/packages/aml-backoffice-ui/src/stories.test.ts
+++ b/packages/aml-backoffice-ui/src/stories.test.ts
@@ -18,7 +18,7 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { setupI18n } from "@gnu-taler/taler-util";
+import { TalerExchangeApi, setupI18n } from "@gnu-taler/taler-util";
import { parseGroupImport } from "@gnu-taler/web-util/browser";
import * as tests from "@gnu-taler/web-util/testing";
@@ -26,12 +26,13 @@ import * as tests from "@gnu-taler/web-util/testing";
import * as pages from "./pages/index.stories.js";
import { ComponentChildren, Fragment, VNode, h as create } from "preact";
+import { ExchangeApiContextTesting } from "./context/config.js";
// import { BackendStateProviderTesting } from "./context/backend.js";
setupI18n("en", { en: {} });
describe("All the examples:", () => {
- const cms = parseGroupImport({pages});
+ const cms = parseGroupImport({ pages });
cms.forEach((group) => {
describe(`Example for group "${group.title}:"`, () => {
group.list.forEach((component) => {
@@ -47,10 +48,24 @@ describe("All the examples:", () => {
});
});
+
function DefaultTestingContext({
children,
}: {
children: ComponentChildren;
}): VNode {
- return create(Fragment, {});
+ const config: TalerExchangeApi.ExchangeVersionResponse = {
+ currency: "ARS",
+ currency_specification: {
+ alt_unit_names: {},
+ name: "ARS",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2
+ },
+ name: "taler-exchange",
+ supported_kyc_requirements: [],
+ version: "asd",
+ }
+ return create(ExchangeApiContextTesting, { config, children });
}
diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx
index 5ef54309c..7685195e5 100644
--- a/packages/aml-backoffice-ui/src/stories.tsx
+++ b/packages/aml-backoffice-ui/src/stories.tsx
@@ -26,16 +26,40 @@ import * as pages from "./pages/index.stories.js";
import { renderStories } from "@gnu-taler/web-util/browser";
import "./scss/main.css";
+import { h, ComponentChildren, FunctionComponent, VNode } from "preact";
+import { ExchangeApiContextTesting } from "./context/config.js";
function main(): void {
renderStories(
- {pages},
+ { pages },
{
strings,
+ getWrapperForGroup
},
);
}
+function getWrapperForGroup(): FunctionComponent {
+ return function All({ children }: { children?: ComponentChildren }): VNode {
+ return <ExchangeApiContextTesting
+ config={{
+ currency: "ARS",
+ currency_specification: {
+ alt_unit_names: {},
+ name: "ARS",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2
+ },
+ name: "taler-exchange",
+ supported_kyc_requirements: [],
+ version: "asd",
+ }}>
+ {children}
+ </ExchangeApiContextTesting>
+ }
+}
+
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
diff --git a/packages/web-util/src/forms/DefaultForm.tsx b/packages/web-util/src/forms/DefaultForm.tsx
index 92c379459..12babf39a 100644
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -9,7 +9,6 @@ export interface FlexibleForm<T extends object> {
design: DoubleColumnForm;
behavior: (form: Partial<T>) => FormState<T>;
}
-
export function DefaultForm<T extends object>({
initial,
onUpdate,
diff --git a/packages/web-util/src/stories.tsx b/packages/web-util/src/stories.tsx
index 4edbf83b5..d9c2406eb 100644
--- a/packages/web-util/src/stories.tsx
+++ b/packages/web-util/src/stories.tsx
@@ -184,8 +184,8 @@ function ExampleList({
backgroundColor: isSelected
? "green"
: i % 2
- ? "lightgray"
- : "lightblue",
+ ? "lightgray"
+ : "lightblue",
marginLeft: "1em",
padding: 4,
cursor: "pointer",
@@ -395,10 +395,10 @@ function folder(groupName: string, value: ComponentOrFolder): ComponentItem[] {
try {
title =
typeof value === "object" &&
- typeof value.default === "object" &&
- value.default !== undefined &&
- "title" in value.default &&
- typeof value.default.title === "string"
+ typeof value.default === "object" &&
+ value.default !== undefined &&
+ "title" in value.default &&
+ typeof value.default.title === "string"
? value.default.title
: undefined;
} catch (e) {
@@ -430,12 +430,12 @@ function Application({
examplesInGroups,
getWrapperForGroup,
}: Props): VNode {
+ const url = new URL(window.location.href);
const initialSelection = getSelectionFromLocationHash(
- location.hash,
+ url.hash,
examplesInGroups,
);
- const url = new URL(window.location.href);
const currentLang = url.searchParams.get("lang") || "en";
if (!langs["en"]) {
@@ -448,15 +448,15 @@ function Application({
);
const [sidebarWidth, setSidebarWidth] = useState(200);
useEffect(() => {
- if (location.hash) {
- const hash = location.hash.substring(1);
+ if (url.hash) {
+ const hash = url.hash.substring(1);
const found = document.getElementById(hash);
if (found) {
setTimeout(() => {
found.scrollIntoView({
block: "center",
});
- }, 10);
+ }, 50);
}
}
}, []);
@@ -500,11 +500,11 @@ function Application({
))}
<hr />
</SideBar>
- <ResizeHandle
+ {/* <ResizeHandle
onUpdate={(x) => {
setSidebarWidth((s) => s + x);
}}
- />
+ /> */}
<Content>
<ErrorReport selected={selected}>
<PreventLinkNavigation>