diff options
author | Sebastian <sebasjm@gmail.com> | 2023-05-16 01:23:44 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-05-16 01:23:44 -0300 |
commit | 245ab840baf1926ef2c03a8965fce85012887d92 (patch) | |
tree | 1acec43e991100292f82db241eec70d073b6365b | |
parent | 02fb71c0ff69d293911f4b0945ab964a87402d0c (diff) | |
download | wallet-core-245ab840baf1926ef2c03a8965fce85012887d92.tar.xz |
one form left
14 files changed, 750 insertions, 46 deletions
diff --git a/packages/exchange-backoffice-ui/src/Dashborad.tsx b/packages/exchange-backoffice-ui/src/Dashborad.tsx index f31160fee..dce41b496 100644 --- a/packages/exchange-backoffice-ui/src/Dashborad.tsx +++ b/packages/exchange-backoffice-ui/src/Dashborad.tsx @@ -77,17 +77,17 @@ const navigation = [ impl: form_902_13e_v1, }, { - name: "WIP (902.15e)", + name: "Information on life insurance policies (902.15e)", icon: DocumentDuplicateIcon, impl: form_902_15e_v1, }, { - name: "WIP (902.9e)", + name: "Declaration of beneficial owner (902.9e)", icon: DocumentDuplicateIcon, impl: form_902_9e_v1, }, { - name: "WIP (902.5e)", + name: "Customer profile (902.5e)", icon: DocumentDuplicateIcon, impl: form_902_5e_v1, }, @@ -167,7 +167,7 @@ export function Dashboard({ children?: ComponentChildren; }): VNode { const [sidebarOpen, setSidebarOpen] = useState(false); - const [selectedForm, setSelectedForm] = useState(3); + const [selectedForm, setSelectedForm] = useState(6); function changeForm(next: number) { setSelectedForm(next); } @@ -177,6 +177,9 @@ export function Dashboard({ when: { t_ms: new Date().getTime(), }, + originOfAssets: { + currency: "usd", + }, }; function showFormOnSidebar(v: any) { if (!logRef.current) return; diff --git a/packages/exchange-backoffice-ui/src/forms/902_11e.ts b/packages/exchange-backoffice-ui/src/forms/902_11e.ts index 49d94736f..0e9a28dce 100644 --- a/packages/exchange-backoffice-ui/src/forms/902_11e.ts +++ b/packages/exchange-backoffice-ui/src/forms/902_11e.ts @@ -1,6 +1,6 @@ import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; -import { FlexibleForm, languageList } from "./index.js"; import { FormState } from "../handlers/FormProvider.js"; +import { FlexibleForm } from "./index.js"; export const v1: FlexibleForm<Form902_11e.Form> = { versionId: "2023-05-15", diff --git a/packages/exchange-backoffice-ui/src/forms/902_13e.ts b/packages/exchange-backoffice-ui/src/forms/902_13e.ts index 04dd95bc0..bca96e842 100644 --- a/packages/exchange-backoffice-ui/src/forms/902_13e.ts +++ b/packages/exchange-backoffice-ui/src/forms/902_13e.ts @@ -97,10 +97,12 @@ export const v1: FlexibleForm<Form902_13e.Form> = { }, }, { - type: "text", + type: "date", props: { name: "dateOfBirth", label: "Date of birth" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "format 'dd/MM/yyyy'" as TranslatedString, }, }, { @@ -111,11 +113,12 @@ export const v1: FlexibleForm<Form902_13e.Form> = { }, }, { - type: "text", + type: "date", props: { name: "dateOfDeath", label: "Date of death" as TranslatedString, - help: "if deceased" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString, }, }, { @@ -172,10 +175,12 @@ export const v1: FlexibleForm<Form902_13e.Form> = { }, }, { - type: "text", + type: "date", props: { name: "dateOfBirth", label: "Date of birth" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "format 'dd/MM/yyyy'" as TranslatedString, }, }, { @@ -186,11 +191,12 @@ export const v1: FlexibleForm<Form902_13e.Form> = { }, }, { - type: "text", + type: "date", props: { name: "dateOfDeath", label: "Date of death" as TranslatedString, - help: "if deceased" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString, }, }, ], @@ -228,10 +234,12 @@ export const v1: FlexibleForm<Form902_13e.Form> = { }, }, { - type: "text", + type: "date", props: { name: "dateOfBirth", label: "Date of birth" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "format 'dd/MM/yyyy'" as TranslatedString, }, }, { @@ -417,8 +425,8 @@ export const v1: FlexibleForm<Form902_13e.Form> = { type: "date", props: { name: "when", - pattern: "dd/MM/yyyy", label: "Date" as TranslatedString, + pattern: "dd/MM/yyyy", help: "format 'dd/MM/yyyy'" as TranslatedString, }, }, diff --git a/packages/exchange-backoffice-ui/src/forms/902_15e.ts b/packages/exchange-backoffice-ui/src/forms/902_15e.ts index 8f927cdc2..07290858e 100644 --- a/packages/exchange-backoffice-ui/src/forms/902_15e.ts +++ b/packages/exchange-backoffice-ui/src/forms/902_15e.ts @@ -1,14 +1,13 @@ import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; -import { FlexibleForm, languageList } from "./index.js"; import { FormState } from "../handlers/FormProvider.js"; +import { FlexibleForm } from "./index.js"; -export const v1: FlexibleForm<Form902_12e.Form> = { +export const v1: FlexibleForm<Form902_15e.Form> = { versionId: "2023-05-15", design: [ { - title: "15" as TranslatedString, - description: - "for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners." as TranslatedString, + title: + "Information on life insurance policies with separately managed accounts/securities accounts" as TranslatedString, fields: [ { type: "textArea", @@ -18,6 +17,123 @@ export const v1: FlexibleForm<Form902_12e.Form> = { }, }, { + type: "text", + props: { + name: "contractualRelationship", + label: + "Name or number of the contractual relationship between the contracting party and the financial intermediary" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "insurancePolicy", + label: "Insurance policy" as TranslatedString, + }, + }, + { + type: "separator", + props: { + label: + "The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above." as TranslatedString, + }, + }, + { + type: "separator", + props: { + label: + "In relation with the above insurance policy, the contracting partner gives the following further details" as TranslatedString, + }, + }, + { + type: "group", + props: { + before: "Policy holder" as TranslatedString, + fields: [ + { + type: "text", + props: { + name: "holder.fullName", + label: + "Last name(s), first name(s)/entity" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "holder.address", + label: + "Actual address of domicile/registered office (incl. country)" as TranslatedString, + }, + }, + { + type: "date", + props: { + name: "holder.dateOfBirth", + label: "Date of birth" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "format 'dd/MM/yyyy'" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "holder.nationality", + label: "Nationality" as TranslatedString, + }, + }, + ], + }, + }, + { + type: "group", + props: { + before: + "Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above)" as TranslatedString, + fields: [ + { + type: "text", + props: { + name: "premiumPayer.fullName", + label: + "Last name(s), first name(s)/entity" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "premiumPayer.address", + label: + "Actual address of domicile/registered office (incl. country)" as TranslatedString, + }, + }, + { + type: "date", + props: { + name: "premiumPayer.dateOfBirth", + label: "Date of birth" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "format 'dd/MM/yyyy'" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "premiumPayer.nationality", + label: "Nationality" as TranslatedString, + }, + }, + ], + }, + }, + { + type: "separator", + props: { + label: + "The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary" as TranslatedString, + }, + }, + { type: "date", props: { name: "when", @@ -26,12 +142,26 @@ export const v1: FlexibleForm<Form902_12e.Form> = { help: "format 'dd/MM/yyyy'" as TranslatedString, }, }, + { + type: "text", + props: { + name: "signature", + label: "Signature" as TranslatedString, + }, + }, + { + type: "separator", + props: { + label: + "It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)" as TranslatedString, + }, + }, ], }, ], behavior: function formBehavior( - v: Partial<Form902_12e.Form>, - ): FormState<Form902_12e.Form> { + v: Partial<Form902_15e.Form>, + ): FormState<Form902_15e.Form> { return { when: { disabled: true, @@ -40,9 +170,21 @@ export const v1: FlexibleForm<Form902_12e.Form> = { }, }; -namespace Form902_12e { +namespace Form902_15e { + interface Person { + fullName: string; + address: string; + dateOfBirth: AbsoluteTime; + nationality: string; + } + export interface Form { contractingPartner: string; + contractualRelationship: string; + insurancePolicy: string; + holder: Person; + premiumsPayer: Person; when: AbsoluteTime; + signature: string; } } diff --git a/packages/exchange-backoffice-ui/src/forms/902_4e.ts b/packages/exchange-backoffice-ui/src/forms/902_4e.ts index 6e8d7f1e2..dd00beccf 100644 --- a/packages/exchange-backoffice-ui/src/forms/902_4e.ts +++ b/packages/exchange-backoffice-ui/src/forms/902_4e.ts @@ -1,6 +1,6 @@ import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; -import { FlexibleForm, languageList } from "./index.js"; import { FormState } from "../handlers/FormProvider.js"; +import { FlexibleForm } from "./index.js"; export const v1: FlexibleForm<Form902_12e.Form> = { versionId: "2023-05-15", diff --git a/packages/exchange-backoffice-ui/src/forms/902_5e.ts b/packages/exchange-backoffice-ui/src/forms/902_5e.ts index 20013e727..60bd551d5 100644 --- a/packages/exchange-backoffice-ui/src/forms/902_5e.ts +++ b/packages/exchange-backoffice-ui/src/forms/902_5e.ts @@ -1,20 +1,32 @@ -import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; -import { FlexibleForm, languageList } from "./index.js"; +import { + AbsoluteTime, + AmountJson, + TranslatedString, +} from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; +import { FlexibleForm, currencyList } from "./index.js"; export const v1: FlexibleForm<Form902_12e.Form> = { versionId: "2023-05-15", design: [ { - title: "5" as TranslatedString, + title: "Customer Profile" as TranslatedString, description: - "for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners." as TranslatedString, + "The information below has to refer to the persons from whom the assets originate ultimately (e.g. beneficial owner of the assets, founder/creator of a trust or foundation). Is the customer an operational legal entity or partnership the information may refer to the entity itself (not to the controlling person), unless the entity holds the assets in trust for a third party." as TranslatedString, fields: [ { - type: "textArea", + type: "text", + props: { + name: "customer", + label: "Customer" as TranslatedString, + help: "Pursuant Identification Form (VQF doc. No. 902.1) numeral 1" as TranslatedString, + }, + }, + { + type: "text", props: { - name: "contractingPartner", - label: "Contracting partner" as TranslatedString, + name: "fullName", + label: "Full name" as TranslatedString, }, }, { @@ -28,6 +40,194 @@ export const v1: FlexibleForm<Form902_12e.Form> = { }, ], }, + { + title: "Business activity" as TranslatedString, + fields: [ + { + type: "textArea", + props: { + label: "Profession, business activities" as TranslatedString, + name: "businessActivity", + help: "former, current, potentially planned" as TranslatedString, + }, + }, + ], + }, + { + title: "Financial circumstances" as TranslatedString, + fields: [ + { + type: "textArea", + props: { + label: "Income and assets, liabilities" as TranslatedString, + name: "financial", + help: "estimated" as TranslatedString, + }, + }, + ], + }, + { + title: "Origin of the deposited assets involved" as TranslatedString, + fields: [ + { + type: "text", + props: { + label: "Nature" as TranslatedString, + name: "originOfAssets.nature", + help: "nature of the involved assets" as TranslatedString, + }, + }, + { + type: "selectOne", + props: { + name: "originOfAssets.currency", + label: "Currency" as TranslatedString, + choices: currencyList, + }, + }, + { + type: "integer", + props: { + label: "Amount" as TranslatedString, + name: "originOfAssets.amount", + }, + }, + { + type: "choiceStacked", + props: { + label: "Category" as TranslatedString, + name: "originOfAssets.category", + choices: [ + { + label: "Savings" as TranslatedString, + value: "savings", + }, + { + label: "Own business operations" as TranslatedString, + value: "own-business", + }, + { + label: "Inheritance" as TranslatedString, + value: "inheritance", + }, + { + label: "Other, what?" as TranslatedString, + value: "other", + }, + ], + }, + }, + { + type: "text", + props: { + label: "Other category" as TranslatedString, + name: "originOfAssets.categoryOther", + required: true, + }, + }, + { + type: "textArea", + props: { + label: + "Detailed description of the origins/economical background of the assets involved in the business relationship" as TranslatedString, + name: "originOfAssets.details", + }, + }, + ], + }, + { + title: + "Nature and purpose of the business relationship" as TranslatedString, + fields: [ + { + type: "textArea", + props: { + label: "Purpose of the business relationship" as TranslatedString, + name: "nature.purpose", + help: "nature of the involved assets" as TranslatedString, + }, + }, + { + type: "textArea", + props: { + label: + "Information on the planned development of the business relationship and the assets" as TranslatedString, + name: "nature.plan", + }, + }, + { + type: "textArea", + props: { + label: + "Especially in the case of cash or money and asset transfer transactions with regular customers: Details on usual business volume, Information on the beneficiaries, (Full name, address, bank account)" as TranslatedString, + name: "nature.cashOrMoneyTransfer", + }, + }, + ], + }, + { + title: "Relationship with third parties" as TranslatedString, + fields: [ + { + type: "textArea", + props: { + label: + "Relation of the customer to the beneficial owner involved in the business relationship" as TranslatedString, + name: "relations.beneficialOwners", + }, + }, + { + type: "textArea", + props: { + label: + "Relation of the customer to the controlling persons involved in the business relationship" as TranslatedString, + name: "relations.controllingPersons", + }, + }, + { + type: "textArea", + props: { + label: + "Relation of the customer to the authorized signatories involved in the business relationship" as TranslatedString, + name: "relations.authorizedSignatories", + }, + }, + { + type: "textArea", + props: { + label: + "Relation of the customer to other persons involved in the business relationship" as TranslatedString, + name: "relations.otherPersons", + }, + }, + { + type: "textArea", + props: { + label: "Relation to other AMLA-Files" as TranslatedString, + name: "relations.withOtherAmlaFiles", + }, + }, + { + type: "textArea", + props: { + label: "Introducer / agents / references" as TranslatedString, + name: "relations.references", + }, + }, + ], + }, + { + title: "Further information" as TranslatedString, + fields: [ + { + type: "textArea", + props: { + label: "Other relevant information" as TranslatedString, + name: "furtherInformation", + }, + }, + ], + }, ], behavior: function formBehavior( v: Partial<Form902_12e.Form>, @@ -36,13 +236,43 @@ export const v1: FlexibleForm<Form902_12e.Form> = { when: { disabled: true, }, + originOfAssets: { + categoryOther: { + hidden: v.originOfAssets?.category !== "other", + }, + }, }; }, }; namespace Form902_12e { export interface Form { - contractingPartner: string; + customer: string; + fullName: string; when: AbsoluteTime; + businessActivity: string; + financial: string; + originOfAssets: { + nature: string; + currency: string; + amount: number; + category: "savings" | "own-business" | "inheritance" | "other"; + categoryOther: string; + details: string; + }; + nature: { + purpose: string; + plan: string; + cashOrMoneyTransfer: string; + }; + relations: { + beneficialOwners: string; + controllingPersons: string; + authorizedSignatories: string; + otherPersons: string; + withOtherAmlaFiles: string; + references: string; + }; + furtherInformation: string; } } diff --git a/packages/exchange-backoffice-ui/src/forms/902_9e.ts b/packages/exchange-backoffice-ui/src/forms/902_9e.ts index d68caba23..053cfaa69 100644 --- a/packages/exchange-backoffice-ui/src/forms/902_9e.ts +++ b/packages/exchange-backoffice-ui/src/forms/902_9e.ts @@ -1,14 +1,13 @@ import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; -import { FlexibleForm, languageList } from "./index.js"; import { FormState } from "../handlers/FormProvider.js"; +import { FlexibleForm } from "./index.js"; -export const v1: FlexibleForm<Form902_12e.Form> = { +export const v1: FlexibleForm<Form902_9e.Form> = { versionId: "2023-05-15", design: [ { - title: "9" as TranslatedString, - description: - "for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners." as TranslatedString, + title: + "Declaration of identity of the beneficial owner" as TranslatedString, fields: [ { type: "textArea", @@ -26,12 +25,87 @@ export const v1: FlexibleForm<Form902_12e.Form> = { help: "format 'dd/MM/yyyy'" as TranslatedString, }, }, + { + type: "separator", + props: { + label: + "The contracting partner hereby declares that 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" as TranslatedString, + }, + }, + { + type: "array", + props: { + label: "Persons" as TranslatedString, + labelField: "surname", + name: "persons", + fields: [ + { + type: "text", + props: { + name: "surname", + label: "Surname(s)" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "firstName", + label: "First name(s)" as TranslatedString, + }, + }, + { + type: "date", + props: { + name: "dateOfBirth", + label: "Date of birth" as TranslatedString, + pattern: "dd/MM/yyyy", + help: "format 'dd/MM/yyyy'" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "nationality", + label: "Nationality" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "address", + label: "Actual address of domicile" as TranslatedString, + }, + }, + ], + }, + }, + { + type: "separator", + props: { + label: + "The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "signature", + label: "Signature" as TranslatedString, + }, + }, + { + type: "separator", + props: { + label: + "It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)" as TranslatedString, + }, + }, ], }, ], behavior: function formBehavior( - v: Partial<Form902_12e.Form>, - ): FormState<Form902_12e.Form> { + v: Partial<Form902_9e.Form>, + ): FormState<Form902_9e.Form> { return { when: { disabled: true, @@ -40,9 +114,18 @@ export const v1: FlexibleForm<Form902_12e.Form> = { }, }; -namespace Form902_12e { +namespace Form902_9e { + interface Person { + surname: string; + firstName: string; + dateOfBirth: AbsoluteTime; + nationality: string; + address: string; + } export interface Form { contractingPartner: string; + persons: Person; when: AbsoluteTime; + signature: string; } } diff --git a/packages/exchange-backoffice-ui/src/forms/index.ts b/packages/exchange-backoffice-ui/src/forms/index.ts index 2eb1fba60..c236bbaa3 100644 --- a/packages/exchange-backoffice-ui/src/forms/index.ts +++ b/packages/exchange-backoffice-ui/src/forms/index.ts @@ -118,3 +118,29 @@ export const languageList = [ value: "hau", }, ]; +export const currencyList = [ + { + label: "United States dollar" as TranslatedString, + value: "usd", + }, + { + label: "Euro" as TranslatedString, + value: "eur", + }, + { + label: "Swiss franc" as TranslatedString, + value: "chf", + }, + { + label: "Argentine peso" as TranslatedString, + value: "ars", + }, + { + label: "Mexican peso" as TranslatedString, + value: "mxn", + }, + { + label: "Brazilian real" as TranslatedString, + value: "brl", + }, +]; diff --git a/packages/exchange-backoffice-ui/src/handlers/Group.tsx b/packages/exchange-backoffice-ui/src/handlers/Group.tsx new file mode 100644 index 000000000..04af0647b --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/Group.tsx @@ -0,0 +1,41 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; +import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; + +interface Props { + before?: TranslatedString; + after?: TranslatedString; + tooltipBefore?: TranslatedString; + tooltipAfter?: TranslatedString; + fields: UIFormField[]; +} + +export function Group({ + before, + after, + tooltipAfter, + tooltipBefore, + fields, +}: Props): VNode { + return ( + <div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50"> + <div class="pb-4"> + {before && ( + <LabelWithTooltipMaybeRequired + label={before} + tooltip={tooltipBefore} + /> + )} + </div> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <RenderAllFieldsByUiConfig fields={fields} /> + </div> + <div class="pt-4"> + {after && ( + <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} /> + )} + </div> + </div> + ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputChoice.tsx b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx index b19a0d82e..b19a0d82e 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputChoice.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx index 05733fe19..dd85453e5 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx @@ -1,5 +1,5 @@ import { Fragment, VNode, h } from "preact"; -import { Choice } from "./InputChoice.js"; +import { Choice } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; import { useField } from "./useField.js"; import { useState } from "preact/hooks"; @@ -8,9 +8,10 @@ export function InputSelectMultiple( props: { choices: Choice[]; unique?: boolean; + max?: number; } & UIFormProps<Array<string>>, ): VNode { - const { name, label, choices, placeholder, tooltip, required, unique } = + const { name, label, choices, placeholder, tooltip, required, unique, max } = props; const { value, onChange } = useField<{ [s: string]: Array<string> }>(name); @@ -113,6 +114,9 @@ export function InputSelectMultiple( if (unique && list.indexOf(v.value) !== -1) { return; } + if (max !== undefined && list.length >= max) { + return; + } const newValue = [...list]; newValue.splice(0, 0, v.value); onChange(newValue); diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx new file mode 100644 index 000000000..a4ed1ba1c --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx @@ -0,0 +1,136 @@ +import { Fragment, VNode, h } from "preact"; +import { Choice } from "./InputChoiceStacked.js"; +import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; +import { useField } from "./useField.js"; +import { useState } from "preact/hooks"; + +export function InputSelectOne( + props: { + choices: Choice[]; + } & UIFormProps<Array<string>>, +): VNode { + const { name, label, choices, placeholder, tooltip, required } = props; + const { value, onChange } = useField<{ [s: string]: string | undefined }>( + name, + ); + + const [filter, setFilter] = useState<string | undefined>(undefined); + const regex = new RegExp(`.*${filter}.*`, "i"); + const choiceMap = choices.reduce((prev, curr) => { + return { ...prev, [curr.value]: curr.label }; + }, {} as Record<string, string>); + + const filteredChoices = + filter === undefined + ? undefined + : choices.filter((v) => { + return regex.test(v.label); + }); + return ( + <div class="sm:col-span-6"> + <LabelWithTooltipMaybeRequired + label={label} + required={required} + tooltip={tooltip} + /> + {value ? ( + <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600"> + {choiceMap[value]} + <button + type="button" + onClick={() => { + onChange(undefined); + }} + class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" + > + <span class="sr-only">Remove</span> + <svg + viewBox="0 0 14 14" + class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75" + > + <path d="M4 4l6 6m0-6l-6 6" /> + </svg> + <span class="absolute -inset-1"></span> + </button> + </span> + ) : ( + <div class="relative mt-2"> + <input + id="combobox" + type="text" + value={filter ?? ""} + onChange={(e) => { + setFilter(e.currentTarget.value); + }} + 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" + aria-controls="options" + aria-expanded="false" + /> + <button + type="button" + onClick={() => { + setFilter(filter === undefined ? "" : undefined); + }} + class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" + > + <svg + class="h-5 w-5 text-gray-400" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" + > + <path + fill-rule="evenodd" + d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" + clip-rule="evenodd" + /> + </svg> + </button> + + {filteredChoices !== undefined && ( + <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" + > + {filteredChoices.map((v, idx) => { + return ( + <li + 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" + onClick={() => { + setFilter(undefined); + onChange(v.value); + }} + + // tabindex="-1" + > + {/* <!-- Selected: "font-semibold" --> */} + <span class="block truncate">{v.label}</span> + + {/* <!-- + Checkmark, only display for selected option. + + Active: "text-white", Not Active: "text-indigo-600" + --> */} + </li> + ); + })} + + {/* <!-- + Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. + + Active: "text-white bg-indigo-600", Not Active: "text-gray-900" + --> */} + + {/* <!-- More items... --> */} + </ul> + )} + </div> + )} + </div> + ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/Separator.tsx b/packages/exchange-backoffice-ui/src/handlers/Separator.tsx new file mode 100644 index 000000000..5fa25c3ca --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/Separator.tsx @@ -0,0 +1,26 @@ +import { VNode, h } from "preact"; +import { + InputLine, + LabelWithTooltipMaybeRequired, + UIFormProps, +} from "./InputLine.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +interface Props { + label: TranslatedString; + tooltip?: TranslatedString; + help?: TranslatedString; +} + +export function Separator({ label, tooltip, help }: Props): VNode { + return ( + <div class="sm:col-span-6"> + <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} /> + {help && ( + <p class="mt-2 text-sm text-gray-500" id="email-description"> + {help} + </p> + )} + </div> + ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/forms.ts b/packages/exchange-backoffice-ui/src/handlers/forms.ts index b5d1a2b20..418754919 100644 --- a/packages/exchange-backoffice-ui/src/handlers/forms.ts +++ b/packages/exchange-backoffice-ui/src/handlers/forms.ts @@ -3,11 +3,14 @@ import { InputText } from "./InputText.js"; import { InputDate } from "./InputDate.js"; import { InputInteger } from "./InputInteger.js"; import { h as create, Fragment, VNode } from "preact"; -import { InputChoiceStacked } from "./InputChoice.js"; +import { InputChoiceStacked } from "./InputChoiceStacked.js"; import { InputArray } from "./InputArray.js"; import { InputSelectMultiple } from "./InputSelectMultiple.js"; import { InputTextArea } from "./InputTextArea.js"; import { InputFile } from "./InputFile.js"; +import { Separator } from "./Separator.js"; +import { Group } from "./Group.js"; +import { InputSelectOne } from "./InputSelectOne.js"; export type DoubleColumnForm = DoubleColumnFormSection[]; @@ -21,9 +24,11 @@ type DoubleColumnFormSection = { * Constrain the type with the ui props */ type FieldType = { - separator: {}; + group: Parameters<typeof Group>[0]; + separator: Parameters<typeof Separator>[0]; array: Parameters<typeof InputArray>[0]; file: Parameters<typeof InputFile>[0]; + selectOne: Parameters<typeof InputSelectOne>[0]; selectMultiple: Parameters<typeof InputSelectMultiple>[0]; text: Parameters<typeof InputText>[0]; textArea: Parameters<typeof InputTextArea>[0]; @@ -36,9 +41,11 @@ type FieldType = { * List all the form fields so typescript can type-check the form instance */ export type UIFormField = + | { type: "group"; props: FieldType["group"] } | { type: "separator"; props: FieldType["separator"] } | { type: "array"; props: FieldType["array"] } | { type: "file"; props: FieldType["file"] } + | { type: "selectOne"; props: FieldType["selectOne"] } | { type: "selectMultiple"; props: FieldType["selectMultiple"] } | { type: "text"; props: FieldType["text"] } | { type: "textArea"; props: FieldType["textArea"] } @@ -54,14 +61,11 @@ type UIFormFieldMap = { [key in keyof FieldType]: FieldComponentFunction<key>; }; -function Separator(): VNode { - return create("div", {}); -} - /** * Maps input type with component implementation */ const UIFormConfiguration: UIFormFieldMap = { + group: Group, separator: Separator, array: InputArray, text: InputText, @@ -70,6 +74,7 @@ const UIFormConfiguration: UIFormFieldMap = { date: InputDate, choiceStacked: InputChoiceStacked, integer: InputInteger, + selectOne: InputSelectOne, selectMultiple: InputSelectMultiple, }; |