commit 6cb78d3c8b8bfe95e153946e5d0da8114bd2039e
parent 966a411197dee9225ed7442c2145b052d674c1bb
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 17 Mar 2025 17:18:19 -0300
information form
Diffstat:
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,
+ })),
},
],
});