gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: new forms api


From: gnunet
Subject: [taler-wallet-core] branch master updated: new forms api
Date: Mon, 20 Nov 2023 16:38:22 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 613884605 new forms api
613884605 is described below

commit 6138846050563e0dca95b0b6d792776925e4c35f
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Nov 20 12:38:16 2023 -0300

    new forms api
---
 packages/aml-backoffice-ui/src/App.tsx             |  22 +-
 packages/aml-backoffice-ui/src/Dashboard.tsx       |  27 +-
 packages/aml-backoffice-ui/src/NiceForm.tsx        |   3 +
 packages/aml-backoffice-ui/src/context/config.ts   |   8 +
 packages/aml-backoffice-ui/src/forms/902_11e.ts    |  18 +-
 packages/aml-backoffice-ui/src/forms/902_12e.ts    |  20 +-
 packages/aml-backoffice-ui/src/forms/902_13e.ts    |  32 +-
 packages/aml-backoffice-ui/src/forms/902_15e.ts    |  22 +-
 packages/aml-backoffice-ui/src/forms/902_1e.ts     |  49 +--
 packages/aml-backoffice-ui/src/forms/902_4e.ts     |  69 ++--
 packages/aml-backoffice-ui/src/forms/902_5e.ts     |  27 +-
 packages/aml-backoffice-ui/src/forms/902_9e.ts     |  22 +-
 packages/aml-backoffice-ui/src/forms/simplest.ts   |  23 +-
 .../aml-backoffice-ui/src/handlers/Calendar.tsx    | 116 ++++++
 packages/aml-backoffice-ui/src/handlers/Dialog.tsx |  15 +
 .../src/handlers/FormProvider.tsx                  |  25 +-
 .../aml-backoffice-ui/src/handlers/InputArray.tsx  |  35 +-
 .../src/handlers/InputChoiceHorizontal.tsx         |   1 +
 .../src/handlers/InputChoiceStacked.tsx            |   1 +
 .../aml-backoffice-ui/src/handlers/InputDate.tsx   |  81 +++--
 .../aml-backoffice-ui/src/handlers/InputFile.tsx   |  88 ++---
 .../aml-backoffice-ui/src/handlers/InputLine.tsx   |  36 +-
 .../src/handlers/InputSelectMultiple.tsx           |  14 +-
 .../aml-backoffice-ui/src/handlers/useField.ts     |   5 +-
 packages/aml-backoffice-ui/src/hooks/useCases.ts   |  25 +-
 .../src/pages/AntiMoneyLaunderingForm.stories.tsx  |  46 ++-
 .../src/pages/AntiMoneyLaunderingForm.tsx          | 152 ++++++--
 .../aml-backoffice-ui/src/pages/CaseDetails.tsx    | 288 +++++++++------
 .../src/{stories.tsx => pages/Cases.stories.tsx}   |  43 ++-
 packages/aml-backoffice-ui/src/pages/Cases.tsx     | 392 +++++++++------------
 .../aml-backoffice-ui/src/pages/NewFormEntry.tsx   |  68 ++--
 .../src/pages/ShowConsolidated.stories.tsx         |  69 +++-
 .../aml-backoffice-ui/src/pages/index.stories.ts   |   1 +
 packages/aml-backoffice-ui/src/route.ts            |  24 +-
 packages/aml-backoffice-ui/src/stories.test.ts     |  21 +-
 packages/aml-backoffice-ui/src/stories.tsx         |  26 +-
 packages/web-util/src/forms/DefaultForm.tsx        |   1 -
 packages/web-util/src/stories.tsx                  |  26 +-
 38 files changed, 1116 insertions(+), 825 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: [
     {
@@ -346,15 +346,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: {
@@ -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,
                 },
               },
               {
@@ -423,15 +425,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: {
@@ -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,
                 },
               },
               {
@@ -135,15 +135,6 @@ export const v1 = (current: State): 
FlexibleForm<Form902_15.Form> => ({
               "The contracting partner hereby undertakes to automatically 
inform the financial intermediary of any changes. The contracting partner 
hereby also declares having been given permission by the above individuals 
and/or entities to transmit their data to the financial intermediary" as 
TranslatedString,
           },
         },
-        {
-          type: "date",
-          props: {
-            name: "when",
-            pattern: "dd/MM/yyyy",
-            label: "Date" as TranslatedString,
-            help: "format 'dd/MM/yyyy'" as TranslatedString,
-          },
-        },
         {
           type: "text",
           props: {
@@ -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,35 +1,12 @@
 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:
@@ -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: [
     {
@@ -32,27 +32,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,
@@ -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: [
     {
@@ -18,15 +18,6 @@ export const v1 = (current: State): 
FlexibleForm<Form902_9.Form> => ({
             label: "Contracting partner" as TranslatedString,
           },
         },
-        {
-          type: "date",
-          props: {
-            name: "when",
-            pattern: "dd/MM/yyyy",
-            label: "Date" as TranslatedString,
-            help: "format 'dd/MM/yyyy'" as TranslatedString,
-          },
-        },
         {
           type: "caption",
           props: {
@@ -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(
@@ -63,13 +55,6 @@ export function resolutionSection(current: State): 
DoubleColumnFormSection {
       current.threshold,
     )}` as TranslatedString,
     fields: [
-      {
-        type: "date",
-        props: {
-          name: "when",
-          label: "Decision Time" as TranslatedString,
-        },
-      },
       {
         type: "choiceHorizontal",
         props: {
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/stories.tsx 
b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
similarity index 57%
copy from packages/aml-backoffice-ui/src/stories.tsx
copy to packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
index 5ef54309c..0355d5a31 100644
--- a/packages/aml-backoffice-ui/src/stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -18,26 +18,25 @@
  *
  * @author Sebastian Javier Marchano (sebasjm)
  */
-import { strings } from "./i18n/strings.js";
 
-import * as pages from "./pages/index.stories.js";
-// import * as components from "./components/index.examples.js";
-
-import { renderStories } from "@gnu-taler/web-util/browser";
-
-import "./scss/main.css";
-
-function main(): void {
-  renderStories(
-    {pages},
-    {
-      strings,
-    },
-  );
-}
-
-if (document.readyState === "loading") {
-  document.addEventListener("DOMContentLoaded", main);
-} else {
-  main();
-}
+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>

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]