commit 847a8dd65c74fdd3cf00d4ee5f0fe5cbb783ca65 parent f6c6c38e51f05499e15e578d133d92b0503ccaa8 Author: Sebastian <sebasjm@gmail.com> Date: Tue, 17 Jun 2025 11:37:53 -0300 fix #10113 lint Diffstat:
29 files changed, 364 insertions(+), 1381 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx @@ -58,8 +58,8 @@ export function App(): VNode { <TranslationProvider source={strings} completeness={{ - es: strings["es"].completeness, - de: strings["de"].completeness, + es: strings["es"].completeness ?? 0, + de: strings["de"].completeness ?? 0, }} > <ExchangeApiProvider diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -317,7 +317,7 @@ function PrivateRouting(): VNode { return <div>not yet implemented</div>; } case "dashboard": { - return <Dashboard routeToDownloadStats={privatePages.statsDownload} />; + return <Dashboard />; } case "transfers": { return <Transfers routeToAccountById={privatePages.account} />; diff --git a/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx @@ -117,9 +117,9 @@ export function CurrentMeasureTable({ </tr> </thead> <tbody class="divide-y divide-gray-200 bg-white"> - {measures.forms.map((m) => { + {measures.forms.map((m, k) => { return ( - <tr> + <tr key={k}> {!onSelect ? undefined : ( <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> <button @@ -195,9 +195,9 @@ export function CurrentMeasureTable({ </tr> </thead> <tbody class="divide-y divide-gray-200 bg-white"> - {measures.info.map((m) => { + {measures.info.map((m, k) => { return ( - <tr> + <tr key={k}> {onSelect ? ( <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> <button @@ -280,9 +280,9 @@ export function CurrentMeasureTable({ </tr> </thead> <tbody class="divide-y divide-gray-200 bg-white"> - {measures.procedures.map((m) => { + {measures.procedures.map((m, k) => { return ( - <tr> + <tr key={k}> {onSelect ? ( <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> <button diff --git a/packages/aml-backoffice-ui/src/components/NewMeasure.tsx b/packages/aml-backoffice-ui/src/components/NewMeasure.tsx @@ -19,7 +19,7 @@ import { AvailableMeasureSummary, KycCheckInformation, TalerError, - TranslatedString + TranslatedString, } from "@gnu-taler/taler-util"; import { design_challenger_email, @@ -30,7 +30,7 @@ import { InternationalizationAPI, RecursivePartial, useForm, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -51,7 +51,7 @@ export type MeasureDefinition = { type VerificationMeasureDefinition = { name: string; readOnly: boolean; - address: any; + address: object; }; /** @@ -161,7 +161,7 @@ function NormalMeasureForm({ prev[cur.key] = getContextValueByType(cur.type, cur.value); return prev; }, - {} as Record<string, any>, + {} as Record<string, object>, ), }; updateRequest("add new measure", { @@ -184,7 +184,7 @@ function NormalMeasureForm({ prev[cur.key] = getContextValueByType(cur.type, cur.value); return prev; }, - {} as Record<string, any>, + {} as Record<string, object>, ), }; updateRequest("update measure", { @@ -326,7 +326,7 @@ function VerificationMeasureForm({ prev[cur.key] = getContextValueByType(cur.type, cur.value); return prev; }, - {} as Record<string, any>, + {} as Record<string, object>, ); function addNewCustomMeasure() { @@ -462,7 +462,9 @@ function MeasureForm({ switch (formType) { case "verification": { - const cType = JSON.parse(challengeType?.value as any); + const cType = !challengeType + ? undefined + : JSON.parse(challengeType.value); return ( <div> <h2 class="mt-4 mb-2"> diff --git a/packages/aml-backoffice-ui/src/components/RulesInfo.tsx b/packages/aml-backoffice-ui/src/components/RulesInfo.tsx @@ -1,3 +1,18 @@ +/* + 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 { amountFractionalBase, AmountJson, @@ -125,9 +140,9 @@ export function RulesInfo({ </thead> <tbody id="thetable" class="divide-y divide-gray-200 bg-white "> - {sorted.map((r) => { + {sorted.map((r, k) => { return ( - <tr class="even:bg-gray-200 "> + <tr class="even:bg-gray-200 " key={k}> <td class="flex whitespace-nowrap py-2 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 text-left"> <span class="mx-2"> {r.exposed ? ( diff --git a/packages/aml-backoffice-ui/src/components/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/components/ShowConsolidated.tsx @@ -1,144 +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 { - AbsoluteTime, - TalerExchangeApi, - TranslatedString, -} from "@gnu-taler/taler-util"; -import { - FormDesign, - FormUI, - UIFormElementConfig, - useForm, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { VNode, h } from "preact"; -import { useEffect } from "preact/hooks"; -// import { AmlEvent } from "./CaseDetails.js"; - -/** - * the exchange doesn't have a consistent api - * https://bugs.gnunet.org/view.php?id=9142 - * - * @param data - * @returns - */ -function fixProvidedInfo(data: object): object { - return Object.entries(data).reduce((prev, [key, value]) => { - prev[key] = value; - if (typeof value === "object" && value["value"]) { - const v = value["value"]; - if (typeof v === "object" && v["text"]) { - prev[key].value = v["text"]; - } - } - return prev; - }, {} as any); -} - -export function ShowConsolidated({ - history, - until, -}: { - history: TalerExchangeApi.KycAttributeCollectionEvent[]; - until: AbsoluteTime; -}): VNode { - const { i18n } = useTranslationContext(); - - const cons = getConsolidated(history, until); - - const fixed = fixProvidedInfo(cons.kyc); - - const design: FormDesign = { - type: "double-column", - sections: - Object.entries(fixed).length > 0 - ? [ - { - title: i18n.str`Collected information`, - description: - until.t_ms === "never" - ? undefined - : i18n.str`All information known until ${format( - until.t_ms, - "dd/MM/yyyy HH:mm:ss", - )}`, - fields: Object.entries(fixed).map(([key, field]) => { - const result: UIFormElementConfig = { - type: "text", - label: key as TranslatedString, - id: `${key}.value`, - disabled: true, - help: `At ${ - field.since.t_ms === "never" - ? "never" - : format(field.since.t_ms, "dd/MM/yyyy HH:mm:ss") - }` as TranslatedString, - }; - return result; - }), - }, - ] - : [], - }; - - const { model: handler, update } = useForm(design, fixed); - - useEffect(() => { - update(fixed); - }, [until.t_ms]); - - return <FormUI design={design} model={handler} />; -} - -interface Consolidated { - kyc: { - [field: string]: { - value: unknown; - provider?: string; - since: AbsoluteTime; - }; - }; -} - -function getConsolidated( - history: TalerExchangeApi.KycAttributeCollectionEvent[], - when: AbsoluteTime, -): Consolidated { - const initial: Consolidated = { - kyc: {}, - }; - return history.reduce((prev, cur) => { - const collectionTime = AbsoluteTime.fromProtocolTimestamp( - cur.collection_time, - ); - if (AbsoluteTime.cmp(when, collectionTime) <= 0) { - return prev; - } - - const formValues = cur.attributes ?? {}; - Object.keys(formValues).forEach((field) => { - const value = (formValues as Record<string, unknown>)[field]; - prev.kyc[field] = { - value, - provider: cur.provider_name, - since: collectionTime, - }; - }); - return prev; - }, initial); -} diff --git a/packages/aml-backoffice-ui/src/declaration.d.ts b/packages/aml-backoffice-ui/src/declaration.d.ts @@ -18,23 +18,23 @@ declare const __VERSION__: string; declare const __GIT_HASH__: string; declare module "*.po" { - const content: any; + const content: string; export default content; } declare module "jed" { - const x: any; + const x: string; export = x; } declare module "*.jpeg" { - const content: any; + const content: string; export default content; } declare module "*.png" { - const content: any; + const content: string; export default content; } declare module "*.svg" { - const content: any; + const content: string; export default content; } diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -37,7 +37,7 @@ import { FormErrors, useLocalStorage, } from "@gnu-taler/web-util/browser"; -import { useState, useMemo } from "preact/hooks"; +import { useState } from "preact/hooks"; export interface AccountAttributes { data: object; formId: string | undefined; @@ -96,7 +96,7 @@ export interface DecisionRequest { /** * Custom properties not listed on GANA */ - custom_properties: Record<string, any> | undefined; + custom_properties: Record<string, string> | undefined; /** * Supported events to be triggered */ diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -18,12 +18,14 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { OfficerAccount, - OperationOk, opFixedSuccess, TalerExchangeResultByMethod2, - TalerHttpError, + TalerHttpError } from "@gnu-taler/taler-util"; -import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; +import { + buildPaginatedResult, + useExchangeApiContext, +} from "@gnu-taler/web-util/browser"; import _useSWR, { mutate, SWRHook } from "swr"; import { useOfficer } from "./officer.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -77,8 +79,12 @@ export function useCurrentDecisions({ if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.records, offset, setOffset, (d) => - String(d.rowid), + return buildPaginatedResult( + data.body.records, + offset, + setOffset, + (d) => String(d.rowid), + PAGINATED_LIST_REQUEST, ); } @@ -128,8 +134,12 @@ export function useAccountDecisions(accountStr: string) { if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.records, offset, setOffset, (d) => - String(d.rowid), + return buildPaginatedResult( + data.body.records, + offset, + setOffset, + (d) => String(d.rowid), + PAGINATED_LIST_REQUEST, ); } @@ -150,16 +160,13 @@ export function useAccountActiveDecision(accountStr?: string) { lib: { exchange: api }, } = useExchangeApiContext(); - const [offset, setOffset] = useState<string>(); - - async function fetcher([officer, account, offset]: [ + async function fetcher([officer, account]: [ OfficerAccount, string, string | undefined, ]) { return await api.getAmlDecisions(officer, { order: "dec", - offset, account, active: true, limit: PAGINATED_LIST_REQUEST, @@ -170,7 +177,7 @@ export function useAccountActiveDecision(accountStr?: string) { TalerExchangeResultByMethod2<"getAmlDecisions">, TalerHttpError >( - !session ? undefined : [session, accountStr, offset, "getAmlDecisions"], + !session ? undefined : [session, accountStr, "getAmlDecisions"], fetcher, ); @@ -181,42 +188,3 @@ export function useAccountActiveDecision(accountStr?: string) { if (!data.body.records.length) return opFixedSuccess(undefined); return opFixedSuccess(data.body.records[0]); } - -type PaginatedResult<T> = OperationOk<T> & { - isLastPage: boolean; - isFirstPage: boolean; - loadNext(): void; - loadFirst(): void; -}; - -// FIXME: consider moving this to web-util -// FIXME: reconsider return type, this is not an HTTP response! -export function buildPaginatedResult<R, OffId>( - data: R[], - offset: OffId | undefined, - setOffset: (o: OffId | undefined) => void, - getId: (r: R) => OffId, -): PaginatedResult<R[]> { - const isLastPage = data.length < PAGINATED_LIST_REQUEST; - const isFirstPage = offset === undefined; - - const result = structuredClone(data); - if (result.length == PAGINATED_LIST_REQUEST) { - result.pop(); - } - return { - type: "ok", - case: "ok", - body: result, - isLastPage, - isFirstPage, - loadNext: () => { - if (!result.length) return; - const id = getId(result[result.length - 1]); - setOffset(id); - }, - loadFirst: () => { - setOffset(undefined); - }, - }; -} diff --git a/packages/aml-backoffice-ui/src/hooks/server-info.ts b/packages/aml-backoffice-ui/src/hooks/server-info.ts @@ -20,13 +20,11 @@ import { EventReporting_VQF_queries, fetchTopsInfoFromServer, fetchVqfInfoFromServer, - GLS_AmlEventsName, OfficerAccount, OperationOk, opFixedSuccess, TalerExchangeResultByMethod2, - TalerHttpError, - TOPS_AmlEventsName, + TalerHttpError } from "@gnu-taler/taler-util"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; @@ -67,13 +65,6 @@ export function useServerMeasures() { return undefined; } -const GLS_EVENTS: GLS_AmlEventsName[] = Object.values(GLS_AmlEventsName).map( - (c) => GLS_AmlEventsName[c], -); -const TESTING_EVENTS: TOPS_AmlEventsName[] = Object.values( - TOPS_AmlEventsName, -).map((c) => TOPS_AmlEventsName[c]); - export function useTopsServerStatistics() { const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; diff --git a/packages/aml-backoffice-ui/src/hooks/transfers.ts b/packages/aml-backoffice-ui/src/hooks/transfers.ts @@ -19,12 +19,14 @@ import { useState } from "preact/hooks"; import { AmountJson, OfficerAccount, - OperationOk, PaytoHash, TalerExchangeResultByMethod2, TalerHttpError, } from "@gnu-taler/taler-util"; -import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; +import { + buildPaginatedResult, + useExchangeApiContext, +} from "@gnu-taler/web-util/browser"; import _useSWR, { mutate, SWRHook } from "swr"; import { useOfficer } from "./officer.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -76,8 +78,12 @@ export function useTransferDebit() { if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.transfers, offset, setOffset, (d) => - String(d.rowid), + return buildPaginatedResult( + data.body.transfers, + offset, + setOffset, + (d) => String(d.rowid), + PAGINATED_LIST_REQUEST, ); } @@ -162,45 +168,11 @@ export function useTransferList({ if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.transfers, offset, setOffset, (d) => - String(d.rowid), + return buildPaginatedResult( + data.body.transfers, + offset, + setOffset, + (d) => String(d.rowid), + PAGINATED_LIST_REQUEST, ); } - -type PaginatedResult<T> = OperationOk<T> & { - isLastPage: boolean; - isFirstPage: boolean; - loadNext(): void; - loadFirst(): void; -}; - -//TODO: consider sending this to web-util -export function buildPaginatedResult<R, OffId>( - data: R[], - offset: OffId | undefined, - setOffset: (o: OffId | undefined) => void, - getId: (r: R) => OffId, -): PaginatedResult<R[]> { - const isLastPage = data.length < PAGINATED_LIST_REQUEST; - const isFirstPage = offset === undefined; - - const result = structuredClone(data); - if (result.length == PAGINATED_LIST_REQUEST) { - result.pop(); - } - return { - type: "ok", - case: "ok", - body: result, - isLastPage, - isFirstPage, - loadNext: () => { - if (!result.length) return; - const id = getId(result[result.length - 1]); - setOffset(id); - }, - loadFirst: () => { - setOffset(undefined); - }, - }; -} diff --git a/packages/aml-backoffice-ui/src/i18n/strings.ts b/packages/aml-backoffice-ui/src/i18n/strings.ts @@ -14,8 +14,16 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -/*eslint quote-props: ["error", "consistent"]*/ -export const strings: { [s: string]: any } = {}; +export interface StringsType { + domain: string; + // lang: string; + completeness?: number; + // plural_forms: string; + locale_data: { + messages: Record<string, unknown>; + }; +} +export const strings: Record<string, StringsType> = {}; strings["de"] = { domain: "messages", diff --git a/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx b/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx @@ -15,58 +15,26 @@ */ import { AbsoluteTime, - AmlDecisionRequest, assertUnreachable, - buildCodecForObject, - Codec, - codecForNumber, - codecForString, - codecOptional, HttpStatusCode, - LimitOperationType, - OperationFail, - OperationOk, - opFixedSuccess, TalerError, - TalerErrorDetail, TalerExchangeApi, - TalerFormAttributes, + TalerFormAttributes } from "@gnu-taler/taler-util"; import { Attention, - Button, CopyButton, - FormDesign, - FormMetadata, - FormUI, Loading, - LocalNotificationBanner, RouteDefinition, - useExchangeApiContext, - useForm, - useLocalNotificationHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { Fragment, h, Ref, VNode } from "preact"; +import { h, VNode } from "preact"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; -import { - CurrentMeasureTable, - MeasureInfo, -} from "../components/MeasuresTable.js"; +import { ShowDecisionLimitInfo } from "../components/ShowDecisionLimitInfo.js"; import { useAccountInformation } from "../hooks/account.js"; import { DecisionRequest } from "../hooks/decision-request.js"; import { useAccountDecisions } from "../hooks/decisions.js"; -import { useOfficer } from "../hooks/officer.js"; -import { useServerMeasures } from "../hooks/server-info.js"; -import { Profile } from "./Profile.js"; -import { ShowDecisionLimitInfo } from "../components/ShowDecisionLimitInfo.js"; -import { computeAvailableMesaures } from "../utils/computeAvailableMesaures.js"; - -type NewDecision = { - request: Omit<Omit<AmlDecisionRequest, "justification">, "officer_sig">; - askInformation: boolean; -}; export function AccountDetails({ account, @@ -91,7 +59,6 @@ export function AccountDetails({ } if (details.type === "fail") { switch (details.case) { - // case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: case HttpStatusCode.Conflict: @@ -105,7 +72,6 @@ export function AccountDetails({ } if (history.type === "fail") { switch (history.case) { - // case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: case HttpStatusCode.Conflict: @@ -174,9 +140,6 @@ export function AccountDetails({ <ShortcutActionButtons /> - {/* {selected && ( - <ShowConsolidated history={collectionEvents} until={selected} /> - )} */} <div class="p-4"> <h1 class="text-base font-semibold leading-6 text-black"> <i18n.Translate>Collected information</i18n.Translate> @@ -223,9 +186,10 @@ export function AccountDetails({ <h1 class="text-base font-semibold leading-6 text-black"> <i18n.Translate>Previous AML decisions</i18n.Translate> </h1> - {restDecisions.map((d) => { + {restDecisions.map((d, k) => { return ( <ShowDecisionLimitInfo + key={k} since={AbsoluteTime.fromProtocolTimestamp(d.decision_time)} until={AbsoluteTime.fromProtocolTimestamp( d.limits.expiration_time, @@ -246,293 +210,6 @@ export function AccountDetails({ ); } -function SubmitNewDecision({ - decision, - onComplete, -}: { - onComplete: () => void; - decision: NewDecision; -}): VNode { - const { i18n } = useTranslationContext(); - const { lib } = useExchangeApiContext(); - const [notification, withErrorHandler] = useLocalNotificationHandler(); - - const formDesign: FormDesign = { - type: "single-column", - fields: [ - { - id: "justification", - type: "textArea", - required: true, - label: i18n.str`Justification`, - }, - ], - }; - - if (decision.askInformation) { - formDesign.fields.push({ - type: "caption", - label: i18n.str`Form definition`, - help: i18n.str`The user will need to complete this form.`, - }); - formDesign.fields.push({ - id: "fields", - type: "array", - required: true, - label: i18n.str`Fields`, - fields: [ - { - id: "name", - type: "text", - required: true, - label: i18n.str`Name`, - help: i18n.str`Name of the field in the form`, - }, - { - id: "type", - type: "choiceStacked", - required: true, - label: i18n.str`Type`, - help: i18n.str`Type of information being asked`, - choices: [ - { - value: "integer", - label: i18n.str`Number`, - description: i18n.str`Numeric information`, - }, - { - value: "text", - label: i18n.str`Text`, - description: i18n.str`Free form text input`, - }, - ], - }, - ], - labelFieldId: "name", - }); - } - const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; - const decisionForm = useForm<{ justification: string; fields: object }>( - formDesign, - { justification: "" }, - ); - - const customFields = decisionForm.status.result.fields as [ - { name: string; type: string }, - ]; - - // const customForm: FormDesign | undefined = !decisionForm.status.result.fields - // ? undefined - // : { - // type: "double-column", - // sections: [ - // { - // fields: customFields.map((f) => { - // return { - // id: f.name, - // label: f.name, - // type: f.type, - // } as UIFormElementConfig; - // }), - // title: "Required information", - // }, - // ], - // }; - - const submitHandler = - decisionForm === undefined || !session || decision.askInformation //&& customForm === undefined) - ? undefined - : withErrorHandler( - () => { - const request: Omit<AmlDecisionRequest, "officer_sig"> = { - ...decision.request, - properties: { - ...decision.request.properties, - fields: decisionForm.status.result.fields, - }, - justification: - decisionForm.status.result.justification ?? "empty", - new_rules: { - ...decision.request.new_rules, - custom_measures: { - ...decision.request.new_rules.custom_measures, - askMoreInfo: { - context: { - // form: customForm, - }, - // 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: "preserve-investigate", - }, - }, - }, - }; - return lib.exchange.makeAmlDesicion(session, request); - }, - onComplete, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Forbidden: - if (session) { - return i18n.str`Wrong credentials for "${session}"`; - } else { - return i18n.str`Wrong credentials.`; - } - case HttpStatusCode.NotFound: - return i18n.str`The account was not found`; - case HttpStatusCode.Conflict: - return i18n.str`Officer disabled or more recent decision was already submitted.`; - default: - assertUnreachable(fail.case); - } - }, - ); - - return ( - <div> - <LocalNotificationBanner notification={notification} /> - <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 "> - <i18n.Translate>Submit decision</i18n.Translate> - </h1> - <form - class="space-y-6" - noValidate - onSubmit={(e) => { - e.preventDefault(); - }} - autoCapitalize="none" - autoCorrect="off" - > - <FormUI design={formDesign} model={decisionForm.model} /> - - <div class="mt-6 flex items-center justify-end gap-x-6"> - <button - onClick={onComplete} - class="text-sm font-semibold leading-6 text-gray-900" - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - - <Button - type="submit" - handler={submitHandler} - disabled={!submitHandler} - class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 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>Confirm</i18n.Translate> - </Button> - </div> - </form> - - <h1 class="my-2 text-xl font-bold tracking-tight text-gray-900 "> - <i18n.Translate>New rules to submit</i18n.Translate> - </h1> - - <div class="my-2"> - <ShowMesaureInfo - nextMeasures={decision.request.new_measures?.split(" ") ?? []} - /> - </div> - - <ShowDecisionLimitInfo - fixed - since={AbsoluteTime.fromProtocolTimestamp( - decision.request.decision_time, - )} - until={AbsoluteTime.fromProtocolTimestamp( - decision.request.new_rules.expiration_time, - )} - rules={decision.request.new_rules.rules} - startOpen - measure={decision.request.new_rules.successor_measure ?? ""} - /> - </div> - ); -} - -function ShowMesaureInfo({ nextMeasures }: { nextMeasures: string[] }): VNode { - const { i18n } = useTranslationContext(); - const measures = useServerMeasures(); - if (!measures) { - return <Loading />; - } - if (measures instanceof TalerError) { - return <ErrorLoadingWithDebug error={measures} />; - } - if (measures.type === "fail") { - switch (measures.case) { - case HttpStatusCode.Forbidden: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account signature is invalid, contact administrator or - create a new one. - </i18n.Translate> - </Attention> - <Profile /> - </Fragment> - ); - case HttpStatusCode.NotFound: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - The designated AML account is not known, contact administrator - or create a new one. - </i18n.Translate> - </Attention> - <Profile /> - </Fragment> - ); - case HttpStatusCode.Conflict: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - The designated AML account is not enabled, contact administrator - or create a new one. - </i18n.Translate> - </Attention> - <Profile /> - </Fragment> - ); - default: - assertUnreachable(measures); - } - } - const filteredMeasures = nextMeasures.filter((n) => !!n && !!n.trim()); - const allMeasures = computeAvailableMesaures( - measures.body, - // cm, - (m) => filteredMeasures.indexOf(m.name) === -1, - ); - - if (!filteredMeasures.length) { - return <Fragment />; - } - if (filteredMeasures.length === 1) { - return ( - <div> - <i18n.Translate> - The customer needs to complete this measure - </i18n.Translate> - <CurrentMeasureTable measures={allMeasures} /> - </div> - ); - } - return ( - <div> - <i18n.Translate> - The customer needs to complete all of these measures - </i18n.Translate> - <CurrentMeasureTable measures={allMeasures} /> - </div> - ); -} - function ShowTimeline({ history, account, @@ -554,6 +231,7 @@ function ShowTimeline({ return ( <a + key={idx} href={routeToShowCollectedInfo.url({ cid: account, rowId: String(e.rowid), @@ -647,348 +325,3 @@ function ShowTimeline({ </div> ); } - -function InputAmount( - { - currency, - name, - value, - left, - onChange, - }: { - currency: string; - name: string; - left?: boolean | undefined; - value: string | undefined; - onChange?: (s: string) => void; - }, - ref: Ref<HTMLInputElement>, -): VNode { - const FRAC_SEPARATOR = ","; - const { config } = useExchangeApiContext(); - return ( - <div class="mt-2"> - <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> - <div class="pointer-events-none inset-y-0 flex items-center px-3"> - <span class="text-gray-500 sm:text-sm">{currency}</span> - </div> - <input - type="number" - data-left={left} - class="disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6" - placeholder="0.00" - aria-describedby="price-currency" - ref={ref} - name={name} - id={name} - autocomplete="off" - value={value ?? ""} - disabled={!onChange} - onInput={(e) => { - if (!onChange) return; - const l = e.currentTarget.value.length; - const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR); - if ( - sep_pos !== -1 && - l - sep_pos - 1 > - config.config.currency_specification.num_fractional_input_digits - ) { - e.currentTarget.value = e.currentTarget.value.substring( - 0, - sep_pos + - config.config.currency_specification - .num_fractional_input_digits + - 1, - ); - } - onChange(e.currentTarget.value); - }} - /> - </div> - </div> - ); -} - -export type Justification<T = Record<string, unknown>> = { - // form values - value: T; -} & Omit<Omit<FormMetadata, "icon">, "config">; - -type SimpleFormMetadata = { - version?: number; - id?: string; -}; - -const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> => - buildCodecForObject<SimpleFormMetadata>() - .property("id", codecOptional(codecForString())) - .property("version", codecOptional(codecForNumber())) - .build("SimpleFormMetadata"); - -type ParseJustificationFail = - | "not-json" - | "id-not-found" - | "form-not-found" - | "version-not-found"; - -function parseJustification( - s: string, - listOfAllKnownForms: FormMetadata[], -): - | OperationOk<{ - justification: Justification; - metadata: FormMetadata; - }> - | OperationFail<ParseJustificationFail> { - try { - const justification = JSON.parse(s); - const info = codecForSimpleFormMetadata().decode(justification); - if (!info.id) { - return { - type: "fail", - case: "id-not-found", - detail: {} as TalerErrorDetail, - }; - } - if (!info.version) { - return { - type: "fail", - case: "version-not-found", - detail: {} as TalerErrorDetail, - }; - } - const found = listOfAllKnownForms.find((f) => { - return f.id === info.id && f.version === info.version; - }); - if (!found) { - return { - type: "fail", - case: "form-not-found", - detail: {} as TalerErrorDetail, - }; - } - return opFixedSuccess({ - justification, - metadata: found, - }); - } catch (e) { - return { - type: "fail", - case: "not-json", - detail: {} as TalerErrorDetail, - }; - } -} - -const THRESHOLD_2000_WEEK: (currency: string) => TalerExchangeApi.KycRule[] = ( - currency, -) => [ - { - operation_type: LimitOperationType.withdraw, - threshold: `${currency}:2000`, - timeframe: { - d_us: 7 * 24 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.deposit, - threshold: `${currency}:2000`, - timeframe: { - d_us: 7 * 24 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.aggregate, - threshold: `${currency}:2000`, - timeframe: { - d_us: 7 * 24 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.merge, - threshold: `${currency}:2000`, - timeframe: { - d_us: 7 * 24 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.balance, - threshold: `${currency}:2000`, - timeframe: { - d_us: 7 * 24 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.close, - threshold: `${currency}:2000`, - timeframe: { - d_us: 7 * 24 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, -]; - -const THRESHOLD_100_HOUR: (currency: string) => TalerExchangeApi.KycRule[] = ( - currency, -) => [ - { - operation_type: LimitOperationType.withdraw, - threshold: `${currency}:100`, - timeframe: { - d_us: 1 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.deposit, - threshold: `${currency}:100`, - timeframe: { - d_us: 1 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.aggregate, - threshold: `${currency}:100`, - timeframe: { - d_us: 1 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.merge, - threshold: `${currency}:100`, - timeframe: { - d_us: 1 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.balance, - threshold: `${currency}:100`, - timeframe: { - d_us: 1 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.close, - threshold: `${currency}:100`, - timeframe: { - d_us: 1 * 60 * 60 * 1000 * 1000, - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, -]; - -const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = ( - currency, -) => [ - { - operation_type: LimitOperationType.withdraw, - threshold: `${currency}:0`, - timeframe: { - d_us: "forever", - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.deposit, - threshold: `${currency}:0`, - timeframe: { - d_us: "forever", - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.aggregate, - threshold: `${currency}:0`, - timeframe: { - d_us: "forever", - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.merge, - threshold: `${currency}:0`, - timeframe: { - d_us: "forever", - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.balance, - threshold: `${currency}:0`, - timeframe: { - d_us: "forever", - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, - { - operation_type: LimitOperationType.close, - threshold: `${currency}:0`, - timeframe: { - d_us: "forever", - }, - measures: ["verboten"], - display_priority: 1, - exposed: true, - is_and_combinator: true, - }, -]; diff --git a/packages/aml-backoffice-ui/src/pages/AccountList.tsx b/packages/aml-backoffice-ui/src/pages/AccountList.tsx @@ -35,10 +35,6 @@ import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { useOfficer } from "../hooks/officer.js"; import { Profile } from "./Profile.js"; -type FormType = { - // state: TalerExchangeApi.AmlState; -}; - export function AccountList({ routeToAccountById: caseByIdRoute, }: { @@ -99,8 +95,6 @@ export function AccountList({ } const records = list.body; - const onFirstPage = list.isFirstPage ? undefined : list.loadFirst; - const onNext = list.isLastPage ? undefined : list.loadNext; return ( <div> @@ -198,7 +192,7 @@ export function AccountList({ })} </tbody> </table> - <Pagination onFirstPage={onFirstPage} onNext={onNext} /> + <Pagination onFirstPage={list.loadFirst} onNext={list.loadNext} /> </div> )} </div> diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx @@ -14,35 +14,26 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - AbsoluteTime, AmlSpaDialect, assertUnreachable, EventReporting_TOPS_calculation, - GLS_AmlEventsName, - TalerCorebankApi, TalerError, TranslatedString, } from "@gnu-taler/taler-util"; import { InternationalizationAPI, Loading, - RouteDefinition, useExchangeApiContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; +import { HandleAccountNotReady } from "../components/HandleAccountNotReady.js"; import { useOfficer } from "../hooks/officer.js"; import { usePreferences } from "../hooks/preferences.js"; import { useTopsServerStatistics } from "../hooks/server-info.js"; -import { HandleAccountNotReady } from "../components/HandleAccountNotReady.js"; -export function Dashboard({ - routeToDownloadStats, -}: { - routeToDownloadStats: RouteDefinition; -}) { +export function Dashboard() { const officer = useOfficer(); const { i18n } = useTranslationContext(); @@ -55,19 +46,13 @@ export function Dashboard({ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 "> <i18n.Translate>Dashboard</i18n.Translate> </h1> - <EventMetrics routeToDownloadStats={routeToDownloadStats} /> + <EventMetrics /> </div> ); } -const now = new Date(); - -function EventMetrics({ - routeToDownloadStats, -}: { - routeToDownloadStats: RouteDefinition; -}): VNode { - const { i18n, dateLocale } = useTranslationContext(); +function EventMetrics(): VNode { + const { i18n } = useTranslationContext(); // const [metricType, setMetricType] = // useState<TalerCorebankApi.MonitorTimeframeParam>( // TalerCorebankApi.MonitorTimeframeParam.hour, @@ -120,11 +105,11 @@ function EventMetrics({ </div> */} <dl class="mt-5 grid grid-cols-1 md:grid-cols-4 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x md:divide-y-0"> - {Object.entries(resp.body).map(([name, number]) => { + {Object.entries(resp.body).map(([name, number], k) => { const label = labelForEvent(name, i18n, dialect); const desc = descriptionForEvent(name, i18n, dialect); return ( - <div class="px-4 py-5 sm:p-6"> + <div class="px-4 py-5 sm:p-6" key={k}> <dt class="text-base font-normal text-gray-900"> {label} {!desc ? undefined : ( @@ -157,12 +142,24 @@ function labelForEvent( ) { switch (dialect) { case AmlSpaDialect.TOPS: - return labelForEvent_tops(name as any, i18n); + return labelForEvent_tops( + // @ts-expect-error name is the field + name, + i18n, + ); case AmlSpaDialect.GLS: // return labelForEvent_gls(name as GLS_AmlEventsName, i18n); - return labelForEvent_tops(name as any, i18n); + return labelForEvent_tops( + // @ts-expect-error name is the field + name, + i18n, + ); case AmlSpaDialect.TESTING: - return labelForEvent_tops(name as any, i18n); + return labelForEvent_tops( + // @ts-expect-error name is the field + name, + i18n, + ); default: { assertUnreachable(dialect); } @@ -202,20 +199,20 @@ function labelForEvent_tops( } } } -function labelForEvent_gls( - name: GLS_AmlEventsName, - i18n: InternationalizationAPI, -) { - switch (name) { - case GLS_AmlEventsName.ACCOUNT_OPENED: - return i18n.str`Account opened`; - case GLS_AmlEventsName.ACCOUNT_CLOSED: - return i18n.str`Account closed`; - default: { - assertUnreachable(name); - } - } -} +// function labelForEvent_gls( +// name: GLS_AmlEventsName, +// i18n: InternationalizationAPI, +// ) { +// switch (name) { +// case GLS_AmlEventsName.ACCOUNT_OPENED: +// return i18n.str`Account opened`; +// case GLS_AmlEventsName.ACCOUNT_CLOSED: +// return i18n.str`Account closed`; +// default: { +// assertUnreachable(name); +// } +// } +// } function descriptionForEvent( name: string, @@ -224,12 +221,24 @@ function descriptionForEvent( ): TranslatedString | undefined { switch (dialect) { case AmlSpaDialect.TOPS: - return descriptionForEvent_tops(name as any, i18n); + return descriptionForEvent_tops( + // @ts-expect-error name is the field + name, + i18n, + ); case AmlSpaDialect.GLS: - return descriptionForEvent_tops(name as any, i18n); + return descriptionForEvent_tops( + // @ts-expect-error name is the field + name, + i18n, + ); // return descriptionForEvent_gls(name as GLS_AmlEventsName, i18n); case AmlSpaDialect.TESTING: - return descriptionForEvent_tops(name as any, i18n); + return descriptionForEvent_tops( + // @ts-expect-error name is the field + name, + i18n, + ); default: { assertUnreachable(dialect); } @@ -271,20 +280,20 @@ function descriptionForEvent_tops( } } -function descriptionForEvent_gls( - name: GLS_AmlEventsName, - i18n: InternationalizationAPI, -): TranslatedString | undefined { - switch (name) { - case GLS_AmlEventsName.ACCOUNT_OPENED: - return i18n.str`Account opened`; - case GLS_AmlEventsName.ACCOUNT_CLOSED: - return i18n.str`Account closed`; - default: { - assertUnreachable(name); - } - } -} +// function descriptionForEvent_gls( +// name: GLS_AmlEventsName, +// i18n: InternationalizationAPI, +// ): TranslatedString | undefined { +// switch (name) { +// case GLS_AmlEventsName.ACCOUNT_OPENED: +// return i18n.str`Account opened`; +// case GLS_AmlEventsName.ACCOUNT_CLOSED: +// return i18n.str`Account closed`; +// default: { +// assertUnreachable(name); +// } +// } +// } function MetricValueNumber({ current, @@ -376,181 +385,3 @@ function MetricValueNumber({ </Fragment> ); } - -function getDateStringForTimeframe( - date: AbsoluteTime, - timeframe: TalerCorebankApi.MonitorTimeframeParam, - locale: Locale, -): string { - if (date.t_ms === "never") return "--"; - switch (timeframe) { - case TalerCorebankApi.MonitorTimeframeParam.hour: - return `${format(date.t_ms, "HH:00", { locale })}hs`; - case TalerCorebankApi.MonitorTimeframeParam.day: - return format(date.t_ms, "EEEE", { locale }); - case TalerCorebankApi.MonitorTimeframeParam.month: - return format(date.t_ms, "MMMM", { locale }); - case TalerCorebankApi.MonitorTimeframeParam.year: - return format(date.t_ms, "yyyy", { locale }); - case TalerCorebankApi.MonitorTimeframeParam.decade: - return format(date.t_ms, "yyyy", { locale }); - } - assertUnreachable(timeframe); -} - -function SelectTimeframe({ - timeframe, - setTimeframe, -}: { - timeframe: TalerCorebankApi.MonitorTimeframeParam; - setTimeframe: (t: TalerCorebankApi.MonitorTimeframeParam) => void; -}): VNode { - const { i18n } = useTranslationContext(); - return ( - <Fragment> - <div class="sm:hidden"> - <label for="tabs" class="sr-only"> - <i18n.Translate>Select a section</i18n.Translate> - </label> - <select - id="tabs" - name="tabs" - class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" - onChange={(e) => { - setTimeframe( - parseInt( - e.currentTarget.value, - 10, - ) as TalerCorebankApi.MonitorTimeframeParam, - ); - }} - > - <option - value={TalerCorebankApi.MonitorTimeframeParam.hour} - selected={timeframe == TalerCorebankApi.MonitorTimeframeParam.hour} - > - <i18n.Translate>Last hour</i18n.Translate> - </option> - <option - value={TalerCorebankApi.MonitorTimeframeParam.day} - selected={timeframe == TalerCorebankApi.MonitorTimeframeParam.day} - > - <i18n.Translate>Previous day</i18n.Translate> - </option> - <option - value={TalerCorebankApi.MonitorTimeframeParam.month} - selected={timeframe == TalerCorebankApi.MonitorTimeframeParam.month} - > - <i18n.Translate>Last month</i18n.Translate> - </option> - <option - value={TalerCorebankApi.MonitorTimeframeParam.year} - selected={timeframe == TalerCorebankApi.MonitorTimeframeParam.year} - > - <i18n.Translate>Last year</i18n.Translate> - </option> - </select> - </div> - <div class="hidden sm:block"> - {/* FIXME: This should be LINKS */} - <nav - class="isolate flex divide-x divide-gray-200 rounded-lg shadow" - aria-label="Tabs" - > - <button - type="button" - name="set last hour" - onClick={(e) => { - e.preventDefault(); - setTimeframe(TalerCorebankApi.MonitorTimeframeParam.hour); - }} - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.hour - } - class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" - > - <span> - <i18n.Translate>Last hour</i18n.Translate> - </span> - <span - aria-hidden="true" - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.hour - } - class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5" - ></span> - </button> - <button - type="button" - name="set previous day" - onClick={(e) => { - e.preventDefault(); - setTimeframe(TalerCorebankApi.MonitorTimeframeParam.day); - }} - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.day - } - class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" - > - <span> - <i18n.Translate>Previous day</i18n.Translate> - </span> - <span - aria-hidden="true" - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.day - } - class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5" - ></span> - </button> - <button - type="button" - name="set last month" - onClick={(e) => { - e.preventDefault(); - setTimeframe(TalerCorebankApi.MonitorTimeframeParam.month); - }} - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.month - } - class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" - > - <span> - <i18n.Translate>Last month</i18n.Translate> - </span> - <span - aria-hidden="true" - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.month - } - class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5" - ></span> - </button> - <button - type="button" - name="set last year" - onClick={(e) => { - e.preventDefault(); - setTimeframe(TalerCorebankApi.MonitorTimeframeParam.year); - }} - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.year - } - class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" - > - <span> - <i18n.Translate>Last Year</i18n.Translate> - </span> - <span - aria-hidden="true" - data-selected={ - timeframe == TalerCorebankApi.MonitorTimeframeParam.year - } - class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5" - ></span> - </button> - </nav> - </div> - </Fragment> - ); -} diff --git a/packages/aml-backoffice-ui/src/pages/DecisionWizard.tsx b/packages/aml-backoffice-ui/src/pages/DecisionWizard.tsx @@ -14,20 +14,20 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - AbsoluteTime, assertUnreachable, parsePaytoUri, PaytoString, PaytoUri, TalerError, - TranslatedString, + TranslatedString } from "@gnu-taler/taler-util"; import { CopyButton, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { h, VNode } from "preact"; import { DecisionRequest, useCurrentDecisionRequest, } from "../hooks/decision-request.js"; +import { useAccountActiveDecision } from "../hooks/decisions.js"; import { Events } from "./decision/Events.js"; import { Attributes } from "./decision/Information.js"; import { Justification } from "./decision/Justification.js"; @@ -35,7 +35,6 @@ import { Measures } from "./decision/Measures.js"; import { Properties } from "./decision/Properties.js"; import { Rules } from "./decision/Rules.js"; import { Summary } from "./decision/Summary.js"; -import { useAccountActiveDecision } from "../hooks/decisions.js"; export type WizardSteps = | "attributes" // submit more information @@ -239,7 +238,7 @@ function WizardSteps({ role="list" class="overflow-hidden rounded-md lg:flex lg:rounded-none lg:border-l lg:border-r lg:border-gray-200" > - {STEPS_ORDER.map((stepLabel) => { + {STEPS_ORDER.map((stepLabel, k) => { const info = STEP_INFO[stepLabel]; const st = info.isCompleted(request) ? "completed" @@ -254,7 +253,7 @@ function WizardSteps({ : "middle"; return ( - <li class="relative lg:flex-1"> + <li class="relative lg:flex-1" key={k}> <div data-pos={pos} class="overflow-hidden data-[pos=first]:rounded-t-md border data-[pos=first]:border-b-0 border-gray-200 lg:border-0" diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -277,10 +277,8 @@ function ShowResult({ </tbody> </table> <Pagination - onFirstPage={ - history.isFirstPage ? undefined : history.loadFirst - } - onNext={history.isLastPage ? undefined : history.loadNext} + onFirstPage={history.loadFirst} + onNext={history.loadNext} /> </div> )} diff --git a/packages/aml-backoffice-ui/src/pages/Transfers.tsx b/packages/aml-backoffice-ui/src/pages/Transfers.tsx @@ -28,6 +28,7 @@ import { FormDesign, FormUI, Loading, + Pagination, RenderAmount, RouteDefinition, Time, @@ -216,8 +217,6 @@ export function Transfers({ {} as Record<string, typeof transactions>, ); - const onGoNext = resp.isLastPage ? undefined : resp.loadNext; - const onGoStart = resp.isFirstPage ? undefined : resp.loadFirst; return ( <div class="px-4 mt-8"> <div class="sm:flex sm:items-center"> @@ -380,9 +379,6 @@ export function Transfers({ {item.payto_uri} </a> </td> - {/* <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md"> - {item.subject} - </td> */} </tr> ); })} @@ -392,29 +388,7 @@ export function Transfers({ </tbody> </table> - <nav - class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" - aria-label="Pagination" - > - <div class="flex flex-1 justify-between sm:justify-end"> - <button - name="first page" - class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" - disabled={!onGoStart} - onClick={onGoStart} - > - <i18n.Translate>First page</i18n.Translate> - </button> - <button - name="next page" - class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" - disabled={!onGoNext} - onClick={onGoNext} - > - <i18n.Translate>Next</i18n.Translate> - </button> - </div> - </nav> + <Pagination onFirstPage={resp.loadFirst} onNext={resp.loadNext} /> </div> </div> ); diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx @@ -1,12 +1,22 @@ +/* + 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 { AmlDecision, - AmlSpaDialect, - assertUnreachable, EventsDerivation_TOPS, - GLS_AmlEventsName, - MeasureInformation, - TalerFormAttributes, - TOPS_AmlEventsName, + TalerFormAttributes } from "@gnu-taler/taler-util"; import { FormDesign, @@ -14,16 +24,14 @@ import { InternationalizationAPI, onComponentUnload, SelectUiChoice, - useExchangeApiContext, useForm, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { DecisionRequest, useCurrentDecisionRequest, } from "../../hooks/decision-request.js"; -import { usePreferences } from "../../hooks/preferences.js"; /** * Trigger additional events @@ -31,15 +39,15 @@ import { usePreferences } from "../../hooks/preferences.js"; * @param param0 * @returns */ -export function Events({}: {}): VNode { +export function Events(): VNode { const { i18n } = useTranslationContext(); const [request, updateRequest] = useCurrentDecisionRequest(); - const [pref] = usePreferences(); - const { config } = useExchangeApiContext(); + // const [pref] = usePreferences(); + // const { config } = useExchangeApiContext(); - const dialect = - (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ?? - AmlSpaDialect.TESTING; + // const dialect = + // (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ?? + // AmlSpaDialect.TESTING; function ShowEventForm({ events: calculatedEvents }: { events: Events }) { const design = formDesign(i18n, calculatedEvents); @@ -85,8 +93,8 @@ export function Events({}: {}): VNode { const events = calculateEventsBasedOnState( request.original, request, - i18n, - dialect, + // i18n, + // dialect, ); return <ShowEventForm events={events} />; } @@ -160,105 +168,88 @@ const formDesign = ( ], }); -function labelForEvent( - name: string, - i18n: InternationalizationAPI, - dialect: AmlSpaDialect, -) { - switch (dialect) { - case AmlSpaDialect.TOPS: - return labelForEvent_tops(name as TOPS_AmlEventsName, i18n); - case AmlSpaDialect.GLS: - return labelForEvent_gls(name as GLS_AmlEventsName, i18n); - case AmlSpaDialect.TESTING: - return labelForEvent_tops(name as TOPS_AmlEventsName, i18n); - default: { - assertUnreachable(dialect); - } - } -} -function labelForEvent_tops( - event: TOPS_AmlEventsName, - i18n: InternationalizationAPI, -) { - switch (event) { - case TOPS_AmlEventsName.INCR_ACCOUNT_OPEN: - case TOPS_AmlEventsName.DECR_ACCOUNT_OPEN: - case TOPS_AmlEventsName.INCR_HIGH_RISK_CUSTOMER: - case TOPS_AmlEventsName.DECR_HIGH_RISK_CUSTOMER: - case TOPS_AmlEventsName.INCR_HIGH_RISK_COUNTRY: - case TOPS_AmlEventsName.DECR_HIGH_RISK_COUNTRY: - case TOPS_AmlEventsName.INCR_PEP: - case TOPS_AmlEventsName.DECR_PEP: - case TOPS_AmlEventsName.INCR_PEP_FOREIGN: - case TOPS_AmlEventsName.DECR_PEP_FOREIGN: - case TOPS_AmlEventsName.INCR_PEP_DOMESTIC: - case TOPS_AmlEventsName.DECR_PEP_DOMESTIC: - case TOPS_AmlEventsName.INCR_PEP_INTERNATIONAL_ORGANIZATION: - case TOPS_AmlEventsName.DECR_PEP_INTERNATIONAL_ORGANIZATION: - case TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SIMPLE: - case TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SUBSTANTIATED: - case TOPS_AmlEventsName.INCR_INVESTIGATION_CONCLUDED: - case TOPS_AmlEventsName.DECR_INVESTIGATION_CONCLUDED: - // case TOPS_AmlEventsName.ACCOUNT_OPENED: - return i18n.str`Account opened`; - // case TOPS_AmlEventsName.ACCOUNT_CLOSED: - // return i18n.str`Account closed`; - // case TOPS_AmlEventsName.ACCOUNT_OPENED_HIGH_RISK: - // return i18n.str`High risk account incorporated`; - // case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK: - // return i18n.str`High risk account removed`; - // case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP: - // return i18n.str`Account from dometic PEP incorporated`; - // case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP: - // return i18n.str`Account from dometic PEP removed`; - // case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP: - // return i18n.str`Account from foreign PEP incorporated`; - // case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP: - // return i18n.str`Account from foreign PEP removed`; - // case TOPS_AmlEventsName.ACCOUNT_OPENED_HR_COUNTRY: - // return i18n.str`Account from high-risk country incorporated`; - // case TOPS_AmlEventsName.ACCOUNT_CLOSED_HR_COUNTRY: - // return i18n.str`Account from high-risk country removed`; - // case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE: - // return i18n.str`MROS reported by obligation`; - // case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED: - // return i18n.str`MROS reported by right`; - // case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_COMPLETED: - // return i18n.str`Investigations after Art 6 Gwg completed`; - // case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_STARTED: - // return i18n.str`Investigations after Art 6 Gwg started`; - // case TOPS_AmlEventsName.ACCOUNT_OPENED_INT_ORG_PEP: - // return i18n.str`Account from international organization PEP incorporated`; - // case TOPS_AmlEventsName.ACCOUNT_CLOSED_INT_ORG_PEP: - // return i18n.str`Account from international organization PEP removed`; - default: { - assertUnreachable(event); - } - } -} +// function labelForEvent_tops( +// event: TOPS_AmlEventsName, +// i18n: InternationalizationAPI, +// ) { +// switch (event) { +// case TOPS_AmlEventsName.INCR_ACCOUNT_OPEN: +// case TOPS_AmlEventsName.DECR_ACCOUNT_OPEN: +// case TOPS_AmlEventsName.INCR_HIGH_RISK_CUSTOMER: +// case TOPS_AmlEventsName.DECR_HIGH_RISK_CUSTOMER: +// case TOPS_AmlEventsName.INCR_HIGH_RISK_COUNTRY: +// case TOPS_AmlEventsName.DECR_HIGH_RISK_COUNTRY: +// case TOPS_AmlEventsName.INCR_PEP: +// case TOPS_AmlEventsName.DECR_PEP: +// case TOPS_AmlEventsName.INCR_PEP_FOREIGN: +// case TOPS_AmlEventsName.DECR_PEP_FOREIGN: +// case TOPS_AmlEventsName.INCR_PEP_DOMESTIC: +// case TOPS_AmlEventsName.DECR_PEP_DOMESTIC: +// case TOPS_AmlEventsName.INCR_PEP_INTERNATIONAL_ORGANIZATION: +// case TOPS_AmlEventsName.DECR_PEP_INTERNATIONAL_ORGANIZATION: +// case TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SIMPLE: +// case TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SUBSTANTIATED: +// case TOPS_AmlEventsName.INCR_INVESTIGATION_CONCLUDED: +// case TOPS_AmlEventsName.DECR_INVESTIGATION_CONCLUDED: +// // case TOPS_AmlEventsName.ACCOUNT_OPENED: +// return i18n.str`Account opened`; +// // case TOPS_AmlEventsName.ACCOUNT_CLOSED: +// // return i18n.str`Account closed`; +// // case TOPS_AmlEventsName.ACCOUNT_OPENED_HIGH_RISK: +// // return i18n.str`High risk account incorporated`; +// // case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK: +// // return i18n.str`High risk account removed`; +// // case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP: +// // return i18n.str`Account from dometic PEP incorporated`; +// // case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP: +// // return i18n.str`Account from dometic PEP removed`; +// // case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP: +// // return i18n.str`Account from foreign PEP incorporated`; +// // case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP: +// // return i18n.str`Account from foreign PEP removed`; +// // case TOPS_AmlEventsName.ACCOUNT_OPENED_HR_COUNTRY: +// // return i18n.str`Account from high-risk country incorporated`; +// // case TOPS_AmlEventsName.ACCOUNT_CLOSED_HR_COUNTRY: +// // return i18n.str`Account from high-risk country removed`; +// // case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE: +// // return i18n.str`MROS reported by obligation`; +// // case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED: +// // return i18n.str`MROS reported by right`; +// // case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_COMPLETED: +// // return i18n.str`Investigations after Art 6 Gwg completed`; +// // case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_STARTED: +// // return i18n.str`Investigations after Art 6 Gwg started`; +// // case TOPS_AmlEventsName.ACCOUNT_OPENED_INT_ORG_PEP: +// // return i18n.str`Account from international organization PEP incorporated`; +// // case TOPS_AmlEventsName.ACCOUNT_CLOSED_INT_ORG_PEP: +// // return i18n.str`Account from international organization PEP removed`; +// default: { +// assertUnreachable(event); +// } +// } +// } -function labelForEvent_gls( - event: GLS_AmlEventsName, - i18n: InternationalizationAPI, -) { - switch (event) { - case GLS_AmlEventsName.ACCOUNT_OPENED: - return i18n.str`Account opened`; - case GLS_AmlEventsName.ACCOUNT_CLOSED: - return i18n.str`Account closed`; - default: { - assertUnreachable(event); - } - } -} +// function labelForEvent_gls( +// event: GLS_AmlEventsName, +// i18n: InternationalizationAPI, +// ) { +// switch (event) { +// case GLS_AmlEventsName.ACCOUNT_OPENED: +// return i18n.str`Account opened`; +// case GLS_AmlEventsName.ACCOUNT_CLOSED: +// return i18n.str`Account closed`; +// default: { +// assertUnreachable(event); +// } +// } +// } function calculateEventsBasedOnState( currentState: AmlDecision | undefined, request: DecisionRequest, - i18n: InternationalizationAPI, - dialect: AmlSpaDialect, + // i18n: InternationalizationAPI, + // dialect: AmlSpaDialect, ): Events { const init: Events = { triggered: [], rest: [] }; const form = (request.attributes ?? {}) as Record<string, unknown>; diff --git a/packages/aml-backoffice-ui/src/pages/decision/Information.tsx b/packages/aml-backoffice-ui/src/pages/decision/Information.tsx @@ -83,7 +83,7 @@ function FillCustomerData({ }): VNode { const [request, updateRequest] = useCurrentDecisionRequest(); const [expiration, setExpiration] = useState(request.attributes?.expiration); - const expirationHandler: UIFieldHandler<any> = { + const expirationHandler: UIFieldHandler = { onChange: setExpiration, value: expiration, name: "expiration", @@ -160,7 +160,7 @@ function FillCustomerData({ /> </div> <div> - {!errors ? undefined : <ErrorsSummary errors={errors as any} />} + {!errors ? undefined : <ErrorsSummary errors={errors} />} <FormUI design={formDesign} model={form.model} /> </div> </div> @@ -176,7 +176,7 @@ function SelectForm({ }): VNode { const { i18n } = useTranslationContext(); const design = formDesign(i18n, forms); - const [request, updateRequest] = useCurrentDecisionRequest(); + const [, updateRequest] = useCurrentDecisionRequest(); const form = useForm<SelectFormType>(design, { formId: undefined, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx @@ -29,23 +29,23 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; -import { useMemo, useState } from "preact/hooks"; -import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; -import { useServerMeasures } from "../../hooks/server-info.js"; -import { computeAvailableMesaures } from "../../utils/computeAvailableMesaures.js"; +import { useState } from "preact/hooks"; import { CurrentMeasureTable, MeasureInfo, Mesaures, } from "../../components/MeasuresTable.js"; import { MeasureDefinition, NewMeasure } from "../../components/NewMeasure.js"; +import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; +import { useServerMeasures } from "../../hooks/server-info.js"; +import { computeAvailableMesaures } from "../../utils/computeAvailableMesaures.js"; /** * Ask for more information, define new paths to proceed * @param param0 * @returns */ -export function Measures({}: {}): VNode { +export function Measures(): VNode { const [request, updateRequest] = useCurrentDecisionRequest(); const [addMeasure, setAddMeasure] = useState<{ isNew: boolean; @@ -161,8 +161,8 @@ function convertToMeasureType([name, measure]: [ string, MeasureInformation, ]): MeasureType { - if (measure.context) { - // @ts-expect-error + if (measure.context && typeof measure.context === "object") { + // @ts-expect-error measure is an object, should have a string key const challengeType = measure.context["challenge-type"] as string; if (validChallengeType.indexOf(challengeType) !== -1) { return { @@ -237,9 +237,10 @@ function ActiveMeasureForm({ <FormUI design={design} model={form.model} /> <div> - {selectedVerifyMeasure.map((ver) => { + {selectedVerifyMeasure.map((ver, ky) => { return ( <button + key={ky} onClick={() => { editMeasure({ check: ver.measure.check_name, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -33,13 +33,11 @@ import { onComponentUnload, UIFormElementConfig, UIHandlerId, - undefinedIfEmpty, useExchangeApiContext, useForm, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; import { DecisionRequest, useCurrentDecisionRequest, @@ -52,8 +50,8 @@ import { DEFAULT_LIMITS_WHEN_NEW_ACCOUNT } from "./Rules.js"; * @param param0 * @returns */ -export function Properties({}: {}): VNode { - const [request, updateRequest] = useCurrentDecisionRequest(); +export function Properties(): VNode { + const [request] = useCurrentDecisionRequest(); const { config } = useExchangeApiContext(); const [pref] = usePreferences(); @@ -92,14 +90,15 @@ function officerMustCheckInvestigationState( return true; } -function ReloadForm({ merged }: { merged: any }): VNode { +function ReloadForm({ merged }: { merged: Record<string, string | boolean> }): VNode { const { i18n } = useTranslationContext(); const [request, updateRequest] = useCurrentDecisionRequest(); const design = propertiesForm( i18n, propertiesByDialect(i18n, { MANDATORY_INVESTIGATION_STATE: officerMustCheckInvestigationState( - (request.attributes?.data ?? {}) as any, + // @ts-expect-error data is the form + (request.attributes?.data ?? {}), ), }), ); @@ -145,14 +144,14 @@ function ReloadForm({ merged }: { merged: any }): VNode { <i18n.Translate>Reset to default</i18n.Translate> </button> - {!errors ? undefined : <ErrorsSummary errors={errors.defined as any} />} + {!errors ? undefined : <ErrorsSummary errors={errors.defined} />} <FormUI design={design} model={form.model} /> </div> ); } type PropertiesForm = { - defined: { [name: string]: boolean }; + defined: { [name: string]: string | boolean }; custom: { name: string; value: string }[]; }; @@ -306,20 +305,16 @@ function propertiesByDialect( ]; } -type PartialRecord<K extends keyof any, T> = { - [P in K]?: T; -}; - function calculatePropertiesBasedOnState( currentLimits: LegitimizationRuleSet, state: AccountProperties, request: DecisionRequest, dialect: AmlSpaDialect, -): PartialRecord<UIHandlerId, string | boolean | undefined> { +): Record<UIHandlerId, string | boolean | undefined> { const newNewAttributes = ( request.attributes ? request.attributes.data : {} ) as Record<string, unknown>; - type Result = PartialRecord<UIHandlerId, string | boolean | undefined>; + type Result = Record<UIHandlerId, string | boolean | undefined>; const initial: Result = {}; const formId = request.attributes?.formId; diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx @@ -24,12 +24,11 @@ import { KycRule, LegitimizationRuleSet, LimitOperationType, - MeasureInformation, parsePaytoUri, PaytoString, PaytoUri, TalerError, - TranslatedString, + TranslatedString } from "@gnu-taler/taler-util"; import { Attention, @@ -43,10 +42,10 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import { RulesInfo } from "../../components/RulesInfo.js"; +import { ShowDecisionLimitInfo } from "../../components/ShowDecisionLimitInfo.js"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; import { useServerMeasures } from "../../hooks/server-info.js"; -import { ShowDecisionLimitInfo } from "../../components/ShowDecisionLimitInfo.js"; -import { RulesInfo } from "../../components/RulesInfo.js"; const DEFAULT_MEASURE_IF_NONE = ["VERBOTEN"]; export const DEFAULT_LIMITS_WHEN_NEW_ACCOUNT: LegitimizationRuleSet = { @@ -257,7 +256,6 @@ function AddNewRuleForm({ ); } -type MeasureListWithId = (MeasureInformation & { id: string })[]; function UpdateRulesForm({ config, @@ -621,27 +619,31 @@ const expirationFormDesignTemplate = ( choices: [ { label: i18n.str`In a week`, + // @ts-expect-error we have to format as string 'dd/MM/yyyy' and convert back value: AbsoluteTime.addDuration( AbsoluteTime.now(), Duration.fromSpec({ days: 7 }), - ) as any, + ), }, { label: i18n.str`In a month`, + // @ts-expect-error we have to format as string 'dd/MM/yyyy' and convert back value: AbsoluteTime.addDuration( AbsoluteTime.now(), Duration.fromSpec({ months: 1 }), - ) as any, + ), }, { label: i18n.str`In a year`, + // @ts-expect-error we have to format as string 'dd/MM/yyyy' and convert back value: AbsoluteTime.addDuration( AbsoluteTime.now(), Duration.fromSpec({ years: 1 }), - ) as any, + ), }, { label: i18n.str`Never`, + // @ts-expect-error we have to format as string 'dd/MM/yyyy' and convert back value: AbsoluteTime.never(), }, ], diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -72,7 +72,7 @@ export function Summary({ const { i18n } = useTranslationContext(); const [decision, , cleanUpDecision] = useCurrentDecisionRequest(); const measures = useServerMeasures(); - const [notification, withErrorHandler] = useLocalNotificationHandler(); + const [, withErrorHandler] = useLocalNotificationHandler(); const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; const allMeasures = computeAvailableMesaures( diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts @@ -68,7 +68,8 @@ function DefaultTestingContext({ supported_kyc_requirements: [], version: "asd", }; - const keys: TalerExchangeApi.ExchangeKeysResponse = {} as any; + // @ts-expect-error don't care about the content of keys + const keys: TalerExchangeApi.ExchangeKeysResponse = {}; const value: ExchangeContextType = { cancelRequest: () => null, diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx @@ -57,7 +57,8 @@ function getWrapperForGroup(): FunctionComponent { supported_kyc_requirements: [], version: "asd", }; - const keys: TalerExchangeApi.ExchangeKeysResponse = {} as any; + // @ts-expect-error don't care about the content of keys + const keys: TalerExchangeApi.ExchangeKeysResponse = {}; const value: ExchangeContextType = { cancelRequest: () => null, config: { config, keys }, diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts @@ -4,9 +4,4 @@ export * from "./forms/index.js"; export * from "./hooks/index.js"; export { parseGroupImport, renderStories } from "./stories-utils.js"; export { decodeCrockFromURI, encodeCrockForURI } from "./utils/base64.js"; -export * from "./utils/http-impl.browser.js"; -export * from "./utils/http-impl.sw.js"; -export * from "./utils/observable.js"; -export * from "./utils/request.js"; -export * from "./utils/route.js"; -export * from "./utils/select-ui-lists.js"; +export * from "./utils/index.js"; diff --git a/packages/web-util/src/utils/buildPaginatedResult.ts b/packages/web-util/src/utils/buildPaginatedResult.ts @@ -0,0 +1,49 @@ +import { OperationOk } from "@gnu-taler/taler-util"; + +export type PaginatedResult<T> = OperationOk<T> & { + loadNext?(): void; + loadFirst?(): void; +}; + +/** + * + * @param data the result of the requested list + * @param offset offset id or index + * @param setOffset function to be call on loadNext or loadFirst to specify the new offset + * @param getId return the offset id of a row + * @param PAGINATED_LIST_REQUEST the limit of the request, the UI is expted to show N -1 elements + * @returns an OperationOk with two function. If the function is missing is because the offset is on the limit + */ +export function buildPaginatedResult<R, OffId>( + data: R[], + offset: OffId | undefined, + setOffset: (o: OffId | undefined) => void, + getId: (r: R) => OffId, + PAGINATED_LIST_REQUEST: number +): PaginatedResult<R[]> { + const isLastPage = data.length < PAGINATED_LIST_REQUEST; + const isFirstPage = offset === undefined; + + const result = structuredClone(data); + + if (result.length == PAGINATED_LIST_REQUEST) { + result.pop(); + } + return { + type: "ok", + case: "ok", + body: result, + loadNext: isLastPage + ? undefined + : () => { + if (!result.length) return; + const id = getId(result[result.length - 1]); + setOffset(id); + }, + loadFirst: isFirstPage + ? undefined + : () => { + setOffset(undefined); + }, + }; +} diff --git a/packages/web-util/src/utils/index.ts b/packages/web-util/src/utils/index.ts @@ -0,0 +1,7 @@ +export * from "./http-impl.browser.js"; +export * from "./http-impl.sw.js"; +export * from "./observable.js"; +export * from "./request.js"; +export * from "./route.js"; +export * from "./select-ui-lists.js"; +export * from "./buildPaginatedResult.js";