taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 6cb78d3c8b8bfe95e153946e5d0da8114bd2039e
parent 966a411197dee9225ed7442c2145b052d674c1bb
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 17 Mar 2025 17:18:19 -0300

information form

Diffstat:
Mpackages/aml-backoffice-ui/src/App.tsx | 8+++-----
Mpackages/aml-backoffice-ui/src/Routing.tsx | 3+++
Mpackages/aml-backoffice-ui/src/context/ui-forms.ts | 28++++++++++++++++++++++------
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 9++++++++-
Mpackages/aml-backoffice-ui/src/pages/Cases.tsx | 24+++++++++++++-----------
Mpackages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx | 35++++++++++++++++++-----------------
Mpackages/aml-backoffice-ui/src/pages/decision/Information.tsx | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
7 files changed, 200 insertions(+), 67 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx @@ -35,7 +35,7 @@ import { UiSettingsProvider } from "./context/ui-settings.js"; import { strings } from "./i18n/strings.js"; import "./scss/main.css"; import { UiSettings, fetchUiSettings } from "./context/ui-settings.js"; -import { UiFormsProvider, fetchUiForms } from "./context/ui-forms.js"; +import { UiFormsProvider } from "./context/ui-forms.js"; import { revalidateAccountDecisions } from "./hooks/decisions.js"; import { revalidateAccountInformation } from "./hooks/account.js"; @@ -43,12 +43,10 @@ const WITH_LOCAL_STORAGE_CACHE = false; export function App(): VNode { const [settings, setSettings] = useState<UiSettings>(); - const [forms, setForms] = useState<UiForms>(); useEffect(() => { fetchUiSettings(setSettings); - fetchUiForms(setForms); }, []); - if (!settings || !forms) return <Loading />; + if (!settings) return <Loading />; const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( @@ -95,7 +93,7 @@ export function App(): VNode { }} > <BrowserHashNavigationProvider> - <UiFormsProvider value={forms}> + <UiFormsProvider> <Routing /> </UiFormsProvider> </BrowserHashNavigationProvider> diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -161,6 +161,7 @@ function PrivateRouting(): VNode { return ( <AmlDecisionRequestWizard account={location.values.cid} + formId={location.params.formId ? location.params.formId[0] : undefined} onMove={(step) => { if (!step) { if (location.values.cid) { @@ -186,6 +187,7 @@ function PrivateRouting(): VNode { return ( <AmlDecisionRequestWizard account={location.values.cid} + formId={location.params.formId ? location.params.formId[0] : undefined} onMove={(step) => { if (!step) { if (location.values.cid) { @@ -211,6 +213,7 @@ function PrivateRouting(): VNode { return ( <AmlDecisionRequestWizard account={location.values.cid} + formId={location.params.formId ? location.params.formId[0] : undefined} step={location.values.step as WizardSteps} onMove={(step) => { if (!step) { diff --git a/packages/aml-backoffice-ui/src/context/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts @@ -14,9 +14,15 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { codecForUIForms, UiForms } from "@gnu-taler/web-util/browser"; +import { + codecForUIForms, + FormMetadata, + UiForms, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { ComponentChildren, createContext, h, VNode } from "preact"; -import { useContext } from "preact/hooks"; +import { useContext, useState, useEffect } from "preact/hooks"; +import { preloadedForms } from "../forms/index.js"; /** * @@ -36,13 +42,23 @@ export const useUiFormsContext = (): Type => useContext(Context); export const UiFormsProvider = ({ children, - value, }: { - value: UiForms; children: ComponentChildren; }): VNode => { + const { i18n } = useTranslationContext(); + const [forms, setForms] = useState<FormMetadata[]>(); + const pf = preloadedForms(i18n); + + useEffect(() => { + fetchUiForms((resp) => { + setForms(resp.forms); + }); + },[]); + + const value = !forms || !forms.length ? pf : [...pf, ...forms]; + return h(Context.Provider, { - value, + value: { forms: value }, children, }); }; @@ -57,7 +73,7 @@ function removeUndefineField<T extends object>(obj: T): T { }, obj); } -export function fetchUiForms(listener: (s: UiForms) => void): void { +function fetchUiForms(listener: (s: UiForms) => void): void { fetch("./forms.json") .then((resp) => resp.json()) .then((json) => codecForUIForms().decode(json)) diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -28,15 +28,19 @@ import { codecForKycRules, codecForList, codecForMap, + codecForNumber, codecForString, codecOptional, codecOptionalDefault, } from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { buildStorageKey, FormErrors, useLocalStorage } from "@gnu-taler/web-util/browser"; export interface ExtraInformation { data: object; + formId: string; + formVersion: number; expiration: AbsoluteTime; + errors: FormErrors<object> | undefined; } export interface DecisionRequest { @@ -56,7 +60,10 @@ export interface DecisionRequest { export const codecForExtraInformation = (): Codec<ExtraInformation> => buildCodecForObject<ExtraInformation>() .property("expiration", codecForAbsoluteTime) + .property("formId", codecForString()) + .property("formVersion", codecForNumber()) .property("data", codecForAny()) + .property("errors", codecForAny()) .build("ExtraInformation"); export const codecForDecisionRequest = (): Codec<DecisionRequest> => diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -350,7 +350,8 @@ function JumpByIdForm({ const { i18n } = useTranslationContext(); const [account, setAccount] = useState<string>(""); return ( - <form class="mt-5 sm:flex sm:items-center flex flex-col"> + <form class="mt-5 grid grid-cols-1"> + <div class="flex flex-row"> <div class="w-full sm:max-w-xs"> <input @@ -382,16 +383,17 @@ function JumpByIdForm({ </svg> </a> </div> - <div class="flex flex-row"> - <InputToggle<any, string> - threeState - name="inv" - label={i18n.str`Only investigated`} - handler={{ - onChange: onTog, - value: fitered, - }} - /> + <div class="mt-2 cursor-default"> + + <InputToggle<any, string> + threeState + name="inv" + label={i18n.str`Only investigated`} + handler={{ + onChange: onTog, + value: fitered, + }} + /> </div> </form> ); diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx @@ -15,14 +15,10 @@ */ import { assertUnreachable, - MeasureInformation, - TranslatedString, + TranslatedString } from "@gnu-taler/taler-util"; import { - FormDesign, - InternationalizationAPI, - UIHandlerId, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { @@ -30,12 +26,12 @@ import { useCurrentDecisionRequest, } from "../../hooks/decision-request.js"; import { Events } from "./Events.js"; +import { Information } from "./Information.js"; +import { Justification } from "./Justification.js"; +import { Measures } from "./Measures.js"; import { Properties } from "./Properties.js"; import { Rules } from "./Rules.js"; -import { Measures } from "./Measures.js"; -import { Justification } from "./Justification.js"; import { Summary } from "./Summary.js"; -import { Information } from "./Information.js"; export type WizardSteps = | "information" // submit more information @@ -72,33 +68,38 @@ const STEPS_ORDER_MAP = STEPS_ORDER.reduce( }, ); -export function isRulesCompleted(request: DecisionRequest): boolean { +function isInformationCompleted(request: DecisionRequest): boolean { + return request.information !== undefined && request.information.errors === undefined; +} +function isRulesCompleted(request: DecisionRequest): boolean { return request.rules !== undefined; } -export function isPropertiesCompleted(request: DecisionRequest): boolean { +function isPropertiesCompleted(request: DecisionRequest): boolean { return request.properties !== undefined; } -export function isEventsCompleted(request: DecisionRequest): boolean { +function isEventsCompleted(request: DecisionRequest): boolean { return request.custom_events !== undefined; } -export function isMeasuresCompleted(request: DecisionRequest): boolean { +function isMeasuresCompleted(request: DecisionRequest): boolean { return request.new_measures !== undefined; } -export function isJustificationCompleted(request: DecisionRequest): boolean { +function isJustificationCompleted(request: DecisionRequest): boolean { return request.keep_investigating !== undefined && !!request.justification; } export function AmlDecisionRequestWizard({ account, step, + formId, onMove, }: { account: string; + formId: string | undefined; step?: WizardSteps; onMove: (n: WizardSteps | undefined) => void; }): VNode { const { i18n } = useTranslationContext(); - const stepOrDefault = step ?? "rules"; + const stepOrDefault = step ?? "information"; const content = (function () { switch (stepOrDefault) { case "rules": @@ -112,7 +113,7 @@ export function AmlDecisionRequestWizard({ case "justification": return <Justification />; case "information": - return <Information />; + return <Information formId={formId}/>; case "summary": return <Summary account={account} onMove={onMove} />; } @@ -163,7 +164,7 @@ function WizardSteps({ information: { label: i18n.str`Information`, description: i18n.str`Add more inforamtion to the account`, - isCompleted: isRulesCompleted, + isCompleted: isInformationCompleted, }, rules: { label: i18n.str`Rules`, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Information.tsx b/packages/aml-backoffice-ui/src/pages/decision/Information.tsx @@ -1,12 +1,10 @@ +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; import { - AbsoluteTime, - Duration, - MeasureInformation, -} from "@gnu-taler/taler-util"; -import { + ErrorsSummary, FormDesign, FormMetadata, FormUI, + InputAbsoluteTime, InternationalizationAPI, onComponentUnload, UIHandlerId, @@ -14,64 +12,172 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; +import { useEffect, useState } from "preact/compat"; +import { useUiFormsContext } from "../../context/ui-forms.js"; import { preloadedForms } from "../../forms/index.js"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; -import { useUiFormsContext } from "../../context/ui-forms.js"; -import { useState } from "preact/compat"; /** * Mark for further investigation and explain decision * @param param0 * @returns */ -export function Information({}: {}): VNode { +export function Information({ + formId: defaultForm, +}: { + formId: string | undefined; +}): VNode { const { i18n } = useTranslationContext(); - const [request, _, updateRequest] = useCurrentDecisionRequest(); - - const FORM_ID = request.information?.data ?? {}; - const [formId, setFormId] = useState<string>(); + const [request] = useCurrentDecisionRequest(); + const formByState = request.information?.formId; + const [selectedFormId, setSelectedFormId] = useState<string | undefined>( + formByState ?? defaultForm, + ); const { forms } = useUiFormsContext(); - const theForm = !formId ? undefined : searchForm(i18n, forms, formId); + const theForm = !selectedFormId + ? undefined + : searchForm(i18n, forms, selectedFormId); + if (!theForm) { - return <div>form with id {formId} not found</div>; + return ( + <SelectForm + forms={forms} + onSelectForm={(f) => { + setSelectedFormId(f); + }} + /> + ); } - const form = useForm<FormType>(theForm.config, { - data: request.information?.data, - expiration: request.information?.expiration, - }); + return ( + <FillCustomerData + theForm={theForm} + changeForm={() => { + setSelectedFormId(undefined); + }} + /> + ); +} + +function FillCustomerData({ + theForm, + changeForm, +}: { + theForm: FormMetadata; + changeForm: () => void; +}): VNode { + const defaultExp = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ months: 1 }), + ); + + const [request, _, updateRequest] = useCurrentDecisionRequest(); + const [expiration, setExpiration] = useState( + request.information?.expiration ?? defaultExp, + ); + const expirationHandler = { + onChange: setExpiration, + value: expiration, + }; + + const form = useForm<object>(theForm.config, request.information?.data ?? {}); + + const data = form.status.result; + const errors = form.status.errors; onComponentUnload(() => { updateRequest({ ...request, + information: { + data, + expiration, + formId: theForm.id, + formVersion: theForm.version, + errors, + }, }); }); + const { i18n } = useTranslationContext(); + + return ( + <div> + <div class="flex flex-column justify-between"> + <div> + <h1> + Form: {theForm.id} ({theForm.version}) + </h1> + <a + class="text-indigo-700 cursor-pointer p-2 text-sm leading-6 font-semibold" + onClick={changeForm} + > + <i18n.Translate>change form</i18n.Translate> + </a> + </div> + <InputAbsoluteTime<any, any> + label={i18n.str`Expiration`} + help={i18n.str`Expiration date of the information filled in this form.`} + name="expiration" + pattern="dd/MM/yyyy" + handler={expirationHandler} + /> + </div> + <div> + {!errors ? undefined : <ErrorsSummary errors={errors as any} />} + <FormUI design={theForm.config} handler={form.handler} /> + </div> + </div> + ); +} + +function SelectForm({ + forms, + onSelectForm, +}: { + forms: FormMetadata[]; + onSelectForm: (id: string | undefined) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const design = formDesign(i18n, forms); + + const form = useForm<SelectFormType>(design, { + formId: undefined, + }); + + const fid = form.status.result?.formId; + + useEffect(() => { + onSelectForm(fid); + }, [fid]); return ( <div> - <FormUI design={theForm.config} handler={form.handler} /> + <FormUI design={design} handler={form.handler} /> </div> ); } -type FormType = { - data: Record<string, any> | undefined; - expiration: AbsoluteTime | undefined; +type SelectFormType = { + formId: string | undefined; }; const formDesign = ( i18n: InternationalizationAPI, - mi: (MeasureInformation & { id: string })[], -): FormDesign<FormType> => ({ + mi: FormMetadata[], +): FormDesign => ({ type: "single-column", fields: [ { - id: "justification" as UIHandlerId, - type: "textArea", + id: "formId" as UIHandlerId, + type: "selectOne", required: true, - label: i18n.str`Justification`, + label: i18n.str`Form:`, + help: i18n.str`Select a form to submit new information about the customer`, + choices: mi.map((f) => ({ + label: f.label, + value: f.id, + })), }, ], });