taler-typescript-core

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

commit 4f719b2da928f910b3ad6d94ce521762e90586ad
parent a4dccd0ee51a09e4217401ce80ea4f82d37064b1
Author: Sebastian <sebasjm@gmail.com>
Date:   Sun, 27 Apr 2025 04:17:06 -0300

fixing custom measure

Diffstat:
Dpackages/aml-backoffice-ui/src/hooks/custom-measures.ts | 94-------------------------------------------------------------------------------
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 23+++++++++++++++++------
Mpackages/aml-backoffice-ui/src/hooks/preferences.ts | 22++++++++++++++++------
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 56+++++++++-----------------------------------------------
Mpackages/aml-backoffice-ui/src/pages/MeasuresTable.tsx | 26++++++++------------------
Mpackages/aml-backoffice-ui/src/pages/NewMeasure.tsx | 281++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Measures.tsx | 205++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Rules.tsx | 30+++++++++++++++---------------
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 20++++++++++----------
9 files changed, 373 insertions(+), 384 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/hooks/custom-measures.ts b/packages/aml-backoffice-ui/src/hooks/custom-measures.ts @@ -1,94 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -import { - Codec, - buildCodecForObject, - codecForAny, - codecForConstString, - codecForEither, - codecForList, - codecForString, - codecOptionalDefault, -} from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; - -export interface CustomMeasures { - measures: CustomMeasure[]; -} - -type CustomMeasure = { - type: "procedure" | "form"; - program: string; - name: string; - check_name: string; - context: Object; -}; - -export const codecForCustomMeasures = (): Codec<CustomMeasures> => - buildCodecForObject<CustomMeasures>() - .property( - "measures", - codecOptionalDefault(codecForList(codecForCustomMeasure()), []), - ) - .build("CustomMeasures"); -export const codecForCustomMeasure = (): Codec<CustomMeasure> => - buildCodecForObject<CustomMeasure>() - .property("check_name", codecForString()) - .property("program", codecForString()) - .property("name", codecForString()) - .property("context", codecForAny()) - .property( - "type", - codecForEither( - codecForConstString("procedure"), - codecForConstString("form"), - ), - ) - .build("CustomMeasure"); - -const defaultCustomMeasures: CustomMeasures = { - measures: [], -}; - -const CUSTOM_MEASURE_KEY = buildStorageKey( - "aml-custom-measures", - codecForCustomMeasures(), -); -/** - * User preferences. - * - * @returns tuple of [state, update()] - */ -export function useCustomMeasures(): [ - Readonly<CustomMeasures>, - <T extends keyof CustomMeasures>(key: T, value: CustomMeasures[T]) => void, - (s: CustomMeasures) => void, -] { - const { value, update } = useLocalStorage( - CUSTOM_MEASURE_KEY, - defaultCustomMeasures, - ); - - function updateField<T extends keyof CustomMeasures>( - k: T, - v: CustomMeasures[T], - ) { - const newValue = { ...value, [k]: v }; - update(newValue); - } - return [value, updateField, update]; -} diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -16,22 +16,19 @@ import { AbsoluteTime, - Codec, - Duration, - KycRule, buildCodecForObject, + Codec, codecForAbsoluteTime, codecForAny, codecForBoolean, - codecForDuration, - codecForDurationMs, codecForKycRules, codecForList, codecForMap, codecForNumber, codecForString, codecOptional, - codecOptionalDefault, + KycRule, + MeasureInformation, } from "@gnu-taler/taler-util"; import { buildStorageKey, @@ -101,8 +98,20 @@ export interface DecisionRequest { * Custom unsupported events to be triggered */ custom_events: string[] | undefined; + + /** + * Custom measures defined by the officer + */ + custom_measures: Record<string, MeasureInformation> | undefined; } +export const codecForMeasure = (): Codec<MeasureInformation> => + buildCodecForObject<MeasureInformation>() + .property("check_name", codecForString()) + .property("prog_name", codecForString()) + .property("context", codecOptional(codecForMap(codecForString()))) + .build("MeasureInformation"); + export const codecForAccountAttributes = (): Codec<AccountAttributes> => buildCodecForObject<AccountAttributes>() .property("expiration", codecOptional(codecForAbsoluteTime)) @@ -122,6 +131,7 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> => .property("justification", codecOptional(codecForString())) .property("accountName", codecOptional(codecForString())) .property("custom_events", codecOptional(codecForList(codecForString()))) + .property("custom_measures", codecOptional(codecForMap(codecForMeasure()))) .property( "triggering_events", codecOptional(codecForList(codecForString())), @@ -142,6 +152,7 @@ export const DECISION_REQUEST_EMPTY: DecisionRequest = { justification: undefined, keep_investigating: undefined, new_measures: undefined, + custom_measures: undefined, properties: undefined, rules: undefined, }; diff --git a/packages/aml-backoffice-ui/src/hooks/preferences.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts @@ -19,6 +19,7 @@ import { TranslatedString, buildCodecForObject, codecForBoolean, + codecOptionalDefault, } from "@gnu-taler/taler-util"; import { buildStorageKey, @@ -36,11 +37,20 @@ interface Preferences { export const codecForPreferences = (): Codec<Preferences> => buildCodecForObject<Preferences>() - .property("allowInsecurePassword", codecForBoolean()) - .property("showDebugInfo", codecForBoolean()) - .property("testingDialect", codecForBoolean()) - .property("keepSessionAfterReload", codecForBoolean()) - .property("preventCompression", codecForBoolean()) + .property( + "allowInsecurePassword", + codecOptionalDefault(codecForBoolean(), false), + ) + .property("showDebugInfo", codecOptionalDefault(codecForBoolean(), false)) + .property("testingDialect", codecOptionalDefault(codecForBoolean(), false)) + .property( + "keepSessionAfterReload", + codecOptionalDefault(codecForBoolean(), false), + ) + .property( + "preventCompression", + codecOptionalDefault(codecForBoolean(), false), + ) .build("Preferences"); const defaultPreferences: Preferences = { @@ -82,7 +92,7 @@ export function getAllBooleanPreferences(): Array<keyof Preferences> { "allowInsecurePassword", "keepSessionAfterReload", "testingDialect", - "preventCompression" + "preventCompression", ]; } diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -54,7 +54,6 @@ import { Fragment, h, Ref, VNode } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { useAccountInformation } from "../hooks/account.js"; -import { CustomMeasures, useCustomMeasures } from "../hooks/custom-measures.js"; import { DECISION_REQUEST_EMPTY, DecisionRequest, @@ -181,7 +180,15 @@ export function CaseDetails({ <div> <button onClick={async () => { - onNewDecision(DECISION_REQUEST_EMPTY); + // the wizard should not require checking the account state + // instead here all the values from the current decision should be + // loaded into the new decision request, like we are doing with e + // custom measures + // FIXME add properties, limits, investigation state + onNewDecision({ + ...DECISION_REQUEST_EMPTY, + custom_measures: activeDecision?.limits.custom_measures, + }); }} class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > @@ -493,7 +500,6 @@ function SubmitNewDecision({ function ShowMesaureInfo({ nextMeasures }: { nextMeasures: string[] }): VNode { const { i18n } = useTranslationContext(); const measures = useServerMeasures(); - const [cm] = useCustomMeasures(); if (!measures) { return <Loading />; } @@ -1204,7 +1210,6 @@ export function ShowMeasuresToSelect({ }): VNode { const measures = useServerMeasures(); const { i18n } = useTranslationContext(); - const [cm] = useCustomMeasures(); if (!measures) { return <Loading />; } @@ -1341,46 +1346,3 @@ export function computeAvailableMesaures( // return serverAndCustom; } - -export function computeAvailableMesauresCustom( - customMeasures: Readonly<CustomMeasures>, - serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, - skpiFilter?: (m: MeasureInfo) => boolean, -): Mesaures { - const init: Mesaures = { forms: [], procedures: [] }; - - if (!customMeasures || !serverMeasures) { - return init; - } - - const custom = customMeasures.measures.reduce((prev, value) => { - if (value.check_name !== "SKIP") { - const r: MeasureInfo = { - type: "form", - name: value.name, - context: value.context, - programName: value.program, - program: serverMeasures.programs[value.program], - checkName: value.check_name, - check: serverMeasures.checks[value.check_name], - custom: true, - }; - if (skpiFilter && skpiFilter(r)) return prev; // skip - prev.forms.push(r); - } else { - const r: MeasureInfo = { - type: "procedure", - name: value.name, - context: value.context, - programName: value.program, - program: serverMeasures.programs[value.program], - custom: true, - }; - if (skpiFilter && skpiFilter(r)) return prev; // skip - prev.procedures.push(r); - } - return prev; - }, init); - - return custom; -} diff --git a/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx @@ -59,11 +59,11 @@ export function CurrentMeasureTable({ <div class="sm:flex sm:items-center"> <div class="sm:flex-auto"> <h1 class="text-base font-semibold text-gray-900"> - <i18n.Translate>Gather information</i18n.Translate> + <i18n.Translate>Forms</i18n.Translate> </h1> <p class="mt-2 text-sm text-gray-700"> <i18n.Translate> - These measures will ask the customer for information. + Measures used to get gather information about the customer. </i18n.Translate> </p> </div> @@ -109,28 +109,19 @@ export function CurrentMeasureTable({ </thead> <tbody class="divide-y divide-gray-200 bg-white"> {measures.forms.map((m) => { - // if ( - // m.context && - // "internal" in m.context && - // m.context.internal - // ) { - // return <Fragment />; - // } return ( <tr> - {onSelect ? ( + {!onSelect ? undefined : ( <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> <button onClick={() => onSelect(m)} - class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + class="rounded-md w-fit border-0 p-1 mr-1 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > - <i18n.Translate>Select</i18n.Translate> + <i18n.Translate>Modify</i18n.Translate> </button> </td> - ) : ( - <Fragment /> )} - <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6"> + <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 "> {m.name} </td> <td class="whitespace-nowrap p-2 text-sm text-gray-500"> @@ -161,8 +152,7 @@ export function CurrentMeasureTable({ </h1> <p class="mt-2 text-sm text-gray-700"> <i18n.Translate> - These measures will be triggered immediately without customer - interaction. + Triggered immediately without customer interaction. </i18n.Translate> </p> </div> @@ -223,7 +213,7 @@ export function CurrentMeasureTable({ onClick={() => onSelect(m)} class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > - <i18n.Translate>Select</i18n.Translate> + <i18n.Translate>Modify</i18n.Translate> </button> </td> ) : ( diff --git a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx @@ -10,13 +10,13 @@ import { FormDesign, FormUI, InternationalizationAPI, + undefinedIfEmpty, useForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useServerMeasures } from "../hooks/server-info.js"; -import { computeAvailableMesaures } from "./CaseDetails.js"; -import { CurrentMeasureTable } from "./MeasuresTable.js"; +import { useCurrentDecisionRequest } from "../hooks/decision-request.js"; export type MeasureDefinition = { name: string; @@ -33,95 +33,84 @@ export type MeasureDefinition = { export function NewMeasure({ initial, onCancel, - onNewMeasure, }: { - initial?: MeasureDefinition; + initial?: Partial<MeasureDefinition>; onCancel: () => void; - onNewMeasure: (m: MeasureDefinition) => void; }): VNode { - const { i18n } = useTranslationContext(); const measures = useServerMeasures(); - // const [rules, setRules] = useState<KycRule[]>([]); + const { i18n } = useTranslationContext(); const summary = !measures || measures instanceof TalerError || measures.type === "fail" ? undefined : measures.body; - const names = !summary - ? { measures: [], programs: [], checks: [] } - : { - measures: Object.entries(summary.roots).map(([key, value]) => ({ - key, - value, - })), - programs: Object.entries(summary.programs).map(([key, value]) => ({ - key, - value, - })), - checks: Object.entries(summary.checks).map(([key, value]) => ({ - key, - value, - })), - }; + if (!summary) { + return ( + <div> + <i18n.Translate>loading...</i18n.Translate> + </div> + ); + } - const design = formDesign(i18n, names.programs, names.checks, summary); + const addingNew = !undefinedIfEmpty(initial); - const form = useForm<MeasureDefinition>( - design, - initial ?? { - program: "check-tos", - // check: "form-accept-tos", - check: "askEmail", // testing invalid - context: [ - { - key: "domain", - value: "taler.net", - }, - ], - }, - // (f) => { - // if (!summary) return undefined; - // return undefinedIfEmpty<FormErrors<FormType>>({ - // // name: !f.name - // // ? i18n.str`required` - // // : summary.roots[f.name] - // // ? i18n.str`already exist` - // // : undefined, - // // program: !f.program - // // ? i18n.str`required` - // // : programAndCheckMatch(i18n, summary, f.program, f.check) ?? - // // undefined, - // // check: checkAndcontextMatch( - // // i18n, - // // summary, - // // f.check, - // // (f.context ?? []) as { - // // key: string; - // // value: string; - // // }[], - // // ), - // // context: checkAndcontextMatch( - // // i18n, - // // summary, - // // f.check, - // // (f.context ?? []) as { - // // key: string; - // // value: string; - // // }[], - // // ) as any, - // }); - // }, + return ( + <MeasureForm + summary={summary} + initial={initial} + onCancel={onCancel} + addingNew={addingNew} + /> ); +} - if (!summary) { - return <div>loading...</div>; - } +export function MeasureForm({ + summary, + onCancel, + initial, + addingNew, +}: { + initial?: Partial<MeasureDefinition>; + addingNew?: boolean; + summary: AvailableMeasureSummary; + onCancel: () => void; +}) { + const { i18n } = useTranslationContext(); + const [request, _, update] = useCurrentDecisionRequest(); + + const names = { + measures: Object.entries(summary.roots).map(([key, value]) => ({ + key, + value, + })), + programs: Object.entries(summary.programs).map(([key, value]) => ({ + key, + value, + })), + checks: Object.entries(summary.checks).map(([key, value]) => ({ + key, + value, + })), + }; + + const design = formDesign( + i18n, + names.programs, + names.checks, + summary, + !addingNew, + ); + + initial?.context; + const form = useForm<MeasureDefinition>(design, initial ?? {}); const name = !form.status.result ? undefined : form.status.result.name; const program = - !form.status.result || !form.status.result.program + !form.status.result || + !form.status.result.program || + !summary.programs[form.status.result.program] ? undefined : { ...summary.programs[form.status.result.program], @@ -129,7 +118,9 @@ export function NewMeasure({ }; const check = - !form.status.result || !form.status.result.check + !form.status.result || + !form.status.result.check || + !summary.checks[form.status.result.check] ? undefined : { ...summary.checks[form.status.result.check], @@ -141,25 +132,6 @@ export function NewMeasure({ ? [] : (form.status.result.context as { key: string; value: string }[]); - // const related = computeAvailableMesaures( - // summary, - // // custom, - // (m) => { - // if (name && m.name === name) { - // return false; - // } - // if (program && m.programName === program.name) { - // return false; - // } - // if (m.type === "form" && check && m.checkName === check.name) { - // return false; - // } - // return true; - // }, - // ); - - // const haveRelated = related.forms.length > 0 || related.procedures.length > 0; - return ( <div> <h2 class="mt-4 mb-2"> @@ -177,15 +149,77 @@ export function NewMeasure({ <i18n.Translate>Cancel</i18n.Translate> </button> - <button - disabled={form.status.status === "fail"} - onClick={() => { - onNewMeasure(form.status.result as MeasureDefinition); - }} - class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600" - > - <i18n.Translate>Add</i18n.Translate> - </button> + {addingNew ? ( + <button + disabled={form.status.status === "fail"} + onClick={() => { + const newMeasure = form.status.result as MeasureDefinition; + const currentMeasures = { ...request.custom_measures }; + currentMeasures[name!] = { + check_name: newMeasure.check, + prog_name: newMeasure.program, + context: (newMeasure.context ?? []).reduce( + (prev, cur) => { + prev[cur.key] = cur.value; + return prev; + }, + {} as Record<string, string>, + ), + }; + update({ + ...request, + custom_measures: currentMeasures, + }); + onCancel(); + }} + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600" + > + <i18n.Translate>Add</i18n.Translate> + </button> + ) : ( + <Fragment> + <button + disabled={form.status.status === "fail"} + onClick={() => { + const newMeasure = form.status.result as MeasureDefinition; + const currentMeasures = { ...request.custom_measures }; + currentMeasures[name!] = { + check_name: newMeasure.check, + prog_name: newMeasure.program, + context: (newMeasure.context ?? []).reduce( + (prev, cur) => { + prev[cur.key] = cur.value; + return prev; + }, + {} as Record<string, string>, + ), + }; + update({ + ...request, + custom_measures: currentMeasures, + }); + onCancel(); + }} + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600" + > + <i18n.Translate>Update</i18n.Translate> + </button> + <button + onClick={() => { + const currentMeasures = { ...request.custom_measures }; + delete currentMeasures[name!]; + update({ + ...request, + custom_measures: currentMeasures, + }); + onCancel(); + }} + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600" + > + <i18n.Translate>Remove</i18n.Translate> + </button> + </Fragment> + )} <h2 class="mt-4 mb-2"> <i18n.Translate>Description</i18n.Translate> @@ -302,25 +336,6 @@ export function NewMeasure({ <div class="px-4 pb-2"></div> </div> )} - - {/* {!haveRelated ? undefined : ( - <div class="px-4 mt-4"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h1 class="text-base font-semibold text-gray-900"> - <i18n.Translate>Related measures</i18n.Translate> - </h1> - <p class="mt-2 text-sm text-gray-700"> - <i18n.Translate> - This measures share checks or programs - </i18n.Translate> - </p> - </div> - </div> - - <CurrentMeasureTable measures={related} /> - </div> - )} */} </div> ); } @@ -329,7 +344,8 @@ const formDesign = ( i18n: InternationalizationAPI, programs: { key: string; value: AmlProgramRequirement }[], checks: { key: string; value: KycCheckInformation }[], - summary: AvailableMeasureSummary | undefined, + summary: AvailableMeasureSummary, + cantChangeName: boolean, ): FormDesign<KycRule> => ({ type: "single-column", fields: [ @@ -337,11 +353,12 @@ const formDesign = ( id: "name", type: "text", required: true, + disabled: cantChangeName, label: i18n.str`Name`, validator(value) { return !value ? i18n.str`required` - : summary && summary.roots[value] + : summary.roots[value] ? i18n.str`There is already a measure with that name` : undefined; }, @@ -360,9 +377,7 @@ const formDesign = ( validator(value, form) { return !value ? i18n.str`required` - : !summary - ? undefined - : programAndCheckMatch(i18n, summary, value, form.check) ?? + : programAndCheckMatch(i18n, summary, value, form.check) ?? programAndContextMatch(i18n, summary, value, form.context); }, }, @@ -380,7 +395,7 @@ const formDesign = ( validator(value, form) { return checkAndcontextMatch( i18n, - summary!, + summary, value, (form.context ?? []) as { key: string; @@ -398,7 +413,7 @@ const formDesign = ( { type: "text", id: "key", - label: i18n.str`Key`, + label: i18n.str`Field name`, }, { type: "text", @@ -439,13 +454,13 @@ function checkAndcontextMatch( i18n: InternationalizationAPI, summary: AvailableMeasureSummary, checkName: string | undefined, - context: { key: string; value: string }[], + context: { key: string; value: string }[] | undefined, ): TranslatedString | undefined { if (checkName === undefined) { return undefined; } const check = summary.checks[checkName]; - const output = context.map((d) => d.key); + const output = !context ? [] : context.map((d) => d.key); const missing = check.requires.filter((d) => { return output.indexOf(d) === -1; }); @@ -459,10 +474,10 @@ function programAndContextMatch( i18n: InternationalizationAPI, summary: AvailableMeasureSummary, program: string, - context: { key: string; value: string }[], + context: { key: string; value: string }[] | undefined, ): TranslatedString | undefined { const check = summary.programs[program]; - const output = context.map((d) => d.key); + const output = !context ? [] : context.map((d) => d.key); const missing = check.context.filter((d) => { return output.indexOf(d) === -1; }); diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx @@ -1,4 +1,8 @@ -import { MeasureInformation, TalerError } from "@gnu-taler/taler-util"; +import { + MeasureInformation, + TalerError, + TalerExchangeApi, +} from "@gnu-taler/taler-util"; import { FormDesign, FormUI, @@ -9,18 +13,15 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useMemo, useState } from "preact/hooks"; -import { - CustomMeasures, - useCustomMeasures, -} from "../../hooks/custom-measures.js"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; import { useServerMeasures } from "../../hooks/server-info.js"; +import { computeAvailableMesaures } from "../CaseDetails.js"; import { - computeAvailableMesaures, - computeAvailableMesauresCustom, -} from "../CaseDetails.js"; -import { CurrentMeasureTable } from "../MeasuresTable.js"; -import { NewMeasure } from "../NewMeasure.js"; + CurrentMeasureTable, + MeasureInfo, + Mesaures, +} from "../MeasuresTable.js"; +import { MeasureDefinition, NewMeasure } from "../NewMeasure.js"; /** * Ask for more information, define new paths to proceed @@ -28,19 +29,45 @@ import { NewMeasure } from "../NewMeasure.js"; * @returns */ export function Measures({}: {}): VNode { + const [initMeasure, setAddMeasure] = useState<Partial<MeasureDefinition>>(); //test; + if (initMeasure) { + return ( + <NewMeasure + onCancel={() => { + setAddMeasure(undefined); + }} + initial={initMeasure} + /> + ); + } + + return ( + <Fragment> + <ActiveMeasureForm /> + <ShowAllMeasures + addNewMeasure={(m) => { + setAddMeasure(m); + }} + /> + </Fragment> + ); +} + +function ActiveMeasureForm(): VNode { const { i18n } = useTranslationContext(); const [request, _, updateRequest] = useCurrentDecisionRequest(); const measures = useServerMeasures(); - const [custom] = useCustomMeasures(); const measureBody = !measures || measures instanceof TalerError || measures.type === "fail" ? undefined : measures.body; - const measureList = !measureBody - ? [] - : Object.entries(measureBody.roots).map(([id, mi]) => ({ id, ...mi })); + const measureList = !measureBody ? [] : Object.keys(measureBody.roots); + const design = formDesign(i18n, [ + ...measureList, + ...Object.keys(request?.custom_measures ?? {}), + ]); const nm = !request.new_measures ? [] : request.new_measures; @@ -49,9 +76,7 @@ export function Measures({}: {}): VNode { [request.new_measures], ); - const design = formDesign(i18n, measureList, custom); const form = useForm<FormType>(design, initValue); - onComponentUnload(() => { updateRequest({ ...request, @@ -59,42 +84,78 @@ export function Measures({}: {}): VNode { }); }); - const haveCustomMeasures = Object.keys(custom.measures).length > 0; + return <FormUI design={design} model={form.model} />; +} + +function ShowAllMeasures({ + addNewMeasure, +}: { + addNewMeasure: (m: Partial<MeasureDefinition>) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const measures = useServerMeasures(); + + const measureBody = + !measures || measures instanceof TalerError || measures.type === "fail" + ? undefined + : measures.body; + + const [request] = useCurrentDecisionRequest(); + const haveCustomMeasures = + Object.keys(request?.custom_measures ?? {}).length > 0; - const [addMesaure, setAddMeasure] = useState<boolean>(true)//test; - if (addMesaure) { - return ( - <NewMeasure - onCancel={() => { - setAddMeasure(false); - }} - onNewMeasure={() => { - - }} - /> - ); - } return ( <div> - <FormUI design={design} model={form.model} /> <button onClick={() => { - setAddMeasure(true); + addNewMeasure({}); }} class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600" > <i18n.Translate>Add custom measure</i18n.Translate> </button> {!haveCustomMeasures ? undefined : ( - <Fragment> - <h1>Custom measures</h1> + <div class="divide-y divide-gray-200 overflow-x-scroll rounded-lg bg-white shadow-sm"> + <div class="p-2"> + <h1> + <i18n.Translate>Custom measures</i18n.Translate> + </h1> + </div> + <div class="p-2"> + <CurrentMeasureTable + measures={computeAvailableMesauresCustom( + request.custom_measures, + measureBody, + )} + onSelect={(m) => { + addNewMeasure({ + check: m.type === "form" ? m.checkName : undefined, + context: !m.context + ? [] + : Object.entries(m.context).map(([key, value]) => ({ + key, + value, + })), + name: m.name, + program: m.programName, + }); + }} + /> + </div> + </div> + )} + <div class="divide-y divide-gray-200 overflow-x-scroll rounded-lg bg-white shadow-sm"> + <div class="p-2"> + <h1> + <i18n.Translate>Server measures</i18n.Translate> + </h1> + </div> + <div class="p-2"> <CurrentMeasureTable - measures={computeAvailableMesauresCustom(custom, measureBody)} + measures={computeAvailableMesaures(measureBody)} /> - </Fragment> - )} - <h1>Server measures</h1> - <CurrentMeasureTable measures={computeAvailableMesaures(measureBody)} /> + </div> + </div> </div> ); } @@ -105,8 +166,7 @@ type FormType = { function formDesign( i18n: InternationalizationAPI, - mi: (MeasureInformation & { id: string })[], - cm: CustomMeasures, + measureNames: string[], ): FormDesign<FormType> { return { type: "single-column", @@ -114,20 +174,12 @@ function formDesign( { type: "selectMultiple", unique: true, - choices: [ - ...mi.map((m) => { - return { - value: m.id, - label: m.id, - }; - }), - ...cm.measures.map((m) => { - return { - value: m.name, - label: m.name, - }; - }), - ], + choices: measureNames.map((name) => { + return { + value: name, + label: name, + }; + }), id: "measures", label: i18n.str`Active measures`, help: i18n.str`Measures that the customer will need to satisfy while the rules are active.`, @@ -135,3 +187,46 @@ function formDesign( ], }; } + +function computeAvailableMesauresCustom( + customMeasures: Record<string, MeasureInformation> | undefined, + serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, + skpiFilter?: (m: MeasureInfo) => boolean, +): Mesaures { + const init: Mesaures = { forms: [], procedures: [] }; + + if (!customMeasures || !serverMeasures) { + return init; + } + + const custom = Object.entries(customMeasures).reduce((prev, [key, value]) => { + if (value.check_name !== "SKIP") { + const r: MeasureInfo = { + type: "form", + name: key, + context: value.context, + programName: value.prog_name, + program: serverMeasures.programs[value.prog_name], + checkName: value.check_name, + check: serverMeasures.checks[value.check_name], + custom: true, + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.forms.push(r); + } else { + const r: MeasureInfo = { + type: "procedure", + name: key, + context: value.context, + programName: value.prog_name, + program: serverMeasures.programs[value.prog_name], + custom: true, + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.procedures.push(r); + } + return prev; + }, init); + + return custom; +} diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx @@ -212,7 +212,7 @@ function AddNewRuleForm({ }: { onAdd: (nr: RuleFormType) => void; config: ExchangeVersionResponse; - measureList: MeasureListWithId; + measureList: string[]; onClose: () => void; isWallet: boolean; }): VNode { @@ -271,12 +271,12 @@ function UpdateRulesForm({ const [request, updateRequestField, updateRequest] = useCurrentDecisionRequest(); const [showAddRuleForm, setShowAddRuleForm] = useState(false); - const measureList: MeasureListWithId = !rootMeasures - ? [] - : Object.entries(rootMeasures).map(([id, mi]) => ({ id, ...mi })); - - const expirationFormDesign = expirationFormDesignTemplate(i18n, measureList); - + const measureList = !rootMeasures ? [] : Object.keys(rootMeasures); + const customMeasures = Object.keys(request.custom_measures ?? {}); + const expirationFormDesign = expirationFormDesignTemplate(i18n, [ + ...measureList, + ...customMeasures, + ]); const expirationForm = useForm<ExpirationFormType>(expirationFormDesign, { expiration: request.deadline ?? @@ -520,7 +520,7 @@ function labelForOperationType( const ruleFormDesignTemplate = ( i18n: InternationalizationAPI, currency: string, - mi: (MeasureInformation & { id: string })[], + measureNames: string[], isWallet: boolean, ): FormDesign<KycRule> => ({ type: "single-column", @@ -561,10 +561,10 @@ const ruleFormDesignTemplate = ( { type: "selectMultiple", unique: true, - choices: mi.map((m) => { + choices: measureNames.map((name) => { return { - value: m.id, - label: m.id, + value: name, + label: name, }; }), id: "measures", @@ -581,7 +581,7 @@ const ruleFormDesignTemplate = ( }); const expirationFormDesignTemplate = ( i18n: InternationalizationAPI, - mi: (MeasureInformation & { id: string })[], + measureNames: string[], ): FormDesign<KycRule> => ({ type: "single-column", fields: [ @@ -628,10 +628,10 @@ const expirationFormDesignTemplate = ( }, { type: "selectOne", - choices: mi.map((m) => { + choices: measureNames.map((name) => { return { - value: m.id, - label: m.id, + value: name, + label: name, }; }), id: "measure", diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -2,8 +2,6 @@ import { AbsoluteTime, AmlDecisionRequest, assertUnreachable, - buildPayto, - Duration, HttpStatusCode, parsePaytoUri, PaytoString, @@ -19,14 +17,17 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; -import { DECISION_REQUEST_EMPTY, useCurrentDecisionRequest } from "../../hooks/decision-request.js"; +import { + DECISION_REQUEST_EMPTY, + useCurrentDecisionRequest, +} from "../../hooks/decision-request.js"; +import { useOfficer } from "../../hooks/officer.js"; import { useServerMeasures } from "../../hooks/server-info.js"; import { computeAvailableMesaures, ShowDecisionLimitInfo, } from "../CaseDetails.js"; import { CurrentMeasureTable, Mesaures } from "../MeasuresTable.js"; -import { useOfficer } from "../../hooks/officer.js"; import { isAttributesCompleted, isEventsCompleted, @@ -37,7 +38,6 @@ import { isRulesCompleted, WizardSteps, } from "./AmlDecisionRequestWizard.js"; -import { useCustomMeasures } from "../../hooks/custom-measures.js"; /** * Mark for further investigation and explain decision @@ -99,7 +99,7 @@ export function Summary({ onMove(undefined); } - const fullPayto = !newPayto? undefined: parsePaytoUri(newPayto) + const fullPayto = !newPayto ? undefined : parsePaytoUri(newPayto); if (fullPayto && decision.accountName) { fullPayto.params["receiver-name"] = decision.accountName; } @@ -124,15 +124,15 @@ export function Summary({ rules: decision.rules!, successor_measure: decision.onExpire_measure, custom_measures: { - "asd": { + asd: { check_name: "form-accept-tos", prog_name: "check-tos", context: { tos_url: "taler.net", provider_name: "asd", - } - } - }, + }, + }, + }, }, attributes_expiration: decision.attributes?.expiration ? AbsoluteTime.toProtocolTimestamp(