aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-05-03 18:23:01 -0300
committerSebastian <sebasjm@gmail.com>2024-05-03 18:23:01 -0300
commit35fee72ef3d75b7a9681353ab7a1ca5bacff150e (patch)
treeab5fd0588c50d389a24651a18ca8756b39cd7772 /packages/aml-backoffice-ui/src
parent5db79542f34477911f14f2e454925368c0d2c33f (diff)
downloadwallet-core-35fee72ef3d75b7a9681353ab7a1ca5bacff150e.tar.xz
form implemented, moving functions to web-utils some final testing still pedning
Diffstat (limited to 'packages/aml-backoffice-ui/src')
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx7
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-forms.ts439
-rw-r--r--packages/aml-backoffice-ui/src/forms.json539
-rw-r--r--packages/aml-backoffice-ui/src/forms/index.ts3
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts8
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts249
-rw-r--r--packages/aml-backoffice-ui/src/index.html1
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx19
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx175
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx31
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/settings.json4
-rw-r--r--packages/aml-backoffice-ui/src/utils/converter.ts73
15 files changed, 871 insertions, 683 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
index ae8b574b6..e9be84441 100644
--- a/packages/aml-backoffice-ui/src/App.tsx
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -19,6 +19,7 @@ import {
ExchangeApiProvider,
Loading,
TranslationProvider,
+ UiForms,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
@@ -29,7 +30,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 { UiForms, fetchUiForms } from "./context/ui-forms.js";
+import { UiFormsProvider, fetchUiForms } from "./context/ui-forms.js";
const WITH_LOCAL_STORAGE_CACHE = false;
@@ -84,7 +85,9 @@ export function App(): VNode {
}}
>
<BrowserHashNavigationProvider>
- <Routing />
+ <UiFormsProvider value={forms}>
+ <Routing />
+ </UiFormsProvider>
</BrowserHashNavigationProvider>
</SWRConfig>
</ExchangeApiProvider>
diff --git a/packages/aml-backoffice-ui/src/context/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts
index 9cf6125c9..3a25234d2 100644
--- a/packages/aml-backoffice-ui/src/context/ui-forms.ts
+++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts
@@ -14,20 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- buildCodecForObject,
- buildCodecForUnion,
- Codec,
- codecForBoolean,
- codecForConstString,
- codecForList,
- codecForNumber,
- codecForString,
- codecForTimestamp,
- codecOptional,
- Integer,
- TalerProtocolTimestamp,
-} from "@gnu-taler/taler-util";
+import { codecForUIForms, UiForms } from "@gnu-taler/web-util/browser";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
@@ -60,431 +47,7 @@ export const UiFormsProvider = ({
});
};
-export type FormMetadata = {
- label: string;
- id: string;
- version: number;
- config: FlexibleForm;
-};
-
-type FlexibleForm = DoubleColumnForm;
-
-export interface DoubleColumnForm {
- type: "double-column";
- design: Array<DoubleColumnFormSection>;
- // behavior?: (form: Partial<T>) => FormState<T>;
-}
-
-export type DoubleColumnFormSection = {
- title: string;
- description?: string;
- fields: UIFormFieldConfig[];
-};
-
-// export interface BaseForm {
-// state: TalerExchangeApi.AmlState;
-// threshold: AmountJson;
-// }
-
-export interface UiForms {
- // Where libeufin backend is localted
- // default: window.origin without "webui/"
- forms: Array<FormMetadata>;
-}
-
-export type UIFormFieldConfig =
- | UIFormFieldConfigAbsoluteTime
- | UIFormFieldConfigAmount
- | UIFormFieldConfigArray
- | UIFormFieldConfigCaption
- | UIFormFieldConfigChoiseHorizontal
- | UIFormFieldConfigChoiseStacked
- | UIFormFieldConfigFile
- | UIFormFieldConfigGroup
- | UIFormFieldConfigInteger
- | UIFormFieldConfigSelectMultiple
- | UIFormFieldConfigSelectOne
- | UIFormFieldConfigText
- | UIFormFieldConfigTextArea
- | UIFormFieldConfigToggle;
-
-type UIFormFieldConfigAbsoluteTime = {
- type: "absoluteTime";
- properties: UIFormFieldBaseConfig & {
- max?: TalerProtocolTimestamp;
- min?: TalerProtocolTimestamp;
- pattern: string;
- };
-};
-
-type UIFormFieldConfigAmount = {
- type: "amount";
- properties: UIFormFieldBaseConfig & {
- max?: Integer;
- min?: Integer;
- currency: string;
- };
-};
-
-type UIFormFieldConfigArray = {
- type: "array";
- properties: UIFormFieldBaseConfig & {
- // id of the field shown when the array is collapsed
- labelFieldId: UIHandlerId;
- fields: UIFormFieldConfig[];
- };
-};
-
-type UIFormFieldConfigCaption = {
- type: "caption";
- properties: UIFieldBaseDescription;
-};
-
-type UIFormFieldConfigGroup = {
- type: "group";
- properties: UIFieldBaseDescription & {
- fields: UIFormFieldConfig[];
- };
-};
-
-type UIFormFieldConfigChoiseHorizontal = {
- type: "choiceHorizontal";
- properties: UIFormFieldBaseConfig & {
- choices: Array<SelectUiChoice>;
- };
-};
-
-type UIFormFieldConfigChoiseStacked = {
- type: "choiceStacked";
- properties: UIFormFieldBaseConfig & {
- choices: Array<SelectUiChoice>;
- };
-};
-
-type UIFormFieldConfigFile = {
- type: "file";
- properties: UIFormFieldBaseConfig & {
- maxBytes?: Integer;
- minBytes?: Integer;
- // comma-separated list of one or more file types
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers
- accept?: string;
- };
-};
-type UIFormFieldConfigInteger = {
- type: "integer";
- properties: UIFormFieldBaseConfig & {
- max?: Integer;
- min?: Integer;
- };
-};
-
-interface SelectUiChoice {
- label: string;
- description?: string;
- value: string;
-}
-
-type UIFormFieldConfigSelectMultiple = {
- type: "selectMultiple";
- properties: UIFormFieldBaseConfig & {
- max?: Integer;
- min?: Integer;
- unique?: boolean;
- choices: Array<SelectUiChoice>;
- };
-};
-type UIFormFieldConfigSelectOne = {
- type: "selectOne";
- properties: UIFormFieldBaseConfig & {
- choices: Array<SelectUiChoice>;
- };
-};
-type UIFormFieldConfigText = {
- type: "text";
- properties: UIFormFieldBaseConfig;
-};
-type UIFormFieldConfigTextArea = {
- type: "textArea";
- properties: UIFormFieldBaseConfig;
-};
-type UIFormFieldConfigToggle = {
- type: "toggle";
- properties: UIFormFieldBaseConfig;
-};
-
-export type UIFieldBaseDescription = {
- /* label if the field, visible for the user */
- label: string;
- /* long text to be shown on user demand */
- tooltip?: string;
-
- /* short text to be shown close to the field */
- help?: string;
-
- /* name of the field, useful for a11y */
- name: string;
-
- /* if the field should be initialy hidden */
- hidden?: boolean;
- /* ui element to show before */
- addonBeforeId?: string;
- /* ui element to show after */
- addonAfterId?: string;
-};
-
-export type UIFormFieldBaseConfig = UIFieldBaseDescription & {
- /* example to be shown inside the field */
- placeholder?: string;
-
- /* show a mark as required */
- required?: boolean;
-
- /* readonly and dim */
- disabled?: boolean;
-
- /* conversion id to conver the string into the value type
- the id should be known to the ui impl
- */
- converterId?: string;
-
- /* property id of the form */
- id: UIHandlerId;
-};
-
-declare const __handlerId: unique symbol;
-export type UIHandlerId = string & { [__handlerId]: true };
-
-// FIXME: validate well formed ui field id
-const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>;
-
-const codecForUIFormFieldBaseDescriptionTemplate = <
- T extends UIFieldBaseDescription,
->() =>
- buildCodecForObject<T>()
- .property("addonAfterId", codecOptional(codecForString()))
- .property("addonBeforeId", codecOptional(codecForString()))
- .property("hidden", codecOptional(codecForBoolean()))
- .property("help", codecOptional(codecForString()))
- .property("label", codecForString())
- .property("name", codecForString())
- .property("tooltip", codecOptional(codecForString()));
-
-const codecForUIFormFieldBaseConfigTemplate = <
- T extends UIFormFieldBaseConfig,
->() =>
- codecForUIFormFieldBaseDescriptionTemplate<T>()
- .property("id", codecForUiFieldId())
- .property("converterId", codecOptional(codecForString()))
- .property("disabled", codecOptional(codecForBoolean()))
- .property("required", codecOptional(codecForBoolean()))
- .property("placeholder", codecOptional(codecForString()));
-const codecForUIFormFieldBaseConfig = (): Codec<UIFormFieldBaseConfig> =>
- codecForUIFormFieldBaseConfigTemplate().build("UIFieldToggleProperties");
-
-const codecForUIFormFieldAbsoluteTimeConfig = (): Codec<
- UIFormFieldConfigAbsoluteTime["properties"]
-> =>
- codecForUIFormFieldBaseConfigTemplate<
- UIFormFieldConfigAbsoluteTime["properties"]
- >()
- .property("pattern", codecForString())
- .property("max", codecOptional(codecForTimestamp))
- .property("min", codecOptional(codecForTimestamp))
- .build("UIFormFieldConfigAbsoluteTime.properties");
-
-const codecForUiFormFieldAbsoluteTime =
- (): Codec<UIFormFieldConfigAbsoluteTime> =>
- buildCodecForObject<UIFormFieldConfigAbsoluteTime>()
- .property("type", codecForConstString("absoluteTime"))
- .property("properties", codecForUIFormFieldAbsoluteTimeConfig())
- .build("UIFormFieldConfigAbsoluteTime");
-
-const codecForUIFormFieldAmountConfig = (): Codec<
- UIFormFieldConfigAmount["properties"]
-> =>
- codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigAmount["properties"]>()
- .property("currency", codecForString())
- .property("max", codecOptional(codecForNumber()))
- .property("min", codecOptional(codecForNumber()))
- .build("UIFormFieldConfigAmount.properties");
-
-const codecForUiFormFieldAmount = (): Codec<UIFormFieldConfigAmount> =>
- buildCodecForObject<UIFormFieldConfigAmount>()
- .property("type", codecForConstString("amount"))
- .property("properties", codecForUIFormFieldAmountConfig())
- .build("UIFormFieldConfigAmount");
-
-const codecForUIFormFieldArrayConfig = (): Codec<
- UIFormFieldConfigArray["properties"]
-> =>
- codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigArray["properties"]>()
- .property("labelFieldId", codecForUiFieldId())
- .property("fields", codecForList(codecForUiFormField()))
- .build("UIFormFieldConfigArray.properties");
-
-const codecForUiFormFieldArray = (): Codec<UIFormFieldConfigArray> =>
- buildCodecForObject<UIFormFieldConfigArray>()
- .property("type", codecForConstString("array"))
- .property("properties", codecForUIFormFieldArrayConfig())
- .build("UIFormFieldConfigArray");
-
-const codecForUiFormFieldCaption = (): Codec<UIFormFieldConfigCaption> =>
- buildCodecForObject<UIFormFieldConfigCaption>()
- .property("type", codecForConstString("caption"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigCaption");
-
-const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> =>
- buildCodecForObject<SelectUiChoice>()
- .property("description", codecForString())
- .property("label", codecForString())
- .property("value", codecForString())
- .build("SelectUiChoice");
-
-const codecForUIFormFieldWithChoiseConfig = (): Codec<
- UIFormFieldConfigChoiseHorizontal["properties"]
-> =>
- codecForUIFormFieldBaseConfigTemplate<
- UIFormFieldConfigChoiseHorizontal["properties"]
- >()
- .property("choices", codecForList(codecForUiFormSelectUiChoice()))
- .build("UIFormFieldConfigChoiseHorizontal.properties");
-
-const codecForUiFormFieldChoiceHorizontal =
- (): Codec<UIFormFieldConfigChoiseHorizontal> =>
- buildCodecForObject<UIFormFieldConfigChoiseHorizontal>()
- .property("type", codecForConstString("choiceHorizontal"))
- .property("properties", codecForUIFormFieldWithChoiseConfig())
- .build("UIFormFieldConfigChoiseHorizontal");
-
-const codecForUiFormFieldChoiceStacked =
- (): Codec<UIFormFieldConfigChoiseStacked> =>
- buildCodecForObject<UIFormFieldConfigChoiseStacked>()
- .property("type", codecForConstString("choiceStacked"))
- .property("properties", codecForUIFormFieldWithChoiseConfig())
- .build("UIFormFieldConfigChoiseStacked");
-
-const codecForUiFormFieldFile = (): Codec<UIFormFieldConfigFile> =>
- buildCodecForObject<UIFormFieldConfigFile>()
- .property("type", codecForConstString("file"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigFile");
-
-const codecForUIFormFieldWithFieldsConfig = (): Codec<
- UIFormFieldConfigGroup["properties"]
-> =>
- codecForUIFormFieldBaseDescriptionTemplate<
- UIFormFieldConfigGroup["properties"]
- >()
- .property("fields", codecForList(codecForUiFormField()))
- .build("UIFormFieldConfigGroup.properties");
-
-const codecForUiFormFieldGroup = (): Codec<UIFormFieldConfigGroup> =>
- buildCodecForObject<UIFormFieldConfigGroup>()
- .property("type", codecForConstString("group"))
- .property("properties", codecForUIFormFieldWithFieldsConfig())
- .build("UiFormFieldGroup");
-
-const codecForUiFormFieldInteger = (): Codec<UIFormFieldConfigInteger> =>
- buildCodecForObject<UIFormFieldConfigInteger>()
- .property("type", codecForConstString("integer"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigInteger");
-
-const codecForUIFormFieldSelectMultipleConfig = (): Codec<
- UIFormFieldConfigSelectMultiple["properties"]
-> =>
- codecForUIFormFieldBaseConfigTemplate<
- UIFormFieldConfigSelectMultiple["properties"]
- >()
- .property("max", codecOptional(codecForNumber()))
- .property("min", codecOptional(codecForNumber()))
- .property("unique", codecOptional(codecForBoolean()))
- .property("choices", codecForList(codecForUiFormSelectUiChoice()))
- .build("UIFormFieldConfigSelectMultiple.properties");
-
-const codecForUiFormFieldSelectMultiple =
- (): Codec<UIFormFieldConfigSelectMultiple> =>
- buildCodecForObject<UIFormFieldConfigSelectMultiple>()
- .property("type", codecForConstString("selectMultiple"))
- .property("properties", codecForUIFormFieldSelectMultipleConfig())
- .build("UiFormFieldSelectMultiple");
-
-const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldConfigSelectOne> =>
- buildCodecForObject<UIFormFieldConfigSelectOne>()
- .property("type", codecForConstString("selectOne"))
- .property("properties", codecForUIFormFieldWithChoiseConfig())
- .build("UIFormFieldConfigSelectOne");
-
-const codecForUiFormFieldText = (): Codec<UIFormFieldConfigText> =>
- buildCodecForObject<UIFormFieldConfigText>()
- .property("type", codecForConstString("text"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigText");
-
-const codecForUiFormFieldTextArea = (): Codec<UIFormFieldConfigTextArea> =>
- buildCodecForObject<UIFormFieldConfigTextArea>()
- .property("type", codecForConstString("textArea"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigTextArea");
-
-const codecForUiFormFieldToggle = (): Codec<UIFormFieldConfigToggle> =>
- buildCodecForObject<UIFormFieldConfigToggle>()
- .property("type", codecForConstString("toggle"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigToggle");
-
-const codecForUiFormField = (): Codec<UIFormFieldConfig> =>
- buildCodecForUnion<UIFormFieldConfig>()
- .discriminateOn("type")
- .alternative("absoluteTime", codecForUiFormFieldAbsoluteTime())
- .alternative("amount", codecForUiFormFieldAmount())
- .alternative("array", codecForUiFormFieldArray())
- .alternative("caption", codecForUiFormFieldCaption())
- .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal())
- .alternative("choiceStacked", codecForUiFormFieldChoiceStacked())
- .alternative("file", codecForUiFormFieldFile())
- .alternative("group", codecForUiFormFieldGroup())
- .alternative("integer", codecForUiFormFieldInteger())
- .alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
- .alternative("selectOne", codecForUiFormFieldSelectOne())
- .alternative("text", codecForUiFormFieldText())
- .alternative("textArea", codecForUiFormFieldTextArea())
- .alternative("toggle", codecForUiFormFieldToggle())
- .build("UIFormField");
-
-const codecForDoubleColumnFormSection = (): Codec<DoubleColumnFormSection> =>
- buildCodecForObject<DoubleColumnFormSection>()
- .property("title", codecForString())
- .property("description", codecForString())
- .property("fields", codecForList(codecForUiFormField()))
- .build("DoubleColumnFormSection");
-
-const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> =>
- buildCodecForObject<DoubleColumnForm>()
- .property("type", codecForConstString("double-column"))
- .property("design", codecForList(codecForDoubleColumnFormSection()))
- .build("DoubleColumnForm");
-
-const codecForFlexibleForm = (): Codec<FlexibleForm> =>
- buildCodecForUnion<FlexibleForm>()
- .discriminateOn("type")
- .alternative("double-column", codecForDoubleColumnForm())
- .build<FlexibleForm>("FlexibleForm");
-
-const codecForFormMetadata = (): Codec<FormMetadata> =>
- buildCodecForObject<FormMetadata>()
- .property("label", codecForString())
- .property("id", codecForString())
- .property("version", codecForNumber())
- .property("config", codecForFlexibleForm())
- .build("FormMetadata");
-const codecForUIForms = (): Codec<UiForms> =>
- buildCodecForObject<UiForms>()
- .property("forms", codecForList(codecForFormMetadata()))
- .build("UiForms");
function removeUndefineField<T extends object>(obj: T): T {
const keys = Object.keys(obj) as Array<keyof T>;
diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json
new file mode 100644
index 000000000..de0da601c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms.json
@@ -0,0 +1,539 @@
+{
+ "forms": [
+ {
+ "label": "Information on customer",
+ "id": "902_1e",
+ "version": 1,
+ "config": {
+ "type": "double-column",
+ "design": [
+ {
+ "title": "Information on customer",
+ "description": "The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.",
+ "fields": [
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "customerType",
+ "id": ".customerType",
+ "label": "Type of customer",
+ "required": true,
+ "choices": [
+ {
+ "label": "Natural person",
+ "value": "natural"
+ },
+ {
+ "label": "Legal entity",
+ "value": "legal"
+ }
+ ]
+ }
+ },
+ {
+ "type": "group",
+ "properties": {
+ "label": "Natural customer form",
+ "name": "algo",
+ "id": "algo",
+ "before": "a) Country risk (nationality)",
+ "after": "a) Country risk (nationality)",
+ "fields": [
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.fullName",
+ "id": ".naturalCustomer.fullName",
+ "label": "Full name",
+ "required": true
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.address",
+ "id": ".naturalCustomer.address",
+ "label": "Residential address",
+ "required": true
+ }
+ },
+ {
+ "type": "integer",
+ "properties": {
+ "name": "naturalCustomer.telephone",
+ "id": ".naturalCustomer.telephone",
+ "label": "Telephone"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.email",
+ "id": ".naturalCustomer.email",
+ "label": "E-mail"
+ }
+ },
+ {
+ "type": "absoluteTime",
+ "properties": {
+ "pattern": "dd/MM/yyyy",
+ "name": "naturalCustomer.dateOfBirth",
+ "id": ".naturalCustomer.dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.nationality",
+ "id": ".naturalCustomer.nationality",
+ "label": "Nationality",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.document",
+ "id": ".naturalCustomer.document",
+ "label": "Identification document",
+ "required": true
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "naturalCustomer.documentAttachment",
+ "id": ".naturalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.companyName",
+ "id": ".naturalCustomer.companyName",
+ "label": "Company name"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.office",
+ "id": ".naturalCustomer.office",
+ "label": "Registered office"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.companyDocument",
+ "id": ".naturalCustomer.companyDocument",
+ "label": "Company identification document"
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "naturalCustomer.companyDocumentAttachment",
+ "id": ".naturalCustomer.companyDocumentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.companyName",
+ "id": ".legalCustomer.companyName",
+ "label": "Company name",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.domicile",
+ "id": ".legalCustomer.domicile",
+ "label": "Domicile",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.contactPerson",
+ "id": ".legalCustomer.contactPerson",
+ "label": "Contact person"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.telephone",
+ "id": ".legalCustomer.telephone",
+ "label": "Telephone"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.email",
+ "id": ".legalCustomer.email",
+ "label": "E-mail"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.document",
+ "id": ".legalCustomer.document",
+ "label": "Identification document",
+ "help": "Not older than 12 month"
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "legalCustomer.documentAttachment",
+ "id": ".legalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ }
+ ]
+ },
+ {
+ "title": "Information on the natural persons who establish the business relationship for legal entities and partnerships",
+ "description": "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.",
+ "fields": [
+ {
+ "type": "array",
+ "properties": {
+ "name": "businessEstablisher",
+ "id": ".businessEstablisher",
+ "label": "Persons",
+ "required": true,
+ "labelFieldId": "fullName",
+ "placeholder": "this is the placeholder",
+ "fields": [
+ {
+ "type": "text",
+ "properties": {
+ "name": "fullName",
+ "id": ".fullName",
+ "label": "Full name",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "address",
+ "id": ".address",
+ "label": "Residential address",
+ "required": true
+ }
+ },
+ {
+ "type": "absoluteTime",
+ "properties": {
+ "pattern": "dd/MM/yyyy",
+ "name": "dateOfBirth",
+ "id": ".dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ }
+ },
+
+ {
+ "type": "text",
+ "properties": {
+ "name": "nationality",
+ "id": ".nationality",
+ "label": "Nationality",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "typeOfAuthorization",
+ "id": ".typeOfAuthorization",
+ "label": "Type of authorization (signatory of representation)",
+ "required": true
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "documentAttachment",
+ "id": ".documentAttachment",
+ "label": "Identification document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ }
+ },
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "powerOfAttorneyArrangements",
+ "id": ".powerOfAttorneyArrangements",
+ "label": "Power of attorney arrangements",
+ "required": true,
+ "choices": [
+ {
+ "label": "CR extract",
+ "value": "cr"
+ },
+ {
+ "label": "Mandate",
+ "value": "mandate"
+ },
+ {
+ "label": "Other",
+ "value": "other"
+ }
+ ]
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "powerOfAttorneyArrangementsOther",
+ "id": ".powerOfAttorneyArrangementsOther",
+ "label": "Power of attorney arrangements",
+ "required": true
+ }
+ }
+ ],
+ "labelField": "fullName"
+ }
+ }
+ ]
+ },
+ {
+ "title": "Acceptance of business relationship",
+ "fields": [
+ {
+ "type": "absoluteTime",
+ "properties": {
+ "name": "acceptance.when",
+ "id": ".acceptance.when",
+ "pattern": "dd/MM/yyyy",
+ "converterId": "Taler.AbsoluteTime",
+ "label": "Date (conclusion of contract)"
+ }
+ },
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "acceptance.acceptedBy",
+ "id": ".acceptance.acceptedBy",
+ "label": "Accepted by",
+ "required": true,
+ "choices": [
+ {
+ "label": "Face-to-face meeting with customer",
+ "value": "face-to-face"
+ },
+ {
+ "label": "Correspondence: authenticated copy of identification document obtained",
+ "value": "correspondence-document"
+ },
+ {
+ "label": "Correspondence: residential address validated",
+ "value": "correspondence-address"
+ }
+ ]
+ }
+ },
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "acceptance.typeOfCorrespondence",
+ "id": ".acceptance.typeOfCorrespondence",
+ "label": "Type of correspondence service",
+ "choices": [
+ {
+ "label": "to the customer",
+ "value": "customer"
+ },
+ {
+ "label": "hold at bank",
+ "value": "bank"
+ },
+ {
+ "label": "to the member",
+ "value": "member"
+ },
+ {
+ "label": "to a third party",
+ "value": "third-party"
+ }
+ ]
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "acceptance.thirdPartyFullName",
+ "id": ".acceptance.thirdPartyFullName",
+ "label": "Third party full name",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "acceptance.thirdPartyAddress",
+ "id": ".acceptance.thirdPartyAddress",
+ "label": "Third party address",
+ "required": true
+ }
+ },
+ {
+ "type": "selectMultiple",
+ "properties": {
+ "name": "acceptance.language",
+ "id": ".acceptance.language",
+ "label": "Languages",
+ "choices": [
+ {
+ "label": "Espanol",
+ "value": "es"
+ }
+ ],
+ "unique": true
+ }
+ },
+ {
+ "type": "textArea",
+ "properties": {
+ "name": "acceptance.furtherInformation",
+ "id": ".acceptance.furtherInformation",
+ "label": "Further information"
+ }
+ }
+ ]
+ },
+ {
+ "title": "Information on the beneficial owner of the assets and/or controlling person",
+ "description": "Establishment of the beneficial owner of the assets and/or controlling person",
+ "fields": [
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "establishment",
+ "id": ".establishment",
+ "label": "The customer is",
+ "required": true,
+ "choices": [
+ {
+ "label": "a natural person and there are no doubts that this person is the sole beneficial owner of the assets",
+ "value": "natural"
+ },
+ {
+ "label": "a foundation (or a similar construct; incl. underlying companies)",
+ "value": "foundation"
+ },
+ {
+ "label": "a trust (incl. underlying companies)",
+ "value": "trust"
+ },
+ {
+ "label": "a life insurance policy with separately managed accounts/securities accounts",
+ "value": "insurance-wrapper"
+ },
+ {
+ "label": "all other cases",
+ "value": "other"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "title": "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship",
+ "description": "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)",
+ "fields": [
+ {
+ "type": "textArea",
+ "properties": {
+ "name": "embargoEvaluation",
+ "id": ".embargoEvaluation",
+ "help": "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.",
+ "label": "Evaluation"
+ }
+ }
+ ]
+ },
+ {
+ "title": "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship",
+ "description": "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created",
+ "fields": [
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "cashTransactions.typeOfBusiness",
+ "id": ".cashTransactions.typeOfBusiness",
+ "label": "Type of business relationship",
+ "choices": [
+ {
+ "label": "Money exchange",
+ "value": "money-exchange"
+ },
+ {
+ "label": "Money and asset transfer",
+ "value": "money-and-asset-transfer"
+ },
+ {
+ "label": "Other cash transactions. Specify below",
+ "value": "other"
+ }
+ ]
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "cashTransactions.otherTypeOfBusiness",
+ "id": ".cashTransactions.otherTypeOfBusiness",
+ "required": true,
+ "label": "Specify other cash transactions:"
+ }
+ },
+ {
+ "type": "textArea",
+ "properties": {
+ "name": "cashTransactions.purpose",
+ "id": ".cashTransactions.purpose",
+ "label": "Purpose of the business relationship (purpose of service requested)"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "not_yet_supported": []
+}
diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts
index abeebfc6a..e89a8fb10 100644
--- a/packages/aml-backoffice-ui/src/forms/index.ts
+++ b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -13,8 +13,7 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { FormMetadata } from "../context/ui-forms.js";
+import type { FormMetadata, InternationalizationAPI } from "@gnu-taler/web-util/browser";
import { v1 as simplest } from "./simplest.js";
const languages = (i18n: InternationalizationAPI) => [
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index b52f2bf74..37ab0913d 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -15,9 +15,11 @@
*/
import type {
- InternationalizationAPI
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId
} from "@gnu-taler/web-util/browser";
-import { DoubleColumnForm, DoubleColumnFormSection, UIHandlerId } from "../context/ui-forms.js";
export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
type: "double-column" as const,
@@ -30,7 +32,7 @@ export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
properties: {
id: ".comment" as UIHandlerId,
name: "comment",
- label: i18n.str`Comments`,
+ label: i18n.str`Comment`,
},
},
],
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
index 752444bd2..033d1d950 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -19,10 +19,11 @@ import {
AmountJson,
TalerExchangeApi,
TranslatedString,
+ assertUnreachable,
} from "@gnu-taler/taler-util";
-import { UIFieldHandler } from "@gnu-taler/web-util/browser";
+import { Addon, InternationalizationAPI, UIFieldBaseDescription, UIFieldHandler, UIFormField, UIFormFieldBaseConfig, UIFormFieldConfig, UIHandlerId } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
-import { UIFormFieldConfig, UIHandlerId } from "../context/ui-forms.js";
+import { getConverterById } from "../utils/converter.js";
// export type UIField = {
// value: string | undefined;
@@ -93,41 +94,17 @@ function constructFormHandler<T>(
updateForm(setValueDeeper(form, path, newValue));
}
- const currentValue: unknown = getValueDeeper(form, path)
- const currentError: unknown = errors === undefined ? undefined : getValueDeeper(errors, path)
+ const currentValue = getValueDeeper<string>(form as any, path, undefined)
+ const currentError = getValueDeeper<TranslatedString>(errors as any, path, undefined)
const field: UIFieldHandler = {
- // @ts-expect-error FIXME better typing
error: currentError,
- // @ts-expect-error FIXME better typing
value: currentValue,
onChange: updater,
- state: {},
+ state: {}, //FIXME: add the state of the field (hidden, )
};
return setValueDeeper(handleForm, path, field)
- /**
- * There is no clear way to know if this object is a custom field
- * or a group of fields
- */
- // if (typeof currentValue === "object") {
- // // @ts-expect-error FIXME better typing
- // const group = constructFormHandler(currentValue, updater, currentError);
- // // @ts-expect-error FIXME better typing
- // prev[fieldName] = group;
- // return prev;
- // }
-
- // const field: UIFieldHandler = {
- // // @ts-expect-error FIXME better typing
- // error: currentError,
- // // @ts-expect-error FIXME better typing
- // value: currentValue,
- // onChange: updater,
- // state: {},
- // };
- // handleForm[fieldName] = field;
- // return handleForm;
}, {} as FormHandler<T>);
return handler;
@@ -156,22 +133,40 @@ export function useFormState<T>(
return [handler, status];
}
+interface Tree<T> extends Record<string, Tree<T> | T> {}
-function getValueDeeper(
+function getValueDeeper<T>(
+ object: Tree<T> | undefined,
+ names: string[],
+ notFoundValue?: T,
+): T | undefined {
+ if (names.length === 0) return object as T;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper(object, rest, notFoundValue);
+ }
+ if (object === undefined) {
+ return notFoundValue
+ }
+ return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+}
+
+function getValueDeeper2(
object: Record<string, any>,
names: string[],
): UIFieldHandler {
if (names.length === 0) return object as UIFieldHandler;
const [head, ...rest] = names;
if (!head) {
- return getValueDeeper(object, rest);
+ return getValueDeeper2(object, rest);
}
if (object === undefined) {
throw Error("handler not found");
}
- return getValueDeeper(object[head], rest);
+ return getValueDeeper2(object[head], rest);
}
+
function setValueDeeper(object: any, names: string[], value: any): any {
if (names.length === 0) return value;
const [head, ...rest] = names;
@@ -183,3 +178,193 @@ function setValueDeeper(object: any, names: string[], value: any): any {
}
return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
}
+
+function getAddonById(_id: string | undefined): Addon {
+ return undefined!;
+}
+
+
+function converInputFieldsProps(
+ form: FormHandler<unknown>,
+ p: UIFormFieldBaseConfig,
+) {
+ return {
+ converter: getConverterById(p.converterId, p),
+ handler: getValueDeeper2(form, p.id.split(".")),
+ name: p.name,
+ required: p.required,
+ disabled: p.disabled,
+ help: p.help,
+ placeholder: p.placeholder,
+ tooltip: p.tooltip,
+ label: p.label as TranslatedString,
+ };
+}
+
+function converBaseFieldsProps(
+ i18n_: InternationalizationAPI,
+ p: UIFieldBaseDescription,
+) {
+ return {
+ after: getAddonById(p.addonAfterId),
+ before: getAddonById(p.addonBeforeId),
+ hidden: p.hidden,
+ name: p.name,
+ help: i18n_.str`${p.help}`,
+ label: i18n_.str`${p.label}`,
+ tooltip: i18n_.str`${p.tooltip}`,
+ };
+}
+
+export function convertUiField(
+ i18n_: InternationalizationAPI,
+ fieldConfig: UIFormFieldConfig[],
+ form: FormHandler<unknown>,
+): UIFormField[] {
+ return fieldConfig.map((config) => {
+ // NON input fields
+ switch (config.type) {
+ case "caption": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: converBaseFieldsProps(i18n_, config.properties),
+ };
+ return resp;
+ }
+ case "group": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ fields: convertUiField(i18n_, config.properties.fields, form),
+ },
+ };
+ return resp;
+ }
+ }
+ // Input Fields
+ switch (config.type) {
+ case "array": {
+ return {
+ type: "array",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ labelField: config.properties.labelFieldId,
+ fields: convertUiField(i18n_, config.properties.fields, form),
+ },
+ } as UIFormField;
+ }
+ case "absoluteTime": {
+ return {
+ type: "absoluteTime",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "amount": {
+ return {
+ type: "amount",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "choiceHorizontal": {
+ return {
+ type: "choiceHorizontal",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ choices: config.properties.choices,
+ },
+ } as UIFormField;
+ }
+ case "choiceStacked": {
+ return {
+ type: "choiceStacked",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ choices: config.properties.choices,
+
+ },
+ }as UIFormField;
+ }
+ case "file":{
+ console.log("ASDASD", config.properties.accept)
+ return {
+ type: "file",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ accept: config.properties.accept,
+ maxBites: config.properties.maxBytes,
+ },
+ } as UIFormField;
+ }
+ case "integer":{
+ return {
+ type: "integer",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "selectMultiple":{
+ return {
+ type: "selectMultiple",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ choices: config.properties.choices,
+ },
+ } as UIFormField;
+ }
+ case "selectOne": {
+ return {
+ type: "selectOne",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ choices: config.properties.choices,
+ },
+ } as UIFormField;
+ }
+ case "text": {
+ return {
+ type: "text",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "textArea": {
+ return {
+ type: "text",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "toggle": {
+ return {
+ type: "toggle",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ default: {
+ assertUnreachable(config);
+ }
+ }
+ });
+}
diff --git a/packages/aml-backoffice-ui/src/index.html b/packages/aml-backoffice-ui/src/index.html
index b7f73d0a2..0ed2f8178 100644
--- a/packages/aml-backoffice-ui/src/index.html
+++ b/packages/aml-backoffice-ui/src/index.html
@@ -30,7 +30,6 @@
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<title>Exchange Backoffice</title>
<!-- Entry point for the SPA. -->
- <script type="module" src="forms.js"></script>
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 11b6d053e..bb936cebf 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -34,24 +34,26 @@ import {
import {
DefaultForm,
ErrorLoading,
+ FormMetadata,
InternationalizationAPI,
Loading,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { privatePages } from "../Routing.js";
-import { FormMetadata, useUiFormsContext } from "../context/ui-forms.js";
+import { useUiFormsContext } from "../context/ui-forms.js";
+import { preloadedForms } from "../forms/index.js";
import { useCaseDetails } from "../hooks/useCaseDetails.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
-import { preloadedForms } from "../forms/index.js";
export type AmlEvent =
| AmlFormEvent
| AmlFormEventError
| KycCollectionEvent
| KycExpirationEvent;
+
type AmlFormEvent = {
type: "aml-form";
when: AbsoluteTime;
@@ -163,9 +165,9 @@ export function CaseDetails({ account }: { account: string }) {
const { i18n } = useTranslationContext();
const details = useCaseDetails(account);
- const {forms} = useUiFormsContext()
+ const { forms } = useUiFormsContext();
- const allForms = [...forms, ...preloadedForms(i18n)]
+ const allForms = [...forms, ...preloadedForms(i18n)];
if (!details) {
return <Loading />;
}
@@ -185,7 +187,12 @@ export function CaseDetails({ account }: { account: string }) {
}
const { aml_history, kyc_attributes } = details.body;
- const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, allForms);
+ const events = getEventsFromAmlHistory(
+ aml_history,
+ kyc_attributes,
+ i18n,
+ allForms,
+ );
if (showForm !== undefined) {
return (
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index bbfa65ca7..2f3ee054d 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -23,35 +23,27 @@ import {
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
- Addon,
Button,
+ FormMetadata,
InternationalizationAPI,
LocalNotificationBanner,
RenderAllFieldsByUiConfig,
- StringConverter,
- UIFieldHandler,
- UIFormField,
+ UIHandlerId,
useExchangeApiContext,
useLocalNotificationHandler,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { privatePages } from "../Routing.js";
import {
- FormMetadata,
- UIFieldBaseDescription,
- UIFormFieldBaseConfig,
- UIFormFieldConfig,
- UIHandlerId,
- useUiFormsContext,
+ useUiFormsContext
} from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.js";
-import { FormErrors, FormHandler, useFormState } from "../hooks/form.js";
+import { FormErrors, convertUiField, useFormState } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { Justification } from "./CaseDetails.js";
-import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
-import { getConverterById } from "../utils/converter.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
function searchForm(
i18n: InternationalizationAPI,
@@ -112,16 +104,18 @@ export function CaseUpdate({
theForm.config.design.forEach((section) => {
section.fields.forEach((field) => {
if ("id" in field.properties) {
+ //FIXME: this should be a validation
+ if (shape.indexOf(field.properties.id) !== -1) {
+ throw Error(`already present: ${field.properties.id}`)
+ }
shape.push(field.properties.id);
- // const path = field.properties.id.split(".");
- // defaultValue = setValueDeeper(defaultValue, path, undefined);
}
});
});
const [form, state] = useFormState<FormType>(shape, initial, (st) => {
const errors = undefinedIfEmpty<FormErrors<FormType>>({
- state: !st.state ? i18n.str`required` : undefined,
+ state: st.state === undefined ? i18n.str`required` : undefined,
threshold: !st.threshold ? i18n.str`required` : undefined,
when: !st.when ? i18n.str`required` : undefined,
comment: !st.comment ? i18n.str`required` : undefined,
@@ -140,8 +134,6 @@ export function CaseUpdate({
};
});
- console.log("NOW FORM", form);
-
const validatedForm = state.status !== "ok" ? undefined : state.result;
const submitHandler =
@@ -249,7 +241,6 @@ export function SelectForm({ account }: { account: string }) {
const { i18n } = useTranslationContext();
const { forms } = useUiFormsContext();
const pf = preloadedForms(i18n);
-
return (
<div>
<pre>New form for account: {account.substring(0, 16)}...</pre>
@@ -279,147 +270,3 @@ export function SelectForm({ account }: { account: string }) {
);
}
-function getValueDeeper(
- object: Record<string, any>,
- names: string[],
-): UIFieldHandler {
- if (names.length === 0) return object as UIFieldHandler;
- const [head, ...rest] = names;
- if (!head) {
- return getValueDeeper(object, rest);
- }
- if (object === undefined) {
- throw Error("handler not found");
- }
- return getValueDeeper(object[head], rest);
-}
-
-function setValueDeeper(object: any, names: string[], value: any): any {
- if (names.length === 0) return value;
- const [head, ...rest] = names;
- if (!head) {
- return setValueDeeper(object, rest, value);
- }
- if (object === undefined) {
- return { [head]: setValueDeeper({}, rest, value) };
- }
- return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
-}
-
-function getAddonById(_id: string | undefined): Addon {
- return undefined!;
-}
-
-
-function converInputFieldsProps(
- form: FormHandler<unknown>,
- p: UIFormFieldBaseConfig,
-) {
- return {
- converter: getConverterById(p.converterId),
- handler: getValueDeeper(form, p.id.split(".")),
- };
-}
-
-function converBaseFieldsProps(
- i18n_: InternationalizationAPI,
- p: UIFieldBaseDescription,
-) {
- return {
- after: getAddonById(p.addonAfterId),
- before: getAddonById(p.addonBeforeId),
- hidden: p.hidden,
- name: p.name,
- help: i18n_.str`${p.help}`,
- label: i18n_.str`${p.label}`,
- tooltip: i18n_.str`${p.tooltip}`,
- };
-}
-
-function convertUiField(
- i18n_: InternationalizationAPI,
- fieldConfig: UIFormFieldConfig[],
- form: FormHandler<unknown>,
-): UIFormField[] {
- return fieldConfig.map((config) => {
- // NON input fields
- switch (config.type) {
- case "caption": {
- const resp: UIFormField = {
- type: config.type,
- properties: converBaseFieldsProps(i18n_, config.properties),
- };
- return resp;
- }
- case "group": {
- const resp: UIFormField = {
- type: config.type,
- properties: {
- ...converBaseFieldsProps(i18n_, config.properties),
- fields: convertUiField(i18n_, config.properties.fields, form),
- },
- };
- return resp;
- }
- }
- // Input Fields
- switch (config.type) {
- case "absoluteTime": {
- return undefined!;
- }
- case "amount": {
- return {
- type: "amount",
- properties: {
- ...converBaseFieldsProps(i18n_, config.properties),
- ...converInputFieldsProps(form, config.properties),
- },
- } as UIFormField;
- }
- case "array": {
- return undefined!;
- }
- case "choiceHorizontal": {
- return {
- type: "choiceHorizontal",
- properties: {
- ...converBaseFieldsProps(i18n_, config.properties),
- ...converInputFieldsProps(form, config.properties),
- choices: config.properties.choices,
- },
- } as UIFormField;
- }
- case "choiceStacked":
- case "file":
- case "integer":
- case "selectMultiple":
- case "selectOne": {
- return undefined!;
- }
- case "text": {
- return {
- type: "text",
- properties: {
- ...converBaseFieldsProps(i18n_, config.properties),
- ...converInputFieldsProps(form, config.properties),
- },
- } as UIFormField;
- }
- case "textArea": {
- return {
- type: "text",
- properties: {
- ...converBaseFieldsProps(i18n_, config.properties),
- ...converInputFieldsProps(form, config.properties),
- },
- } as UIFormField;
- }
- case "toggle": {
- return undefined!;
- }
- default: {
- assertUnreachable(config);
- }
- }
- });
-}
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 3860bcd98..7b848487a 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -24,6 +24,7 @@ import {
ErrorLoading,
InputChoiceHorizontal,
Loading,
+ UIHandlerId,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
@@ -34,7 +35,6 @@ import { privatePages } from "../Routing.js";
import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
import { Officer } from "./Officer.js";
-import { UIHandlerId } from "../context/ui-forms.js";
import { amlStateConverter } from "../utils/converter.js";
type FormType = {
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 98160fb3a..f4904933b 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -18,6 +18,7 @@ import {
InputLine,
InternationalizationAPI,
LocalNotificationBanner,
+ UIHandlerId,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
@@ -31,7 +32,6 @@ import {
} from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { usePreferences } from "../hooks/preferences.js";
-import { UIHandlerId } from "../context/ui-forms.js";
type FormType = {
password: string;
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index 0978d8bcc..3c0301e9f 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -23,6 +23,8 @@ import {
DefaultForm,
FlexibleForm,
UIFormField,
+ UIFormFieldConfig,
+ UIHandlerId,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
@@ -40,22 +42,8 @@ export function ShowConsolidated({
const cons = getConsolidated(history, until);
- const form: FlexibleForm<Consolidated> = {
- behavior: (form) => {
- return {
- aml: {
- threshold: {
- hidden: !form.aml,
- },
- since: {
- hidden: !form.aml,
- },
- state: {
- hidden: !form.aml,
- },
- },
- };
- },
+ const form: FlexibleForm = {
+ type: "double-column",
design: [
{
title: i18n.str`AML`,
@@ -63,6 +51,8 @@ export function ShowConsolidated({
{
type: "amount",
properties: {
+ id: ".aml.threshold" as UIHandlerId,
+ currency: "NETZBON",
label: i18n.str`Threshold`,
name: "aml.threshold",
},
@@ -72,7 +62,7 @@ export function ShowConsolidated({
properties: {
label: i18n.str`State`,
name: "aml.state",
-
+ id: ".aml.state" as UIHandlerId,
choices: [
{
label: i18n.str`Frozen`,
@@ -95,10 +85,11 @@ export function ShowConsolidated({
? {
title: i18n.str`KYC`,
fields: Object.entries(cons.kyc).map(([key, field]) => {
- const result: UIFormField = {
+ const result: UIFormFieldConfig = {
type: "text",
properties: {
label: key as TranslatedString,
+ id: `kyc.${key}.value` as UIHandlerId,
name: `kyc.${key}.value`,
help: `${field.provider} since ${
field.since.t_ms === "never"
@@ -110,7 +101,7 @@ export function ShowConsolidated({
return result;
}),
}
- : undefined,
+ : undefined!,
],
};
return (
@@ -123,7 +114,7 @@ export function ShowConsolidated({
</h1>
<DefaultForm
key={`${String(Date.now())}`}
- form={form}
+ form={form as any}
initial={cons}
readOnly
onUpdate={() => {}}
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index b81e66468..084e639bf 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -17,6 +17,7 @@ import {
Button,
InputLine,
LocalNotificationBanner,
+ UIHandlerId,
useLocalNotificationHandler,
useTranslationContext
} from "@gnu-taler/web-util/browser";
@@ -24,7 +25,6 @@ import { VNode, h } from "preact";
import { FormErrors, useFormState } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
-import { UIHandlerId } from "../context/ui-forms.js";
type FormType = {
password: string;
diff --git a/packages/aml-backoffice-ui/src/settings.json b/packages/aml-backoffice-ui/src/settings.json
new file mode 100644
index 000000000..932202b81
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/settings.json
@@ -0,0 +1,4 @@
+{
+ "backendBaseURL": "http://exchange.taler.test:1180/",
+ "signupEmail": "do-not-contact-me@exchange.taler.test"
+} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts
index 25a824697..187a5412f 100644
--- a/packages/aml-backoffice-ui/src/utils/converter.ts
+++ b/packages/aml-backoffice-ui/src/utils/converter.ts
@@ -14,8 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, Amounts, TalerExchangeApi } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ TalerExchangeApi,
+} from "@gnu-taler/taler-util";
import { StringConverter } from "@gnu-taler/web-util/browser";
+import { format, parse } from "date-fns";
export const amlStateConverter = {
toStringUI: stringifyAmlState,
@@ -47,20 +53,63 @@ function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
}
}
-const amountConverter: StringConverter<AmountJson> = {
- fromStringUI(v: string | undefined): AmountJson {
- // FIXME: requires currency
- return Amounts.parse(`NETZBON:${v}`) ?? Amounts.zeroOfCurrency("NETZBON");
- },
- toStringUI(v: unknown): string {
- return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
- },
-};
+function amountConverter(config: any): StringConverter<AmountJson> {
+ const currency = config["currency"];
+ if (!currency || typeof currency !== "string") {
+ throw Error(`amount converter needs a currency`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AmountJson {
+ // FIXME: requires currency
+ return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency);
+ },
+ toStringUI(v: unknown): string {
+ return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
+ },
+ };
+}
+
+function absTimeConverter(config: any): StringConverter<AbsoluteTime> {
+ const pattern = config["pattern"];
+ if (!pattern || typeof pattern !== "string") {
+ throw Error(`absTime converter needs a pattern`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AbsoluteTime {
+ if (v === undefined) {
+ return AbsoluteTime.never();
+ }
+ try {
+ const time = parse(v, pattern, new Date());
+ return AbsoluteTime.fromMilliseconds(time.getTime());
+ } catch(e) {
+ return AbsoluteTime.never();
+ }
+ },
+ toStringUI(v: unknown): string {
+ if (v === undefined) return "";
+ const d = v as AbsoluteTime;
+ if (d.t_ms === "never") return "never";
+ try {
+ return format(d.t_ms, pattern)
+ } catch (e) {
+ return ""
+ }
+ },
+ };
+}
-export function getConverterById(id: string | undefined): StringConverter<unknown> {
+export function getConverterById(
+ id: string | undefined,
+ config: unknown,
+): StringConverter<unknown> {
+ if (id === "Taler.AbsoluteTime") {
+ // @ts-expect-error check this
+ return absTimeConverter(config);
+ }
if (id === "Taler.Amount") {
// @ts-expect-error check this
- return amountConverter;
+ return amountConverter(config);
}
if (id === "TalerExchangeApi.AmlState") {
// @ts-expect-error check this