taler-typescript-core

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

commit 2ba33b6e71d21f523f1730f2ae2cf7803a8f67c0
parent b90c8b01c375c656426d13d241e80263d2fe03ca
Author: Florian Dold <dold@taler.net>
Date:   Mon, 31 Aug 2026 14:23:33 +0200

exchange AML web UI: share detailed measure cards

Diffstat:
Apackages/taler-exchange-aml-webui/src/components/MeasureDetails.stories.tsx | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-exchange-aml-webui/src/components/MeasureDetails.tsx | 339+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-exchange-aml-webui/src/components/MeasureList.tsx | 124-------------------------------------------------------------------------------
Apackages/taler-exchange-aml-webui/src/components/MeasureSelection.test.ts | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-exchange-aml-webui/src/components/MeasureSelection.ts | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-exchange-aml-webui/src/components/MeasuresTable.tsx | 327-------------------------------------------------------------------------------
Mpackages/taler-exchange-aml-webui/src/components/ShowLegitimizationInfo.tsx | 19++++++++++++-------
Mpackages/taler-exchange-aml-webui/src/pages/Info.tsx | 235++++---------------------------------------------------------------------------
Mpackages/taler-exchange-aml-webui/src/pages/decision/Summary.tsx | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Dpackages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.test.ts | 32--------------------------------
Dpackages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.ts | 163-------------------------------------------------------------------------------
11 files changed, 650 insertions(+), 904 deletions(-)

diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureDetails.stories.tsx b/packages/taler-exchange-aml-webui/src/components/MeasureDetails.stories.tsx @@ -0,0 +1,114 @@ +/* + This file is part of GNU Taler + (C) 2026 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. +*/ + +import { + AvailableMeasureSummary, + LimitOperationType, +} from "@gnu-taler/taler-util"; +import * as tests from "@gnu-taler/web-util/testing"; +import { h, VNode } from "preact"; +import { MeasureCard } from "./MeasureDetails.js"; + +export default { title: "AML configuration/measure details" }; + +const summary: AvailableMeasureSummary = { + roots: { + "collect-address": { + check_name: "address-form", + prog_name: "review-address", + operation_type: LimitOperationType.withdraw, + context: { document_type: "utility-bill" }, + }, + "show-information": { + check_name: "information-notice", + voluntary: true, + }, + "freeze-account": { + check_name: "SKIP", + prog_name: "freeze-account", + }, + "broken-reference": { + check_name: "missing-check", + prog_name: "missing-program", + context: { reason: "configuration example" }, + }, + }, + checks: { + "address-form": { + description: "Collect a recent proof of address", + description_i18n: { de: "Aktuellen Adressnachweis erfassen" }, + requires: ["document_type"], + outputs: ["street", "city", "country"], + fallback: "show-information", + }, + "information-notice": { + description: "Show information without starting an AML program", + requires: [], + outputs: [], + fallback: "collect-address", + }, + }, + programs: { + "review-address": { + description: "Review the submitted address information", + context: ["case_id"], + inputs: ["street", "city", "country"], + }, + "freeze-account": { + description: "Apply an immediate account freeze", + context: [], + inputs: [], + }, + }, + default_rules: [], +}; + +function AvailableCatalog(): VNode { + return ( + <div class="grid gap-4 lg:grid-cols-2"> + {Object.entries(summary.roots).map(([name, measure]) => ( + <MeasureCard + key={name} + name={name} + measure={measure} + summary={summary} + language="en" + action={ + <button + type="button" + class="rounded-md border border-primary px-3 py-2 text-sm font-semibold text-primary" + > + Customize a copy + </button> + } + /> + ))} + </div> + ); +} + +export const AvailableExchangeMeasures = tests.createExample( + AvailableCatalog, + {}, +); + +export const LocalizedCustomerForm = tests.createExample(MeasureCard, { + name: "collect-address", + measure: summary.roots["collect-address"], + summary, + language: "de-CH", +}); + +export const StandaloneCheck = tests.createExample(MeasureCard, { + name: "check-information-notice", + measure: { check_name: "information-notice" }, + summary, + language: "en", + category: "standalone-check", +}); diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureDetails.tsx b/packages/taler-exchange-aml-webui/src/components/MeasureDetails.tsx @@ -0,0 +1,339 @@ +/* + This file is part of GNU Taler + (C) 2026 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. +*/ + +import { + AvailableMeasureSummary, + KycCheckInformation, + MeasureInformation, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { labelForOperationType } from "./RulesInfo.js"; +import { isBuiltInSkipCheck } from "../utils/measure-check.js"; + +export type MeasureCategory = + | "customer-form" + | "information" + | "instant" + | "standalone-check"; + +export interface MeasureCardProps { + name?: string; + measure: MeasureInformation; + summary: AvailableMeasureSummary | undefined; + language: string; + category?: MeasureCategory; + action?: ComponentChildren; +} + +export function MeasureCard({ + name, + measure, + summary, + language, + category, + action, +}: MeasureCardProps): VNode { + const { i18n } = useTranslationContext(); + const builtInSkip = isBuiltInSkipCheck(measure.check_name); + const check = builtInSkip ? undefined : summary?.checks[measure.check_name]; + const program = measure.prog_name + ? summary?.programs[measure.prog_name] + : undefined; + const effectiveCategory = + category ?? + (builtInSkip + ? "instant" + : measure.prog_name + ? "customer-form" + : "information"); + + return ( + <article class="flex h-full flex-col rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-950"> + <div class="flex flex-wrap items-start justify-between gap-3"> + <div class="min-w-0"> + <span class="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-200"> + {labelForMeasureCategory(effectiveCategory, i18n)} + </span> + {name ? ( + <div class="mt-2"> + <Code>{name}</Code> + </div> + ) : undefined} + </div> + {action} + </div> + + <dl class="mt-4 space-y-3"> + <ReferenceField + label={i18n.str`KYC check`} + name={measure.check_name} + description={ + builtInSkip + ? i18n.str`Built-in check that proceeds without collecting customer data.` + : check && localizedCheckDescription(check, language) + } + missing={!builtInSkip && !!summary && !check} + /> + {measure.prog_name ? ( + <ReferenceField + label={i18n.str`AML program`} + name={measure.prog_name} + description={program?.description} + missing={!!summary && !program} + /> + ) : ( + <SimpleField label={i18n.str`AML program`} value={i18n.str`None`} /> + )} + <div class="grid gap-3 sm:grid-cols-2"> + <SimpleField + label={i18n.str`Operation`} + value={ + measure.operation_type + ? labelForOperationType(measure.operation_type, i18n) + : i18n.str`Not specified` + } + /> + <SimpleField + label={i18n.str`Voluntary`} + value={measure.voluntary ? i18n.str`Yes` : i18n.str`No`} + /> + </div> + </dl> + + <details class="mt-4 border-t border-gray-200 pt-3 dark:border-gray-700"> + <summary class="min-h-9 cursor-pointer py-1 text-sm font-semibold text-gray-700 dark:text-gray-200"> + <i18n.Translate>Technical details</i18n.Translate> + </summary> + <dl class="mt-3 space-y-3"> + {check ? ( + <> + <TokenField + label={i18n.str`Required check context`} + values={check.requires} + /> + <TokenField + label={i18n.str`Check outputs`} + values={check.outputs} + /> + <ReferenceField + label={i18n.str`Fallback measure`} + name={check.fallback} + missing={!!summary && !summary.roots[check.fallback]} + /> + </> + ) : undefined} + {program ? ( + <> + <TokenField + label={i18n.str`Required program context`} + values={program.context} + /> + <TokenField + label={i18n.str`Required program inputs`} + values={program.inputs} + /> + </> + ) : undefined} + <ContextField context={measure.context} /> + </dl> + </details> + </article> + ); +} + +export function CheckCard({ + name, + check, + language, + fallbackAvailable, +}: { + name: string; + check: KycCheckInformation; + language: string; + fallbackAvailable: boolean; +}): VNode { + const { i18n } = useTranslationContext(); + return ( + <DataCard + title={name} + description={localizedCheckDescription(check, language)} + > + <TokenField label={i18n.str`Required context`} values={check.requires} /> + <TokenField label={i18n.str`Outputs`} values={check.outputs} /> + <ReferenceField + label={i18n.str`Fallback measure`} + name={check.fallback} + missing={!fallbackAvailable} + /> + </DataCard> + ); +} + +export function DataCard({ + title, + description, + children, +}: { + title: string; + description?: string; + children: ComponentChildren; +}): VNode { + return ( + <article class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-950"> + <Code>{title}</Code> + {description ? ( + <p class="mt-3 text-sm leading-6 text-gray-700 dark:text-gray-300"> + {description} + </p> + ) : undefined} + <dl class="mt-4 space-y-3">{children}</dl> + </article> + ); +} + +export function SimpleField({ + label, + value, +}: { + label: TranslatedString; + value: string | TranslatedString; +}): VNode { + return ( + <div> + <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> + {label} + </dt> + <dd class="mt-1 text-sm text-gray-800 dark:text-gray-200">{value}</dd> + </div> + ); +} + +export function ReferenceField({ + label, + name, + description, + missing = false, +}: { + label: TranslatedString; + name: string; + description?: string; + missing?: boolean; +}): VNode { + const { i18n } = useTranslationContext(); + return ( + <div> + <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> + {label} + </dt> + <dd class="mt-1 text-sm text-gray-800 dark:text-gray-200"> + <Code>{name}</Code>{" "} + {missing ? ( + <span class="font-semibold text-red-700 dark:text-red-300"> + <i18n.Translate>Unavailable reference</i18n.Translate> + </span> + ) : description ? ( + <span class="ml-1 text-gray-600 dark:text-gray-300"> + {description} + </span> + ) : undefined} + </dd> + </div> + ); +} + +export function TokenField({ + label, + values, +}: { + label: TranslatedString; + values: string[]; +}): VNode { + const { i18n } = useTranslationContext(); + return ( + <div> + <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> + {label} + </dt> + <dd class="mt-1 flex flex-wrap gap-1.5"> + {values.length ? ( + values.map((value) => <Code key={value}>{value}</Code>) + ) : ( + <span class="text-sm text-gray-500 dark:text-gray-400"> + <i18n.Translate>None</i18n.Translate> + </span> + )} + </dd> + </div> + ); +} + +export function ContextField({ + context, +}: { + context: object | undefined; +}): VNode { + const { i18n } = useTranslationContext(); + const hasContext = context && Object.keys(context).length > 0; + return ( + <div> + <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> + <i18n.Translate>Configured context</i18n.Translate> + </dt> + <dd class="mt-1"> + {hasContext ? ( + <pre class="overflow-x-auto rounded bg-gray-50 p-3 text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"> + {JSON.stringify(context, undefined, 2)} + </pre> + ) : ( + <span class="text-sm text-gray-500 dark:text-gray-400"> + <i18n.Translate>None</i18n.Translate> + </span> + )} + </dd> + </div> + ); +} + +export function Code({ children }: { children: string }): VNode { + return ( + <code class="break-all rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-800 dark:text-gray-200"> + {children} + </code> + ); +} + +export function localizedCheckDescription( + check: KycCheckInformation, + language: string, +): string { + const baseLanguage = language.split("-")[0]; + return ( + check.description_i18n?.[language] ?? + check.description_i18n?.[baseLanguage] ?? + check.description + ); +} + +function labelForMeasureCategory( + category: MeasureCategory, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): TranslatedString { + switch (category) { + case "customer-form": + return i18n.str`Customer form`; + case "information": + return i18n.str`Information notice`; + case "instant": + return i18n.str`Instant AML program`; + case "standalone-check": + return i18n.str`Standalone KYC check`; + } +} diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureList.tsx b/packages/taler-exchange-aml-webui/src/components/MeasureList.tsx @@ -1,124 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2025 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 { - assertUnreachable, - HttpStatusCode, - TalerError, -} from "@gnu-taler/taler-util"; -import { - Attention, - ErrorLoading, - FailLoading, - Loading, - RouteDefinition, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, h } from "preact"; -import { useServerMeasures } from "../hooks/server-info.js"; -import { Profile } from "../pages/Profile.js"; -import { computeMeasureInformation } from "../utils/computeAvailableMesaures.js"; -import { CurrentMeasureTable } from "./MeasuresTable.js"; - -const TALER_SCREEN_ID = 124; - -export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { - const { i18n } = useTranslationContext(); - - const measures = useServerMeasures(); - // const [custom] = useCustomMeasures(); - - if (!measures) { - return <Loading />; - } - if (measures instanceof TalerError) { - return ( - <ErrorLoading - title={i18n.str`Failed to load server measures.`} - error={measures} - /> - ); - } - - if (measures.type === "fail") { - return ( - <Fragment> - <Profile /> - <FailLoading - operation={measures} - title={i18n.str`Failed to load the measures`} - translate={(d) => { - switch (d.case) { - case HttpStatusCode.Forbidden: - return ( - <i18n.Translate> - This session signature is invalid, contact administrator or - create a new one. - </i18n.Translate> - ); - case HttpStatusCode.NotFound: - return ( - <i18n.Translate> - The designated AML session is not known, contact - administrator or create a new one. - </i18n.Translate> - ); - case HttpStatusCode.Conflict: - return ( - <i18n.Translate> - The designated AML session is not enabled, contact - administrator or create a new one. - </i18n.Translate> - ); - default: - assertUnreachable(d.case); - } - }} - /> - </Fragment> - ); - } - - const ms = computeMeasureInformation(measures.body); - - return ( - <div> - <div class="px-4 sm:px-6 lg:px-8"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h1 class="text-base font-semibold text-gray-900"> - <i18n.Translate>Measures</i18n.Translate> - </h1> - <p class="mt-2 text-sm text-gray-700"> - <i18n.Translate> - A list of all the predefined measures you can use. - </i18n.Translate> - </p> - </div> - <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none"> - <a - href={routeToNew.url({})} - class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - > - <i18n.Translate>Add custom measure</i18n.Translate> - </a> - </div> - </div> - - <CurrentMeasureTable measures={ms} /> - </div> - </div> - ); -} diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureSelection.test.ts b/packages/taler-exchange-aml-webui/src/components/MeasureSelection.test.ts @@ -0,0 +1,56 @@ +/* + This file is part of GNU Taler + (C) 2026 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. +*/ + +import { + AvailableMeasureSummary, + i18n, + setupI18n, +} from "@gnu-taler/taler-util"; +import assert from "node:assert/strict"; +import test from "node:test"; +import { + categoryForMeasureDefinition, + descriptionForMeasureDefinition, +} from "./MeasureSelection.js"; + +setupI18n("de-CH", {}); + +const summary: AvailableMeasureSummary = { + roots: {}, + checks: { + identity: { + description: "Collect identity", + description_i18n: { de: "Identität erfassen" }, + requires: [], + outputs: [], + fallback: "manual-review", + }, + }, + programs: { + review: { + description: "Review the submission", + context: [], + inputs: [], + }, + }, + default_rules: [], +}; + +test("measure choices use the same category and localized detail summary", () => { + const measure = { check_name: "identity", prog_name: "review" }; + assert.equal(categoryForMeasureDefinition(measure), "customer-form"); + assert.equal( + descriptionForMeasureDefinition(measure, summary, "de-CH", i18n), + "Customer form · Identität erfassen · Review the submission", + ); + assert.equal( + categoryForMeasureDefinition({ check_name: "SKIP", prog_name: "review" }), + "instant", + ); +}); diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureSelection.ts b/packages/taler-exchange-aml-webui/src/components/MeasureSelection.ts @@ -0,0 +1,55 @@ +/* + This file is part of GNU Taler + (C) 2026 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. +*/ + +import { + AvailableMeasureSummary, + InternationalizationAPI, + MeasureInformation, +} from "@gnu-taler/taler-util"; +import { + MeasureCategory, + localizedCheckDescription, +} from "./MeasureDetails.js"; +import { isBuiltInSkipCheck } from "../utils/measure-check.js"; + +export function categoryForMeasureDefinition( + measure: MeasureInformation, +): MeasureCategory { + if (isBuiltInSkipCheck(measure.check_name)) return "instant"; + return measure.prog_name ? "customer-form" : "information"; +} + +export function descriptionForMeasureDefinition( + measure: MeasureInformation, + summary: AvailableMeasureSummary, + language: string, + i18n: InternationalizationAPI, + category = categoryForMeasureDefinition(measure), +): string { + const check = summary.checks[measure.check_name]; + const program = measure.prog_name + ? summary.programs[measure.prog_name] + : undefined; + const categoryLabel = + category === "instant" + ? i18n.str`Instant AML program` + : category === "customer-form" + ? i18n.str`Customer form` + : category === "standalone-check" + ? i18n.str`Standalone KYC check` + : i18n.str`Information notice`; + const checkDescription = check + ? localizedCheckDescription(check, language) + : isBuiltInSkipCheck(measure.check_name) + ? i18n.str`Runs without customer interaction` + : undefined; + return [categoryLabel, checkDescription, program?.description] + .filter((part): part is string => !!part) + .join(" · "); +} diff --git a/packages/taler-exchange-aml-webui/src/components/MeasuresTable.tsx b/packages/taler-exchange-aml-webui/src/components/MeasuresTable.tsx @@ -1,327 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2025 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 { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, Fragment, h, VNode } from "preact"; -import { - MeasureInfo, - UiMeasureInformation, -} from "../utils/computeAvailableMesaures.js"; - -const TALER_SCREEN_ID = 123; - -export function CurrentMeasureTable({ - measures, - onSelect, - hideMeasureNames, - actionLabel, -}: { - measures: UiMeasureInformation; - hideMeasureNames?: boolean; - onSelect?: (m: MeasureInfo) => void; - actionLabel?: ComponentChildren; -}): VNode { - const { i18n } = useTranslationContext(); - return ( - <Fragment> - {!measures.forms.length ? undefined : ( - <div class="mt-4 flow-root"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h4 class="text-base font-semibold text-gray-900 dark:text-gray-100"> - <i18n.Translate>Forms</i18n.Translate> - </h4> - <p class="mt-2 text-sm text-gray-700 dark:text-gray-300"> - <i18n.Translate> - Measures used to gather information about the customer. - </i18n.Translate> - </p> - </div> - </div> - - <div class="min-w-full py-2 align-middle"> - <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700"> - <table class="min-w-[42rem] divide-y divide-gray-300 dark:divide-gray-700"> - <thead class="bg-gray-50 dark:bg-gray-900"> - <tr> - {onSelect ? ( - <th scope="col" class="relative p-2 "> - <span class="sr-only"> - <i18n.Translate>Select</i18n.Translate> - </span> - </th> - ) : ( - <Fragment /> - )} - {hideMeasureNames ? undefined : ( - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100" - > - <i18n.Translate>Name</i18n.Translate> - </th> - )} - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100" - > - <i18n.Translate>Check</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100" - > - <i18n.Translate>Program</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100" - > - <i18n.Translate>Context</i18n.Translate> - </th> - </tr> - </thead> - <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-950"> - {measures.forms.map((m, k) => { - return ( - <tr class="even:bg-gray-50 dark:even:bg-gray-900" key={k}> - {!onSelect ? undefined : ( - <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> - <button - onClick={() => onSelect(m)} - class="min-h-10 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-onPrimary hover:bg-primary/90 dark:bg-darkPrimary dark:text-darkOnPrimary" - > - {actionLabel ?? ( - <i18n.Translate>Modify</i18n.Translate> - )} - </button> - </td> - )} - {hideMeasureNames ? undefined : ( - <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 dark:text-gray-100"> - {m.name} - </td> - )} - <td class="p-2 text-sm text-gray-500 dark:text-gray-300"> - {m.check?.description ?? ""} - </td> - <td class="p-2 text-sm text-gray-500 dark:text-gray-300"> - {m.program?.description} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300"> - {Object.keys(m.context ?? {}).join(", ")} - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - </div> - </div> - )} - - {!measures.info.length ? undefined : ( - <div class="mt-4 flow-root"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h4 class="text-base font-semibold text-gray-900 dark:text-gray-100"> - <i18n.Translate>Information</i18n.Translate> - </h4> - <p class="mt-2 text-sm text-gray-700 dark:text-gray-300"> - <i18n.Translate> - Shows information to the customer without running an AML - program. - </i18n.Translate> - </p> - </div> - </div> - - <div class="min-w-full py-2 align-middle"> - <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700"> - <table class="min-w-[36rem] divide-y divide-gray-300 dark:divide-gray-700"> - <thead class="bg-gray-50 dark:bg-gray-900"> - <tr> - {onSelect ? ( - <th scope="col" class="relative p-2 "> - <span class="sr-only"> - <i18n.Translate>Select</i18n.Translate> - </span> - </th> - ) : ( - <Fragment /> - )} - {hideMeasureNames ? undefined : ( - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100" - > - <i18n.Translate>Name</i18n.Translate> - </th> - )} - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100" - > - <i18n.Translate>Check</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100" - > - <i18n.Translate>Context</i18n.Translate> - </th> - </tr> - </thead> - <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-950"> - {measures.info.map((m, k) => { - return ( - <tr class="even:bg-gray-50 dark:even:bg-gray-900" key={k}> - {onSelect ? ( - <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> - <button - onClick={() => onSelect(m)} - class="min-h-10 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-onPrimary hover:bg-primary/90 dark:bg-darkPrimary dark:text-darkOnPrimary" - > - {actionLabel ?? ( - <i18n.Translate>Modify</i18n.Translate> - )} - </button> - </td> - ) : ( - <Fragment /> - )} - {hideMeasureNames ? undefined : ( - <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6 dark:text-gray-100"> - {m.name} - </td> - )} - <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300"> - {m.checkName} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300"> - {Object.keys(m.context ?? {}).join(", ")} - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - </div> - </div> - )} - - {!measures.procedures.length ? undefined : ( - <div class="mt-4 flow-root"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h4 class="text-base font-semibold text-gray-900 dark:text-gray-100"> - <i18n.Translate>Instant measures</i18n.Translate> - </h4> - <p class="mt-2 text-sm text-gray-700 dark:text-gray-300"> - <i18n.Translate> - Triggered immediately without customer interaction. - </i18n.Translate> - </p> - </div> - </div> - - <div class="min-w-full py-2 align-middle"> - <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700"> - <table class="min-w-[42rem] divide-y divide-gray-300 dark:divide-gray-700"> - <thead class="bg-gray-50 dark:bg-gray-900"> - <tr> - {onSelect ? ( - <th scope="col" class="relative p-2 "> - <span class="sr-only"> - <i18n.Translate>Select</i18n.Translate> - </span> - </th> - ) : ( - <Fragment /> - )} - {hideMeasureNames ? undefined : ( - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100" - > - <i18n.Translate>Name</i18n.Translate> - </th> - )} - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100" - > - <i18n.Translate>Program</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100" - > - <i18n.Translate>Input requirement</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100" - > - <i18n.Translate>Context</i18n.Translate> - </th> - </tr> - </thead> - <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-950"> - {measures.procedures.map((m, k) => { - return ( - <tr class="even:bg-gray-50 dark:even:bg-gray-900" key={k}> - {onSelect ? ( - <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> - <button - onClick={() => onSelect(m)} - class="min-h-10 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-onPrimary hover:bg-primary/90 dark:bg-darkPrimary dark:text-darkOnPrimary" - > - {actionLabel ?? ( - <i18n.Translate>Modify</i18n.Translate> - )} - </button> - </td> - ) : ( - <Fragment /> - )} - {hideMeasureNames ? undefined : ( - <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6 dark:text-gray-100"> - {m.name} - </td> - )} - <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300"> - {m.program.description} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300"> - {m.program.inputs.join(",")} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300"> - {Object.keys(m.context ?? {}).join(", ")} - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - </div> - </div> - )} - </Fragment> - ); -} diff --git a/packages/taler-exchange-aml-webui/src/components/ShowLegitimizationInfo.tsx b/packages/taler-exchange-aml-webui/src/components/ShowLegitimizationInfo.tsx @@ -14,8 +14,7 @@ import { import { Time, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { computeMeasureInformation } from "../utils/computeAvailableMesaures.js"; -import { CurrentMeasureTable } from "./MeasuresTable.js"; +import { MeasureCard } from "./MeasureDetails.js"; export function ShowLegistimizationInfo({ since, @@ -32,12 +31,9 @@ export function ShowLegistimizationInfo({ completed: boolean; serverMeasures: AvailableMeasureSummary | undefined; }): VNode { - const { i18n } = useTranslationContext(); + const { i18n, lang } = useTranslationContext(); const [opened, setOpened] = useState(startOpen ?? false); const expandable = !fixed; - const info = computeMeasureInformation(serverMeasures, { - measureList: legitimization.measures, - }); return ( <div class="overflow-hidden rounded-lg border border-gray-300 dark:border-gray-700"> @@ -106,7 +102,16 @@ export function ShowLegistimizationInfo({ </i18n.Translate> )} </p> - <CurrentMeasureTable measures={info} hideMeasureNames /> + <div class="grid gap-4 lg:grid-cols-2"> + {legitimization.measures.map((measure, index) => ( + <MeasureCard + key={index} + measure={measure} + summary={serverMeasures} + language={lang} + /> + ))} + </div> </> )} </div> diff --git a/packages/taler-exchange-aml-webui/src/pages/Info.tsx b/packages/taler-exchange-aml-webui/src/pages/Info.tsx @@ -11,8 +11,6 @@ import { assertUnreachable, AvailableMeasureSummary, HttpStatusCode, - KycCheckInformation, - MeasureInformation, TalerError, TranslatedString, } from "@gnu-taler/taler-util"; @@ -25,7 +23,14 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { RulesInfo, labelForOperationType } from "../components/RulesInfo.js"; +import { + CheckCard, + Code, + DataCard, + MeasureCard, + TokenField, +} from "../components/MeasureDetails.js"; +import { RulesInfo } from "../components/RulesInfo.js"; import { useAmlSpaDialect, usePreferences } from "../hooks/preferences.js"; import { revalidateServerMeasures, @@ -36,7 +41,6 @@ import { DialectPropertyInfo, getDialectInfo, } from "../utils/dialect-info.js"; -import { isBuiltInSkipCheck } from "../utils/measure-check.js"; export function Info(): VNode { const { config, url } = useExchangeApiContext(); @@ -352,7 +356,7 @@ export function MeasuresContent({ {roots.length ? ( <div class="grid gap-4 lg:grid-cols-2"> {roots.map(([name, measure]) => ( - <RootMeasureCard + <MeasureCard key={name} name={name} measure={measure} @@ -457,207 +461,6 @@ function MeasureGroup({ ); } -function RootMeasureCard({ - name, - measure, - summary, - language, -}: { - name: string; - measure: MeasureInformation; - summary: AvailableMeasureSummary; - language: string; -}): VNode { - const { i18n } = useTranslationContext(); - const check = summary.checks[measure.check_name]; - const isBuiltInSkip = isBuiltInSkipCheck(measure.check_name); - const program = measure.prog_name - ? summary.programs[measure.prog_name] - : undefined; - return ( - <DataCard title={name}> - <ReferenceField - label={i18n.str`KYC check`} - name={measure.check_name} - description={ - isBuiltInSkip - ? i18n.str`Built-in check that proceeds without collecting customer data.` - : check && localizedCheckDescription(check, language) - } - missing={!isBuiltInSkip && !check} - /> - {measure.prog_name ? ( - <ReferenceField - label={i18n.str`AML program`} - name={measure.prog_name} - description={program?.description} - missing={!program} - /> - ) : ( - <SimpleField label={i18n.str`AML program`} value={i18n.str`None`} /> - )} - <SimpleField - label={i18n.str`Operation`} - value={ - measure.operation_type - ? labelForOperationType(measure.operation_type, i18n) - : i18n.str`Not specified` - } - /> - <SimpleField - label={i18n.str`Voluntary`} - value={measure.voluntary ? i18n.str`Yes` : i18n.str`No`} - /> - <ContextField context={measure.context} /> - </DataCard> - ); -} - -function CheckCard({ - name, - check, - language, - fallbackAvailable, -}: { - name: string; - check: KycCheckInformation; - language: string; - fallbackAvailable: boolean; -}): VNode { - const { i18n } = useTranslationContext(); - return ( - <DataCard - title={name} - description={localizedCheckDescription(check, language)} - > - <TokenField label={i18n.str`Required context`} values={check.requires} /> - <TokenField label={i18n.str`Outputs`} values={check.outputs} /> - <ReferenceField - label={i18n.str`Fallback measure`} - name={check.fallback} - missing={!fallbackAvailable} - /> - </DataCard> - ); -} - -function DataCard({ - title, - description, - children, -}: { - title: string; - description?: string; - children: VNode | VNode[]; -}): VNode { - return ( - <article class="rounded-lg border border-gray-200 bg-white p-5"> - <Code>{title}</Code> - {description ? ( - <p class="mt-3 text-sm leading-6 text-gray-700">{description}</p> - ) : undefined} - <dl class="mt-4 space-y-3">{children}</dl> - </article> - ); -} - -function SimpleField({ - label, - value, -}: { - label: TranslatedString; - value: string | TranslatedString; -}): VNode { - return ( - <div> - <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500"> - {label} - </dt> - <dd class="mt-1 text-sm text-gray-800">{value}</dd> - </div> - ); -} - -function ReferenceField({ - label, - name, - description, - missing = false, -}: { - label: TranslatedString; - name: string; - description?: string; - missing?: boolean; -}): VNode { - const { i18n } = useTranslationContext(); - return ( - <div> - <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500"> - {label} - </dt> - <dd class="mt-1 text-sm text-gray-800"> - <Code>{name}</Code>{" "} - {missing ? ( - <span class="font-semibold text-red-700"> - <i18n.Translate>Unavailable reference</i18n.Translate> - </span> - ) : description ? ( - <span class="ml-1 text-gray-600">{description}</span> - ) : undefined} - </dd> - </div> - ); -} - -function TokenField({ - label, - values, -}: { - label: TranslatedString; - values: string[]; -}): VNode { - const { i18n } = useTranslationContext(); - return ( - <div> - <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500"> - {label} - </dt> - <dd class="mt-1 flex flex-wrap gap-1.5"> - {values.length ? ( - values.map((value) => <Code key={value}>{value}</Code>) - ) : ( - <span class="text-sm text-gray-500"> - <i18n.Translate>None</i18n.Translate> - </span> - )} - </dd> - </div> - ); -} - -function ContextField({ context }: { context: object | undefined }): VNode { - const { i18n } = useTranslationContext(); - const hasContext = context && Object.keys(context).length > 0; - return ( - <div> - <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500"> - <i18n.Translate>Context</i18n.Translate> - </dt> - <dd class="mt-1"> - {hasContext ? ( - <pre class="overflow-x-auto rounded bg-gray-50 p-3 text-xs text-gray-800"> - {JSON.stringify(context, undefined, 2)} - </pre> - ) : ( - <span class="text-sm text-gray-500"> - <i18n.Translate>None</i18n.Translate> - </span> - )} - </dd> - </div> - ); -} - function EmptyData({ children }: { children: VNode }): VNode { return ( <p class="rounded-lg border border-dashed border-gray-300 bg-gray-50 p-5 text-sm text-gray-600"> @@ -666,22 +469,4 @@ function EmptyData({ children }: { children: VNode }): VNode { ); } -function Code({ children }: { children: string }): VNode { - return ( - <code class="break-all rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800"> - {children} - </code> - ); -} - -export function localizedCheckDescription( - check: KycCheckInformation, - language: string, -): string { - const baseLanguage = language.split("-")[0]; - return ( - check.description_i18n?.[language] ?? - check.description_i18n?.[baseLanguage] ?? - check.description - ); -} +export { localizedCheckDescription } from "../components/MeasureDetails.js"; diff --git a/packages/taler-exchange-aml-webui/src/pages/decision/Summary.tsx b/packages/taler-exchange-aml-webui/src/pages/decision/Summary.tsx @@ -39,15 +39,11 @@ import { import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { ConfirmationModal } from "../../components/ConfirmationModal.js"; -import { CurrentMeasureTable } from "../../components/MeasuresTable.js"; +import { Code, MeasureCard } from "../../components/MeasureDetails.js"; import { RulesInfo } from "../../components/RulesInfo.js"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; import { OfficerReady } from "../../hooks/officer.js"; import { useServerMeasures } from "../../hooks/server-info.js"; -import { - computeMeasureInformation, - UiMeasureInformation, -} from "../../utils/computeAvailableMesaures.js"; import { buildAmlDecisionRequest } from "../../utils/decision-validation.js"; import { deadlineIsValid, rulesAreValid } from "../../utils/rule-validation.js"; import { @@ -80,7 +76,7 @@ export function Summary({ officer: OfficerReady; onMove: (n: WizardSteps | undefined) => void; }): VNode { - const { i18n } = useTranslationContext(); + const { i18n, lang } = useTranslationContext(); const [decision, , cleanUpDecision] = useCurrentDecisionRequest(); const measures = useServerMeasures(); @@ -91,25 +87,18 @@ export function Summary({ measures && !(measures instanceof TalerError) && measures.type === "ok" ? measures.body : undefined; - const configuredMeasures = computeMeasureInformation(measureSummary); - const customMeasures = computeMeasureInformation(measureSummary, { - measureMap: decision.custom_measures, - }); - const allMeasures: UiMeasureInformation = { - forms: [...configuredMeasures.forms, ...customMeasures.forms], - procedures: [ - ...configuredMeasures.procedures, - ...customMeasures.procedures, - ], - info: [...configuredMeasures.info, ...customMeasures.info], - }; - const d = decision.new_measures === undefined ? [] : decision.new_measures; - const activeMeasureInfo: UiMeasureInformation = { - forms: allMeasures.forms.filter((m) => d.indexOf(m.name) !== -1), - procedures: allMeasures.procedures.filter((m) => d.indexOf(m.name) !== -1), - info: allMeasures.info.filter((m) => d.indexOf(m.name) !== -1), + const allMeasureDefinitions = { + ...(measureSummary?.roots ?? {}), + ...(decision.custom_measures ?? {}), }; + const activeMeasureInfo = d.flatMap((name) => { + const measure = allMeasureDefinitions[name]; + return measure ? [{ name, measure }] : []; + }); + const unresolvedMeasureNames = d.filter( + (name) => allMeasureDefinitions[name] === undefined, + ); // preserve-investigate const { lib } = useExchangeApiContext(); @@ -304,9 +293,12 @@ export function Summary({ </Attention> ) : undefined} {built.errors.includes("multiple-skip-measures") ? ( - <Attention type="danger" title={i18n.str`Too many SKIP measures`}> + <Attention + type="danger" + title={i18n.str`Only one instant measure is allowed`} + > <i18n.Translate> - At most one immediate measure may use the SKIP check. + Remove all but one instant measure before submitting this decision. </i18n.Translate> </Attention> ) : undefined} @@ -402,7 +394,53 @@ export function Summary({ <ReviewStepButton onClick={() => onMove("measures")} /> </Attention> ) : ( - <CurrentMeasureTable measures={activeMeasureInfo} /> + <section class="space-y-3"> + <div class="flex flex-wrap items-baseline justify-between gap-2"> + <h2 class="text-lg font-semibold text-gray-950 dark:text-gray-50"> + <i18n.Translate>Immediate measures</i18n.Translate> + </h2> + {decision.new_measures!.length > 1 ? ( + <p class="text-sm font-medium text-gray-600 dark:text-gray-300"> + {decision.measures_and ? ( + <i18n.Translate> + Every selected measure is required. + </i18n.Translate> + ) : ( + <i18n.Translate> + Any one selected measure is sufficient. + </i18n.Translate> + )} + </p> + ) : undefined} + </div> + <div class="grid gap-4 lg:grid-cols-2"> + {activeMeasureInfo.map(({ name, measure }) => ( + <MeasureCard + key={name} + name={name} + measure={measure} + summary={measureSummary} + language={lang} + /> + ))} + </div> + {unresolvedMeasureNames.length ? ( + <Attention type="warning" title={i18n.str`Unavailable measures`}> + <p> + <i18n.Translate> + The exchange did not return definitions for these selected + measures: + </i18n.Translate>{" "} + {unresolvedMeasureNames.map((name) => ( + <span class="mr-2" key={name}> + <Code>{name}</Code> + </span> + ))} + </p> + </Attention> + ) : undefined} + <ReviewStepButton onClick={() => onMove("measures")} /> + </section> )} {INVALID_PROPERTIES ? ( <Attention diff --git a/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.test.ts b/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.test.ts @@ -1,32 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2026 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. -*/ -import { AvailableMeasureSummary } from "@gnu-taler/taler-util"; -import assert from "node:assert/strict"; -import test from "node:test"; -import { computeMeasureInformation } from "./computeAvailableMesaures.js"; - -test("lowercase skip measures remain available as procedures", () => { - const summary: AvailableMeasureSummary = { - roots: { - automatic: { check_name: "skip", prog_name: "review" }, - }, - checks: {}, - programs: { - review: { description: "Review account", context: [], inputs: [] }, - }, - default_rules: [], - }; - const measures = computeMeasureInformation(summary); - assert.deepEqual( - measures.procedures.map(({ name, programName }) => ({ name, programName })), - [{ name: "automatic", programName: "review" }], - ); - assert.deepEqual(measures.forms, []); - assert.deepEqual(measures.info, []); -}); diff --git a/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.ts b/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.ts @@ -1,163 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2025 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 type { - AmlProgramRequirement, - AvailableMeasureSummary, - KycCheckInformation, - MeasureInformation, -} from "@gnu-taler/taler-util"; -import { isBuiltInSkipCheck } from "./measure-check.js"; - -export type MeasureInfo = ProcedureMeasure | FormMeasure | InfoMeasure; - -export type ProcedureMeasure = { - type: "procedure"; - name: string; - programName: string; - program: AmlProgramRequirement; - context?: object; -}; -export type FormMeasure = { - type: "form"; - name: string; - programName?: string; - program?: AmlProgramRequirement; - checkName: string; - check: KycCheckInformation; - context?: object; -}; -export type InfoMeasure = { - type: "info"; - name: string; - checkName: string; - check: KycCheckInformation; - context?: object; -}; -export type UiMeasureInformation = { - procedures: ProcedureMeasure[]; - forms: FormMeasure[]; - info: InfoMeasure[]; -}; -/** - * Take a list of measures and fills it with information from server for the UI - * - * If measureList is not present then measureMap is going to be used - * If measureMap is not present then serverMeasures.roots is going to be used - * - * @param serverMeasures reference from the server, where the real info is - * @param opts.measureList a list of measures from which the information is needed - * @param opts.measureMap a map of measures from which the information is needed - * @returns - */ -export function computeMeasureInformation( - serverMeasures: AvailableMeasureSummary | undefined, - opts: { - measureList?: MeasureInformation[]; - measureMap?: Record<string, MeasureInformation> | undefined; - } = {}, -): UiMeasureInformation { - const init: UiMeasureInformation = { forms: [], procedures: [], info: [] }; - if (!serverMeasures) { - return init; - } - - type MeasuerList = [string | undefined, MeasureInformation][]; - const measures: MeasuerList = opts.measureList - ? opts.measureList.map((m) => [undefined, m]) // we don't have the names in this case - : opts.measureMap - ? Object.entries(opts.measureMap) - : Object.entries(serverMeasures.roots); - - return measures.reduce((prev, [key, value]) => { - const measure = buildMeasureInformation(serverMeasures, key, value); - if (measure) { - switch (measure.type) { - case "procedure": { - prev.procedures.push(measure); - break; - } - case "form": { - prev.forms.push(measure); - break; - } - case "info": { - prev.info.push(measure); - break; - } - } - } - return prev; - }, init); -} - -/** - * - * @param serverMeasures server information about measures, checks and programs - * @param name the name of the measure - * @param measure the incomplete measure - * @returns - */ -function buildMeasureInformation( - serverMeasures: AvailableMeasureSummary, - name: string | undefined, - measure: MeasureInformation, -): MeasureInfo | undefined { - if (!isBuiltInSkipCheck(measure.check_name)) { - const check = serverMeasures.checks[measure.check_name]; - if (!check) return undefined; - if (!measure.prog_name) { - const r: MeasureInfo = { - type: "info", - name: name ?? "", - context: measure.context, - checkName: measure.check_name, - check, - // custom: true, - }; - return r; - } else { - const program = serverMeasures.programs[measure.prog_name]; - if (!program) return undefined; - const r: MeasureInfo = { - type: "form", - name: name ?? "", - context: measure.context, - programName: measure.prog_name, - program, - checkName: measure.check_name, - check: serverMeasures.checks[measure.check_name], - // custom: false, - }; - return r; - } - } else { - if (!measure.prog_name) { - console.error(`ERROR: program name can't be empty for measure "${name}"`); - return undefined; - } - const program = serverMeasures.programs[measure.prog_name]; - if (!program) return undefined; - const r: MeasureInfo = { - type: "procedure", - name: name ?? "", - context: measure.context, - programName: measure.prog_name, - program, - // custom: false, - }; - return r; - } -}