taler-typescript-core

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

commit 6b291c6dcda98c9b7c3561b23d7af91c0376c84a
parent f4e7ad275ce9d612f15eed0dd6480c94a75a7d15
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 20 Dec 2024 15:03:26 -0300

show server measures

Diffstat:
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/aml-backoffice-ui/src/pages/Measures.tsx | 57++++++++++++++++++++++++++++++++++++++++++++++++++-------
Apackages/aml-backoffice-ui/src/pages/MeasuresTable.tsx | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 353 insertions(+), 10 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -61,12 +61,13 @@ import { useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { useUiFormsContext } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; -import { useAccountInformation } from "../hooks/account.js"; +import { useAccountInformation, useServerMeasures } from "../hooks/account.js"; import { useAccountDecisions } from "../hooks/decisions.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; import { useOfficer } from "../hooks/officer.js"; import { getShapeFromFields, useFormState } from "../hooks/form.js"; import { privatePages } from "../Routing.js"; +import { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js"; export type AmlEvent = | AmlFormEvent @@ -201,6 +202,7 @@ export function CaseDetails({ const [request, setDesicionRequest] = useState<NewDecision | undefined>( undefined, ); + const [selectMeasure, setSelectMeasure] = useState<boolean>(); const { config } = useExchangeApiContext(); const { i18n } = useTranslationContext(); @@ -249,6 +251,37 @@ export function CaseDetails({ const events = getEventsFromAmlHistory(accountDetails, i18n, allForms); + if (selectMeasure) { + return ( + <ShowMeasuresToSelect + onSelect={(d) => { + setSelectMeasure(false); + setDesicionRequest({ + request: { + payto_uri: paytoString, + decision_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ), + h_payto: account, + keep_investigating: false, + properties: {}, + // the custom measure with context + new_measures: d.name, + new_rules: { + // this value is going to be overridden + custom_measures: {}, + expiration_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.never(), + ), + rules: FREEZE_RULES(config.currency), + }, + }, + askInformation: false, + }); + }} + /> + ); + } if (request) { return ( <SubmitNewDecision @@ -391,6 +424,15 @@ export function CaseDetails({ > <i18n.Translate>Ask for more information</i18n.Translate> </button> + + <button + onClick={async () => { + setSelectMeasure(true); + }} + 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" + > + <i18n.Translate>Set predefined measure</i18n.Translate> + </button> </div> {!activeDecision ? ( @@ -575,7 +617,9 @@ function SubmitNewDecision({ }; const submitHandler = - decisionForm === undefined || !session || customForm === undefined + decisionForm === undefined || + !session || + (decision.askInformation && customForm === undefined) ? undefined : withErrorHandler( () => { @@ -598,7 +642,7 @@ function SubmitNewDecision({ // check of type form, it will use the officer defined form check_name: "askContext", // after that, mark as investigate to read what the user sent - prog_name: "markInvestigate", + prog_name: "preserve-investigate", }, }, }, @@ -673,6 +717,11 @@ function SubmitNewDecision({ <i18n.Translate>New rules to submit</i18n.Translate> </h1> + <ShowMesaureInfo + nextMeasures={separateMeasures(decision.request.new_measures)} + customMeasure={decision.request.new_rules.custom_measures} + /> + <ShowDecisionLimitInfo since={AbsoluteTime.fromProtocolTimestamp( decision.request.decision_time, @@ -687,6 +736,99 @@ function SubmitNewDecision({ ); } +function separateMeasures(measureList: string | undefined): string[][] { + if (!measureList) return new Array(); + const orList = measureList.trim().split(" "); + const orAndList = orList.map((or) => or.split("+")); + return orAndList; +} + +function ShowMesaureInfo({ + nextMeasures, + customMeasure, +}: { + nextMeasures: string[][]; + customMeasure: { [d: string]: TalerExchangeApi.MeasureInformation }; +}): VNode { + const measures = useServerMeasures(); + const { i18n } = useTranslationContext(); + if (!measures) { + return <Loading />; + } + if (measures instanceof TalerError) { + return <ErrorLoadingWithDebug error={measures} />; + } + const summary: TalerExchangeApi.AvailableMeasureSummary = measures.body; + + const map: { [d: string]: MeasureInfo } = {}; + + function addUpIntoMap([key, value]: [ + string, + TalerExchangeApi.MeasureInformation, + ]): void { + if (value.check_name !== "SKIP") { + map[key] = { + name: key, + context: value.context, + program: summary.programs[value.prog_name], + check: summary.checks[value.check_name], + }; + } else { + map[key] = { + name: key, + context: value.context, + program: summary.programs[value.prog_name], + }; + } + } + + Object.entries(measures.body.roots).forEach(addUpIntoMap); + Object.entries(customMeasure).forEach(addUpIntoMap); + + const filteredMeasures = nextMeasures.filter((n) => !!n.length); + + if (!filteredMeasures.length) { + return ( + <div> + <i18n.Translate>no new measure</i18n.Translate> + </div> + ); + } + if (filteredMeasures.length === 1) { + const measurePath = filteredMeasures[0]; + if (measurePath.length === 1) { + const m = map[measurePath[0]]; + + return ( + <div> + <i18n.Translate> + the user needs to complete this measure + </i18n.Translate> + <CurrentMeasureTable list={[m]} /> + </div> + ); + } + return ( + <div> + <i18n.Translate> + the user needs to complete all of these measures + </i18n.Translate> + <CurrentMeasureTable list={measurePath.map((name) => map[name])} /> + </div> + ); + } + + return ( + <div> + <i18n.Translate> + the user has more than one option with different measures to complete. + For any options, if there is more than one measure then the user will + need to complete all the measures. + </i18n.Translate> + </div> + ); +} + function ShowDecisionLimitInfo({ ruleSet, since, @@ -1381,3 +1523,38 @@ const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = ( is_and_combinator: true, }, ]; + +function ShowMeasuresToSelect({ + onSelect, +}: { + onSelect?: (m: MeasureInfo) => void; +}): VNode { + const measures = useServerMeasures(); + + if (!measures) { + return <Loading />; + } + if (measures instanceof TalerError) { + return <ErrorLoadingWithDebug error={measures} />; + } + + const list = Object.entries(measures.body.roots).map( + ([key, value]): MeasureInfo => { + if (value.check_name !== "SKIP") { + return { + name: key, + context: value.context, + program: measures.body.programs[value.prog_name], + check: measures.body.checks[value.check_name], + }; + } + return { + name: key, + context: value.context, + program: measures.body.programs[value.prog_name], + }; + }, + ); + + return <CurrentMeasureTable list={list} onSelect={onSelect} />; +} diff --git a/packages/aml-backoffice-ui/src/pages/Measures.tsx b/packages/aml-backoffice-ui/src/pages/Measures.tsx @@ -14,20 +14,21 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - TalerError + AmlProgramRequirement, + KycCheckInformation, + TalerError, } from "@gnu-taler/taler-util"; import { Loading, useExchangeApiContext, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, h } from "preact"; +import { Fragment, h, VNode } from "preact"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { useServerMeasures } from "../hooks/account.js"; +import { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js"; export function Measures({}: {}) { - const { config } = useExchangeApiContext(); - const { i18n } = useTranslationContext(); // const { forms } = useUiFormsContext(); @@ -35,16 +36,58 @@ export function Measures({}: {}) { // const allForms = [...forms, ...preloadedForms(i18n)]; const measures = useServerMeasures(); - if (!measures || !history) { + if (!measures) { return <Loading />; } if (measures instanceof TalerError) { return <ErrorLoadingWithDebug error={measures} />; } + const list = Object.entries(measures.body.roots).map( + ([key, value]): MeasureInfo => { + if (value.check_name !== "SKIP") { + return { + name: key, + context: value.context, + program: measures.body.programs[value.prog_name], + check: measures.body.checks[value.check_name], + }; + } + return { + name: key, + context: value.context, + program: measures.body.programs[value.prog_name], + }; + }, + ); + return ( <div> - <pre>{JSON.stringify(measures, undefined ,2)}</pre> + <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 pre-define measures in your that can used with + the user. + </i18n.Translate> + </p> + </div> + <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none"> + {/* <button + type="button" + 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" + > + Add user + </button> */} + </div> + </div> + + <CurrentMeasureTable list={list} /> + </div> </div> ); } diff --git a/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx @@ -0,0 +1,123 @@ +/* + 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 { + AmlProgramRequirement, + KycCheckInformation, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; + +export type MeasureInfo = { + name: string; + program: AmlProgramRequirement; + check?: KycCheckInformation; + context?: object; +}; +export function CurrentMeasureTable({ + list, + onSelect, +}: { + list: MeasureInfo[]; + onSelect?: (m: MeasureInfo) => void; +}): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="mt-8 flow-root"> + <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + <div class="overflow-hidden shadow ring-1 ring-black/5 sm:rounded-lg"> + <table class="min-w-full divide-y divide-gray-300"> + <thead class="bg-gray-50"> + <tr> + {onSelect ? ( + <th scope="col" class="relative p-2 "> + <span class="sr-only">Select</span> + </th> + ) : ( + <Fragment /> + )} + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" + > + <i18n.Translate>Name</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Check</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Program</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Context</i18n.Translate> + </th> + </tr> + </thead> + <tbody class="divide-y divide-gray-200 bg-white"> + {list.map((m) => { + if ( + m.context && + "internal" in m.context && + m.context.internal + ) { + return <Fragment />; + } + return ( + <tr> + {onSelect ? ( + <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" + > + <i18n.Translate>Select</i18n.Translate> + </button> + </td> + ) : ( + <Fragment /> + )} + <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6"> + {m.name} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {m.check?.description ?? ""} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {m.program.description} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {Object.keys(m.context ?? {}).join(", ")} + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + </div> + </div> + </div> + ); +}