/* This file is part of GNU Taler (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ import { AbsoluteTime, AmountJson, Amounts, HttpStatusCode, TalerExchangeApi, TalerProtocolTimestamp, assertUnreachable } from "@gnu-taler/taler-util"; import { Button, InternationalizationAPI, LocalNotificationBanner, RenderAllFieldsByUiConfig, UIFieldHandler, UIFormField, useExchangeApiContext, useLocalNotificationHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { privatePages } from "../Routing.js"; import { FormMetadata, UIFormFieldConfig, useUiFormsContext, } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; import { FormHandler, useFormState } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { Justification } from "./CaseDetails.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; function searchForm( i18n: InternationalizationAPI, forms: FormMetadata[], formId: string, ): FormMetadata | undefined { { const found = forms.find((v) => v.id === formId); if (found) return found; } { const pf = preloadedForms(i18n); const found = pf.find((v) => v.id === formId); if (found) return found; } return undefined; } type FormType = { when: AbsoluteTime; state: TalerExchangeApi.AmlState; threshold: AmountJson; }; export function CaseUpdate({ account, type: formId, }: { account: string; type: string; }): VNode { const { i18n } = useTranslationContext(); const officer = useOfficer(); const { lib: { exchange: api }, } = useExchangeApiContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const { config } = useExchangeApiContext(); const { forms } = useUiFormsContext(); const initial: FormType = { when: AbsoluteTime.now(), state: TalerExchangeApi.AmlState.pending, threshold: Amounts.zeroOfCurrency(config.currency), }; if (officer.state !== "ready") { return ; } const theForm = searchForm(i18n, forms, formId); if (!theForm) { return
form with id {formId} not found
; } let defaultValue = initial theForm.config.design.forEach((section) => { section.fields.forEach((field) => { if ("id" in field.properties) { const path = field.properties.id.split("."); defaultValue = setValueDeeper(defaultValue, path, undefined); } }); }); const [form, state] = useFormState(defaultValue, (st) => { return { status: "ok", result: st as any, errors: undefined, }; }); console.log("FORM", form) const validatedForm = state.status === "fail" ? undefined : state.result; const submitHandler = validatedForm === undefined ? undefined : withErrorHandler( () => { const justification: Justification = { id: theForm.id, label: theForm.label, version: theForm.version, value: validatedForm, }; const decision: Omit = { justification: JSON.stringify(justification), decision_time: TalerProtocolTimestamp.now(), h_payto: account, new_state: justification.value .state as TalerExchangeApi.AmlState, new_threshold: Amounts.stringify( justification.value.threshold as AmountJson, ), kyc_requirements: undefined, }; return api.addDecisionDetails(officer.account, decision); }, () => { window.location.href = privatePages.cases.url({}); }, (fail) => { switch (fail.case) { case HttpStatusCode.Forbidden: case HttpStatusCode.Unauthorized: return i18n.str`Wrong credentials for "${officer.account}"`; case HttpStatusCode.NotFound: return i18n.str`Officer or account not found`; case HttpStatusCode.Conflict: return i18n.str`Officer disabled or more recent decision was already submitted.`; default: assertUnreachable(fail); } }, ); function convertUiField( fieldConfig: UIFormFieldConfig[], form: FormHandler, ): UIFormField[] { return fieldConfig.map((config) => { switch (config.type) { case "absoluteTime": { return undefined!; } case "amount": { return { type: "amount", properties: { ...(config.properties as any), handler: getValueDeeper(form, config.properties.id.split(".")), }, } as UIFormField; } case "array": { return undefined!; } case "caption": { return undefined!; } case "choiceHorizontal": { return { type: "choiceHorizontal", properties: { handler: getValueDeeper(form, config.properties.id.split(".")), choices: config.properties.choices, }, } as UIFormField; } case "choiceStacked": case "file": case "group": case "integer": case "selectMultiple": case "selectOne": { return undefined!; } case "text": { return { type: "text", properties: { ...(config.properties as any), handler: getValueDeeper(form, config.properties.id.split(".")), }, } as UIFormField; } case "textArea": { return { type: "text", properties: { ...(config.properties as any), handler: getValueDeeper(form, config.properties.id.split(".")), }, } as UIFormField; } case "toggle": { return undefined!; } default: { assertUnreachable(config); } } }); } return (
{theForm.config.design.map((section, i) => { if (!section) return ; return (

{section.title}

{section.description && (

{section.description}

)}
); })}
Cancel
); } export function SelectForm({ account }: { account: string }) { const { i18n } = useTranslationContext(); const { forms } = useUiFormsContext(); const pf = preloadedForms(i18n); return (
New form for account: {account.substring(0, 16)}...
{forms.map((form) => { return ( {form.label} ); })} {pf.map((form) => { return ( {form.label} ); })}
); } function getValueDeeper( object: Record, 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) }; }