From df64119729e9d55430c3b9133d660c431070f67b Mon Sep 17 00:00:00 2001 From: Nic Eigel Date: Fri, 26 Apr 2024 13:10:29 +0200 Subject: incomplete changes to be more like merchant backend --- packages/auditor-backoffice-ui/src/Application.tsx | 170 ++++++++- .../src/ApplicationReadyRoutes.tsx | 155 ++++++++ .../auditor-backoffice-ui/src/InstanceRoutes.tsx | 121 ++++++ .../src/components/exception/loading.tsx | 48 +++ .../src/components/menu/NavigationBar.tsx | 73 ++++ .../src/components/menu/index.tsx | 123 ++++-- .../auditor-backoffice-ui/src/context/backend.ts | 68 ++++ .../auditor-backoffice-ui/src/context/config.ts | 29 ++ .../auditor-backoffice-ui/src/context/instance.ts | 28 ++ .../auditor-backoffice-ui/src/declaration.d.ts | 208 +++++++++- .../auditor-backoffice-ui/src/hooks/backend.ts | 267 ++++++++++++- packages/auditor-backoffice-ui/src/hooks/config.ts | 24 ++ .../src/hooks/deposit-confirmation.ts | 211 +++++++++++ packages/auditor-backoffice-ui/src/hooks/index.ts | 79 ++++ .../deposit_confirmations/list/List.stories.tsx | 107 ++++++ .../paths/deposit_confirmations/list/ListPage.tsx | 226 +++++++++++ .../src/paths/deposit_confirmations/list/Table.tsx | 417 +++++++++++++++++++++ .../src/paths/deposit_confirmations/list/index.tsx | 224 +++++++++++ .../merchant-backoffice-ui/src/Application.tsx | 2 +- 19 files changed, 2531 insertions(+), 49 deletions(-) create mode 100644 packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx create mode 100644 packages/auditor-backoffice-ui/src/InstanceRoutes.tsx create mode 100644 packages/auditor-backoffice-ui/src/components/exception/loading.tsx create mode 100644 packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx create mode 100644 packages/auditor-backoffice-ui/src/context/backend.ts create mode 100644 packages/auditor-backoffice-ui/src/context/config.ts create mode 100644 packages/auditor-backoffice-ui/src/context/instance.ts create mode 100644 packages/auditor-backoffice-ui/src/hooks/config.ts create mode 100644 packages/auditor-backoffice-ui/src/hooks/deposit-confirmation.ts create mode 100644 packages/auditor-backoffice-ui/src/hooks/index.ts create mode 100644 packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx create mode 100644 packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx create mode 100644 packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx create mode 100644 packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx diff --git a/packages/auditor-backoffice-ui/src/Application.tsx b/packages/auditor-backoffice-ui/src/Application.tsx index 36059fe1a..e3ea17703 100644 --- a/packages/auditor-backoffice-ui/src/Application.tsx +++ b/packages/auditor-backoffice-ui/src/Application.tsx @@ -16,7 +16,26 @@ import {Fragment, VNode, h, render} from "preact"; import "./scss/main.scss"; -import { AuditorBackend } from "./declaration.js" +import {AuditorBackend} from "./declaration.js" +import { + ErrorType, + TranslationProvider, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import {strings} from "./i18n/strings.js"; +import {useMemo} from "preact/hooks"; +import {HttpStatusCode, LibtoolVersion} from "@gnu-taler/taler-util"; +import { + NotConnectedAppMenu, + NotificationCard +} from "./components/menu/index.js"; +import { + BackendContextProvider +} from "./context/backend.js"; +import {ConfigContextProvider} from "./context/config.js"; +import {Loading} from "./components/exception/loading.js"; +import {ApplicationReadyRoutes} from "./ApplicationReadyRoutes.js"; +import {useBackendConfig} from "./hooks/backend.js"; /** * @author Nic Eigel @@ -25,11 +44,150 @@ import { AuditorBackend } from "./declaration.js" export function Application(): VNode { return ( - + + + + + + ); +} + + +/** + * Check connection testing against /config + * + * @returns + */ +function ApplicationStatusRoutes(): VNode { + const result = useBackendConfig(); + const {i18n} = useTranslationContext(); + + const configData = result.ok && result.data + ? result.data + : undefined; + const ctx = useMemo(() => (configData), [configData]); + + if (!result.ok) { + if (result.loading) return ; + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) { + return ( + + + + + ); + } + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) { + return ( + + + + + ); + } + if (result.type === ErrorType.SERVER) { + + + + ; + } + if (result.type === ErrorType.UNREADABLE) { + + + + ; + } + return ( + + + + + ); + } + + const SUPPORTED_VERSION = "1:0:1" + if (result.data && !LibtoolVersion.compare( + SUPPORTED_VERSION, + result.data.version, + )?.compatible) { + return + + + + } + + return ( +
+ + + +
); } +/* + + + + + + + + + + + + + + + + + + + export class Configer { name?: string version?: string @@ -91,8 +249,8 @@ api('http://localhost:8083/config') teste() }) .catch(error => { - /* show error message */ - }) + /* show error message *//* +}) function MyComponent() { @@ -271,4 +429,6 @@ export function NavigationBar({title}: Props): VNode { ); -} \ No newline at end of file +} + +*/ \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx new file mode 100644 index 000000000..2837e8d27 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx @@ -0,0 +1,155 @@ +/* + This file is part of GNU Taler + (C) 2021-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 + */ + +/** + * + * @author Nic Eigel + */ +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { createHashHistory } from "history"; +import { Fragment, VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useEffect, useErrorBoundary, useState } from "preact/hooks"; +import { InstanceRoutes } from "./InstanceRoutes.js"; +import { + NotConnectedAppMenu, + NotificationCard, +} from "./components/menu/index.js"; +import { useBackendContext } from "./context/backend.js"; +import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; + +/** + * Check if admin against /management/instances + * @returns + */ +export function ApplicationReadyRoutes(): VNode { + const { i18n } = useTranslationContext(); + const [unauthorized, setUnauthorized] = useState(false) + const { + url: backendURL, + /*updateToken, + alreadyTriedLogin,*/ + } = useBackendContext(); + + /*function updateLoginStatus(token: LoginToken | undefined) { + updateToken(token) + setUnauthorized(false) + } + const result = useBackendInstancesTestForAdmin(); + + const clearTokenAndGoToRoot = () => { + route("/"); + };*/ + const [showSettings, setShowSettings] = useState(false) + /* const unauthorizedAdmin = !result.loading + && !result.ok + && result.type === ErrorType.CLIENT + && result.status === HttpStatusCode.Unauthorized; + + if (!alreadyTriedLogin && !result.ok) { + return ( + + + + + ); + } + + if (showSettings) { + return + setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + setShowSettings(false)} /> + + } + + if (result.loading) { + return setShowSettings(true)} title="Loading..." isPasswordOk={false} />; + } + + let admin = result.ok || unauthorizedAdmin; + let instanceNameByBackendURL: string | undefined; +*/ +// if (!admin) { +// // * the testing against admin endpoint failed and it's not +// // an authorization problem +// // * merchant backend will return this SPA under the main +// // endpoint or /instance/ endpoint +// // => trying to infer the instance id +// const path = new URL(backendURL).pathname; +// const match = INSTANCE_ID_LOOKUP.exec(path); +// if (!match || !match[1]) { +// // this should be rare because +// // query to /config is ok but the URL +// // does not match our pattern +// return ( +// +// setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> +// +// {/* */} +// +// ); +// } +// +// instanceNameByBackendURL = match[1]; +// } +// +// if (unauthorized || unauthorizedAdmin) { +// return +// setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> +// +// +// +// } +// + const history = createHashHistory(); + return ( + + + + ); +} + +function DefaultMainRoute({ + instance, + url, //from preact-router + }: any): VNode { + const [instanceName, setInstanceName] = useState( + "default", + ); + + return ( + + ); +} diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx new file mode 100644 index 000000000..e6ea1569a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx @@ -0,0 +1,121 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { TranslatedString } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { Fragment, FunctionComponent, VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useEffect, useErrorBoundary, useMemo, useState } from "preact/hooks"; +import { Loading } from "./components/exception/loading.js"; +import { Menu, NotificationCard } from "./components/menu/index.js"; +import { AuditorBackend } from "./declaration.js"; +import { + useSimpleLocalStorage, +} from "./hooks/index.js"; +import DepositConfirmationList from "./paths/deposit_confirmations/list/index.js"; +import { Notification } from "./utils/types.js"; +import { InstanceContextProvider } from "./context/instance.js"; + +export enum InstancePaths { + error = "/error", + + deposit_confirmation_list = "/deposit-confirmation", + deposit_confirmation_update = "/deposit-confirmation/:rowid/update", +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => { }; + +export interface Props { + id: string; + path: string; +} + +export function InstanceRoutes({ + id, + path, + }: Props): VNode { + const { i18n } = useTranslationContext(); + + type GlobalNotifState = (Notification & { to: string | undefined }) | undefined; + const [globalNotification, setGlobalNotification] = + useState(undefined); + + const [error] = useErrorBoundary(); + const value = useMemo( + () => ({ id }), + [id], + ); + + return ( + + + + {error && + + {(error instanceof Error ? error.stack : String(error)) as TranslatedString} + + }} /> + } + + { + const movingOutFromNotification = + globalNotification && e.url !== globalNotification.to; + if (movingOutFromNotification) { + setGlobalNotification(undefined); + } + }} + > + + {/** + * Deposit confirmation pages + */} + + {/** + * Example pages + */} + + + + ); +} + +export function Redirect({ to }: { to: string }): null { + useEffect(() => { + route(to, true); + }); + return null; +} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/components/exception/loading.tsx b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx new file mode 100644 index 000000000..11b62c124 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx @@ -0,0 +1,48 @@ +/* + This file is part of GNU Taler + (C) 2021-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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; + +export function Loading(): VNode { + return ( +
+ +
+ ); +} + +export function Spinner(): VNode { + return ( +
+
+
+
+
+
+ ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx new file mode 100644 index 000000000..21561087e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx @@ -0,0 +1,73 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +// @ts-ignore +import logo from "../../assets/logo-2021.svg"; + +interface Props { + onMobileMenu: () => void; + title: string; +} + +export function NavigationBar({ onMobileMenu, title }: Props): VNode { + return ( + + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/index.tsx b/packages/auditor-backoffice-ui/src/components/menu/index.tsx index 4c02797ab..717e22894 100644 --- a/packages/auditor-backoffice-ui/src/components/menu/index.tsx +++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx @@ -16,13 +16,88 @@ GNU Taler; see the file COPYING. If not, see */ +import {ComponentChildren, Fragment, h, VNode} from "preact"; +import {Notification} from "../../utils/types.js"; +import {useEffect, useState} from "preact/hooks"; +import {NavigationBar} from "./NavigationBar.js"; +import {InstancePaths} from "../../InstanceRoutes.js"; + /** * @author Nic Eigel * */ -/* -import {ComponentChildren, Fragment, h, VNode} from "preact"; -import {useEffect} from "preact/hooks"; + +function getInstanceTitle(path: string, id: string): string { + switch (path) { + case InstancePaths.deposit_confirmation_list: + return `${id}: Deposit confirmations`; + case InstancePaths.deposit_confirmation_update: + return `${id}: Update deposit confirmation`; + default: + return ""; + } +} + +interface NotifProps { + notification?: Notification; +} + +export function NotificationCard({notification: n}: NotifProps): VNode | null { + if (!n) return null; + return ( +
+
+
+
+
+

{n.message}

+
+ {n.description && ( +
+
{n.description}
+ {n.details &&
{n.details}
} +
+ )} +
+
+
+
+ ); +} + +interface NotConnectedAppMenuProps { + title: string; +} + +export function NotConnectedAppMenu({ + title, + }: NotConnectedAppMenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + + return ( +
setMobileOpen(false)} + > + setMobileOpen(!mobileOpen)} + title={title} + /> +
+ ); +} function WithTitle({ title, @@ -37,25 +112,19 @@ function WithTitle({ return {children}; } +interface MenuProps { + title?: string; + path: string; + instance: string; +} + export function Menu({ - onLogout, - onShowSettings, title, instance, path, - admin, - setInstanceName, - isPasswordOk }: MenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); - - const titleWithSubtitle = title - ? title - : !admin - ? getInstanceTitle(path, instance) - : getAdminTitle(path, instance); - const adminInstance = instance === "default"; - const mimic = admin && !adminInstance; + const titleWithSubtitle = getInstanceTitle(path, instance); return (
- {onLogout && ( - - )} - - {mimic && ( + + {/*mimic && ( - )} + )*/}
); -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/context/backend.ts b/packages/auditor-backoffice-ui/src/context/backend.ts new file mode 100644 index 000000000..1f95dc9ba --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/backend.ts @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021-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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { createContext, h, VNode } from "preact"; +import { useContext } from "preact/hooks"; +import { useBackendURL } from "../hooks/index.js"; + +//TODO: add token +interface BackendContextType { + url: string, + /*alreadyTriedLogin: boolean; + token?: LoginToken; + updateToken: (token: LoginToken | undefined) => void;*/ +} + +function useBackendContextState( + defaultUrl?: string, +): BackendContextType { + const [url] = useBackendURL(defaultUrl); + //const [token, updateToken] = useBackendDefaultToken(); + + return { + url, + /* token, + alreadyTriedLogin: token !== undefined, + updateToken,*/ + }; +} + +export const BackendContextProvider = ({ + children, + defaultUrl, + }: { + children: any; + defaultUrl?: string; +}): VNode => { + const value = useBackendContextState(defaultUrl); + + return h(BackendContext.Provider, { value, children }); +}; + +const BackendContext = createContext({ + url: "", + /*alreadyTriedLogin: false, + token: undefined, + updateToken: () => null,*/ +}); + +export const useBackendContext = (): BackendContextType => + useContext(BackendContext); \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/context/config.ts b/packages/auditor-backoffice-ui/src/context/config.ts new file mode 100644 index 000000000..58ee5a594 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/config.ts @@ -0,0 +1,29 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; +import { AuditorBackend } from "../declaration.js"; + +const Context = createContext(null!); + +export const ConfigContextProvider = Context.Provider; +export const useConfigContext = (): AuditorBackend.VersionResponse => useContext(Context); diff --git a/packages/auditor-backoffice-ui/src/context/instance.ts b/packages/auditor-backoffice-ui/src/context/instance.ts new file mode 100644 index 000000000..55bf4dd8c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/instance.ts @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; + +interface Type { + id: string; +} + +const Context = createContext({} as any); + +export const InstanceContextProvider = Context.Provider; +export const useInstanceContext = (): Type => useContext(Context); diff --git a/packages/auditor-backoffice-ui/src/declaration.d.ts b/packages/auditor-backoffice-ui/src/declaration.d.ts index d5301c115..c4fbc4474 100644 --- a/packages/auditor-backoffice-ui/src/declaration.d.ts +++ b/packages/auditor-backoffice-ui/src/declaration.d.ts @@ -16,24 +16,96 @@ /** * + * @author Sebastian Javier Marchano (sebasjm) * @author Nic Eigel */ +type HashCode = string; +type EddsaPublicKey = string; +type EddsaSignature = string; +type WireTransferIdentifierRawP = string; +type RelativeTime = TalerProtocolDuration; +type ImageDataUrl = string; +type AuditorUserType = "business" | "individual"; + + +export interface WithId { + id: string; +} + +interface Timestamp { + // Milliseconds since epoch, or the special + // value "forever" to represent an event that will + // never happen. + t_s: number | "never"; +} +interface TalerProtocolDuration { + d_us: number | "forever"; +} +interface Duration { + d_ms: number | "forever"; +} + +interface WithId { + id: string; +} + +type Amount = string; +type UUID = string; +type Integer = number; + export namespace AuditorBackend { interface DepositConfirmation { // identifier - serial_id: number; + deposit_confirmation_serial_id: number; + + //TODO Comment + h_contract_terms: string; + + //TODO Comment + h_policy: string; + + //TODO Comment + h_wire: string; + + //TODO Comment + exchange_timestamp: string; + + //TODO Comment + refund_deadline: string; + + //TODO Comment + wire_deadline: string; + + //TODO Comment + total_without_fee: string; + + //TODO Comment + coin_pubs: string; - // amount of deposit confirmation - amount: string; + //TODO Comment + coin_sigs: string; - // timestamp of deposit confirmation - timestamp: string; + //TODO Comment + merchant_pub: string; - // account - account: string; + //TODO Comment + merchant_sig: string; + + //TODO Comment + exchange_pub: string; + + //TODO Comment + exchange_sig: string; + + //TODO Comment + suppressed: string; + + //TODO Comment + ancient: string; } + //TODO rename interface Config { name: string; version: string; @@ -42,4 +114,126 @@ export namespace AuditorBackend { auditor_public_key: string; exchange_master_public_key: string; } + + interface ErrorDetail { + // Numeric error code unique to the condition. + // The other arguments are specific to the error value reported here. + code: number; + + // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... + // Should give a human-readable hint about the error's nature. Optional, may change without notice! + hint?: string; + + // Optional detail about the specific input value that failed. May change without notice! + detail?: string; + + // Name of the parameter that was bogus (if applicable). + parameter?: string; + + // Path to the argument that was bogus (if applicable). + path?: string; + + // Offset of the argument that was bogus (if applicable). + offset?: string; + + // Index of the argument that was bogus (if applicable). + index?: string; + + // Name of the object that was bogus (if applicable). + object?: string; + + // Name of the currency than was problematic (if applicable). + currency?: string; + + // Expected type (if applicable). + type_expected?: string; + + // Type that was provided instead (if applicable). + type_actual?: string; + } + + interface VersionResponse { + // libtool-style representation of the Merchant protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Name of the protocol. + name: "taler-merchant"; + + // Default (!) currency supported by this backend. + // This is the currency that the backend should + // suggest by default to the user when entering + // amounts. See currencies for a list of + // supported currencies and how to render them. + currency: string; + + // How services should render currencies supported + // by this backend. Maps + // currency codes (e.g. "EUR" or "KUDOS") to + // the respective currency specification. + // All currencies in this map are supported by + // the backend. Note that the actual currency + // specifications are a *hint* for applications + // that would like *advice* on how to render amounts. + // Applications *may* ignore the currency specification + // if they know how to render currencies that they are + // used with. + currencies: { currency: CurrencySpecification }; + + // Array of exchanges trusted by the merchant. + // Since protocol v6. + exchanges: ExchangeConfigInfo[]; + } +} + +namespace DepositConfirmations { + interface DepositConfirmationDetail { + deposit_confirmation_serial_id: number; + + //TODO Comment + h_contract_terms: string; + + //TODO Comment + h_policy: string; + + //TODO Comment + h_wire: string; + + //TODO Comment + exchange_timestamp: string; + + //TODO Comment + refund_deadline: string; + + //TODO Comment + wire_deadline: string; + + //TODO Comment + total_without_fee: string; + + //TODO Comment + coin_pubs: string; + + //TODO Comment + coin_sigs: string; + + //TODO Comment + merchant_pub: string; + + //TODO Comment + merchant_sig: string; + + //TODO Comment + exchange_pub: string; + + //TODO Comment + exchange_sig: string; + + //TODO Comment + suppressed: string; + + //TODO Comment + ancient: string; + } } \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts index 7f293162f..d60446ab3 100644 --- a/packages/auditor-backoffice-ui/src/hooks/backend.ts +++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts @@ -1,7 +1,41 @@ -//import { HttpResponse, RequestError } from "@gnu-taler/web-util/lib/utils/request.js"; -import { AuditorBackend } from "../declaration.js"; -import Config = AuditorBackend.Config; +/* + This file is part of GNU Taler + (C) 2021-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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel + */ + +import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + HttpResponse, + HttpResponseOk, + RequestError, + RequestOptions, + useApiContext, +} from "@gnu-taler/web-util/browser"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useSWRConfig } from "swr"; +import { useBackendContext } from "../context/backend.js"; +import { useInstanceContext } from "../context/instance.js"; +import { AuditorBackend, Timestamp } from "../declaration.js"; +/* export function tryConfig(): Promise { // const request: RequestInfo = new Request('./Config.json', { // method: 'GET', @@ -17,5 +51,232 @@ export function tryConfig(): Promise { return res as Config; }); +}*/ + + +export function useMatchMutate(): ( + re?: RegExp, + value?: unknown, +) => Promise { + const { cache, mutate } = useSWRConfig(); + + if (!(cache instanceof Map)) { + throw new Error( + "matchMutate requires the cache provider to be a Map instance", + ); + } + + return function matchRegexMutate(re?: RegExp) { + return mutate((key) => { + // evict if no key or regex === all + if (!key || !re) return true + // match string + if (typeof key === 'string' && re.test(key)) return true + // record or object have the path at [0] + if (typeof key === 'object' && re.test(key[0])) return true + //key didn't match regex + return false + }, undefined, { + revalidate: true, + }); + }; } + + + + + + + + + + +type YesOrNo = "yes" | "no"; + +interface useBackendInstanceRequestType { + request: ( + endpoint: string, + options?: RequestOptions, + ) => Promise>; + depositConfirmationFetcher: ( + params: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] + ) => Promise>; +} +interface useBackendBaseRequestType { + request: ( + endpoint: string, + options?: RequestOptions, + ) => Promise>; +} + +/** + * + * @param root the request is intended to the base URL and no the instance URL + * @returns request handler to + */ +//TODO: Add token +export function useBackendBaseRequest(): useBackendBaseRequestType { + const { url: backend} = useBackendContext(); + const { request: requestHandler } = useApiContext(); + + const request = useCallback( + function requestImpl( + endpoint: string, + options: RequestOptions = {}, + ): Promise> { + return requestHandler(backend, endpoint, { ...options }).then(res => { + return res + }).catch(err => { + throw err + }); + }, + [backend], + ); + + return { request }; +} + +const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; +const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + +export function useBackendConfig(): HttpResponse< + AuditorBackend.VersionResponse | undefined, + RequestError +> { + const { request } = useBackendBaseRequest(); + + type Type = AuditorBackend.VersionResponse; + type State = { data: HttpResponse>, timer: number } + const [result, setResult] = useState({ data: { loading: true }, timer: 0 }); + + useEffect(() => { + if (result.timer) { + clearTimeout(result.timer) + } + function tryConfig(): void { + //TODO change back to /config + request(`http://localhost:8083/config'`) + .then((data) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_OK) + setResult({ data, timer }) + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_FAIL) + const data = error.cause + setResult({ data, timer }) + }); + } + tryConfig() + }, [request]); + + return result.data; +} + + + + + + + + + + + + + + + + + + + + + +export function useBackendInstanceRequest(): useBackendInstanceRequestType { + const { url: rootBackendUrl} = useBackendContext(); + const { id } = useInstanceContext(); + const { request: requestHandler } = useApiContext(); + + const baseUrl = rootBackendUrl; + + + const request = useCallback( + function requestImpl( + endpoint: string, + options: RequestOptions = {}, + ): Promise> { + return requestHandler(baseUrl, endpoint, { ...options }); + }, + [baseUrl], + ); + + const multiFetcher = useCallback( + function multiFetcherImpl( + args: [endpoints: string[]], + ): Promise[]> { + const [endpoints] = args + return Promise.all( + endpoints.map((endpoint) => + requestHandler(baseUrl, endpoint, ), + ), + ); + }, + [baseUrl], + ); + + const fetcher = useCallback( + function fetcherImpl(endpoint: string): Promise> { + return requestHandler(baseUrl, endpoint ); + }, + [baseUrl], + ); + + const depositConfirmationFetcher = useCallback( + function orderFetcherImpl( + args: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] + ): Promise> { + const [endpoint, paid, refunded, wired, searchDate, delta] = args + const date_s = + delta && delta < 0 && searchDate + ? Math.floor(searchDate.getTime() / 1000) + 1 + : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined; + const params: any = {}; + if (paid !== undefined) params.paid = paid; + if (delta !== undefined) params.delta = delta; + if (refunded !== undefined) params.refunded = refunded; + if (wired !== undefined) params.wired = wired; + if (date_s !== undefined) params.date_s = date_s; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { orders: [] } as T, + }) + } + return requestHandler(baseUrl, endpoint, { params }); + }, + [baseUrl], + ); + + + + return { + request, + depositConfirmationFetcher, + };} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/config.ts b/packages/auditor-backoffice-ui/src/hooks/config.ts new file mode 100644 index 000000000..a57fa15d5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/config.ts @@ -0,0 +1,24 @@ +/* + This file is part of GNU Taler + (C) 2021-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 + */ + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; +import { AuditorBackend } from "../declaration.js"; + +const Context = createContext(null!); + +export const ConfigContextProvider = Context.Provider; +export const useConfigContext = (): AuditorBackend.VersionResponse => useContext(Context); diff --git a/packages/auditor-backoffice-ui/src/hooks/deposit-confirmation.ts b/packages/auditor-backoffice-ui/src/hooks/deposit-confirmation.ts new file mode 100644 index 000000000..e45150d0b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/deposit-confirmation.ts @@ -0,0 +1,211 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { AuditorBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +export interface DepositConfirmationAPI { + deleteDepositConfirmation: (id: string) => Promise>; +} + +type YesOrNo = "yes" | "no"; + +export function useDepositConfirmationAPI(): DepositConfirmationAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const deleteDepositConfirmation = async ( + depositConfirmationSerialId: string, + ): Promise> => { + mutateAll(/@"\/deposit-confirmation"@/); + const res = request(`/deposit-confirmation/${depositConfirmationSerialId}`, { + method: "DELETE", + }); + await mutateAll(/.*deposit-confirmation.*/); + return res; + }; + + + return { deleteDepositConfirmation }; +} + +/*export function useOrderDetails( + oderId: string, +): HttpResponse< + MerchantBackend.Orders.MerchantOrderStatusResponse, + AuditorBackend.ErrorDetail +> { + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk, + RequestError + >([`/private/orders/${oderId}`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} +*/ +export interface InstanceOrderFilter { + suppressed?: YesOrNo; + suppressDate?: Date; +} +/* +export function useInstanceOrders( + args?: InstanceOrderFilter, + updateFilter?: (d: Date) => void, +): HttpResponsePaginated< + MerchantBackend.Orders.OrderHistory, + MerchantBackend.ErrorDetail +> { + const { orderFetcher } = useBackendInstanceRequest(); + + const [pageBefore, setPageBefore] = useState(1); + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0; + + /** + * FIXME: this can be cleaned up a little + * + * the logic of double query should be inside the orderFetch so from the hook perspective and cache + * is just one query and one error status + *//* + const { + data: beforeData, + error: beforeError, + isValidating: loadingBefore, + } = useSWR< + HttpResponseOk, + RequestError + >( + [ + `/private/orders`, + args?.paid, + args?.refunded, + args?.wired, + args?.date, + totalBefore, + ], + orderFetcher, + ); + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk, + RequestError + >( + [ + `/private/orders`, + args?.paid, + args?.refunded, + args?.wired, + args?.date, + -totalAfter, + ], + orderFetcher, + ); + + //this will save last result + const [lastBefore, setLastBefore] = useState< + HttpResponse< + MerchantBackend.Orders.OrderHistory, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.Orders.OrderHistory, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + if (beforeData) setLastBefore(beforeData); + }, [afterData, beforeData]); + + if (beforeError) return beforeError.cause; + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = afterData && afterData.data.orders.length < totalAfter; + const isReachingStart = + args?.date === undefined || + (beforeData && beforeData.data.orders.length < totalBefore); + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.orders.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = + afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s; + if (from && from !== "never" && updateFilter) + updateFilter(new Date(from * 1000)); + } + }, + loadMorePrev: () => { + if (!beforeData || isReachingStart) return; + if (beforeData.data.orders.length < MAX_RESULT_SIZE) { + setPageBefore(pageBefore + 1); + } else if (beforeData) { + const from = + beforeData.data.orders[beforeData.data.orders.length - 1].timestamp + .t_s; + if (from && from !== "never" && updateFilter) + updateFilter(new Date(from * 1000)); + } + }, + }; + + const orders = + !beforeData || !afterData + ? [] + : (beforeData || lastBefore).data.orders + .slice() + .reverse() + .concat((afterData || lastAfter).data.orders); + if (loadingAfter || loadingBefore) return { loading: true, data: { orders } }; + if (beforeData && afterData) { + return { ok: true, data: { orders }, ...pagination }; + } + return { loading: true }; +}*/ diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts new file mode 100644 index 000000000..cf1c57771 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/index.ts @@ -0,0 +1,79 @@ +/* + This file is part of GNU Taler + (C) 2021-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 + */ + +import {StateUpdater, useState} from "preact/hooks"; +import { ValueOrFunction } from "../utils/types.js"; + +export function useBackendURL( + url?: string, +): [string, StateUpdater] { + const [value, setter] = useSimpleLocalStorage( + "auditor-base-url", + url || calculateRootPath(), + ); + + const checkedSetter = (v: ValueOrFunction) => { + return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); + }; + + return [value!, checkedSetter]; +} + +const calculateRootPath = () => { + const rootPath = + typeof window !== undefined + ? window.location.origin + window.location.pathname + : "/"; + + /** + * By default, auditor backend serves the html content + * from the /webui root. This should cover most of the + * cases and the rootPath will be the auditor backend + * URL where the instances are + */ + return rootPath.replace("/webui/", ""); +}; + +export function useSimpleLocalStorage( + key: string, + initialValue?: string, +): [string | undefined, StateUpdater] { + const [storedValue, setStoredValue] = useState( + (): string | undefined => { + return typeof window !== "undefined" + ? window.localStorage.getItem(key) || initialValue + : initialValue; + }, + ); + + const setValue = ( + value?: string | ((val?: string) => string | undefined), + ) => { + setStoredValue((p) => { + const toStore = value instanceof Function ? value(p) : value; + if (typeof window !== "undefined") { + if (!toStore) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, toStore); + } + } + return toStore; + }); + }; + + return [storedValue, setValue]; +} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx new file mode 100644 index 000000000..156c577f4 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx @@ -0,0 +1,107 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Order/List", + component: TestedComponent, + argTypes: { + onShowAll: { action: "onShowAll" }, + onShowPaid: { action: "onShowPaid" }, + onShowRefunded: { action: "onShowRefunded" }, + onShowNotWired: { action: "onShowNotWired" }, + onCopyURL: { action: "onCopyURL" }, + onSelectDate: { action: "onSelectDate" }, + onLoadMoreBefore: { action: "onLoadMoreBefore" }, + onLoadMoreAfter: { action: "onLoadMoreAfter" }, + onSelectOrder: { action: "onSelectOrder" }, + onRefundOrder: { action: "onRefundOrder" }, + onSearchOrderById: { action: "onSearchOrderById" }, + onCreate: { action: "onCreate" }, + }, +}; + +function createExample( + Component: FunctionalComponent, + props: Partial, +) { + const r = (args: any) => ; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + orders: [ + { + id: "123", + amount: "TESTKUDOS:10", + paid: false, + refundable: true, + row_id: 1, + summary: "summary", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "123", + }, + { + id: "234", + amount: "TESTKUDOS:12", + paid: true, + refundable: true, + row_id: 2, + summary: + "summary with long text, very very long text that someone want to add as a description of the order", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "234", + }, + { + id: "456", + amount: "TESTKUDOS:1", + paid: false, + refundable: false, + row_id: 3, + summary: + "summary with long text, very very long text that someone want to add as a description of the order", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "456", + }, + { + id: "234", + amount: "TESTKUDOS:12", + paid: false, + refundable: false, + row_id: 4, + summary: + "summary with long text, very very long text that someone want to add as a description of the order", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "234", + }, + ], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx new file mode 100644 index 000000000..9f80719a1 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx @@ -0,0 +1,226 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { h, VNode, Fragment } from "preact"; +import { useState } from "preact/hooks"; +import { DatePicker } from "../../../../components/picker/DatePicker.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { CardTable } from "./Table.js"; +import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +export interface ListPageProps { + onShowAll: () => void; + onShowNotPaid: () => void; + onShowPaid: () => void; + onShowRefunded: () => void; + onShowNotWired: () => void; + onShowWired: () => void; + onCopyURL: (id: string) => void; + isAllActive: string; + isPaidActive: string; + isNotPaidActive: string; + isRefundedActive: string; + isNotWiredActive: string; + isWiredActive: string; + + jumpToDate?: Date; + onSelectDate: (date?: Date) => void; + + orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[]; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; + + onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; + onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; + onCreate: () => void; +} + +export function ListPage({ + hasMoreAfter, + hasMoreBefore, + onLoadMoreAfter, + onLoadMoreBefore, + orders, + isAllActive, + onSelectOrder, + onRefundOrder, + jumpToDate, + onCopyURL, + onShowAll, + onShowPaid, + onShowNotPaid, + onShowRefunded, + onShowNotWired, + onShowWired, + onSelectDate, + isPaidActive, + isRefundedActive, + isNotWiredActive, + onCreate, + isNotPaidActive, + isWiredActive, +}: ListPageProps): VNode { + const { i18n } = useTranslationContext(); + const dateTooltip = i18n.str`select date to show nearby orders`; + const [pickDate, setPickDate] = useState(false); + const [settings] = useSettings(); + + return ( + +
+ +
+
+
+ {jumpToDate && ( + + )} +
+ + { + setPickDate(true); + }} + /> + +
+ +
+
+
+
+ + setPickDate(false)} + dateReceiver={onSelectDate} + /> + + + + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx new file mode 100644 index 000000000..b2806bb79 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx @@ -0,0 +1,417 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputGroup } from "../../../../components/form/InputGroup.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { ConfirmModal } from "../../../../components/modal/index.js"; +import { useConfigContext } from "../../../../context/config.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { mergeRefunds } from "../../../../utils/amount.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId; +interface Props { + orders: Entity[]; + onRefund: (value: Entity) => void; + onCopyURL: (id: string) => void; + onCreate: () => void; + onSelect: (order: Entity) => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + orders, + onCreate, + onRefund, + onCopyURL, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState([]); + + const { i18n } = useTranslationContext(); + + return ( +
+
+

+ + + + Orders +

+ +
+ +
+ + + +
+
+
+
+
+ {orders.length > 0 ? ( + onCopyURL(o.id)} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + + )} + + + + + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onRefund: (id: Entity) => void; + onCopyURL: (id: Entity) => void; + onSelect: (id: Entity) => void; + rowSelectionHandler: StateUpdater; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function Table({ + instances, + onSelect, + onRefund, + onCopyURL, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + return ( +
+ {hasMoreBefore && ( + + )} +
+ + + + + + + + + {instances.map((i) => { + return ( + + + + + + + ); + })} + +
+ Date + + Amount + + Summary + +
onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.timestamp.t_s === "never" + ? "never" + : format( + new Date(i.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} + onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.amount} + onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.summary} + +
+ {i.refundable && ( + + )} + {!i.paid && ( + + )} +
+
+ {hasMoreAfter && ( + + )} +
+ ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( +
+

+ + + +

+

+ + No orders have been found matching your query! + +

+
+ ); +} + +interface RefundModalProps { + onCancel: () => void; + onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void; + order: MerchantBackend.Orders.MerchantOrderStatusResponse; +} + +export function RefundModal({ + order, + onCancel, + onConfirm, +}: RefundModalProps): VNode { + type State = { mainReason?: string; description?: string; refund?: string }; + const [form, setValue] = useState({}); + const [settings] = useSettings(); + const { i18n } = useTranslationContext(); + // const [errors, setErrors] = useState>({}); + + const refunds = ( + order.order_status === "paid" ? order.refund_details : [] + ).reduce(mergeRefunds, []); + + const config = useConfigContext(); + const totalRefunded = refunds + .map((r) => r.amount) + .reduce( + (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount, + Amounts.zeroOfCurrency(config.currency), + ); + const orderPrice = + order.order_status === "paid" + ? Amounts.parseOrThrow(order.contract_terms.amount) + : undefined; + const totalRefundable = !orderPrice + ? Amounts.zeroOfCurrency(totalRefunded.currency) + : refunds.length + ? Amounts.sub(orderPrice, totalRefunded).amount + : orderPrice; + + const isRefundable = Amounts.isNonZero(totalRefundable); + const duplicatedText = i18n.str`duplicated`; + + const errors: FormErrors = { + mainReason: !form.mainReason ? i18n.str`required` : undefined, + description: + !form.description && form.mainReason !== duplicatedText + ? i18n.str`required` + : undefined, + refund: !form.refund + ? i18n.str`required` + : !Amounts.parse(form.refund) + ? i18n.str`invalid format` + : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1 + ? i18n.str`this value exceed the refundable amount` + : undefined, + }; + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const validateAndConfirm = () => { + try { + if (!form.refund) return; + onConfirm({ + refund: Amounts.stringify( + Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount, + ), + reason: + form.description === undefined + ? form.mainReason || "" + : `${form.mainReason}: ${form.description}`, + }); + } catch (err) { + console.log(err); + } + }; + + //FIXME: parameters in the translation + return ( + + {refunds.length > 0 && ( +
+
+ + + + + + + + + + + {refunds.map((r) => { + return ( + + + + + + ); + })} + +
+ date + + amount + + reason +
+ {r.timestamp.t_s === "never" + ? "never" + : format( + new Date(r.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} + {r.amount}{r.reason}
+
+
+
+ )} + + {isRefundable && ( + + errors={errors} + object={form} + valueHandler={(d) => setValue(d as any)} + > + + name="refund" + label={i18n.str`Refund`} + tooltip={i18n.str`amount to be refunded`} + > + Max refundable:{" "} + {Amounts.stringify(totalRefundable)} + + + {form.mainReason && form.mainReason !== duplicatedText ? ( + + label={i18n.str`Description`} + name="description" + tooltip={i18n.str`more information to give context`} + /> + ) : undefined} + + )} +
+ ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx new file mode 100644 index 000000000..1bec26e28 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx @@ -0,0 +1,224 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading.js"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { AuditorBackend } from "../../../declaration.js"; +import { Notification } from "../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { RefundModal } from "./Table.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => VNode; + onNotFound: () => VNode; + onSelect: (id: string) => void; + onCreate: () => void; +} + +export default function DepositConfirmationList({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const [filter, setFilter] = useState({ paid: "no" }); + const [orderToBeRefunded, setOrderToBeRefunded] = useState< + MerchantBackend.Orders.OrderHistoryEntry | undefined + >(undefined); + + const setNewDate = (date?: Date): void => + setFilter((prev) => ({ ...prev, date })); + + const result = useInstanceOrders(filter, setNewDate); + const { refundOrder, getPaymentURL } = useOrderAPI(); + + const [notif, setNotif] = useState(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return ; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + const isNotPaidActive = filter.paid === "no" ? "is-active" : ""; + const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : ""; + const isRefundedActive = filter.refunded === "yes" ? "is-active" : ""; + const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : ""; + const isWiredActive = filter.wired === "yes" ? "is-active" : ""; + const isAllActive = + filter.paid === undefined && + filter.refunded === undefined && + filter.wired === undefined + ? "is-active" + : ""; + + return ( +
+ + + + + ({ ...o, id: o.order_id }))} + onLoadMoreBefore={result.loadMorePrev} + hasMoreBefore={!result.isReachingStart} + onLoadMoreAfter={result.loadMore} + hasMoreAfter={!result.isReachingEnd} + onSelectOrder={(order) => onSelect(order.id)} + onRefundOrder={(value) => setOrderToBeRefunded(value)} + isAllActive={isAllActive} + isNotWiredActive={isNotWiredActive} + isWiredActive={isWiredActive} + isPaidActive={isPaidActive} + isNotPaidActive={isNotPaidActive} + isRefundedActive={isRefundedActive} + jumpToDate={filter.date} + onCopyURL={(id) => + getPaymentURL(id).then((resp) => copyToClipboard(resp.data)) + } + onCreate={onCreate} + onSelectDate={setNewDate} + onShowAll={() => setFilter({})} + onShowNotPaid={() => setFilter({ paid: "no" })} + onShowPaid={() => setFilter({ paid: "yes" })} + onShowRefunded={() => setFilter({ refunded: "yes" })} + onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })} + onShowWired={() => setFilter({ wired: "yes" })} + /> + + {orderToBeRefunded && ( + setOrderToBeRefunded(undefined)} + onConfirm={(value) => + refundOrder(orderToBeRefunded.order_id, value) + .then(() => + setNotif({ + message: i18n.str`refund created successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not create the refund`, + type: "ERROR", + description: error.message, + }), + ) + .then(() => setOrderToBeRefunded(undefined)) + } + onLoadError={(error) => { + setNotif({ + message: i18n.str`could not create the refund`, + type: "ERROR", + description: error.message, + }); + setOrderToBeRefunded(undefined); + return
; + }} + onUnauthorized={onUnauthorized} + onNotFound={() => { + setNotif({ + message: i18n.str`could not get the order to refund`, + type: "ERROR", + // description: error.message + }); + setOrderToBeRefunded(undefined); + return
; + }} + /> + )} +
+ ); +} + +interface RefundProps { + id: string; + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => VNode; + onNotFound: () => VNode; + onCancel: () => void; + onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void; +} + +function RefundModalForTable({ + id, + onUnauthorized, + onLoadError, + onNotFound, + onConfirm, + onCancel, +}: RefundProps): VNode { + const result = useOrderDetails(id); + + if (result.loading) return ; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + + ); +} + +async function copyToClipboard(text: string): Promise { + return navigator.clipboard.writeText(text); +} diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index cf46a34d5..f1b8526b3 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -42,7 +42,7 @@ import { strings } from "./i18n/strings.js"; export function Application(): VNode { return ( - + -- cgit v1.2.3