[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-typescript-core] 02/02: dashboard
From: |
gnunet |
Subject: |
[taler-typescript-core] 02/02: dashboard |
Date: |
Tue, 21 Jan 2025 22:17:22 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository taler-typescript-core.
commit 8d834d43c51f58140e31581656a1a4cfad90aea6
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Jan 21 18:17:07 2025 -0300
dashboard
---
.../aml-backoffice-ui/src/ExchangeAmlFrame.tsx | 9 +-
packages/aml-backoffice-ui/src/Routing.tsx | 11 +-
packages/aml-backoffice-ui/src/hooks/account.ts | 32 --
.../src/hooks/decision-request.ts | 9 +-
.../src/hooks/{account.ts => server-info.ts} | 90 +++-
.../aml-backoffice-ui/src/pages/CaseDetails.tsx | 104 ++--
packages/aml-backoffice-ui/src/pages/Dashboard.tsx | 570 +++++++++++++++++++++
packages/aml-backoffice-ui/src/pages/Measures.tsx | 5 +-
.../pages/decision/AmlDecisionRequestWizard.tsx | 12 +-
.../src/pages/decision/Events.tsx | 188 +++----
.../src/pages/decision/Justification.tsx | 119 ++++-
.../src/pages/decision/Measures.tsx | 16 +-
.../src/pages/decision/Properties.tsx | 21 +-
.../aml-backoffice-ui/src/pages/decision/Rules.tsx | 16 +-
.../decision/{Justification.tsx => Summary.tsx} | 15 +-
.../src/pages/decision/aml-events.ts | 133 +++++
packages/bank-ui/src/hooks/regional.ts | 25 +-
packages/bank-ui/src/pages/admin/AdminHome.tsx | 138 ++++-
packages/taler-util/src/http-client/exchange.ts | 13 +-
packages/taler-util/src/types-taler-exchange.ts | 26 +-
20 files changed, 1266 insertions(+), 286 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
index 8b5911641..2cd82505f 100644
--- a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
+++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -218,7 +218,7 @@ export function ExchangeAmlFrame({
<div class="-mt-32 flex grow ">
{officer?.state !== "ready" ? undefined : <Navigation />}
- <div class="flex mx-auto my-4">
+ <div class="flex mx-auto my-4 min-w-80">
<main
class="block rounded-lg bg-white px-5 py-6 shadow "
style={{ minWidth: 600 }}
@@ -241,7 +241,11 @@ function Navigation(): VNode {
const { i18n } = useTranslationContext();
const [{ showDebugInfo }] = usePreferences();
const pageList = [
- { route: privatePages.profile, Icon: PeopleIcon, label: i18n.str`Profile`
},
+ {
+ route: privatePages.dashboard,
+ Icon: PeopleIcon,
+ label: i18n.str`Dashboard`,
+ },
{
route: privatePages.investigation,
Icon: ToInvestigateIcon,
@@ -253,6 +257,7 @@ function Navigation(): VNode {
Icon: SearchIcon,
label: i18n.str`Search`,
},
+ { route: privatePages.profile, Icon: PeopleIcon, label: i18n.str`Profile`
},
showDebugInfo
? { route: privatePages.measures, Icon: FormIcon, label: i18n.str`Forms`
}
: undefined,
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx
b/packages/aml-backoffice-ui/src/Routing.tsx
index 1358eda50..58a0730f1 100644
--- a/packages/aml-backoffice-ui/src/Routing.tsx
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -43,6 +43,7 @@ import {
WizardSteps,
} from "./pages/decision/AmlDecisionRequestWizard.js";
import { useCurrentDecisionRequest } from "./hooks/decision-request.js";
+import { Dashboard } from "./pages/Dashboard.js";
export function Routing(): VNode {
const session = useOfficer();
@@ -107,6 +108,8 @@ function PublicRounting(): VNode {
export const privatePages = {
profile: urlPattern(/\/profile/, () => "#/profile"),
+ dashboard: urlPattern(/\/dashboard/, () => "#/dashboard"),
+ statsDownload: urlPattern(/\/download-stats/, () => "#/download-stats"),
decideWithStep: urlPattern<{ cid: string; step: string }>(
/\/decide\/(?<cid>[a-zA-Z0-9]+)\/(?<step>[a-z]+)/,
({ cid, step }) => `#/decide/${cid}/${step}`,
@@ -147,7 +150,7 @@ function PrivateRouting(): VNode {
const [request, _, updateRequest] = useCurrentDecisionRequest();
useEffect(() => {
if (location.name === undefined) {
- navigateTo(privatePages.profile.url({}));
+ navigateTo(privatePages.dashboard.url({}));
}
}, [location]);
@@ -261,6 +264,12 @@ function PrivateRouting(): VNode {
case "search": {
return <Search />;
}
+ case "statsDownload": {
+ return <div>not yet implemented</div>;
+ }
+ case "dashboard": {
+ return <Dashboard routeDownloadStats={privatePages.statsDownload} />;
+ }
default:
assertUnreachable(location);
}
diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts
b/packages/aml-backoffice-ui/src/hooks/account.ts
index a4fbcb803..f34da0825 100644
--- a/packages/aml-backoffice-ui/src/hooks/account.ts
+++ b/packages/aml-backoffice-ui/src/hooks/account.ts
@@ -68,35 +68,3 @@ export function useAccountInformation(paytoHash?: string) {
if (error) return error;
return undefined;
}
-
-export function useServerMeasures() {
- const officer = useOfficer();
- const session = officer.state === "ready" ? officer.account : undefined;
-
- const {
- lib: { exchange: api },
- } = useExchangeApiContext();
-
- async function fetcher([officer]: [OfficerAccount]) {
- return await api.getAmlMesasures(officer);
- }
-
- const { data, error } = useSWR<
- TalerExchangeResultByMethod<"getAmlMesasures">,
- TalerHttpError
- >(!session ? undefined : [session], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- keepPreviousData: true,
- });
-
- if (data) return data;
- if (error) return error;
- return undefined;
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts
b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
index 9b67b5654..34c2aeda3 100644
--- a/packages/aml-backoffice-ui/src/hooks/decision-request.ts
+++ b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -35,14 +35,15 @@ import { buildStorageKey, useLocalStorage } from
"@gnu-taler/web-util/browser";
export interface DecisionRequest {
rules: KycRule[] | undefined;
+ new_measures: string | undefined;
deadline: AbsoluteTime | undefined;
- properties: object | undefined;
- custom_properties: object | undefined;
+ onExpire_measures: string | undefined;
+ properties: Record<string, any> | undefined;
+ custom_properties: Record<string, any> | undefined;
custom_events: string[] | undefined;
inhibit_events: string[] | undefined;
keep_investigating: boolean;
justification: string | undefined;
- new_measures: string | undefined;
}
export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
@@ -59,10 +60,12 @@ export const codecForDecisionRequest = ():
Codec<DecisionRequest> =>
codecOptionalDefault(codecForBoolean(), false),
)
.property("new_measures", codecOptional(codecForString()))
+ .property("onExpire_measures", codecOptional(codecForString()))
.build("DecisionRequest");
const defaultDecisionRequest: DecisionRequest = {
deadline: undefined,
+ onExpire_measures: undefined,
custom_events: undefined,
inhibit_events: undefined,
justification: undefined,
diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts
b/packages/aml-backoffice-ui/src/hooks/server-info.ts
similarity index 58%
copy from packages/aml-backoffice-ui/src/hooks/account.ts
copy to packages/aml-backoffice-ui/src/hooks/server-info.ts
index a4fbcb803..47743440a 100644
--- a/packages/aml-backoffice-ui/src/hooks/account.ts
+++ b/packages/aml-backoffice-ui/src/hooks/server-info.ts
@@ -14,8 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ AbsoluteTime,
+ AmlSpaDialect,
+ assertUnreachable,
+ EventCounter,
OfficerAccount,
+ OperationOk,
+ opFixedSuccess,
PaytoString,
+ TalerCorebankApi,
TalerExchangeResultByMethod,
TalerHttpError,
} from "@gnu-taler/taler-util";
@@ -23,36 +30,31 @@ import {
import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
import _useSWR, { mutate, SWRHook } from "swr";
import { useOfficer } from "./officer.js";
+import { AmlProgram } from "../../../taler-util/lib/types-taler-kyc-aml.js";
+import {
+ AML_EVENTS_INFO,
+ AmlEventsName,
+} from "../pages/decision/aml-events.js";
+import { useMemo } from "preact/hooks";
+import { Timeframe } from "../pages/Dashboard.js";
const useSWR = _useSWR as unknown as SWRHook;
-export function revalidateAccountInformation() {
- return mutate(
- (key) =>
- Array.isArray(key) &&
- key[key.length - 1] === "getAmlAttributesForAccount",
- undefined,
- { revalidate: true },
- );
-}
-export function useAccountInformation(paytoHash?: string) {
+export function useServerMeasures() {
const officer = useOfficer();
- const session =
- officer.state === "ready" && paytoHash !== undefined
- ? officer.account
- : undefined;
+ const session = officer.state === "ready" ? officer.account : undefined;
const {
lib: { exchange: api },
} = useExchangeApiContext();
- async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
- return await api.getAmlAttributesForAccount(officer, account);
+ async function fetcher([officer]: [OfficerAccount]) {
+ return await api.getAmlMesasures(officer);
}
const { data, error } = useSWR<
- TalerExchangeResultByMethod<"getAmlAttributesForAccount">,
+ TalerExchangeResultByMethod<"getAmlMesasures">,
TalerHttpError
- >(!session ? undefined : [session, paytoHash], fetcher, {
+ >(!session ? undefined : [session], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -69,7 +71,15 @@ export function useAccountInformation(paytoHash?: string) {
return undefined;
}
-export function useServerMeasures() {
+export type ServerStats = {
+ [name: string]: EventCounter;
+};
+
+export function useServerStatistics(
+ dialect: AmlSpaDialect,
+ current: Timeframe,
+ previous?: Timeframe,
+) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
@@ -77,14 +87,48 @@ export function useServerMeasures() {
lib: { exchange: api },
} = useExchangeApiContext();
- async function fetcher([officer]: [OfficerAccount]) {
- return await api.getAmlMesasures(officer);
+ const keys = useMemo(() => {
+ return Object.entries(AML_EVENTS_INFO)
+ .filter(([name, info]) => {
+ return info.dialect.includes(dialect);
+ })
+ .map(([name]) => name as AmlEventsName);
+ }, [dialect]);
+
+ async function fetcher([officer, keys, current, previous]: [
+ OfficerAccount,
+ AmlEventsName[],
+ Timeframe,
+ Timeframe | undefined,
+ ]) {
+ const queries = keys.map((key) => {
+ return Promise.all([
+ api.getAmlKycStatistics(officer, key, {
+ since: current.start,
+ until: current.end,
+ }),
+ !previous
+ ? undefined
+ : api.getAmlKycStatistics(officer, key, {
+ since: previous.start,
+ until: previous.end,
+ }),
+ ]).then(([c, p]) => {
+ return { key, current: c.body, previous: p?.body };
+ });
+ });
+
+ const p = await Promise.all(queries);
+
+ return opFixedSuccess(p);
}
const { data, error } = useSWR<
- TalerExchangeResultByMethod<"getAmlMesasures">,
+ OperationOk<
+ { key: AmlEventsName; current: EventCounter; previous?: EventCounter }[]
+ >,
TalerHttpError
- >(!session ? undefined : [session], fetcher, {
+ >(!session ? undefined : [session, keys, current, previous], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index dd5aea232..68c037cc7 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -58,13 +58,14 @@ 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, useServerMeasures } from "../hooks/account.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 { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js";
import { Officer } from "./Officer.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
+import { useServerMeasures } from "../hooks/server-info.js";
export type AmlEvent =
| AmlFormEvent
@@ -142,13 +143,13 @@ export function CaseDetails({
// paytoString?: PaytoString;
}) {
const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
- const [request, setDesicionRequest] = useState<NewDecision | undefined>(
- undefined,
- );
+ // const [request, setDesicionRequest] = useState<NewDecision | undefined>(
+ // undefined,
+ // );
// const [decisionWizardStep, setDecisionWizardStep] =
// useState<WizardSteps>("events");
- const [selectMeasure, setSelectMeasure] = useState<boolean>();
- const { config } = useExchangeApiContext();
+ // const [selectMeasure, setSelectMeasure] = useState<boolean>();
+ // const { config } = useExchangeApiContext();
const { i18n } = useTranslationContext();
const details = useAccountInformation(account);
@@ -196,47 +197,47 @@ 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.config.currency),
- },
- },
- askInformation: false,
- });
- }}
- />
- );
- }
- if (request) {
- return (
- <SubmitNewDecision
- decision={request}
- onComplete={() => {
- setDesicionRequest(undefined);
- }}
- />
- );
- }
+ // 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.config.currency),
+ // },
+ // },
+ // askInformation: false,
+ // });
+ // }}
+ // />
+ // );
+ // }
+ // if (request) {
+ // return (
+ // <SubmitNewDecision
+ // decision={request}
+ // onComplete={() => {
+ // setDesicionRequest(undefined);
+ // }}
+ // />
+ // );
+ // }
// if (decisionWizardStep) {
// return (
// <AmlDecisionRequestWizard
@@ -254,6 +255,7 @@ export function CaseDetails({
onNewDecision({
deadline: undefined,
custom_properties: undefined,
+ onExpire_measures: undefined,
custom_events: undefined,
inhibit_events: undefined,
justification: undefined,
@@ -267,7 +269,7 @@ export function CaseDetails({
>
<i18n.Translate>New decision</i18n.Translate>
</button>
-
+ {/*
<button
onClick={async () => {
setDesicionRequest({
@@ -377,16 +379,16 @@ export function CaseDetails({
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>Ask for more information</i18n.Translate>
- </button>
+ </button> */}
- <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>
+ </button> */}
</div>
);
}
diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx
b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx
new file mode 100644
index 000000000..e87236cfc
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx
@@ -0,0 +1,570 @@
+/*
+ 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 {
+ AbsoluteTime,
+ AmlSpaDialect,
+ assertUnreachable,
+ TalerCorebankApi,
+ TalerError,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ InternationalizationAPI,
+ RouteDefinition,
+ useExchangeApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format, sub } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useMemo, useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useOfficer } from "../hooks/officer.js";
+import { usePreferences } from "../hooks/preferences.js";
+import { useServerStatistics } from "../hooks/server-info.js";
+import { AmlEventsName } from "./decision/aml-events.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+
+export function Dashboard({
+ routeDownloadStats,
+}: {
+ routeDownloadStats: RouteDefinition;
+}) {
+ const officer = useOfficer();
+ const { i18n } = useTranslationContext();
+
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+
+ return (
+ <div>
+ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+ <i18n.Translate>Dashboard</i18n.Translate>
+ </h1>
+ <EventMetrics routeDownloadStats={routeDownloadStats} />
+ </div>
+ );
+}
+
+const now = new Date();
+
+function EventMetrics({
+ routeDownloadStats,
+}: {
+ routeDownloadStats: RouteDefinition;
+}): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ const [metricType, setMetricType] =
+ useState<TalerCorebankApi.MonitorTimeframeParam>(
+ TalerCorebankApi.MonitorTimeframeParam.hour,
+ );
+ const params = useMemo(
+ () => getTimeframesForDate(now, metricType),
+ [metricType],
+ );
+
+ const [pref] = usePreferences();
+ const { config } = useExchangeApiContext();
+
+ const dialect =
+ (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ??
+ AmlSpaDialect.TESTING;
+
+ const resp = useServerStatistics(dialect, params.current, params.previous);
+
+ if (!resp) return <Fragment />;
+ if (resp instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={resp} />;
+ }
+
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center mb-4">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Events</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+
+ <SelectTimeframe timeframe={metricType} setTimeframe={setMetricType} />
+
+ <div class="w-full flex justify-between">
+ <h1 class="text-base text-gray-900 mt-5">
+ {i18n.str`Trading volume from ${getDateStringForTimeframe(
+ params.current.start,
+ metricType,
+ dateLocale,
+ )} to ${getDateStringForTimeframe(
+ params.current.end,
+ metricType,
+ dateLocale,
+ )}`}
+ </h1>
+ </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">
+ {resp.body.map((ev) => {
+ const label = labelForEvent(ev.key, i18n);
+ const desc = descriptionForEvent(ev.key, i18n);
+ return (
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ {label}
+ {!desc ? undefined : (
+ <div class="text-xs text-gray-500">{desc}</div>
+ )}
+ </dt>
+ <MetricValueNumber
+ current={ev.current.counter}
+ previous={ev.previous?.counter}
+ />
+ </div>
+ );
+ })}
+ </dl>
+ <div class="flex justify-end mt-4">
+ <a
+ href={routeDownloadStats.url({})}
+ name="download stats"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer
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>Download stats as CSV</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ );
+}
+
+function labelForEvent(name: AmlEventsName, i18n: InternationalizationAPI) {
+ switch (name) {
+ case AmlEventsName.ACCOUNT_FROZEN:
+ return i18n.str`Frozen accounts`;
+ case AmlEventsName.ACCOUNT_PEP:
+ return i18n.str`PEP persons`;
+ case AmlEventsName.TEST_EVENT_KEY_1:
+ return i18n.str`TEST_EVENT_KEY_1`;
+ case AmlEventsName.TEST_EVENT_KEY_2:
+ return i18n.str`TEST_EVENT_KEY_2`;
+ case AmlEventsName.TEST_EVENT_KEY_3:
+ return i18n.str`TEST_EVENT_KEY_3`;
+ case AmlEventsName.TEST_EVENT_KEY_4:
+ return i18n.str`TEST_EVENT_KEY_4`;
+ case AmlEventsName.TEST_EVENT_KEY_5:
+ return i18n.str`TEST_EVENT_KEY_5`;
+ case AmlEventsName.TEST_EVENT_KEY_6:
+ return i18n.str`TEST_EVENT_KEY_6`;
+ case AmlEventsName.TEST_EVENT_KEY_7:
+ return i18n.str`TEST_EVENT_KEY_7`;
+ case AmlEventsName.TEST_EVENT_KEY_8:
+ return i18n.str`TEST_EVENT_KEY_8`;
+ case AmlEventsName.TEST_EVENT_KEY_9:
+ return i18n.str`TEST_EVENT_KEY_9`;
+ case AmlEventsName.TEST_EVENT_KEY_10:
+ return i18n.str`TEST_EVENT_KEY_10`;
+ case AmlEventsName.TEST_EVENT_KEY_11:
+ return i18n.str`TEST_EVENT_KEY_11`;
+ case AmlEventsName.TEST_EVENT_KEY_12:
+ return i18n.str`TEST_EVENT_KEY_12`;
+ case AmlEventsName.TEST_EVENT_KEY_13:
+ return i18n.str`TEST_EVENT_KEY_13`;
+ case AmlEventsName.TEST_EVENT_KEY_14:
+ return i18n.str`TEST_EVENT_KEY_14`;
+ default: {
+ assertUnreachable(name);
+ }
+ }
+}
+
+function descriptionForEvent(
+ name: AmlEventsName,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ switch (name) {
+ case AmlEventsName.ACCOUNT_FROZEN:
+ return i18n.str`Accounts that can move funds`;
+ case AmlEventsName.ACCOUNT_PEP:
+ return i18n.str`Public exposed persons`;
+ case AmlEventsName.TEST_EVENT_KEY_1:
+ return i18n.str`TEST_EVENT_KEY_1`;
+ case AmlEventsName.TEST_EVENT_KEY_2:
+ return i18n.str`TEST_EVENT_KEY_2`;
+ case AmlEventsName.TEST_EVENT_KEY_3:
+ return i18n.str`TEST_EVENT_KEY_3`;
+ case AmlEventsName.TEST_EVENT_KEY_4:
+ return i18n.str`TEST_EVENT_KEY_4`;
+ case AmlEventsName.TEST_EVENT_KEY_5:
+ return i18n.str`TEST_EVENT_KEY_5`;
+ case AmlEventsName.TEST_EVENT_KEY_6:
+ return i18n.str`TEST_EVENT_KEY_6`;
+ case AmlEventsName.TEST_EVENT_KEY_7:
+ return i18n.str`TEST_EVENT_KEY_7`;
+ case AmlEventsName.TEST_EVENT_KEY_8:
+ return i18n.str`TEST_EVENT_KEY_8`;
+ case AmlEventsName.TEST_EVENT_KEY_9:
+ return i18n.str`TEST_EVENT_KEY_9`;
+ case AmlEventsName.TEST_EVENT_KEY_10:
+ return i18n.str`TEST_EVENT_KEY_10`;
+ case AmlEventsName.TEST_EVENT_KEY_11:
+ return i18n.str`TEST_EVENT_KEY_11`;
+ case AmlEventsName.TEST_EVENT_KEY_12:
+ return i18n.str`TEST_EVENT_KEY_12`;
+ case AmlEventsName.TEST_EVENT_KEY_13:
+ return i18n.str`TEST_EVENT_KEY_13`;
+ case AmlEventsName.TEST_EVENT_KEY_14:
+ return i18n.str`TEST_EVENT_KEY_14`;
+ default: {
+ assertUnreachable(name);
+ }
+ }
+}
+
+function MetricValueNumber({
+ current,
+ previous,
+}: {
+ current: number | undefined;
+ previous: number | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const cmp = current && previous ? (current < previous ? -1 : 1) : 0;
+
+ const rate =
+ !current || Number.isNaN(current) || !previous || Number.isNaN(previous)
+ ? 0
+ : cmp === -1
+ ? 1 - Math.round(current) / Math.round(previous)
+ : cmp === 1
+ ? Math.round(current) / Math.round(previous) - 1
+ : 0;
+
+ const negative = cmp === 0 ? undefined : cmp === -1;
+ const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`;
+ return (
+ <Fragment>
+ <dd class="mt-1 block ">
+ <div class="flex justify-start text-2xl items-baseline font-semibold
text-indigo-600">
+ {!current ? "-" : current}
+ </div>
+ <div class="flex flex-col">
+ <div class="flex justify-end items-baseline text-2xl font-semibold
text-indigo-600">
+ <small class="ml-2 text-sm font-medium text-gray-500">
+ <i18n.Translate>previous</i18n.Translate>{" "}
+ {!previous ? "-" : previous}
+ </small>
+ </div>
+ {!!rate && (
+ <span
+ data-negative={negative}
+ class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100
text-green-800 data-[negative=true]:bg-red-100 px-2 py-1 text-xs font-medium
data-[negative=true]:text-red-700 whitespace-pre"
+ >
+ {negative ? (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75"
+ />
+ </svg>
+ ) : (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75"
+ />
+ </svg>
+ )}
+
+ {negative ? (
+ <span class="sr-only">
+ <i18n.Translate>Decreased by</i18n.Translate>
+ </span>
+ ) : (
+ <span class="sr-only">
+ <i18n.Translate>Increased by</i18n.Translate>
+ </span>
+ )}
+ {rateStr}
+ </span>
+ )}
+ </div>
+ </dd>
+ </Fragment>
+ );
+}
+
+export type Timeframe = { start: AbsoluteTime; end: AbsoluteTime };
+
+export function getTimeframesForDate(
+ time: Date,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+): {
+ current: Timeframe;
+ previous: Timeframe;
+} {
+ switch (timeframe) {
+ case TalerCorebankApi.MonitorTimeframeParam.hour: {
+ const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+ AbsoluteTime.fromMilliseconds(
+ sub(time, { hours: timeIndex }).getTime(),
+ ),
+ );
+ return {
+ current: { start: middle, end: high },
+ previous: { start: low, end: middle },
+ };
+ }
+ case TalerCorebankApi.MonitorTimeframeParam.day: {
+ const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+ AbsoluteTime.fromMilliseconds(sub(time, { days: timeIndex
}).getTime()),
+ );
+ return {
+ current: { start: middle, end: high },
+ previous: { start: low, end: middle },
+ };
+ }
+ case TalerCorebankApi.MonitorTimeframeParam.month: {
+ const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+ AbsoluteTime.fromMilliseconds(
+ sub(time, { months: timeIndex }).getTime(),
+ ),
+ );
+ return {
+ current: { start: middle, end: high },
+ previous: { start: low, end: middle },
+ };
+ }
+
+ case TalerCorebankApi.MonitorTimeframeParam.year: {
+ const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+ AbsoluteTime.fromMilliseconds(
+ sub(time, { years: timeIndex }).getTime(),
+ ),
+ );
+ return {
+ current: { start: middle, end: high },
+ previous: { start: low, end: middle },
+ };
+ }
+ case TalerCorebankApi.MonitorTimeframeParam.decade: {
+ const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+ AbsoluteTime.fromMilliseconds(
+ sub(time, { years: timeIndex * 10 }).getTime(),
+ ),
+ );
+ return {
+ current: { start: middle, end: high },
+ previous: { start: low, end: middle },
+ };
+ }
+ default:
+ assertUnreachable(timeframe);
+ }
+}
+
+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/Measures.tsx
b/packages/aml-backoffice-ui/src/pages/Measures.tsx
index 28236fe0c..571b77c8f 100644
--- a/packages/aml-backoffice-ui/src/pages/Measures.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Measures.tsx
@@ -28,16 +28,13 @@ import {
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
-import { useServerMeasures } from "../hooks/account.js";
import { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js";
import { Officer } from "./Officer.js";
+import { useServerMeasures } from "../hooks/server-info.js";
export function Measures({}: {}) {
const { i18n } = useTranslationContext();
- // const { forms } = useUiFormsContext();
-
- // const allForms = [...forms, ...preloadedForms(i18n)];
const measures = useServerMeasures();
if (!measures) {
diff --git
a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
index 3d6f8a1b4..b677e2a0d 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
@@ -34,13 +34,15 @@ import { Properties } from "./Properties.js";
import { Rules } from "./Rules.js";
import { Measures } from "./Measures.js";
import { Justification } from "./Justification.js";
+import { Summary } from "./Summary.js";
export type WizardSteps =
| "rules" // define the limits
| "measures" // define a new form/challenge
| "properties" // define account information
| "events" // define events to trigger
- | "justification"; // finalize, investigate?;
+ | "justification" // finalize, investigate?;
+ | "summary";
const STEPS_ORDER: WizardSteps[] = [
"rules",
@@ -48,6 +50,7 @@ const STEPS_ORDER: WizardSteps[] = [
"properties",
"events",
"justification",
+ "summary",
];
const STEPS_ORDER_MAP = STEPS_ORDER.reduce(
@@ -108,6 +111,8 @@ export function AmlDecisionRequestWizard({
return <Measures />;
case "justification":
return <Justification />;
+ case "summary":
+ return <Summary />;
}
assertUnreachable(stepOrDefault);
})();
@@ -178,6 +183,11 @@ function WizardSteps({
description: i18n.str`Add information about the account.`,
isCompleted: isPropertiesCompleted,
},
+ summary: {
+ label: i18n.str`Summary`,
+ description: i18n.str`Review and send.`,
+ isCompleted: () => false,
+ },
};
return (
<div class="lg:border-b lg:border-t lg:border-gray-200">
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
index e65586a77..a984b430a 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
@@ -1,19 +1,23 @@
import {
- useTranslationContext,
- useExchangeApiContext,
- useForm,
+ AmlSpaDialect,
+ assertUnreachable,
+ MeasureInformation,
+} from "@gnu-taler/taler-util";
+import {
+ FormDesign,
FormUI,
- TalerFormAttributes,
- UIHandlerId,
InternationalizationAPI,
- UIFormElementConfig,
- FormDesign,
onComponentUnload,
+ UIFormElementConfig,
+ UIHandlerId,
+ useExchangeApiContext,
+ useForm,
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { usePreferences } from "../../hooks/preferences.js";
-import { MeasureInformation } from "@gnu-taler/taler-util";
+import { AML_EVENTS_INFO, AmlEventsName } from "./aml-events.js";
/**
* Trigger additional events
@@ -26,21 +30,36 @@ export function Events({}: {}): VNode {
const [pref] = usePreferences();
const { config } = useExchangeApiContext();
- const calculatedProps = {
- ...(request.properties ?? {}),
- ...(request.custom_properties ?? {}),
- };
+ const dialect =
+ (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ??
+ AmlSpaDialect.TESTING;
- const calculatedEvents = eventsByDialect(
- i18n,
- pref.testingDialect ? "testing" : config.config.aml_spa_dialect,
- calculatedProps,
+ const calculatedEvents = Object.entries(AML_EVENTS_INFO).reduce(
+ (prev, [name, info]) => {
+ const field = {
+ id: name as UIHandlerId,
+ type: "toggle",
+ required: true,
+ label: labelForEvent(name as AmlEventsName, i18n),
+ } satisfies UIFormElementConfig;
+
+ if (info.shouldBeTriggered(request, dialect)) {
+ prev.on.push(field);
+ } else {
+ prev.off.push(field);
+ }
+ return prev;
+ },
+ { on: [], off: [] } as {
+ on: UIFormElementConfig[];
+ off: UIFormElementConfig[];
+ },
);
- const design = eventsForm(i18n, calculatedEvents);
+ const design = formDesign(i18n, calculatedEvents.on);
- const form = useForm<EventsForm>(design, {
- inhibit: calculatedEvents.reduce(
+ const form = useForm<FormType>(design, {
+ inhibit: calculatedEvents.on.reduce(
(prev, cur) => {
if (cur.type !== "toggle") return prev;
const isInhibit =
@@ -49,7 +68,7 @@ export function Events({}: {}): VNode {
prev[cur.id] = isInhibit;
return prev;
},
- {} as EventsForm["inhibit"],
+ {} as FormType["inhibit"],
),
trigger: !request.custom_events
? []
@@ -75,28 +94,28 @@ export function Events({}: {}): VNode {
);
}
-export type EventsForm = {
+type FormType = {
trigger: { name: string }[];
inhibit: { [name: string]: boolean };
};
-export const eventsForm = (
+const formDesign = (
i18n: InternationalizationAPI,
- defaultEvents: UIFormElementConfig[],
+ inhibitEvents: UIFormElementConfig[],
): FormDesign<MeasureInformation> => ({
type: "double-column",
sections: [
{
title: i18n.str`Inhibit default events`,
- description: i18n.str`Use this form to prevent events to be triggered by
the current status.`,
- fields: !defaultEvents.length
+ description: i18n.str`Here you can prevent events to be triggered by the
current status.`,
+ fields: !inhibitEvents.length
? [
{
type: "caption",
label: i18n.str`No default events calculated.`,
},
]
- : defaultEvents.map((f) =>
+ : inhibitEvents.map((f) =>
"id" in f ? { ...f, id: ("inhibit." + f.id) as UIHandlerId } : f,
),
},
@@ -120,89 +139,44 @@ export const eventsForm = (
],
},
],
- // fields: [
- // {
- // id: "trigger" as UIHandlerId,
- // type: "array",
- // labelFieldId: "name" as UIHandlerId,
- // label: i18n.str`Trigger`,
- // fields: [],
- // },
- // {
- // id: "inhibit" as UIHandlerId,
- // type: "array",
- // labelFieldId: "name" as UIHandlerId,
- // label: i18n.str`Inhibit`,
- // fields: [],
- // },
- // ],
});
-export function eventsByDialect(
- i18n: InternationalizationAPI,
- dialect: string | undefined,
- properties: object,
-): UIFormElementConfig[] {
- if (!dialect) return [];
- const result: UIFormElementConfig[] = [];
- switch (dialect) {
- case "testing": {
- const props = properties as TalerFormAttributes.AccountProperties_TOPS;
- if (props.ACCOUNT_FROZEN) {
- result.push({
- id: "ACCOUNT_FROZEN" satisfies keyof
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
- label: i18n.str`Is froozen?`,
- // gana_type: "Boolean",
- type: "toggle",
- required: true,
- });
- }
- if (props.ACCOUNT_SANCTIONED) {
- result.push({
- id: "ACCOUNT_SANCTIONED" satisfies keyof
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
- label: i18n.str`Is sacntioned?`,
- // gana_type: "Boolean",
- type: "toggle",
- required: true,
- });
- }
- if (props.ACCOUNT_HIGH_RISK) {
- result.push({
- id: "ACCOUNT_HIGH_RISK" satisfies keyof
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
- label: i18n.str`Is high risk?`,
- // gana_type: "Boolean",
- type: "toggle",
- required: true,
- });
- }
- break;
- }
- case "gls": {
- const props = properties as TalerFormAttributes.AccountProperties_TOPS;
- if (props.ACCOUNT_FROZEN) {
- result.push({
- id: "ACCOUNT_FROZEN" satisfies keyof
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
- label: i18n.str`Is frozen?`,
- // gana_type: "Boolean",
- type: "toggle",
- required: true,
- });
- }
- break;
- }
- case "tops": {
- const props = properties as TalerFormAttributes.AccountProperties_TOPS;
- if (props.ACCOUNT_HIGH_RISK) {
- result.push({
- id: "ACCOUNT_HIGH_RISK" satisfies keyof
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
- label: i18n.str`Is high risk?`,
- // gana_type: "Boolean",
- type: "toggle",
- required: true,
- });
- }
- break;
+function labelForEvent(event: AmlEventsName, i18n: InternationalizationAPI) {
+ switch (event) {
+ case AmlEventsName.ACCOUNT_FROZEN:
+ return i18n.str`new account frozen`;
+ case AmlEventsName.ACCOUNT_PEP:
+ return i18n.str`new exposed person`;
+ case AmlEventsName.TEST_EVENT_KEY_1:
+ return i18n.str`TEST_EVENT_KEY_1`;
+ case AmlEventsName.TEST_EVENT_KEY_2:
+ return i18n.str`TEST_EVENT_KEY_2`;
+ case AmlEventsName.TEST_EVENT_KEY_3:
+ return i18n.str`TEST_EVENT_KEY_3`;
+ case AmlEventsName.TEST_EVENT_KEY_4:
+ return i18n.str`TEST_EVENT_KEY_4`;
+ case AmlEventsName.TEST_EVENT_KEY_5:
+ return i18n.str`TEST_EVENT_KEY_5`;
+ case AmlEventsName.TEST_EVENT_KEY_6:
+ return i18n.str`TEST_EVENT_KEY_6`;
+ case AmlEventsName.TEST_EVENT_KEY_7:
+ return i18n.str`TEST_EVENT_KEY_7`;
+ case AmlEventsName.TEST_EVENT_KEY_8:
+ return i18n.str`TEST_EVENT_KEY_8`;
+ case AmlEventsName.TEST_EVENT_KEY_9:
+ return i18n.str`TEST_EVENT_KEY_9`;
+ case AmlEventsName.TEST_EVENT_KEY_10:
+ return i18n.str`TEST_EVENT_KEY_10`;
+ case AmlEventsName.TEST_EVENT_KEY_11:
+ return i18n.str`TEST_EVENT_KEY_11`;
+ case AmlEventsName.TEST_EVENT_KEY_12:
+ return i18n.str`TEST_EVENT_KEY_12`;
+ case AmlEventsName.TEST_EVENT_KEY_13:
+ return i18n.str`TEST_EVENT_KEY_13`;
+ case AmlEventsName.TEST_EVENT_KEY_14:
+ return i18n.str`TEST_EVENT_KEY_14`;
+ default: {
+ assertUnreachable(event);
}
}
- return result;
}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
index 32c7110b2..a059d755d 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
@@ -1,6 +1,27 @@
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ FormDesign,
+ FormUI,
+ InternationalizationAPI,
+ onComponentUnload,
+ UIHandlerId,
+ useForm,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+import {
+ AbsoluteTime,
+ Duration,
+ MeasureInformation,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import {
+ deserializeMeasures,
+ measureArrayField,
+ MeasurePath,
+ serializeMeasures,
+} from "./Measures.js";
+import { useServerMeasures } from "../../hooks/server-info.js";
/**
* Mark for further investigation and explain decision
@@ -9,6 +30,98 @@ import { useCurrentDecisionRequest } from
"../../hooks/decision-request.js";
*/
export function Justification({}: {}): VNode {
const { i18n } = useTranslationContext();
- const [request] = useCurrentDecisionRequest();
- return <div> not yet impltemented: justification and investigation</div>;
+ const [request, _, updateRequest] = useCurrentDecisionRequest();
+ const measures = useServerMeasures();
+ const measureList =
+ !measures || measures instanceof TalerError || measures.type === "fail"
+ ? []
+ : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
+ const design = formDesign(i18n, measureList);
+
+ const expMeasres: MeasurePath[] = !request.onExpire_measures
+ ? []
+ : deserializeMeasures(request.onExpire_measures);
+
+ const form = useForm<FormType>(design, {
+ investigate: request.keep_investigating,
+ justification: request.justification,
+ expiration: request.deadline,
+ measures: expMeasres,
+ });
+
+ onComponentUnload(() => {
+ updateRequest({
+ ...request,
+ keep_investigating: !!form.status.result.investigate,
+ justification: form.status.result.justification ?? "",
+ onExpire_measures: serializeMeasures(
+ (form.status.result.measures ?? []) as MeasurePath[],
+ ),
+
+ deadline:
+ (form.status.result.expiration as AbsoluteTime) ??
AbsoluteTime.never(),
+ // onExpire_measures,
+ });
+ });
+
+ return (
+ <div>
+ <FormUI design={design} handler={form.handler} />
+ </div>
+ );
}
+
+type FormType = {
+ justification: string;
+ investigate: boolean;
+ expiration: AbsoluteTime;
+ measures: MeasurePath[];
+};
+
+const formDesign = (
+ i18n: InternationalizationAPI,
+ mi: (MeasureInformation & { id: string })[],
+): FormDesign<FormType> => ({
+ type: "single-column",
+ fields: [
+ {
+ id: "justification" as UIHandlerId,
+ type: "textArea",
+ label: i18n.str`Justification`,
+ },
+ {
+ id: "investigate" as UIHandlerId,
+ type: "toggle",
+ label: i18n.str`Keep investigation?`,
+ },
+ {
+ type: "choiceHorizontal",
+ label: i18n.str`Expiration`,
+ id: "expiration" as UIHandlerId,
+ choices: [
+ {
+ label: i18n.str`In a week`,
+ value: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 7 }),
+ ) as any,
+ },
+ {
+ label: i18n.str`In a month`,
+ value: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ months: 1 }),
+ ) as any,
+ },
+ ],
+ },
+ {
+ id: "expiration" as UIHandlerId,
+ type: "absoluteTimeText",
+ placeholder: "dd/MM/yyyy",
+ pattern: "dd/MM/yyyy",
+ label: i18n.str`Expiration`,
+ },
+ measureArrayField(i18n, mi),
+ ],
+});
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
index 1ef4d8859..0969e733b 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -11,9 +11,9 @@ import {
RecursivePartial,
} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
-import { useServerMeasures } from "../../hooks/account.js";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { ShowMeasuresToSelect } from "../CaseDetails.js";
+import { useServerMeasures } from "../../hooks/server-info.js";
export function serializeMeasures(
paths?: RecursivePartial<MeasurePath[]>,
@@ -47,12 +47,12 @@ export function Measures({}: {}): VNode {
? []
: Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
- const initValue: MeasureForm = !request.new_measures
+ const initValue: FormType = !request.new_measures
? { paths: [] }
: { paths: deserializeMeasures(request.new_measures) };
- const design = measureForm(i18n, measureList);
- const form = useForm<MeasureForm>(design, initValue);
+ const design = formDesign(i18n, measureList);
+ const form = useForm<FormType>(design, initValue);
onComponentUnload(() => {
const r = !form.status.result.paths
@@ -77,9 +77,9 @@ export function Measures({}: {}): VNode {
);
}
-type MeasurePath = { steps: string[] };
+export type MeasurePath = { steps: string[] };
-type MeasureForm = {
+type FormType = {
paths: MeasurePath[];
};
@@ -110,10 +110,10 @@ export function measureArrayField(
};
}
-function measureForm(
+function formDesign(
i18n: InternationalizationAPI,
mi: (MeasureInformation & { id: string })[],
-): FormDesign<MeasureForm> {
+): FormDesign<FormType> {
return {
type: "single-column",
fields: [measureArrayField(i18n, mi)],
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
index ef685e857..d74b93cc8 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
@@ -13,6 +13,7 @@ import {
import { h, VNode } from "preact";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { usePreferences } from "../../hooks/preferences.js";
+import { AmlSpaDialect } from "@gnu-taler/taler-util";
/**
* Update account properites
@@ -115,7 +116,7 @@ export function propertiesByDialect(
): UIFormElementConfig[] {
if (!dialect) return [];
switch (dialect) {
- case "testing": {
+ case AmlSpaDialect.TESTING: {
return [
{
id: "ACCOUNT_PEP" satisfies keyof
TalerFormAttributes.AccountProperties_Testing as UIHandlerId,
@@ -124,9 +125,23 @@ export function propertiesByDialect(
type: "toggle",
required: true,
},
+ {
+ id: "ACCOUNT_BUSINESS_DOMAIN" satisfies keyof
TalerFormAttributes.AccountProperties_Testing as UIHandlerId,
+ label: i18n.str`Business domain`,
+ // gana_type: "Text",
+ type: "text",
+ required: true,
+ },
+ {
+ id: "ACCOUNT_FROZEN" satisfies keyof
TalerFormAttributes.AccountProperties_Testing as UIHandlerId,
+ label: i18n.str`Is frozen?`,
+ // gana_type: "Boolean",
+ type: "toggle",
+ required: true,
+ },
];
}
- case "gls": {
+ case AmlSpaDialect.GLS: {
return [
{
id: "ACCOUNT_REPORTED" satisfies keyof
TalerFormAttributes.AccountProperties_GLS as UIHandlerId,
@@ -137,7 +152,7 @@ export function propertiesByDialect(
},
];
}
- case "tops": {
+ case AmlSpaDialect.TOPS: {
return [
{
id: "ACCOUNT_FROZEN" satisfies keyof
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
index db1653e02..9bf5657d7 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -20,12 +20,11 @@ import {
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useServerMeasures } from "../../hooks/account.js";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { useAccountActiveDecision } from "../../hooks/decisions.js";
import { RulesInfo, ShowDecisionLimitInfo } from "../CaseDetails.js";
import { measureArrayField, serializeMeasures } from "./Measures.js";
+import { useServerMeasures } from "../../hooks/server-info.js";
/**
* Defined new limits for the account
@@ -55,6 +54,12 @@ export function Rules({ account }: { account?: string }):
VNode {
? undefined
: activeDecision.body;
+ onComponentUnload(() => {
+ if (!request.rules) {
+ updateRequest("rules", []);
+ }
+ });
+
function addNewRule(nr: FormType) {
const result = !request.rules ? [] : [...request.rules];
result.push({
@@ -132,13 +137,6 @@ type FormType = {
paths: { steps: Array<string> }[];
};
-// operation_type: LimitOperationType;
-// threshold: AmountString;
-// timeframe: RelativeTime;
-// measures: string[];
-// display_priority: Integer;
-// exposed?: boolean;
-// is_and_combinator?: boolean;
function labelForOperationType(
op: LimitOperationType,
i18n: InternationalizationAPI,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
similarity index 55%
copy from packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
copy to packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
index 32c7110b2..37fa9b493 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -1,4 +1,12 @@
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ FormDesign,
+ FormUI,
+ InternationalizationAPI,
+ onComponentUnload,
+ UIHandlerId,
+ useForm,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
@@ -7,8 +15,9 @@ import { useCurrentDecisionRequest } from
"../../hooks/decision-request.js";
* @param param0
* @returns
*/
-export function Justification({}: {}): VNode {
+export function Summary({}: {}): VNode {
const { i18n } = useTranslationContext();
const [request] = useCurrentDecisionRequest();
- return <div> not yet impltemented: justification and investigation</div>;
+
+ return <div>summary</div>;
}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts
b/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts
new file mode 100644
index 000000000..535e68a82
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts
@@ -0,0 +1,133 @@
+import { AmlSpaDialect } from "@gnu-taler/taler-util";
+import { TalerFormAttributes } from "@gnu-taler/web-util/browser";
+import { DecisionRequest } from "../../hooks/decision-request.js";
+
+export enum AmlEventsName {
+ ACCOUNT_FROZEN = "ACCOUNT_FROZEN",
+ ACCOUNT_PEP = "ACCOUNT_PEP",
+ TEST_EVENT_KEY_1 = "TEST_EVENT_KEY_1",
+ TEST_EVENT_KEY_2 = "TEST_EVENT_KEY_2",
+ TEST_EVENT_KEY_3 = "TEST_EVENT_KEY_3",
+ TEST_EVENT_KEY_4 = "TEST_EVENT_KEY_4",
+ TEST_EVENT_KEY_5 = "TEST_EVENT_KEY_5",
+ TEST_EVENT_KEY_6 = "TEST_EVENT_KEY_6",
+ TEST_EVENT_KEY_7 = "TEST_EVENT_KEY_7",
+ TEST_EVENT_KEY_8 = "TEST_EVENT_KEY_8",
+ TEST_EVENT_KEY_9 = "TEST_EVENT_KEY_9",
+ TEST_EVENT_KEY_10 = "TEST_EVENT_KEY_10",
+ TEST_EVENT_KEY_11 = "TEST_EVENT_KEY_11",
+ TEST_EVENT_KEY_12 = "TEST_EVENT_KEY_12",
+ TEST_EVENT_KEY_13 = "TEST_EVENT_KEY_13",
+ TEST_EVENT_KEY_14 = "TEST_EVENT_KEY_14",
+}
+
+export type EventMapInfo = {
+ [name in AmlEventsName]: {
+ // fieldLabel: TranslatedString;
+ dialect: AmlSpaDialect[];
+ shouldBeTriggered: (req: DecisionRequest, d: AmlSpaDialect) => boolean;
+ };
+};
+
+export const AML_EVENTS_INFO: EventMapInfo = {
+ ACCOUNT_FROZEN: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ if (!req.properties) return false;
+ return !!(req.properties as TalerFormAttributes.AccountProperties_TOPS)
+ .ACCOUNT_FROZEN;
+ },
+ },
+ ACCOUNT_PEP: {
+ dialect: [AmlSpaDialect.GLS, AmlSpaDialect.TOPS, AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ if (!req.properties) return false;
+ return !!(req.properties as TalerFormAttributes.AccountProperties_TOPS)
+ .ACCOUNT_PEP;
+ },
+ },
+ TEST_EVENT_KEY_1: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_2: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_3: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_4: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_5: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_6: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_7: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_8: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_9: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_10: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_11: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_12: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_13: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+ TEST_EVENT_KEY_14: {
+ dialect: [AmlSpaDialect.TESTING],
+ shouldBeTriggered(req, dialect) {
+ return true;
+ },
+ },
+};
diff --git a/packages/bank-ui/src/hooks/regional.ts
b/packages/bank-ui/src/hooks/regional.ts
index 417874cf8..d960a0f1f 100644
--- a/packages/bank-ui/src/hooks/regional.ts
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -42,10 +42,10 @@ const useSWR = _useSWR as unknown as SWRHook;
export type TransferCalculation =
| {
- debit: AmountJson;
- credit: AmountJson;
- beforeFee: AmountJson;
- }
+ debit: AmountJson;
+ credit: AmountJson;
+ beforeFee: AmountJson;
+ }
| "amount-is-too-small";
type EstimatorFunction = (
amount: AmountJson,
@@ -112,7 +112,9 @@ export function useCashinEstimator(): ConversionEstimators {
if (resp.detail) {
throw TalerError.fromUncheckedDetail(resp.detail);
} else {
- throw TalerError.fromException(new Error("failed to get
conversion cashin rate"))
+ throw TalerError.fromException(
+ new Error("failed to get conversion cashin rate"),
+ );
}
}
}
@@ -141,7 +143,9 @@ export function useCashinEstimator(): ConversionEstimators {
if (resp.detail) {
throw TalerError.fromUncheckedDetail(resp.detail);
} else {
- throw TalerError.fromException(new Error("failed to get
conversion cashin rate"))
+ throw TalerError.fromException(
+ new Error("failed to get conversion cashin rate"),
+ );
}
}
}
@@ -178,7 +182,9 @@ export function useCashoutEstimator(): ConversionEstimators
{
if (resp.detail) {
throw TalerError.fromUncheckedDetail(resp.detail);
} else {
- throw TalerError.fromException(new Error("failed to get
conversion cashout rate"))
+ throw TalerError.fromException(
+ new Error("failed to get conversion cashout rate"),
+ );
}
}
}
@@ -207,7 +213,9 @@ export function useCashoutEstimator(): ConversionEstimators
{
if (resp.detail) {
throw TalerError.fromUncheckedDetail(resp.detail);
} else {
- throw TalerError.fromException(new Error("failed to get
conversion cashout rate"))
+ throw TalerError.fromException(
+ new Error("failed to get conversion cashout rate"),
+ );
}
}
}
@@ -484,6 +492,7 @@ export function useLastMonitorInfo(
lib: { bank: api },
} = useBankCoreApiContext();
const { state: credentials } = useSessionState();
+
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx
b/packages/bank-ui/src/pages/admin/AdminHome.tsx
index 863886605..1f8b1ad18 100644
--- a/packages/bank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx
@@ -187,7 +187,7 @@ export function getTimeframesForDate(
sub(time, { days: 1 }).getTime(),
),
previous: AbsoluteTime.fromMilliseconds(
- sub(time, { days: 4 }).getTime(),
+ sub(time, { days: 2 }).getTime(),
),
};
case TalerCorebankApi.MonitorTimeframeParam.month:
@@ -241,11 +241,10 @@ function Metrics({
if (resp instanceof TalerError) {
return <ErrorLoadingWithDebug error={resp} />;
}
- if (!respInfo) return <Fragment />;
- if (respInfo instanceof TalerError) {
+ if (respInfo && respInfo instanceof TalerError) {
return <ErrorLoadingWithDebug error={respInfo} />;
}
- if (respInfo.type === "fail") {
+ if (respInfo && respInfo.type === "fail") {
switch (respInfo.case) {
case HttpStatusCode.NotImplemented: {
return (
@@ -483,7 +482,8 @@ function Metrics({
</h1>
</div>
<dl class="mt-5 grid grid-cols-1 md:grid-cols-2 divide-y
divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x
md:divide-y-0">
- {resp.current.body.type !== "with-conversions" ||
+ {!respInfo ||
+ resp.current.body.type !== "with-conversions" ||
resp.previous.body.type !== "with-conversions" ? undefined : (
<Fragment>
<div class="px-4 py-5 sm:p-6">
@@ -496,7 +496,7 @@ function Metrics({
</i18n.Translate>
</div>
</dt>
- <MetricValue
+ <MetricValueAmount
current={resp.current.body.cashinFiatVolume}
previous={resp.previous.body.cashinFiatVolume}
spec={respInfo.body.fiat_currency_specification}
@@ -512,7 +512,7 @@ function Metrics({
account.
</i18n.Translate>
</div>
- <MetricValue
+ <MetricValueAmount
current={resp.current.body.cashoutFiatVolume}
previous={resp.previous.body.cashoutFiatVolume}
spec={respInfo.body.fiat_currency_specification}
@@ -529,7 +529,7 @@ function Metrics({
</i18n.Translate>
</div>
</dt>
- <MetricValue
+ <MetricValueAmount
current={resp.current.body.talerInVolume}
previous={resp.previous.body.talerInVolume}
spec={config.currency_specification}
@@ -544,12 +544,40 @@ function Metrics({
</i18n.Translate>
</div>
</dt>
- <MetricValue
+ <MetricValueAmount
current={resp.current.body.talerOutVolume}
previous={resp.previous.body.talerOutVolume}
spec={config.currency_specification}
/>
</div>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Payin</i18n.Translate>
+ <div class="text-xs text-gray-500">
+ <i18n.Translate>
+ Transferred from an account to a Taler exchange.
+ </i18n.Translate>
+ </div>
+ </dt>
+ <MetricValueNumber
+ current={resp.current.body.talerInCount}
+ previous={resp.previous.body.talerInCount}
+ />
+ </div>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Payout</i18n.Translate>
+ <div class="text-xs text-gray-500">
+ <i18n.Translate>
+ Transferred from a Taler exchange to another account.
+ </i18n.Translate>
+ </div>
+ </dt>
+ <MetricValueNumber
+ current={resp.current.body.talerOutCount}
+ previous={resp.previous.body.talerOutCount}
+ />
+ </div>
</dl>
<div class="flex justify-end mt-4">
<a
@@ -564,7 +592,7 @@ function Metrics({
);
}
-function MetricValue({
+function MetricValueAmount({
current,
previous,
spec,
@@ -678,3 +706,93 @@ function MetricValue({
</Fragment>
);
}
+
+function MetricValueNumber({
+ current,
+ previous,
+}: {
+ current: number | undefined;
+ previous: number | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const cmp = current && previous ? (current < previous ? -1 : 1) : 0;
+
+ const rate =
+ !current || Number.isNaN(current) || !previous || Number.isNaN(previous)
+ ? 0
+ : cmp === -1
+ ? 1 - Math.round(current) / Math.round(previous)
+ : cmp === 1
+ ? Math.round(current) / Math.round(previous) - 1
+ : 0;
+
+ const negative = cmp === 0 ? undefined : cmp === -1;
+ const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`;
+ return (
+ <Fragment>
+ <dd class="mt-1 block ">
+ <div class="flex justify-start text-2xl items-baseline font-semibold
text-indigo-600">
+ {!current ? "-" : current}
+ </div>
+ <div class="flex flex-col">
+ <div class="flex justify-end items-baseline text-2xl font-semibold
text-indigo-600">
+ <small class="ml-2 text-sm font-medium text-gray-500">
+ <i18n.Translate>previous</i18n.Translate>{" "}
+ {!previous ? "-" : previous}
+ </small>
+ </div>
+ {!!rate && (
+ <span
+ data-negative={negative}
+ class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100
text-green-800 data-[negative=true]:bg-red-100 px-2 py-1 text-xs font-medium
data-[negative=true]:text-red-700 whitespace-pre"
+ >
+ {negative ? (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75"
+ />
+ </svg>
+ ) : (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75"
+ />
+ </svg>
+ )}
+
+ {negative ? (
+ <span class="sr-only">
+ <i18n.Translate>Decreased by</i18n.Translate>
+ </span>
+ ) : (
+ <span class="sr-only">
+ <i18n.Translate>Increased by</i18n.Translate>
+ </span>
+ )}
+ {rateStr}
+ </span>
+ )}
+ </div>
+ </dd>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-util/src/http-client/exchange.ts
b/packages/taler-util/src/http-client/exchange.ts
index 1075921dc..4e30882c7 100644
--- a/packages/taler-util/src/http-client/exchange.ts
+++ b/packages/taler-util/src/http-client/exchange.ts
@@ -81,6 +81,7 @@ import { TalerError } from "../errors.js";
import { TalerErrorCode } from "../taler-error-codes.js";
import { codecForEmptyObject } from "../types-taler-wallet.js";
import { canonicalJson } from "../helpers.js";
+import { AbsoluteTime } from "../time.js";
export type TalerExchangeResultByMethod<
prop extends keyof TalerExchangeHttpClient,
@@ -861,17 +862,17 @@ export class TalerExchangeHttpClient {
auth: OfficerAccount,
name: string,
filter: {
- since?: Date;
- until?: Date;
+ since?: AbsoluteTime;
+ until?: AbsoluteTime;
} = {},
) {
const url = new URL(`aml/${auth.id}/kyc-statistics/${name}`, this.baseUrl);
- if (filter.since !== undefined) {
- url.searchParams.set("start_date", String(filter.since.getTime()));
+ if (filter.since !== undefined && filter.since.t_ms !== "never") {
+ url.searchParams.set("start_date", String(filter.since.t_ms));
}
- if (filter.until !== undefined) {
- url.searchParams.set("end_date", String(filter.until.getTime()));
+ if (filter.until !== undefined && filter.until.t_ms !== "never") {
+ url.searchParams.set("end_date", String(filter.until.t_ms));
}
const resp = await this.httpLib.fetch(url.href, {
diff --git a/packages/taler-util/src/types-taler-exchange.ts
b/packages/taler-util/src/types-taler-exchange.ts
index c8d0c925a..f78c71e04 100644
--- a/packages/taler-util/src/types-taler-exchange.ts
+++ b/packages/taler-util/src/types-taler-exchange.ts
@@ -1610,6 +1610,12 @@ export type AmlDecisionRequestWithoutSignature = Omit<
"officer_sig"
>;
+export enum AmlSpaDialect {
+ TOPS = "tops",
+ GLS = "gls",
+ TESTING = "testing",
+}
+
export interface ExchangeVersionResponse {
// libtool-style representation of the Exchange protocol version, see
//
https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
@@ -1638,7 +1644,7 @@ export interface ExchangeVersionResponse {
// to show and sets of properties/events to trigger in
// AML decisions.
// @since protocol **v24**.
- aml_spa_dialect?: string;
+ aml_spa_dialect?: AmlSpaDialect;
}
export interface WireAccount {
@@ -1780,16 +1786,6 @@ export interface AccountKycStatus {
limits?: AccountLimit[];
}
-export type LimitOperationType2 =
- | "WITHDRAW"
- | "DEPOSIT"
- | "MERGE"
- | "AGGREGATE"
- | "BALANCE"
- | "REFUND"
- | "CLOSE"
- | "TRANSACTION";
-
export enum LimitOperationType {
withdraw = "WITHDRAW",
deposit = "DEPOSIT",
@@ -2446,6 +2442,12 @@ interface CSDenominationKey {
cs_public_key: Cs25519Point;
}
+export const codecForAmlSpaDialect = codecForEither(
+ codecForConstString(AmlSpaDialect.GLS),
+ codecForConstString(AmlSpaDialect.TOPS),
+ codecForConstString(AmlSpaDialect.TESTING),
+);
+
export const codecForExchangeConfig = (): Codec<ExchangeVersionResponse> =>
buildCodecForObject<ExchangeVersionResponse>()
.property("version", codecForString())
@@ -2454,7 +2456,7 @@ export const codecForExchangeConfig = ():
Codec<ExchangeVersionResponse> =>
.property("currency", codecForString())
.property("currency_specification", codecForCurrencySpecificiation())
.property("supported_kyc_requirements", codecForList(codecForString()))
- .property("aml_spa_dialect", codecOptional(codecForString()))
+ .property("aml_spa_dialect", codecOptional(codecForAmlSpaDialect))
.deprecatedProperty("shopping_url")
.deprecatedProperty("wallet_balance_limit_without_kyc")
.build("TalerExchangeApi.ExchangeVersionResponse");
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.