taler-typescript-core

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

commit 90e6420f0de7cd1d61e97b3dbd0a95f39f26aba5
parent a53c8c3b87f87fe8178d006e0248d82306a443f9
Author: Florian Dold <florian@dold.me>
Date:   Tue, 25 Mar 2025 01:52:38 +0100

form tweaks

Diffstat:
Mpackages/aml-backoffice-ui/src/pages/Cases.tsx | 1-
Mpackages/aml-backoffice-ui/src/pages/decision/Information.tsx | 1-
Mpackages/web-util/src/forms/Caption.tsx | 15+++++++++++++--
Mpackages/web-util/src/forms/FormProvider.tsx | 5+----
Mpackages/web-util/src/forms/fields/InputArray.tsx | 2+-
Mpackages/web-util/src/forms/fields/InputSelectOne.tsx | 42+++++++++++++++++++++++++++++++++++++++---
Mpackages/web-util/src/forms/forms-types.ts | 13++++++-------
Mpackages/web-util/src/forms/forms-ui.tsx | 30+++++++++++++++++++-----------
Mpackages/web-util/src/forms/forms-utils.ts | 2+-
Mpackages/web-util/src/forms/gana/VQF_902_11.stories.tsx | 12+++---------
Mpackages/web-util/src/forms/gana/VQF_902_11.ts | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mpackages/web-util/src/forms/gana/VQF_902_14.ts | 3+--
Mpackages/web-util/src/forms/gana/VQF_902_5.ts | 62+++++++++++++++++++++++++++++++-------------------------------
Mpackages/web-util/src/forms/gana/VQF_902_9.stories.tsx | 12+++---------
Mpackages/web-util/src/forms/gana/VQF_902_9.ts | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mpackages/web-util/src/forms/gana/taler_form_attributes.ts | 46++++++++++++++--------------------------------
Mpackages/web-util/src/hooks/useForm.ts | 35++++++++++++++++++++++-------------
17 files changed, 271 insertions(+), 159 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -391,7 +391,6 @@ function JumpByIdForm({ name: "inv", onChange: (x) => onTog(x ?? false), value: fitered, - computedProperties: {}, }} /> </div> diff --git a/packages/aml-backoffice-ui/src/pages/decision/Information.tsx b/packages/aml-backoffice-ui/src/pages/decision/Information.tsx @@ -72,7 +72,6 @@ function FillCustomerData({ const expirationHandler: UIFieldHandler<any> = { onChange: setExpiration, value: expiration, - computedProperties: {}, name: "expiration", }; diff --git a/packages/web-util/src/forms/Caption.tsx b/packages/web-util/src/forms/Caption.tsx @@ -1,5 +1,5 @@ import { TranslatedString } from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { LabelWithTooltipMaybeRequired, RenderAddon, @@ -12,9 +12,20 @@ interface Props { help?: TranslatedString; before?: Addon; after?: Addon; + hidden?: boolean; } -export function Caption({ before, after, label, tooltip, help }: Props): VNode { +export function Caption({ + hidden, + before, + after, + label, + tooltip, + help, +}: Props): VNode { + if (hidden) { + return <Fragment />; + } return ( <div class="sm:col-span-6"> {before !== undefined && <RenderAddon addon={before} />} diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx @@ -1,6 +1,5 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { ComponentChildren, VNode } from "preact"; -import { UIFormElementConfig } from "./forms-types.js"; /** * Properties for form elements set at design type. @@ -73,9 +72,7 @@ export type UIFieldHandler<T = any> = { */ formRootResult?: any; - computedProperties: { - hidden?: boolean; - }; + hidden?: boolean; }; export interface IconAddon { diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx @@ -144,7 +144,7 @@ export function InputArray( </p> )} - <div class="overflow-hidden ring-1 ring-gray-900/5 rounded-xl p-4"> + <div class="overflow-visible ring-1 ring-gray-900/5 rounded-xl p-4"> <div class="-space-y-px rounded-md bg-white "> {list.map((v, idx) => { const labelValue = diff --git a/packages/web-util/src/forms/fields/InputSelectOne.tsx b/packages/web-util/src/forms/fields/InputSelectOne.tsx @@ -1,5 +1,6 @@ +import { i18n } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; +import { useRef, useState } from "preact/hooks"; import { UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { ChoiceS } from "./InputChoiceStacked.js"; @@ -42,6 +43,8 @@ export function InputSelectOne<Choices>( ), ); + const inputRef = useRef<HTMLInputElement>(null); + const sortedChoices = [...prefChoices, ...normalChoices]; let filteredChoices = @@ -50,6 +53,11 @@ export function InputSelectOne<Choices>( : sortedChoices.filter((v) => { return regex.test(v.label); }); + + const noItems = + filter === undefined + ? undefined + : filteredChoices === undefined || !filteredChoices.length; return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -83,12 +91,19 @@ export function InputSelectOne<Choices>( <input id="combobox" autocomplete="off" + ref={inputRef} type="text" value={filter ?? ""} onChange={(e) => { setFilter(e.currentTarget.value); setDirty(true); }} + onBlur={(e) => { + setFilter(undefined); + }} + onFocus={(e) => { + setFilter(""); + }} placeholder={placeholder} class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" role="combobox" @@ -97,9 +112,14 @@ export function InputSelectOne<Choices>( /> <button type="button" + onMouseDown={(e) => { + // Input element should not lose focus + e.preventDefault(); + }} onClick={() => { setFilter(filter === undefined ? "" : undefined); setDirty(true); + inputRef.current?.focus(); }} class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" > @@ -116,13 +136,25 @@ export function InputSelectOne<Choices>( /> </svg> </button> - - {filteredChoices !== undefined && ( + {noItems && ( <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" id="options" role="listbox" > + <li class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"> + <span class="block truncate font-bold"> + <i18n.Translate>No element found</i18n.Translate> + </span> + </li> + </ul> + )} + {!noItems && filteredChoices && ( + <ul + class="absolute overflow-y-scroll z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + id="options" + role="listbox" + > {filteredChoices.map((v, idx) => { return ( <li @@ -130,6 +162,10 @@ export function InputSelectOne<Choices>( class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600" id="option-0" role="option" + onMouseDown={(e) => { + // Input element should not lose focus + e.preventDefault(); + }} onClick={() => { setFilter(undefined); onChange(v.value as any); diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -270,6 +270,12 @@ export type UIFieldElementDescription = ComputableFieldConfig & { /* ui element to show after */ addonAfterId?: string; + + /** + * Return if the field should be hidden. + * Receives the value after conversion and the root of the form. + */ + hide?: (value: any, root?: any) => boolean | undefined; }; export type UIFormFieldBaseConfig = UIFieldElementDescription & { @@ -281,13 +287,6 @@ export type UIFormFieldBaseConfig = UIFieldElementDescription & { */ converterId?: string; - // FIXME: deprectate this and move to `computableFieldConfig` - /* - return if the field should be hidden. - receives the value after conversion and the root of the form. - */ - hide?: (value: any, root?: any) => boolean | undefined; - /* return an error message if the value is not valid. should returns un undefined if there is no error. diff --git a/packages/web-util/src/forms/forms-ui.tsx b/packages/web-util/src/forms/forms-ui.tsx @@ -110,7 +110,7 @@ export function FormUI<T>({ if (!section) return <Fragment />; return ( <DoubleColumnFormSectionUI - sectionKey={i} + sectionKey={`${i}`} key={i} name={name} section={section} @@ -150,7 +150,7 @@ export function DoubleColumnFormSectionUI<T>({ focus, handler, }: { - sectionKey: number | string; + sectionKey: string; name: string; handler: FormFieldStateMap; section: DoubleColumnFormSection; @@ -163,15 +163,25 @@ export function DoubleColumnFormSectionUI<T>({ section.fields, handler, ); + console.log(`sectionKey`, sectionKey); + console.log(`hiddenSections`, handler.hiddenSections); const allHidden = fs.every((v) => { // FIXME: Handler should probably be present for all Form UI fields, not just some. if ("handler" in v.properties) { - return v.properties.handler?.computedProperties.hidden; + return v.properties.hidden ?? false; } return false; }); - if (allHidden) { - return <RenderAllFieldsByUiConfig key="fields" fields={fs} focus={focus} />; + const sectionHidden = handler.hiddenSections.has(sectionKey); + if (allHidden || sectionHidden) { + return ( + <RenderAllFieldsByUiConfig + hidden={true} + key="fields" + fields={fs} + focus={focus} + /> + ); } return ( <form @@ -231,9 +241,11 @@ export function SingleColumnFormSectionUI<T>({ export function RenderAllFieldsByUiConfig({ focus, fields, + hidden, }: { fields: UIFormField[]; focus?: boolean; + hidden?: boolean; }): VNode { return create( Fragment, @@ -242,15 +254,11 @@ export function RenderAllFieldsByUiConfig({ const Component = UIFormConfiguration[ field.type ] as FieldComponentFunction<any>; - let computedHidden = false; - if ("handler" in field.properties) { - computedHidden = - field.properties.handler?.computedProperties.hidden ?? false; - } const p = { ...field.properties, focus: !!focus && i === 0, - hidden: computedHidden, + hidden: + hidden || ("hidden" in field.properties && field.properties.hidden), }; return <Component key={i} {...p} />; diff --git a/packages/web-util/src/forms/forms-utils.ts b/packages/web-util/src/forms/forms-utils.ts @@ -92,7 +92,7 @@ export function convertFormConfigToUiField( } } const uiKey = `${parentKey}.${fieldIndex}`; - const handler = form[uiKey]; + const handler = form.fieldHandlers[uiKey]; const name = handler.name; // FIXME: first computed prop, all should be computed const hidden = diff --git a/packages/web-util/src/forms/gana/VQF_902_11.stories.tsx b/packages/web-util/src/forms/gana/VQF_902_11.stories.tsx @@ -19,23 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AbsoluteTime, i18n, setupI18n } from "@gnu-taler/taler-util"; +import { i18n, setupI18n } from "@gnu-taler/taler-util"; import * as tests from "../../tests/hook.js"; import { DefaultForm as TestedComponent } from "../forms-ui.js"; import { VQF_902_11 } from "./VQF_902_11.js"; -import { TalerFormAttributes } from "./taler_form_attributes.js"; export default { - title: "VQF 902 11e", + title: "vqf_902_11", }; setupI18n("en", {}); -type TargetObject = {}; -const initial: TargetObject = { - [TalerFormAttributes.SIGNATURE]: "The officer", - [TalerFormAttributes.SIGN_DATE]: AbsoluteTime.now(), -}; export const EmptyForm = tests.createExample(TestedComponent, { - initial, + initial: {}, design: VQF_902_11(i18n), }); diff --git a/packages/web-util/src/forms/gana/VQF_902_11.ts b/packages/web-util/src/forms/gana/VQF_902_11.ts @@ -9,13 +9,34 @@ export function VQF_902_11( ): DoubleColumnFormDesign { return { type: "double-column", + title: i18n.str`Establishment of the controlling person`, sections: [ { - title: i18n.str`Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange (K)`, - description: i18n.str`(for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners)`, + title: "Information about submitter", fields: [ { - id: TalerFormAttributes.CONTROLLING_ENTITY_CONTRACTING_PARTNER, + id: TalerFormAttributes.SUBMITTED_BY, + label: i18n.str`Who is filling out the form?`, + type: "choiceHorizontal", + required: true, + choices: [ + { + label: "Taler Operations staff", + value: "AML_OFFICER", + }, + { + label: "The customer", + value: "CUSTOMER", + }, + ], + }, + ], + }, + { + title: "Identity of the contracting partner", + fields: [ + { + id: TalerFormAttributes.IDENTITY_CONTRACTING_PARTNER, label: i18n.str`Contracting partner`, type: "textArea", required: true, @@ -27,23 +48,23 @@ export function VQF_902_11( fields: [ { id: TalerFormAttributes.CONTROLLING_ENTITY_LEVEL, - label: i18n.str`Level`, + label: i18n.str`Controlling entity reason:`, type: "choiceStacked", choices: [ { - value: "25_MORE_RIGHTS", + value: "HAS_25_MORE_RIGHTS", label: i18n.str`Holding 25% or more`, - description: i18n.str`the person(s) listed below is/are holding 25% or more of the contracting partner's shares (capital shares or voting rights)`, + description: i18n.str`The person(s) listed below is/are holding 25% or more of the contracting partner's shares (capital shares or voting rights)`, }, { value: "OTHER_WAY", label: i18n.str`Other way`, - description: i18n.str`if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below is/are controlling the contracting partner in other ways`, + description: i18n.str`If the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below is/are controlling the contracting partner in other ways`, }, { value: "DIRECTOR", label: i18n.str`Managing director`, - description: i18n.str`in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the managing director(s)`, + description: i18n.str`In case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the managing director(s)`, }, ], required: true, @@ -58,18 +79,16 @@ export function VQF_902_11( } return undefined; }, - labelFieldId: - TalerFormAttributes.IDENTITY_FULL_NAME, + labelFieldId: TalerFormAttributes.FULL_NAME, fields: [ { - id: TalerFormAttributes.CONTROLLING_ENTITY_FULL_NAME, + id: TalerFormAttributes.FULL_NAME, label: i18n.str`Full name`, - help: i18n.str`Surname(s) and first name(s)`, type: "text", required: true, }, { - id: TalerFormAttributes.CONTROLLING_ENTITY_DOMICILE, + id: TalerFormAttributes.DOMICILE_ADDRESS, label: i18n.str`Actual address of domicile`, type: "textArea", required: true, @@ -90,7 +109,11 @@ export function VQF_902_11( ], }, { - title: i18n.str`It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery)`, + title: i18n.str`Signature(s)`, + description: i18n.str`It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery)`, + hide(root) { + return root[TalerFormAttributes.SUBMITTED_BY] != "CUSTOMER"; + }, fields: [ { type: "caption", @@ -98,10 +121,9 @@ export function VQF_902_11( }, { id: TalerFormAttributes.SIGNATURE, - label: i18n.str`Signature`, + label: i18n.str`Signature(s)`, type: "text", required: true, - disabled: true, }, { id: TalerFormAttributes.SIGN_DATE, @@ -110,7 +132,26 @@ export function VQF_902_11( placeholder: "dd/MM/yyyy", pattern: "dd/MM/yyyy", required: true, - disabled: true, + }, + ], + }, + { + title: i18n.str`Signed Declaration`, + description: i18n.str`Signed declaration by the customer`, + hide(root) { + return root[TalerFormAttributes.SUBMITTED_BY] != "AML_OFFICER"; + }, + fields: [ + { + type: "caption", + label: i18n.str`The uploaded document must contain the customer's signature on the beneficial owner declaration.`, + }, + { + id: TalerFormAttributes.ATTACHMENT_SIGNED_DOCUMENT, + label: i18n.str`Signed Document`, + type: "file", + accept: "application/pdf", + required: true, }, ], }, diff --git a/packages/web-util/src/forms/gana/VQF_902_14.ts b/packages/web-util/src/forms/gana/VQF_902_14.ts @@ -46,14 +46,13 @@ export function VQF_902_14( value: "OTHER", label: i18n.str`Other, which?`, }, - // ], required: true, }, { id: TalerFormAttributes.INCRISK_MEANS_OTHER, type: "text", - label: i18n.str`Means clarification`, + label: i18n.str`Other means of clarification:`, required: true, hide(value, root) { return root[TalerFormAttributes.INCRISK_MEANS] !== "OTHER"; diff --git a/packages/web-util/src/forms/gana/VQF_902_5.ts b/packages/web-util/src/forms/gana/VQF_902_5.ts @@ -16,7 +16,7 @@ export function VQF_902_5( { id: TalerFormAttributes.BIZREL_PROFESSION, label: i18n.str`Profession, business activities, etc. (former, current, potentially planned)`, - type: "text", + type: "textArea", required: false, }, ], @@ -36,16 +36,29 @@ export function VQF_902_5( title: i18n.str`Origin of the deposited assets involved`, fields: [ { - id: TalerFormAttributes.BIZREL_ORIGIN_NATURE, - label: i18n.str`Nature`, - type: "text", + id: TalerFormAttributes.BIZREL_HAVE_ASSETS, + label: i18n.str`Will the the customer deposit assets with Taler Operations S.A.?`, + type: "choiceHorizontal", required: true, + choices: [ + { + label: "Yes", + value: true, + }, + { + label: "No", + value: false, + }, + ], }, { - id: TalerFormAttributes.BIZREL_ORIGIN_AMOUNT, - label: i18n.str`Amount`, - type: "integer", + id: TalerFormAttributes.BIZREL_ORIGIN_NATURE, + label: i18n.str`Nature, amount and currency of deposited assets.`, + type: "textArea", required: true, + hide(value, root) { + return !root[TalerFormAttributes.BIZREL_HAVE_ASSETS]; + }, }, { id: TalerFormAttributes.BIZREL_ORIGIN_CATEGORY, @@ -61,6 +74,9 @@ export function VQF_902_5( { label: i18n.str`Other`, value: "OTHER" }, ], required: true, + hide(value, root) { + return !root[TalerFormAttributes.BIZREL_HAVE_ASSETS]; + }, }, { id: TalerFormAttributes.BIZREL_ORIGIN_CATEGORY_OTHER, @@ -78,6 +94,11 @@ export function VQF_902_5( label: i18n.str`Detail description of the origings/economical background of the assets involved in the business relationship`, type: "textArea", required: false, + hide(value, root) { + return ( + root[TalerFormAttributes.BIZREL_ORIGIN_CATEGORY] !== "OTHER" + ); + }, }, ], }, @@ -87,40 +108,19 @@ export function VQF_902_5( { id: TalerFormAttributes.BIZREL_PURPOSE, label: i18n.str`Purpose of the business relationship`, - type: "text", + type: "textArea", required: false, }, { id: TalerFormAttributes.BIZREL_DEVELOPMENT, label: i18n.str`Information on the planned development of the business relationship and the assets`, - type: "text", + type: "textArea", required: false, }, { id: TalerFormAttributes.BIZREL_FINANCIAL_VOLUME, label: i18n.str`Detail on usual business volume`, - type: "text", - required: false, - }, - { - id: TalerFormAttributes.BIZREL_FINANCIAL_BENEFICIARIES_ADDRESS, - label: i18n.str`Address`, - help: i18n.str`Information of the beneficiary`, - type: "text", - required: false, - }, - { - id: TalerFormAttributes.BIZREL_FINANCIAL_BENEFICIARIES_BANK_ACCOUNT, - label: i18n.str`Bank account`, - help: i18n.str`Information of the beneficiary`, - type: "text", - required: false, - }, - { - id: TalerFormAttributes.BIZREL_FINANCIAL_BENEFICIARIES_FULL_NAME, - label: i18n.str`Full name`, - help: i18n.str`Information of the beneficiary`, - type: "text", + type: "textArea", required: false, }, ], diff --git a/packages/web-util/src/forms/gana/VQF_902_9.stories.tsx b/packages/web-util/src/forms/gana/VQF_902_9.stories.tsx @@ -19,23 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AbsoluteTime, i18n, setupI18n } from "@gnu-taler/taler-util"; +import { i18n, setupI18n } from "@gnu-taler/taler-util"; import * as tests from "../../tests/hook.js"; import { DefaultForm as TestedComponent } from "../forms-ui.js"; import { VQF_902_9 } from "./VQF_902_9.js"; -import { TalerFormAttributes } from "./taler_form_attributes.js"; export default { - title: "VQF 902 9e", + title: "vqf_902_9", }; setupI18n("en", {}); -type TargetObject = {}; -const initial: TargetObject = { - [TalerFormAttributes.SIGNATURE]: "The officer", - [TalerFormAttributes.SIGN_DATE]: AbsoluteTime.now(), -}; export const EmptyForm = tests.createExample(TestedComponent, { - initial, design: VQF_902_9(i18n), + initial: {}, }); diff --git a/packages/web-util/src/forms/gana/VQF_902_9.ts b/packages/web-util/src/forms/gana/VQF_902_9.ts @@ -10,9 +10,31 @@ export function VQF_902_9( ): DoubleColumnFormDesign { return { type: "double-column", + title: i18n.str`Declaration of identity of the beneficial owner`, sections: [ { - title: i18n.str`Declaration of identity of the beneficial owner (A)`, + title: "Information about submitter", + fields: [ + { + id: TalerFormAttributes.SUBMITTED_BY, + label: i18n.str`Who is filling out the form?`, + type: "choiceHorizontal", + required: true, + choices: [ + { + label: "Taler Operations staff", + value: "AML_OFFICER", + }, + { + label: "The customer", + value: "CUSTOMER", + }, + ], + }, + ], + }, + { + title: "Identity of the contracting partner", fields: [ { id: TalerFormAttributes.IDENTITY_CONTRACTING_PARTNER, @@ -23,12 +45,12 @@ export function VQF_902_9( ], }, { - title: i18n.str`The contracting partner hereby declares that:`, + title: i18n.str`Beneficial owner details`, fields: [ { id: TalerFormAttributes.IDENTITY_LIST, - label: i18n.str`Persons`, - help: i18n.str`the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below`, + label: i18n.str`Beneficial owner(s)`, + help: i18n.str`The person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below`, type: "array", validator(persons) { if (!persons || persons.length < 1) { @@ -36,30 +58,30 @@ export function VQF_902_9( } return undefined; }, - labelFieldId: TalerFormAttributes.IDENTITY_FULL_NAME, + labelFieldId: TalerFormAttributes.FULL_NAME, fields: [ { - id: TalerFormAttributes.IDENTITY_FULL_NAME, + id: TalerFormAttributes.FULL_NAME, label: i18n.str`Full name`, type: "text", required: true, }, { - id: TalerFormAttributes.IDENTITY_DOMICILE, - label: i18n.str`Domicile`, + id: TalerFormAttributes.DOMICILE_ADDRESS, + label: i18n.str`Domicile address`, type: "textArea", required: true, }, { - id: TalerFormAttributes.IDENTITY_BIRTHDATE, - label: i18n.str`Birhtdate`, + id: TalerFormAttributes.DATE_OF_BIRTH, + label: i18n.str`Date of birth`, type: "isoDateText", placeholder: "dd/MM/yyyy", pattern: "dd/MM/yyyy", required: true, }, { - id: TalerFormAttributes.IDENTITY_NATIONALITY, + id: TalerFormAttributes.NATIONALITY, label: i18n.str`Nationality`, type: "selectOne", choices: countryNationalityList(i18n), @@ -71,7 +93,11 @@ export function VQF_902_9( ], }, { - title: i18n.str`It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery)`, + title: i18n.str`Signature(s)`, + description: i18n.str`It is a criminal offence to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, documents forgery)`, + hide(root) { + return root[TalerFormAttributes.SUBMITTED_BY] != "CUSTOMER"; + }, fields: [ { type: "caption", @@ -79,10 +105,9 @@ export function VQF_902_9( }, { id: TalerFormAttributes.SIGNATURE, - label: i18n.str`Signature`, + label: i18n.str`Signature(s)`, type: "text", required: true, - disabled: true, }, { id: TalerFormAttributes.SIGN_DATE, @@ -91,7 +116,26 @@ export function VQF_902_9( placeholder: "dd/MM/yyyy", pattern: "dd/MM/yyyy", required: true, - disabled: true, + }, + ], + }, + { + title: i18n.str`Signed Declaration`, + description: i18n.str`Signed declaration by the customer`, + hide(root) { + return root[TalerFormAttributes.SUBMITTED_BY] != "AML_OFFICER"; + }, + fields: [ + { + type: "caption", + label: i18n.str`The uploaded document must contain the customer's signature on the beneficial owner declaration.`, + }, + { + id: TalerFormAttributes.ATTACHMENT_SIGNED_DOCUMENT, + label: i18n.str`Signed Document`, + type: "file", + accept: "application/pdf", + required: true, }, ], }, diff --git a/packages/web-util/src/forms/gana/taler_form_attributes.ts b/packages/web-util/src/forms/gana/taler_form_attributes.ts @@ -430,6 +430,12 @@ export const TalerFormAttributes = { */ BIZREL_INCOME: "BIZREL_INCOME" as UIHandlerId, /** + * Description: Does the customer have assets that will be deposited? + * + * GANA Type: Boolean + */ + BIZREL_HAVE_ASSETS: "BIZREL_HAVE_ASSETS" as UIHandlerId, + /** * Description: Nature of the involved assets. * * GANA Type: String @@ -532,35 +538,23 @@ export const TalerFormAttributes = { */ IDENTITY_CONTRACTING_PARTNER: "IDENTITY_CONTRACTING_PARTNER" as UIHandlerId, /** - * Description: The beneficial owners of the assets involved in the business relationship. - * - * GANA Type: Form<VQF_902_9_identity>[] - */ - IDENTITY_LIST: "IDENTITY_LIST" as UIHandlerId, - /** - * Description: - * - * GANA Type: String - */ - IDENTITY_FULL_NAME: "IDENTITY_FULL_NAME" as UIHandlerId, - /** * Description: * - * GANA Type: AbsoluteDate + * GANA Type: File */ - IDENTITY_BIRTHDATE: "IDENTITY_BIRTHDATE" as UIHandlerId, + ATTACHMENT_SIGNED_DOCUMENT: "ATTACHMENT_SIGNED_DOCUMENT" as UIHandlerId, /** - * Description: + * Description: The beneficial owners of the assets involved in the business relationship. * - * GANA Type: CountryCode + * GANA Type: Form<VQF_902_9_identity>[] */ - IDENTITY_NATIONALITY: "IDENTITY_NATIONALITY" as UIHandlerId, + IDENTITY_LIST: "IDENTITY_LIST" as UIHandlerId, /** - * Description: + * Description: Party that is filling out the form. * - * GANA Type: ResidentialAddress + * GANA Type: 'AML_OFFICER' | 'CUSTOMER' */ - IDENTITY_DOMICILE: "IDENTITY_DOMICILE" as UIHandlerId, + SUBMITTED_BY: "SUBMITTED_BY" as UIHandlerId, /** * Description: * @@ -574,18 +568,6 @@ export const TalerFormAttributes = { */ CONTROLLING_ENTITY_LEVEL: "CONTROLLING_ENTITY_LEVEL" as UIHandlerId, /** - * Description: - * - * GANA Type: String - */ - CONTROLLING_ENTITY_FULL_NAME: "CONTROLLING_ENTITY_FULL_NAME" as UIHandlerId, - /** - * Description: - * - * GANA Type: ResidentialAddress - */ - CONTROLLING_ENTITY_DOMICILE: "CONTROLLING_ENTITY_DOMICILE" as UIHandlerId, - /** * Description: Is a third person the beneficial owner of the assets? * * GANA Type: Boolean diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -33,7 +33,10 @@ import { /** * Mapping from the key of a form field to the state of the form field. */ -export type FormFieldStateMap = { [x: string]: UIFieldHandler }; +export type FormFieldStateMap = { + fieldHandlers: { [x: string]: UIFieldHandler }; + hiddenSections: Set<string | number>; +}; export type FormValues<T> = { [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>; @@ -219,7 +222,10 @@ function constructFormHandler<T>( result: FormStatus<T>; errors: FormErrors<T> | undefined; } { - let handler: FormFieldStateMap = {}; + let handler: FormFieldStateMap = { + fieldHandlers: {}, + hiddenSections: new Set(), + }; let result = {} as FormStatus<T>; let errors: FormErrors<T> | undefined = undefined; @@ -229,17 +235,20 @@ function constructFormHandler<T>( handlerUiPath: string, ): void { if (!("id" in formElement)) { - return; + return undefined; } + + let field: UIFieldHandler; + const path = formElement.id.split("."); const currentValue = getValueFromPath(formValue as any, path, undefined); // compute prop based on state const hidden = - hiddenSection === true || - formElement.hidden === true || - (formElement.hide && formElement.hide(currentValue, result) === true); + hiddenSection || + formElement.hidden || + (formElement.hide && formElement.hide(currentValue, result)); const currentError: ErrorAndLabel | undefined = !hidden ? checkFormFieldIsValid(formElement, currentValue, i18n) @@ -253,28 +262,28 @@ function constructFormHandler<T>( const updated = setValueIntoPath(formValue, path, newValue) ?? {}; onValueChange(updated); } - - const field: UIFieldHandler = { + field = { name: formElement.id, error: currentError?.message, value: currentValue, onChange: updater, formRootResult: result, - computedProperties: { - hidden, - }, + hidden, }; - - handler[handlerUiPath] = field; if (!hidden) { result = setValueIntoPath(result, path, field.value) ?? {}; } + + handler.fieldHandlers[handlerUiPath] = field; } switch (design.type) { case "double-column": { design.sections.forEach((sec, secIndex) => { const hidden = sec.hide && sec.hide(result); + if (hidden) { + handler.hiddenSections.add(`${secIndex}`); + } sec.fields.forEach((f, fieldIndex) => createFieldHandler(f, hidden, `${secIndex}.${fieldIndex}`), );