diff options
author | Nic Eigel <nic@eigel.ch> | 2024-06-24 09:16:33 +0200 |
---|---|---|
committer | Nic Eigel <nic@eigel.ch> | 2024-06-24 09:16:33 +0200 |
commit | 75ff04672c011cf4c47b8f07d327adbf59323396 (patch) | |
tree | 24577e0429f641e3c19005b5a9180af8186232d0 /packages | |
parent | 0b90a34e7c7c5d9bcca9a2ebe74df9fdfafc6577 (diff) | |
download | wallet-core-75ff04672c011cf4c47b8f07d327adbf59323396.tar.xz |
fixing merge-error
Diffstat (limited to 'packages')
177 files changed, 639 insertions, 25470 deletions
diff --git a/packages/auditor-backoffice-ui/src/AdminRoutes.tsx b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx deleted file mode 100644 index 755d734ad..000000000 --- a/packages/auditor-backoffice-ui/src/AdminRoutes.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { h, VNode } from "preact"; -import { Router, route, Route } from "preact-router"; -import InstanceCreatePage from "./paths/admin/create/index.js"; -import InstanceListPage from "./paths/admin/list/index.js"; - -export enum AdminPaths { - list_instances = "/instances", - new_instance = "/instance/new", -} - -export function AdminRoutes(): VNode { - return ( - <Router> - <Route - path={AdminPaths.list_instances} - component={InstanceListPage} - onCreate={() => { - route(AdminPaths.new_instance); - }} - onUpdate={(id: string): void => { - route(`/instance/${id}/update`); - }} - /> - - <Route - path={AdminPaths.new_instance} - component={InstanceCreatePage} - onBack={() => route(AdminPaths.list_instances)} - onConfirm={() => { - // route(AdminPaths.list_instances); - }} - - // onError={(error: any) => { - // }} - /> - </Router> - ); -} diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx index 4d7a423ed..83c1c9f4d 100644 --- a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx @@ -17,469 +17,456 @@ /** * * @author Sebastian Javier Marchano (sebasjm) - * @author Nic Eigel + * @author Nicola Eigel */ +import {TranslatedString} from "@gnu-taler/taler-util"; import { - useTranslationContext, - HttpError, - ErrorType, + useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { Fragment, FunctionComponent, h, VNode } from "preact"; -import { Route, route, Router } from "preact-router"; -import { useEffect, useMemo, useState } from "preact/hooks"; -import { Loading } from "./components/exception/loading.js"; -import { Menu, NotificationCard } from "./components/menu/index.js"; -import { InstanceContextProvider } from "./context/instance.js"; -import { - useBackendDefaultToken, - useBackendInstanceToken, - useSimpleLocalStorage, -} from "./hooks/index.js"; -import { useInstanceKYCDetails } from "./hooks/instance.js"; -import InstanceCreatePage from "./paths/admin/create/index.js"; -import InstanceListPage from "./paths/admin/list/index.js"; -import TokenPage from "./paths/instance/token/index.js"; -import ListKYCPage from "./paths/instance/kyc/list/index.js"; -import OrderCreatePage from "./paths/instance/orders/create/index.js"; -import OrderDetailsPage from "./paths/instance/orders/details/index.js"; -import OrderListPage from "./paths/instance/orders/list/index.js"; -import DepositConfirmationCreatePage from "./paths/instance/deposit_confirmations/create/index.js"; -import DepositConfirmationListPage from "./paths/instance/deposit_confirmations/list/index.js"; -import DepositConfirmationUpdatePage from "./paths/instance/deposit_confirmations/update/index.js"; -import ProductCreatePage from "./paths/instance/products/create/index.js"; -import ProductListPage from "./paths/instance/products/list/index.js"; -import ProductUpdatePage from "./paths/instance/products/update/index.js"; -import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; -import BankAccountListPage from "./paths/instance/accounts/list/index.js"; -import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; -import ReservesCreatePage from "./paths/instance/reserves/create/index.js"; -import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; -import ReservesListPage from "./paths/instance/reserves/list/index.js"; -import TemplateCreatePage from "./paths/instance/templates/create/index.js"; -import TemplateUsePage from "./paths/instance/templates/use/index.js"; -import TemplateQrPage from "./paths/instance/templates/qr/index.js"; -import TemplateListPage from "./paths/instance/templates/list/index.js"; -import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; -import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; -import WebhookListPage from "./paths/instance/webhooks/list/index.js"; -import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; -import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js"; -import ValidatorListPage from "./paths/instance/otp_devices/list/index.js"; -import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js"; - -import TransferCreatePage from "./paths/instance/transfers/create/index.js"; -import TransferListPage from "./paths/instance/transfers/list/index.js"; -import InstanceUpdatePage, { - AdminUpdate as InstanceAdminUpdatePage, - Props as InstanceUpdatePageProps, -} from "./paths/instance/update/index.js"; -import { LoginPage } from "./paths/login/index.js"; +import {VNode, h} from "preact"; +import {Route, Router, route} from "preact-router"; +import {useEffect, useErrorBoundary, useMemo, useState} from "preact/hooks"; +import {Menu, NotificationCard} from "./components/menu/index.js"; +import {EntityContextProvider} from "./context/entity.js"; +import {Notification} from "./utils/types.js"; import NotFoundPage from "./paths/notfound/index.js"; -import { Notification } from "./utils/types.js"; -import { LoginToken, MerchantBackend } from "./declaration.js"; -import { Settings } from "./paths/settings/index.js"; -import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; +import {Settings} from "./paths/settings/index.js"; +import DefaultList from "./paths/default/index.js"; +import { + AuditorBackend, +} from "./declaration.js"; +import FinanceDashboard from "./paths/finance/index.js"; +import DetailsDashboard from "./paths/details/index.js"; +import OperationsDashboard from "./paths/operations/index.js"; +import SecurityDashboard from "./paths/security/index.js"; -export enum InstancePaths { - error = "/error", - settings = "/settings", - token = "/token", +export enum Paths { + error = "/error", + settings = "/settings", - inventory_list = "/inventory", - inventory_update = "/inventory/:pid/update", - inventory_new = "/inventory/new", + key_figures = "/key-figures", + critical_errors = "/critical-errors", + operating_status = "/operating-status", + detail_view = "/detail-view", - deposit_confirmation_list = "/deposit-confirmation", - deposit_confirmation_update = "/deposit-confirmation/:pid/update", - deposit_confirmation_new = "/deposit-confirmation/new", + amount_arithmethic_inconsistency_list = "/amount-arithmetic-inconsistencies", - interface = "/interface", -} + bad_sig_losses_list = "/bad-sig-losses", + + balance_list = "/balance", + + closure_lag_list = "/closure-lags", + + coin_inconsistency_list = "/coin-inconsistencies", + + denomination_key_validity_withdraw_inconsistency_list = "/denomination-key-validity-withdraw-inconsistencies", + + denomination_pending_list = "/denominations-pending", + + denomination_without_sig_list = "/denominations-without-sig", + + deposit_confirmation_list = "/deposit-confirmations", + deposit_confirmation_update = "/deposit-confirmation/:rowid/update", + + emergency_list = "/emergencies", + + emergency_by_count_list = "/emergencies-by-count", + + exchange_signkey_list = "/exchange-sign-keys", + + fee_time_inconsistency_list = "/fee-time-inconsistencies", + + historic_denomination_revenue_list = "/historic-denomination-revenues", + + misattribution_in_inconsistency_list = "/misattribution-in-inconsistencies", + + progress_list = "/progress", + + purse_not_closed_inconsistency_list = "/purse-not-closed-inconsistencies", + + purse_list = "/purses", + + refresh_hanging_list = "/refreshes-hanging", -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => { }; + reserve_balance_insufficient_inconsistency_list = "/reserve-balance-insufficient-inconsistencies", -export enum AdminPaths { - list_instances = "/instances", - new_instance = "/instance/new", - update_instance = "/instance/:id/update", + reserve_balance_summary_wrong_inconsistency_list = "/reserve-balance-summary-wrong-inconsistencies", + + reserve_in_inconsistency_list = "/reserve-in-inconsistencies", + + reserve_not_closed_inconsistency_list = "/reserve-not-closed-inconsistencies", + + reserves_list = "/reserves", + + row_inconsistency_list = "/row-inconsistencies", + + row_minor_inconsistency_list = "/row-minor-inconsistencies", + + wire_format_inconsistency_list = "/wire-format-inconsistencies", + + wire_out_inconsistency_list = "/wire-out-inconsistencies" } -export interface Props { - id: string; - admin?: boolean; - path: string; - onUnauthorized: () => void; - onLoginPass: () => void; - setInstanceName: (s: string) => void; +interface TestProps { + title: string; + endpoint: string; + entity: any; } -export function InstanceRoutes({ - id, - admin, - path, - // onUnauthorized, - onLoginPass, - setInstanceName, -}: Props): VNode { - const [defaultToken, updateDefaultToken] = useBackendDefaultToken(); - const [token, updateToken] = useBackendInstanceToken(id); - const { i18n } = useTranslationContext(); - - type GlobalNotifState = (Notification & { to: string }) | undefined; - const [globalNotification, setGlobalNotification] = - useState<GlobalNotifState>(undefined); - - const changeToken = (token?: LoginToken) => { - if (admin) { - updateToken(token); - } else { - updateDefaultToken(token); +function getInstanceTitle(path: string): TestProps { + switch (path) { + case Paths.key_figures: + return {title: `Key figures`, endpoint: "helper", entity: null}; + case Paths.critical_errors: + return {title: `Critical errors`, endpoint: "helper", entity: null}; + case Paths.operating_status: + return {title: `Operating status`, endpoint: "helper", entity: null}; + case Paths.detail_view: + return {title: `Inconsistencies`, endpoint: "helper", entity: null}; + case Paths.amount_arithmethic_inconsistency_list: + let amountArithmeticInconsistency: AuditorBackend.AmountArithmeticInconsistency.ClassAmountArithmeticInconsistency = {} as AuditorBackend.AmountArithmeticInconsistency.ClassAmountArithmeticInconsistency; + return { + title: `Amount arithmetic inconsistencies`, + endpoint: "amount-arithmetic-inconsistency", + entity: amountArithmeticInconsistency + }; + case Paths.bad_sig_losses_list: + return {title: `Bad Sig Losses`, endpoint: "bad-sig-losses", entity: null}; + case Paths.balance_list: + return {title: "Balances", endpoint: "balances", entity: null}; + case Paths.closure_lag_list: + return {title: `Closure Lags`, endpoint: "closure-lags", entity: null}; + case Paths.coin_inconsistency_list: + return {title: `Coin inconsistencies`, endpoint: "coin-inconsistency", entity: null}; + case Paths.denomination_key_validity_withdraw_inconsistency_list: + return {title: `Denomination key validity withdraw inconsistency`, endpoint: "denomination-key-validity-withdraw-inconsistency", entity: null}; + case Paths.denomination_pending_list: + return {title: `Denominations pending`, endpoint: "denomination-pending", entity: null}; + case Paths.denomination_without_sig_list: + return {title: `Denominations without sigs`, endpoint: "denominations-without-sigs", entity: null}; + case Paths.deposit_confirmation_list: + return {title: "Deposit Confirmations", endpoint: "deposit-confirmation", entity: null}; + case Paths.emergency_list: + return {title: "Emergencies", endpoint: "emergency", entity: null}; + case Paths.emergency_by_count_list: + return {title: "Emergencies by count", endpoint: "emergency-by-count", entity: null}; + case Paths.fee_time_inconsistency_list: + return {title: "Fee time inconsistencies", endpoint: "fee-time-inconsistency", entity: null}; + case Paths.historic_denomination_revenue_list: + return {title: "Historic denomination revenue", endpoint: "historic-denomination-revenue", entity: null}; + case Paths.misattribution_in_inconsistency_list: + return {title: "Misattribution in inconsistencies", endpoint: "misattribution-in-inconsistency", entity: null}; + case Paths.progress_list: + return {title: "Progress", endpoint: "progress", entity: null}; + case Paths.purse_not_closed_inconsistency_list: + return {title: "Purse not closed inconsistencies", endpoint: "purse-not-closed-inconsistencies", entity: null}; + case Paths.purse_list: + return {title: "Purses", endpoint: "purses", entity: null}; + case Paths.refresh_hanging_list: + return {title: "Refreshes hanging", endpoint: "refreshes-hanging", entity: null}; + case Paths.reserves_list: + return {title: "Reserves", endpoint: "reserves ", entity: null}; + case Paths.reserve_balance_insufficient_inconsistency_list: + return {title: "Reserve balance insufficient inconsistencies", endpoint: "reserve-balance-insufficient-inconsistency", entity: null}; + case Paths.reserve_balance_summary_wrong_inconsistency_list: + return {title: "Reserve balance summary wrong inconsistencies", endpoint: "reserve-balance-summary-wrong-inconsistency", entity: null}; + case Paths.reserve_in_inconsistency_list: + return {title: "Reserves in inconsistencies", endpoint: "reserve-in-inconsistency", entity: null}; + case Paths.reserve_not_closed_inconsistency_list: + return {title: "Reserves not closed inconsistencies", endpoint: "reserve-not-closed-inconsistency", entity: null}; + case Paths.row_inconsistency_list: + return {title: "Row inconsistencies", endpoint: "row-inconsistency", entity: null}; + case Paths.row_minor_inconsistency_list: + return {title: "Row minor inconsistencies", endpoint: "row-minor-inconsistencies", entity: null}; + case Paths.wire_format_inconsistency_list: + let wireFormatInconsistency: AuditorBackend.WireFormatInconsistency.ClassWireFormatInconsistency = {} as AuditorBackend.WireFormatInconsistency.ClassWireFormatInconsistency; + return {title: "Wire format inconsistencies", endpoint: "wire-format-inconsistency", entity: wireFormatInconsistency}; + case Paths.wire_out_inconsistency_list: + return {title: "Wire out inconsistencies", endpoint: "wire-out-inconsistency", entity: null}; + case Paths.settings: + return {title: `Settings`, endpoint: "settings", entity: null}; + default: + return {title: "", endpoint: "", entity: null}; } - onLoginPass() - }; - // const updateLoginStatus = (url: string, token?: string) => { - // changeToken(token); - // }; - - const value = useMemo( - () => ({ id, token, admin, changeToken }), - [id, token, admin], - ); - - function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) { - return function ServerErrorRedirectToImpl( - error: HttpError<MerchantBackend.ErrorDetail>, - ) { - if (error.type === ErrorType.TIMEOUT) { - setGlobalNotification({ - message: i18n.str`The request to the backend take too long and was cancelled`, - description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`, - type: "ERROR", - to, - }); - } else { - setGlobalNotification({ - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - details: - error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER - ? error.payload.detail - : undefined, - type: "ERROR", - to, - }); - } - return <Redirect to={to} />; - }; - } - - // const LoginPageAccessDeniend = onUnauthorized - const LoginPageAccessDenied = () => { - return <Fragment> - <NotificationCard - notification={{ - message: i18n.str`Access denied`, - description: i18n.str`Session expired or password changed.`, - type: "ERROR", - }} - /> - <LoginPage onConfirm={changeToken} /> - </Fragment> - - } - - function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) { - return function IfAdminCreateDefaultOrImpl(props?: T) { - if (admin && id === "default") { - return ( - <Fragment> - <NotificationCard - notification={{ - message: i18n.str`No 'default' instance configured yet.`, - description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`, - type: "INFO", - }} - /> - </Fragment> - ); - } - if (props) { - return <Next {...props} />; - } - return <Next />; - }; - } - - const clearTokenAndGoToRoot = () => { - route("/"); - // clear all tokens - updateToken(undefined) - updateDefaultToken(undefined) - }; - - return ( - <InstanceContextProvider value={value}> - <Menu - instance={id} - admin={admin} - onShowSettings={() => { - route(InstancePaths.interface) - }} - path={path} - onLogout={clearTokenAndGoToRoot} - setInstanceName={setInstanceName} - isPasswordOk={defaultToken !== undefined} - /> - <KycBanner /> - <NotificationCard notification={globalNotification} /> - - <Router - onChange={(e) => { - const movingOutFromNotification = - globalNotification && e.url !== globalNotification.to; - if (movingOutFromNotification) { - setGlobalNotification(undefined); - } - }} - > - {/** - * Admin pages - */} - {admin && ( - <Route - path={AdminPaths.list_instances} - component={InstanceListPage} - onCreate={() => { - route(AdminPaths.new_instance); - }} - onUpdate={(id: string): void => { - route(`/instance/${id}/update`); - }} - setInstanceName={setInstanceName} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.error)} - /> - )} - {admin && ( - <Route - path={AdminPaths.update_instance} - component={AdminInstanceUpdatePage} - onBack={() => route(AdminPaths.list_instances)} - onConfirm={() => { - route(AdminPaths.list_instances); - }} - onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)} - onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)} - onNotFound={NotFoundPage} - /> - )} - {/** - * Update instance page - */} - <Route - path={InstancePaths.settings} - component={InstanceUpdatePage} - onBack={() => { - route(`/`); - }} - onConfirm={() => { - route(`/`); - }} - onUpdateError={noop} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.error)} - /> - {/** - * Inventory pages - */} - <Route - path={InstancePaths.inventory_list} - component={ProductListPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} - onCreate={() => { - route(InstancePaths.inventory_new); - }} - onSelect={(id: string) => { - route(InstancePaths.inventory_update.replace(":pid", id)); - }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - /> - <Route - path={InstancePaths.inventory_update} - component={ProductUpdatePage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)} - onConfirm={() => { - route(InstancePaths.inventory_list); - }} - onBack={() => { - route(InstancePaths.inventory_list); - }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - /> - <Route - path={InstancePaths.inventory_new} - component={ProductCreatePage} - onConfirm={() => { - route(InstancePaths.inventory_list); - }} - onBack={() => { - route(InstancePaths.inventory_list); - }} - /> - {/** - * Deposit confirmation pages - */} - <Route - path={InstancePaths.deposit_confirmation_list} - component={DepositConfirmationListPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} - onCreate={() => { - route(InstancePaths.deposit_confirmation_new); - }} - onSelect={(id: string) => { - route(InstancePaths.deposit_confirmation_update.replace(":pid", id)); - }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - /> - <Route - path={InstancePaths.deposit_confirmation_update} - component={DepositConfirmationUpdatePage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.deposit_confirmation_list)} - onConfirm={() => { - route(InstancePaths.deposit_confirmation_list); - }} - onBack={() => { - route(InstancePaths.deposit_confirmation_list); - }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - /> - <Route - path={InstancePaths.deposit_confirmation_new} - component={DepositConfirmationCreatePage} - onConfirm={() => { - route(InstancePaths.deposit_confirmation_list); - }} - onBack={() => { - route(InstancePaths.deposit_confirmation_list); - }} - /> - <Route path={InstancePaths.interface} component={Settings} /> - {/** - * Example pages - */} - <Route path="/loading" component={Loading} /> - <Route default component={NotFoundPage} /> - </Router> - </InstanceContextProvider> - ); } -export function Redirect({ to }: { to: string }): null { - useEffect(() => { - route(to, true); - }); - return null; +export interface Props { + path: string; } -function AdminInstanceUpdatePage({ - id, - ...rest -}: { id: string } & InstanceUpdatePageProps): VNode { - const [token, changeToken] = useBackendInstanceToken(id); - const updateLoginStatus = (token?: LoginToken): void => { - changeToken(token); - }; - const value = useMemo( - () => ({ id, token, admin: true, changeToken }), - [id, token], - ); - const { i18n } = useTranslationContext(); - - return ( - <InstanceContextProvider value={value}> - <InstanceAdminUpdatePage - {...rest} - instanceId={id} - onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => { - const notif = - error.type === ErrorType.TIMEOUT - ? { - message: i18n.str`The request to the backend take too long and was cancelled`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - type: "ERROR" as const, - } - : { - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - details: - error.type === ErrorType.CLIENT || - error.type === ErrorType.SERVER - ? error.payload.detail - : undefined, - type: "ERROR" as const, - }; - return ( - <Fragment> - <NotificationCard notification={notif} /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - ); - }} - onUnauthorized={() => { - return ( - <Fragment> - <NotificationCard - notification={{ - message: i18n.str`Access denied`, - description: i18n.str`The access token provided is invalid`, - type: "ERROR", +export function InstanceRoutes({ + // id, + path, + // setInstanceName + }: Props): VNode { + const {i18n} = useTranslationContext(); + + type GlobalNotifState = (Notification & { to: string | undefined }) | undefined; + const [globalNotification, setGlobalNotification] = + useState<GlobalNotifState>(undefined); + + const [error] = useErrorBoundary(); + const {title, endpoint, entity} = getInstanceTitle(path.replace("app/#", "")); + + const value = useMemo( + () => ({title, path, endpoint, entity}), + [title, path, endpoint, entity], + ); + + //TODO add if needed + /*function ServerErrorRedirectTo(to: Paths) { + return function ServerErrorRedirectToImpl( + error: HttpError<AuditorBackend.ErrorDetail>, + ) { + if (error.type === ErrorType.TIMEOUT) { + setGlobalNotification({ + message: `The request to the backend take too long and was cancelled`, + description: `Diagnostic from ${error.info.url} is "${error.message}"`, + type: "ERROR", + to, + }); + } else { + setGlobalNotification({ + message: `The backend reported a problem: HTTP status #${error.status}`, + description: `Diagnostic from ${error.info.url} is '${error.message}'`, + details: + error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER + ? error.payload.detail + : undefined, + type: "ERROR", + to, + }); + } + return <Redirect to={to} />; + }; + }*/ + + + return ( + <EntityContextProvider value={value}> + <Menu + // instance={id} + path={path} + title={"Settings"} + onShowSettings={() => { + route(Paths.settings); + }}/> + <NotificationCard notification={globalNotification}/> + {error && + <NotificationCard notification={{ + message: "Internal error, please report", + type: "ERROR", + description: <pre> + {(error instanceof Error ? error.stack : String(error)) as TranslatedString} + </pre>, + }}/> + } + <Router + onChange={(e) => { + const movingOutFromNotification = + globalNotification && e.url !== globalNotification.to; + if (movingOutFromNotification) { + setGlobalNotification(undefined); + } }} - /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - ); - }} - /> - </InstanceContextProvider> - ); -} + > + <Route path="/" component={Redirect} to={Paths.key_figures}/> -function KycBanner(): VNode { - const kycStatus = useInstanceKYCDetails(); - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - const today = format(new Date(), dateFormatForSettings(settings)); - const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide"); - const hasBeenHidden = today === lastHide; - const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; - if (hasBeenHidden || !needsToBeShown) return <Fragment />; - return ( - <NotificationCard - notification={{ - type: "WARN", - message: "KYC verification needed", - description: ( - <div> - <p> - Some transfer are on hold until a KYC process is completed. Go to - the KYC section in the left panel for more information - </p> - <div class="buttons is-right"> - <button class="button" onClick={() => setLastHide(today)}> - <i18n.Translate>Hide for today</i18n.Translate> - </button> - </div> - </div> - ), - }} - /> - ); + <Route + path={Paths.key_figures} + component={FinanceDashboard} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.critical_errors} + component={SecurityDashboard} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.operating_status} + component={OperationsDashboard} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.detail_view} + component={DetailsDashboard} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.amount_arithmethic_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.bad_sig_losses_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.balance_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.closure_lag_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.coin_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.denomination_key_validity_withdraw_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.denomination_pending_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.denomination_without_sig_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.deposit_confirmation_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.emergency_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + <Route + path={Paths.emergency_by_count_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + {<Route + path={Paths.exchange_signkey_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.fee_time_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.historic_denomination_revenue_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.misattribution_in_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.progress_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.purse_not_closed_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.purse_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.refresh_hanging_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.reserve_balance_insufficient_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.reserve_balance_summary_wrong_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.reserve_in_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.reserve_not_closed_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.reserves_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.row_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.row_minor_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.wire_out_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + {<Route + path={Paths.wire_format_inconsistency_list} + component={DefaultList} + onNotFound={NotFoundPage} + />} + <Route + path={Paths.settings} + component={Settings} + /> + + {//TODO add if needed + /** + * Example pages + */} + {/* <Route path="/loading" component={Loading}/> + <Route default component={NotFoundPage}/>*/} + </Router> + </EntityContextProvider> + ); } + +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/AsyncButton.tsx b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx deleted file mode 100644 index 58c10e7d7..000000000 --- a/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ComponentChildren, h } from "preact"; -import { LoadingModal } from "../modal/index.js"; -import { useAsync } from "../../hooks/async.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; - -type Props = { - children: ComponentChildren; - disabled: boolean; - onClick?: () => Promise<void>; - [rest: string]: any; -}; - -export function AsyncButton({ onClick, disabled, children, ...rest }: Props) { - const { isSlow, isLoading, request, cancel } = useAsync(onClick); - const { i18n } = useTranslationContext(); - if (isSlow) { - return <LoadingModal onCancel={cancel} />; - } - if (isLoading) { - return ( - <button class="button"> - <i18n.Translate>Loading...</i18n.Translate> - </button> - ); - } - - return ( - <span {...rest}> - <button class="button is-success" onClick={request} disabled={disabled}> - {children} - </button> - </span> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/exception/QR.tsx b/packages/auditor-backoffice-ui/src/components/exception/QR.tsx deleted file mode 100644 index 246ce0229..000000000 --- a/packages/auditor-backoffice-ui/src/components/exception/QR.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -import { h, VNode } from "preact"; -import { useEffect, useRef } from "preact/hooks"; -import qrcode from "qrcode-generator"; - -export function QR({ text }: { text: string }): VNode { - const divRef = useRef<HTMLDivElement>(null); - useEffect(() => { - const qr = qrcode(0, "L"); - qr.addData(text); - qr.make(); - if (divRef.current) { - divRef.current.innerHTML = qr.createSvgTag({ - scalable: true, - }); - } - }); - - return ( - <div - style={{ - width: "100%", - display: "flex", - flexDirection: "column", - alignItems: "center", - }} - > - <div - style={{ width: "50%", minWidth: 200, maxWidth: 300 }} - ref={divRef} - /> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx b/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx deleted file mode 100644 index b0b9eaefc..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { InputProps, useField } from "./useField.js"; - -export interface Props<T> extends InputProps<T> { - isValid?: (e: any) => boolean; - addonBefore?: string; - toStr?: (v?: any) => string; - fromStr?: (s: string) => any; -} - -const defaultToString = (f?: any): string => f || ""; -const defaultFromString = (v: string): any => v as any; - -export function InputArray<T>({ - name, - readonly, - placeholder, - tooltip, - label, - help, - addonBefore, - isValid = () => true, - fromStr = defaultFromString, - toStr = defaultToString, -}: Props<keyof T>): VNode { - const { error: formError, value, onChange, required } = useField<T>(name); - const [localError, setLocalError] = useState<string | null>(null); - - const error = localError || formError; - - const array: any[] = (value ? value! : []) as any; - const [currentValue, setCurrentValue] = useState(""); - const { i18n } = useTranslationContext(); - - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <div class="field has-addons"> - {addonBefore && ( - <div class="control"> - <a class="button is-static">{addonBefore}</a> - </div> - )} - <p class="control is-expanded has-icons-right"> - <input - class={error ? "input is-danger" : "input"} - type="text" - placeholder={placeholder} - readonly={readonly} - disabled={readonly} - name={String(name)} - value={currentValue} - onChange={(e): void => setCurrentValue(e.currentTarget.value)} - /> - {required && ( - <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span> - )} - </p> - <p class="control"> - <button - class="button is-info has-tooltip-left" - disabled={!currentValue} - onClick={(): void => { - const v = fromStr(currentValue); - if (!isValid(v)) { - setLocalError( - i18n.str`The value ${v} is invalid for a payment url`, - ); - return; - } - setLocalError(null); - onChange([v, ...array] as any); - setCurrentValue(""); - }} - data-tooltip={i18n.str`add element to the list`} - > - <i18n.Translate>add</i18n.Translate> - </button> - </p> - </div> - {help} - {error && <p class="help is-danger"> {error} </p>} - {array.map((v, i) => ( - <div key={i} class="tags has-addons mt-3 mb-0"> - <span - class="tag is-medium is-info mb-0" - style={{ maxWidth: "90%" }} - > - {v} - </span> - <a - class="tag is-medium is-danger is-delete mb-0" - onClick={() => { - onChange(array.filter((f) => f !== v) as any); - setCurrentValue(toStr(v)); - }} - /> - </div> - ))} - </div> - </div> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx deleted file mode 100644 index bdb2feb6b..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { h, VNode } from "preact"; -import { InputProps, useField } from "./useField.js"; - -interface Props<T> extends InputProps<T> { - name: T; - readonly?: boolean; - expand?: boolean; - threeState?: boolean; - toBoolean?: (v?: any) => boolean | undefined; - fromBoolean?: (s: boolean | undefined) => any; -} - -const defaultToBoolean = (f?: any): boolean | undefined => f || ""; -const defaultFromBoolean = (v: boolean | undefined): any => v as any; - -export function InputBoolean<T>({ - name, - readonly, - placeholder, - tooltip, - label, - help, - threeState, - expand, - fromBoolean = defaultFromBoolean, - toBoolean = defaultToBoolean, -}: Props<keyof T>): VNode { - const { error, value, onChange } = useField<T>(name); - - const onCheckboxClick = (): void => { - const c = toBoolean(value); - if (c === false && threeState) return onChange(undefined as any); - return onChange(fromBoolean(!c)); - }; - - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class={expand ? "control is-expanded" : "control"}> - <label class="b-checkbox checkbox"> - <input - type="checkbox" - class={toBoolean(value) === undefined ? "is-indeterminate" : ""} - checked={toBoolean(value)} - placeholder={placeholder} - readonly={readonly} - name={String(name)} - disabled={readonly} - onChange={onCheckboxClick} - /> - <span class="check" /> - </label> - {help} - </p> - {error && <p class="help is-danger">{error}</p>} - </div> - </div> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx deleted file mode 100644 index cbcc6af2d..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { ComponentChildren, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { DatePicker } from "../picker/DatePicker.js"; -import { InputProps, useField } from "./useField.js"; -import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js"; - -export interface Props<T> extends InputProps<T> { - readonly?: boolean; - expand?: boolean; - //FIXME: create separated components InputDate and InputTimestamp - withTimestampSupport?: boolean; - side?: ComponentChildren; -} - -export function InputDate<T>({ - name, - readonly, - label, - placeholder, - help, - tooltip, - expand, - withTimestampSupport, - side, -}: Props<keyof T>): VNode { - const [opened, setOpened] = useState(false); - const { i18n } = useTranslationContext(); - const [settings] = useSettings() - - const { error, required, value, onChange } = useField<T>(name); - - let strValue = ""; - if (!value) { - strValue = withTimestampSupport ? "unknown" : ""; - } else if (value instanceof Date) { - strValue = format(value, dateFormatForSettings(settings)); - } else if (value.t_s) { - strValue = - value.t_s === "never" - ? withTimestampSupport - ? "never" - : "" - : format(new Date(value.t_s * 1000), dateFormatForSettings(settings)); - } - - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <div class="field has-addons"> - <p - class={ - expand - ? "control is-expanded has-icons-right" - : "control has-icons-right" - } - > - <input - class="input" - type="text" - readonly - value={strValue} - placeholder={placeholder} - onClick={() => { - if (!readonly) setOpened(true); - }} - /> - {required && ( - <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span> - )} - {help} - </p> - <div - class="control" - onClick={() => { - if (!readonly) setOpened(true); - }} - > - <a class="button is-static"> - <span class="icon"> - <i class="mdi mdi-calendar" /> - </span> - </a> - </div> - </div> - {error && <p class="help is-danger">{error}</p>} - </div> - - {!readonly && ( - <span - data-tooltip={ - withTimestampSupport - ? i18n.str`change value to unknown date` - : i18n.str`change value to empty` - } - > - <button - class="button is-info mr-3" - onClick={() => onChange(undefined as any)} - > - <i18n.Translate>clear</i18n.Translate> - </button> - </span> - )} - {withTimestampSupport && ( - <span data-tooltip={i18n.str`change value to never`}> - <button - class="button is-info" - onClick={() => onChange({ t_s: "never" } as any)} - > - <i18n.Translate>never</i18n.Translate> - </button> - </span> - )} - {side} - </div> - <DatePicker - opened={opened} - closeFunction={() => setOpened(false)} - dateReceiver={(d) => { - if (withTimestampSupport) { - onChange({ t_s: d.getTime() / 1000 } as any); - } else { - onChange(d as any); - } - }} - /> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx deleted file mode 100644 index 0103a97c1..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { formatDuration, intervalToDuration } from "date-fns"; -import { ComponentChildren, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { SimpleModal } from "../modal/index.js"; -import { DurationPicker } from "../picker/DurationPicker.js"; -import { InputProps, useField } from "./useField.js"; -import { Duration } from "@gnu-taler/taler-util"; - -export interface Props<T> extends InputProps<T> { - expand?: boolean; - readonly?: boolean; - withForever?: boolean; - side?: ComponentChildren; - withoutClear?: boolean; -} - -export function InputDuration<T>({ - name, - expand, - placeholder, - tooltip, - label, - help, - readonly, - withForever, - withoutClear, - side, -}: Props<keyof T>): VNode { - const [opened, setOpened] = useState(false); - const { i18n } = useTranslationContext(); - - const { error, required, value: anyValue, onChange } = useField<T>(name); - let strValue = ""; - const value: Duration = anyValue - if (!value) { - strValue = ""; - } else if (value.d_ms === "forever") { - strValue = i18n.str`forever`; - } else { - strValue = formatDuration( - intervalToDuration({ start: 0, end: value.d_ms }), - { - locale: { - formatDistance: (name, value) => { - switch (name) { - case "xMonths": - return i18n.str`${value}M`; - case "xYears": - return i18n.str`${value}Y`; - case "xDays": - return i18n.str`${value}d`; - case "xHours": - return i18n.str`${value}h`; - case "xMinutes": - return i18n.str`${value}min`; - case "xSeconds": - return i18n.str`${value}sec`; - } - }, - localize: { - day: () => "s", - month: () => "m", - ordinalNumber: () => "th", - dayPeriod: () => "p", - quarter: () => "w", - era: () => "e", - }, - }, - }, - ); - } - - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal is-flex-grow-3"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - - <div class="is-flex-grow-3"> - <div class="field-body "> - <div class="field"> - <div class="field has-addons"> - <p class={expand ? "control is-expanded " : "control "}> - <input - class="input" - type="text" - readonly - value={strValue} - placeholder={placeholder} - onClick={() => { - if (!readonly) setOpened(true); - }} - /> - {required && ( - <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span> - )} - </p> - <div - class="control" - onClick={() => { - if (!readonly) setOpened(true); - }} - > - <a class="button is-static"> - <span class="icon"> - <i class="mdi mdi-clock" /> - </span> - </a> - </div> - </div> - {error && <p class="help is-danger">{error}</p>} - </div> - {withForever && ( - <span data-tooltip={i18n.str`change value to never`}> - <button - class="button is-info mr-3" - onClick={() => onChange({ d_ms: "forever" } as any)} - > - <i18n.Translate>forever</i18n.Translate> - </button> - </span> - )} - {!readonly && !withoutClear && ( - <span data-tooltip={i18n.str`change value to empty`}> - <button - class="button is-info " - onClick={() => onChange(undefined as any)} - > - <i18n.Translate>clear</i18n.Translate> - </button> - </span> - )} - {side} - </div> - <span> - {help} - </span> - </div> - - - {opened && ( - <SimpleModal onCancel={() => setOpened(false)}> - <DurationPicker - days - hours - minutes - value={!value || value.d_ms === "forever" ? 0 : value.d_ms} - onChange={(v) => { - onChange({ d_ms: v } as any); - }} - /> - </SimpleModal> - )} - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx b/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx deleted file mode 100644 index 92b9e8b16..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { ComponentChildren, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { useGroupField } from "./useGroupField.js"; - -export interface Props<T> { - name: T; - children: ComponentChildren; - label: ComponentChildren; - tooltip?: ComponentChildren; - alternative?: ComponentChildren; - fixed?: boolean; - initialActive?: boolean; -} - -export function InputGroup<T>({ - name, - label, - children, - tooltip, - alternative, - fixed, - initialActive, -}: Props<keyof T>): VNode { - const [active, setActive] = useState(initialActive || fixed); - const group = useGroupField<T>(name); - - return ( - <div class="card"> - <header class="card-header"> - <p class="card-header-title"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - {group?.hasError && ( - <span class="icon has-text-danger" data-tooltip={tooltip}> - <i class="mdi mdi-alert" /> - </span> - )} - </p> - {!fixed && ( - <button - class="card-header-icon" - aria-label="more options" - onClick={(): void => setActive(!active)} - > - <span class="icon"> - {active ? ( - <i class="mdi mdi-arrow-up" /> - ) : ( - <i class="mdi mdi-arrow-down" /> - )} - </span> - </button> - )} - </header> - {active ? ( - <div class="card-content">{children}</div> - ) : alternative ? ( - <div class="card-content">{alternative}</div> - ) : undefined} - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx b/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx deleted file mode 100644 index d284b476f..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, h, VNode } from "preact"; -import { useRef, useState } from "preact/hooks"; -import { MAX_IMAGE_SIZE as MAX_IMAGE_UPLOAD_SIZE } from "../../utils/constants.js"; -import { InputProps, useField } from "./useField.js"; - -export interface Props<T> extends InputProps<T> { - expand?: boolean; - addonAfter?: ComponentChildren; - children?: ComponentChildren; -} - -export function InputImage<T>({ - name, - readonly, - placeholder, - tooltip, - label, - help, - children, - expand, -}: Props<keyof T>): VNode { - const { error, value, onChange } = useField<T>(name); - - const image = useRef<HTMLInputElement>(null); - const { i18n } = useTranslationContext(); - const [sizeError, setSizeError] = useState(false); - - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class={expand ? "control is-expanded" : "control"}> - {value && ( - <img - src={value} - style={{ width: 200, height: 200 }} - onClick={() => image.current?.click()} - /> - )} - <input - ref={image} - style={{ display: "none" }} - type="file" - name={String(name)} - placeholder={placeholder} - readonly={readonly} - onChange={(e) => { - const f: FileList | null = e.currentTarget.files; - if (!f || f.length != 1) { - return onChange(undefined!); - } - if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) { - setSizeError(true); - return onChange(undefined!); - } - setSizeError(false); - return f[0].arrayBuffer().then((b) => { - const b64 = window.btoa( - new Uint8Array(b).reduce( - (data, byte) => data + String.fromCharCode(byte), - "", - ), - ); - return onChange(`data:${f[0].type};base64,${b64}` as any); - }); - }} - /> - {help} - {children} - </p> - {error && <p class="help is-danger">{error}</p>} - {sizeError && ( - <p class="help is-danger"> - <i18n.Translate>Image should be smaller than 1 MB</i18n.Translate> - </p> - )} - {!value && ( - <button class="button" onClick={() => image.current?.click()}> - <i18n.Translate>Add</i18n.Translate> - </button> - )} - {value && ( - <button class="button" onClick={() => onChange(undefined!)}> - <i18n.Translate>Remove</i18n.Translate> - </button> - )} - </div> - </div> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx b/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx deleted file mode 100644 index d4b13d555..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { Fragment, h } from "preact"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Input } from "./Input.js"; - -export function InputLocation({ name }: { name: string }) { - const { i18n } = useTranslationContext(); - return ( - <> - <Input name={`${name}.country`} label={i18n.str`Country`} /> - <Input - name={`${name}.address_lines`} - inputType="multiline" - label={i18n.str`Address`} - toStr={(v: string[] | undefined) => (!v ? "" : v.join("\n"))} - fromStr={(v: string) => v.split("\n")} - /> - <Input - name={`${name}.building_number`} - label={i18n.str`Building number`} - /> - <Input name={`${name}.building_name`} label={i18n.str`Building name`} /> - <Input name={`${name}.street`} label={i18n.str`Street`} /> - <Input name={`${name}.post_code`} label={i18n.str`Post code`} /> - <Input name={`${name}.town_location`} label={i18n.str`Town location`} /> - <Input name={`${name}.town`} label={i18n.str`Town`} /> - <Input name={`${name}.district`} label={i18n.str`District`} /> - <Input - name={`${name}.country_subdivision`} - label={i18n.str`Country subdivision`} - /> - </> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx deleted file mode 100644 index fcecd8932..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { h, VNode } from "preact"; -import { InputArray } from "./InputArray.js"; -import { PAYTO_REGEX } from "../../utils/constants.js"; -import { InputProps } from "./useField.js"; - -export type Props<T> = InputProps<T>; - -const PAYTO_START_REGEX = /^payto:\/\//; - -export function InputPayto<T>({ - name, - readonly, - placeholder, - tooltip, - label, - help, -}: Props<keyof T>): VNode { - return ( - <InputArray<T> - name={name} - readonly={readonly} - addonBefore="payto://" - label={label} - placeholder={placeholder} - help={help} - tooltip={tooltip} - isValid={(v) => v && PAYTO_REGEX.test(v)} - toStr={(v?: string) => (!v ? "" : v.replace(PAYTO_START_REGEX, ""))} - fromStr={(v: string) => `payto://${v}`} - /> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx deleted file mode 100644 index cc5326bbe..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h } from "preact"; -import * as tests from "@gnu-taler/web-util/testing"; -import { InputPaytoForm } from "./InputPaytoForm.js"; -import { FormProvider } from "./FormProvider.js"; -import { useState } from "preact/hooks"; - -export default { - title: "Components/Form/PayTo", - component: InputPaytoForm, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -export const Example = tests.createExample(() => { - const initial = { - accounts: [], - }; - const [form, updateForm] = useState<Partial<typeof initial>>(initial); - return ( - <FormProvider valueHandler={updateForm} object={form}> - <InputPaytoForm name="accounts" label="Accounts:" /> - </FormProvider> - ); -}, {}); diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx deleted file mode 100644 index 7eba8b0b5..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx +++ /dev/null @@ -1,397 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { COUNTRY_TABLE } from "../../utils/constants.js"; -import { undefinedIfEmpty } from "../../utils/table.js"; -import { FormErrors, FormProvider } from "./FormProvider.js"; -import { Input } from "./Input.js"; -import { InputGroup } from "./InputGroup.js"; -import { InputSelector } from "./InputSelector.js"; -import { InputProps, useField } from "./useField.js"; -import { useEffect, useState } from "preact/hooks"; - -export interface Props<T> extends InputProps<T> { - isValid?: (e: any) => boolean; -} - -// type Entity = PaytoUriGeneric -// https://datatracker.ietf.org/doc/html/rfc8905 -type Entity = { - // iban, bitcoin, x-taler-bank. it defined the format - target: string; - // path1 if the first field to be used - path1?: string; - // path2 if the second field to be used, optional - path2?: string; - // params of the payto uri - params: { - "receiver-name"?: string; - sender?: string; - message?: string; - amount?: string; - instruction?: string; - [name: string]: string | undefined; - }; -}; - -function isEthereumAddress(address: string) { - if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) { - return false; - } else if ( - /^(0x|0X)?[0-9a-f]{40}$/.test(address) || - /^(0x|0X)?[0-9A-F]{40}$/.test(address) - ) { - return true; - } - return checkAddressChecksum(address); -} - -function checkAddressChecksum(address: string) { - //TODO implement ethereum checksum - return true; -} - -function validateBitcoin( - addr: string, - i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { - try { - const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr); - if (valid) return undefined; - } catch (e) { - console.log(e); - } - return i18n.str`This is not a valid bitcoin address.`; -} - -function validateEthereum( - addr: string, - i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { - try { - const valid = isEthereumAddress(addr); - if (valid) return undefined; - } catch (e) { - console.log(e); - } - return i18n.str`This is not a valid Ethereum address.`; -} - -/** - * An IBAN is validated by converting it into an integer and performing a - * basic mod-97 operation (as described in ISO 7064) on it. - * If the IBAN is valid, the remainder equals 1. - * - * The algorithm of IBAN validation is as follows: - * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid - * 2.- Move the four initial characters to the end of the string - * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35 - * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97 - * - * If the remainder is 1, the check digit test is passed and the IBAN might be valid. - * - */ -function validateIBAN( - iban: string, - i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { - // Check total length - if (iban.length < 4) - return i18n.str`IBAN numbers usually have more that 4 digits`; - if (iban.length > 34) - return i18n.str`IBAN numbers usually have less that 34 digits`; - - const A_code = "A".charCodeAt(0); - const Z_code = "Z".charCodeAt(0); - const IBAN = iban.toUpperCase(); - // check supported country - const code = IBAN.substr(0, 2); - const found = code in COUNTRY_TABLE; - if (!found) return i18n.str`IBAN country code not found`; - - // 2.- Move the four initial characters to the end of the string - const step2 = IBAN.substr(4) + iban.substr(0, 4); - const step3 = Array.from(step2) - .map((letter) => { - const code = letter.charCodeAt(0); - if (code < A_code || code > Z_code) return letter; - return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`; - }) - .join(""); - - function calculate_iban_checksum(str: string): number { - const numberStr = str.substr(0, 5); - const rest = str.substr(5); - const number = parseInt(numberStr, 10); - const result = number % 97; - if (rest.length > 0) { - return calculate_iban_checksum(`${result}${rest}`); - } - return result; - } - - const checksum = calculate_iban_checksum(step3); - if (checksum !== 1) - return i18n.str`IBAN number is not valid, checksum is wrong`; - return undefined; -} - -// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void', 'x-taler-bank'] -const targets = [ - "Choose one...", - "iban", - "x-taler-bank", - "bitcoin", - "ethereum", -]; -const noTargetValue = targets[0]; -const defaultTarget: Entity = { - target: noTargetValue, - params: {}, -}; - -export function InputPaytoForm<T>({ - name, - readonly, - label, - tooltip, -}: Props<keyof T>): VNode { - const { value: initialValueStr, onChange } = useField<T>(name); - - const initialPayto = parsePaytoUri(initialValueStr ?? "") - const paths = !initialPayto ? [] : initialPayto.targetPath.split("/") - const initialPath1 = paths.length >= 1 ? paths[0] : undefined; - const initialPath2 = paths.length >= 2 ? paths[1] : undefined; - const initial: Entity = initialPayto === undefined ? defaultTarget : { - target: initialPayto.targetType, - params: initialPayto.params, - path1: initialPath1, - path2: initialPath2, - } - const [value, setValue] = useState<Partial<Entity>>(initial) - - const { i18n } = useTranslationContext(); - - const errors: FormErrors<Entity> = { - target: - value.target === noTargetValue - ? i18n.str`required` - : undefined, - path1: !value.path1 - ? i18n.str`required` - : value.target === "iban" - ? validateIBAN(value.path1, i18n) - : value.target === "bitcoin" - ? validateBitcoin(value.path1, i18n) - : value.target === "ethereum" - ? validateEthereum(value.path1, i18n) - : undefined, - path2: - value.target === "x-taler-bank" - ? !value.path2 - ? i18n.str`required` - : undefined - : undefined, - params: undefinedIfEmpty({ - "receiver-name": !value.params?.["receiver-name"] - ? i18n.str`required` - : undefined, - }), - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({ - targetType: value.target, - targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""), - params: value.params ?? {} as any, - isKnown: false, - }) - useEffect(() => { - onChange(str as any) - }, [str]) - - // const submit = useCallback((): void => { - // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos; - // // const alreadyExists = - // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; - // // if (!alreadyExists) { - // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = { - // payto_uri: paytoURL, - // }; - // if (value.auth) { - // if (value.auth.url) { - // newValue.credit_facade_url = value.auth.url; - // } - // if (value.auth.type === "none") { - // newValue.credit_facade_credentials = { - // type: "none", - // }; - // } - // if (value.auth.type === "basic") { - // newValue.credit_facade_credentials = { - // type: "basic", - // username: value.auth.username ?? "", - // password: value.auth.password ?? "", - // }; - // } - // } - // onChange(newValue as any); - // // } - // // valueHandler(defaultTarget); - // }, [value]); - - //FIXME: translating plural singular - return ( - <InputGroup name="payto" label={label} fixed tooltip={tooltip}> - <FormProvider<Entity> - name="tax" - errors={errors} - object={value} - valueHandler={setValue} - > - <InputSelector<Entity> - name="target" - label={i18n.str`Account type`} - tooltip={i18n.str`Method to use for wire transfer`} - values={targets} - readonly={readonly} - toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)} - /> - - {value.target === "ach" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n.str`Routing`} - readonly={readonly} - tooltip={i18n.str`Routing number.`} - /> - <Input<Entity> - name="path2" - label={i18n.str`Account`} - readonly={readonly} - tooltip={i18n.str`Account number.`} - /> - </Fragment> - )} - {value.target === "bic" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n.str`Code`} - readonly={readonly} - tooltip={i18n.str`Business Identifier Code.`} - /> - </Fragment> - )} - {value.target === "iban" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n.str`IBAN`} - tooltip={i18n.str`International Bank Account Number.`} - readonly={readonly} - placeholder="DE1231231231" - inputExtra={{ style: { textTransform: "uppercase" } }} - /> - </Fragment> - )} - {value.target === "upi" && ( - <Fragment> - <Input<Entity> - name="path1" - readonly={readonly} - label={i18n.str`Account`} - tooltip={i18n.str`Unified Payment Interface.`} - /> - </Fragment> - )} - {value.target === "bitcoin" && ( - <Fragment> - <Input<Entity> - name="path1" - readonly={readonly} - label={i18n.str`Address`} - tooltip={i18n.str`Bitcoin protocol.`} - /> - </Fragment> - )} - {value.target === "ethereum" && ( - <Fragment> - <Input<Entity> - name="path1" - readonly={readonly} - label={i18n.str`Address`} - tooltip={i18n.str`Ethereum protocol.`} - /> - </Fragment> - )} - {value.target === "ilp" && ( - <Fragment> - <Input<Entity> - name="path1" - readonly={readonly} - label={i18n.str`Address`} - tooltip={i18n.str`Interledger protocol.`} - /> - </Fragment> - )} - {value.target === "void" && <Fragment />} - {value.target === "x-taler-bank" && ( - <Fragment> - <Input<Entity> - name="path1" - readonly={readonly} - label={i18n.str`Host`} - tooltip={i18n.str`Bank host.`} - /> - <Input<Entity> - name="path2" - readonly={readonly} - label={i18n.str`Account`} - tooltip={i18n.str`Bank account.`} - /> - </Fragment> - )} - - {/** - * Show additional fields apart from the payto - */} - {value.target !== noTargetValue && ( - <Fragment> - <Input - name="params.receiver-name" - readonly={readonly} - label={i18n.str`Owner's name`} - tooltip={i18n.str`Legal name of the person holding the account.`} - /> - </Fragment> - )} - - </FormProvider> - </InputGroup> - ); -} - diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx deleted file mode 100644 index 9956a6427..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx +++ /dev/null @@ -1,204 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import emptyImage from "../../assets/empty.png"; -import { FormErrors, FormProvider } from "./FormProvider.js"; -import { InputWithAddon } from "./InputWithAddon.js"; -import { TranslatedString } from "@gnu-taler/taler-util"; - -type Entity = { - id: string, - description: string; - image?: string; - extra?: string; -}; - -export interface Props<T extends Entity> { - selected?: T; - onChange: (p?: T) => void; - label: TranslatedString; - list: T[]; - withImage?: boolean; -} - -interface Search { - name: string; -} - -export function InputSearchOnList<T extends Entity>({ - selected, - onChange, - label, - list, - withImage, -}: Props<T>): VNode { - const [nameForm, setNameForm] = useState<Partial<Search>>({ - name: "", - }); - - const errors: FormErrors<Search> = { - name: undefined, - }; - const { i18n } = useTranslationContext(); - - if (selected) { - return ( - <article class="media"> - {withImage && - <figure class="media-left"> - <p class="image is-128x128"> - <img src={selected.image ? selected.image : emptyImage} /> - </p> - </figure> - } - <div class="media-content"> - <div class="content"> - <p class="media-meta"> - <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b> - </p> - <p> - <i18n.Translate>Description</i18n.Translate>:{" "} - {selected.description} - </p> - <div class="buttons is-right mt-5"> - <button - class="button is-info" - onClick={() => onChange(undefined)} - > - clear - </button> - </div> - </div> - </div> - </article> - ); - } - - return ( - <FormProvider<Search> - errors={errors} - object={nameForm} - valueHandler={setNameForm} - > - <InputWithAddon<Search> - name="name" - label={label} - tooltip={i18n.str`enter description or id`} - addonAfter={ - <span class="icon"> - <i class="mdi mdi-magnify" /> - </span> - } - > - <div> - <DropdownList - name={nameForm.name} - list={list} - onSelect={(p) => { - setNameForm({ name: "" }); - onChange(p); - }} - withImage={!!withImage} - /> - </div> - </InputWithAddon> - </FormProvider> - ); -} - -interface DropdownListProps<T extends Entity> { - name?: string; - onSelect: (p: T) => void; - list: T[]; - withImage: boolean; -} - -function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) { - const { i18n } = useTranslationContext(); - if (!name) { - /* FIXME - this BR is added to occupy the space that will be added when the - dropdown appears - */ - return ( - <div> - <br /> - </div> - ); - } - const filtered = list.filter( - (p) => p.id.includes(name) || p.description.includes(name), - ); - - return ( - <div class="dropdown is-active"> - <div - class="dropdown-menu" - id="dropdown-menu" - role="menu" - style={{ minWidth: "20rem" }} - > - <div class="dropdown-content"> - {!filtered.length ? ( - <div class="dropdown-item"> - <i18n.Translate> - no match found with that description or id - </i18n.Translate> - </div> - ) : ( - filtered.map((p) => ( - <div - key={p.id} - class="dropdown-item" - onClick={() => onSelect(p)} - style={{ cursor: "pointer" }} - > - <article class="media"> - {withImage && - <div class="media-left"> - <div class="image" style={{ minWidth: 64 }}> - <img - src={p.image ? p.image : emptyImage} - style={{ width: 64, height: 64 }} - /> - </div> - </div> - } - <div class="media-content"> - <div class="content"> - <p> - <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined} - <br /> - {p.description} - </p> - </div> - </div> - </article> - </div> - )) - )} - </div> - </div> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx deleted file mode 100644 index 4de84d984..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { FormProvider } from "./FormProvider.js"; -import { InputSecured } from "./InputSecured.js"; - -export default { - title: "Components/Form/InputSecured", - component: InputSecured, -}; - -type T = { auth_token: string | null }; - -export const InitialValueEmpty = (): VNode => { - const [state, setState] = useState<Partial<T>>({ auth_token: "" }); - return ( - <FormProvider<T> object={state} errors={{}} valueHandler={setState}> - Initial value: '' - <InputSecured<T> name="auth_token" label="Access token" /> - </FormProvider> - ); -}; - -export const InitialValueToken = (): VNode => { - const [state, setState] = useState<Partial<T>>({ auth_token: "token" }); - return ( - <FormProvider<T> object={state} errors={{}} valueHandler={setState}> - <InputSecured<T> name="auth_token" label="Access token" /> - </FormProvider> - ); -}; - -export const InitialValueNull = (): VNode => { - const [state, setState] = useState<Partial<T>>({ auth_token: null }); - return ( - <FormProvider<T> object={state} errors={{}} valueHandler={setState}> - Initial value: '' - <InputSecured<T> name="auth_token" label="Access token" /> - </FormProvider> - ); -}; diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx deleted file mode 100644 index 4a35ad96c..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { InputProps, useField } from "./useField.js"; - -export type Props<T> = InputProps<T>; - -const TokenStatus = ({ prev, post }: any) => { - const { i18n } = useTranslationContext(); - if ( - (prev === undefined || prev === null) && - (post === undefined || post === null) - ) - return null; - return prev === post ? null : post === null ? ( - <span class="tag is-danger is-align-self-center ml-2"> - <i18n.Translate>Deleting</i18n.Translate> - </span> - ) : ( - <span class="tag is-warning is-align-self-center ml-2"> - <i18n.Translate>Changing</i18n.Translate> - </span> - ); -}; - -export function InputSecured<T>({ - name, - readonly, - placeholder, - tooltip, - label, - help, -}: Props<keyof T>): VNode { - const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name); - - const [active, setActive] = useState(false); - const [newValue, setNuewValue] = useState(""); - - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - {!active ? ( - <Fragment> - <div class="field has-addons"> - <button - class="button" - onClick={(): void => { - setActive(!active); - }} - > - <div class="icon is-left"> - <i class="mdi mdi-lock-reset" /> - </div> - <span> - <i18n.Translate>Manage access token</i18n.Translate> - </span> - </button> - <TokenStatus prev={initial} post={value} /> - </div> - </Fragment> - ) : ( - <Fragment> - <div class="field has-addons"> - <div class="control"> - <a class="button is-static">secret-token:</a> - </div> - <div class="control is-expanded"> - <input - class="input" - type="text" - placeholder={placeholder} - readonly={readonly || !active} - disabled={readonly || !active} - name={String(name)} - value={newValue} - onInput={(e): void => { - setNuewValue(e.currentTarget.value); - }} - /> - {help} - </div> - <div class="control"> - <button - class="button is-info" - disabled={fromStr(newValue) === value} - onClick={(): void => { - onChange(fromStr(newValue)); - setActive(!active); - setNuewValue(""); - }} - > - <div class="icon is-left"> - <i class="mdi mdi-lock-outline" /> - </div> - <span> - <i18n.Translate>Update</i18n.Translate> - </span> - </button> - </div> - </div> - </Fragment> - )} - {error ? <p class="help is-danger">{error}</p> : null} - </div> - </div> - {active && ( - <div class="field is-horizontal"> - <div class="field-body is-flex-grow-3"> - <div class="level" style={{ width: "100%" }}> - <div class="level-right is-flex-grow-1"> - <div class="level-item"> - <button - class="button is-danger" - disabled={null === value || undefined === value} - onClick={(): void => { - onChange(null!); - setActive(!active); - setNuewValue(""); - }} - > - <div class="icon is-left"> - <i class="mdi mdi-lock-open-variant" /> - </div> - <span> - <i18n.Translate>Remove</i18n.Translate> - </span> - </button> - </div> - <div class="level-item"> - <button - class="button " - onClick={(): void => { - onChange(initial!); - setActive(!active); - setNuewValue(""); - }} - > - <div class="icon is-left"> - <i class="mdi mdi-lock-open-variant" /> - </div> - <span> - <i18n.Translate>Cancel</i18n.Translate> - </span> - </button> - </div> - </div> - </div> - </div> - </div> - )} - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx deleted file mode 100644 index d7cf04553..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { addDays } from "date-fns"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { FormProvider } from "./FormProvider.js"; -import { InputStock, Stock } from "./InputStock.js"; - -export default { - title: "Components/Form/InputStock", - component: InputStock, -}; - -type T = { stock?: Stock }; - -export const CreateStockEmpty = () => { - const [state, setState] = useState<Partial<T>>({}); - return ( - <FormProvider<T> - name="product" - object={state} - errors={{}} - valueHandler={setState} - > - <InputStock<T> name="stock" label="Stock" /> - <div> - <pre>{JSON.stringify(state, undefined, 2)}</pre> - </div> - </FormProvider> - ); -}; - -export const CreateStockUnknownRestock = () => { - const [state, setState] = useState<Partial<T>>({ - stock: { - current: 10, - lost: 0, - sold: 0, - }, - }); - return ( - <FormProvider<T> - name="product" - object={state} - errors={{}} - valueHandler={setState} - > - <InputStock<T> name="stock" label="Stock" /> - <div> - <pre>{JSON.stringify(state, undefined, 2)}</pre> - </div> - </FormProvider> - ); -}; - -export const CreateStockNoRestock = () => { - const [state, setState] = useState<Partial<T>>({ - stock: { - current: 10, - lost: 0, - sold: 0, - nextRestock: { t_s: "never" }, - }, - }); - return ( - <FormProvider<T> - name="product" - object={state} - errors={{}} - valueHandler={setState} - > - <InputStock<T> name="stock" label="Stock" /> - <div> - <pre>{JSON.stringify(state, undefined, 2)}</pre> - </div> - </FormProvider> - ); -}; - -export const CreateStockWithRestock = () => { - const [state, setState] = useState<Partial<T>>({ - stock: { - current: 15, - lost: 0, - sold: 0, - nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 }, - }, - }); - return ( - <FormProvider<T> - name="product" - object={state} - errors={{}} - valueHandler={setState} - > - <InputStock<T> name="stock" label="Stock" /> - <div> - <pre>{JSON.stringify(state, undefined, 2)}</pre> - </div> - </FormProvider> - ); -}; - -export const UpdatingProductWithManagedStock = () => { - const [state, setState] = useState<Partial<T>>({ - stock: { - current: 100, - lost: 0, - sold: 0, - nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 }, - }, - }); - return ( - <FormProvider<T> - name="product" - object={state} - errors={{}} - valueHandler={setState} - > - <InputStock<T> name="stock" label="Stock" alreadyExist /> - <div> - <pre>{JSON.stringify(state, undefined, 2)}</pre> - </div> - </FormProvider> - ); -}; - -export const UpdatingProductWithInfiniteStock = () => { - const [state, setState] = useState<Partial<T>>({}); - return ( - <FormProvider<T> - name="product" - object={state} - errors={{}} - valueHandler={setState} - > - <InputStock<T> name="stock" label="Stock" alreadyExist /> - <div> - <pre>{JSON.stringify(state, undefined, 2)}</pre> - </div> - </FormProvider> - ); -}; diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx deleted file mode 100644 index 5c98f7311..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx +++ /dev/null @@ -1,224 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h } from "preact"; -import { useLayoutEffect, useState } from "preact/hooks"; -import { MerchantBackend, Timestamp } from "../../declaration.js"; -import { FormErrors, FormProvider } from "./FormProvider.js"; -import { InputDate } from "./InputDate.js"; -import { InputGroup } from "./InputGroup.js"; -import { InputLocation } from "./InputLocation.js"; -import { InputNumber } from "./InputNumber.js"; -import { InputProps, useField } from "./useField.js"; - -export interface Props<T> extends InputProps<T> { - alreadyExist?: boolean; -} - -type Entity = Stock; - -export interface Stock { - current: number; - lost: number; - sold: number; - address?: MerchantBackend.Location; - nextRestock?: Timestamp; -} - -interface StockDelta { - incoming: number; - lost: number; -} - -export function InputStock<T>({ - name, - tooltip, - label, - alreadyExist, -}: Props<keyof T>) { - const { error, value, onChange } = useField<T>(name); - - const [errors, setErrors] = useState<FormErrors<Entity>>({}); - - const [formValue, valueHandler] = useState<Partial<Entity>>(value); - const [addedStock, setAddedStock] = useState<StockDelta>({ - incoming: 0, - lost: 0, - }); - const { i18n } = useTranslationContext(); - - useLayoutEffect(() => { - if (!formValue) { - onChange(undefined as any); - } else { - onChange({ - ...formValue, - current: (formValue?.current || 0) + addedStock.incoming, - lost: (formValue?.lost || 0) + addedStock.lost, - } as any); - } - }, [formValue, addedStock]); - - if (!formValue) { - return ( - <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field has-addons"> - {!alreadyExist ? ( - <button - class="button" - data-tooltip={i18n.str`click here to configure the stock of the product, leave it as is and the backend will not control stock`} - onClick={(): void => { - valueHandler({ - current: 0, - lost: 0, - sold: 0, - } as Stock as any); - }} - > - <span> - <i18n.Translate>Manage stock</i18n.Translate> - </span> - </button> - ) : ( - <button - class="button" - data-tooltip={i18n.str`this product has been configured without stock control`} - disabled - > - <span> - <i18n.Translate>Infinite</i18n.Translate> - </span> - </button> - )} - </div> - </div> - </div> - </Fragment> - ); - } - - const currentStock = - (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0); - - const stockAddedErrors: FormErrors<typeof addedStock> = { - lost: - currentStock + addedStock.incoming < addedStock.lost - ? i18n.str`lost cannot be greater than current and incoming (max ${ - currentStock + addedStock.incoming - })` - : undefined, - }; - - // const stockUpdateDescription = stockAddedErrors.lost ? '' : ( - // !!addedStock.incoming || !!addedStock.lost ? - // i18n.str`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` : - // i18n.str`current stock will stay at ${currentStock}` - // ) - - return ( - <Fragment> - <div class="card"> - <header class="card-header"> - <p class="card-header-title"> - {label} - {tooltip && ( - <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </p> - </header> - <div class="card-content"> - <FormProvider<Entity> - name="stock" - errors={errors} - object={formValue} - valueHandler={valueHandler} - > - {alreadyExist ? ( - <Fragment> - <FormProvider - name="added" - errors={stockAddedErrors} - object={addedStock} - valueHandler={setAddedStock as any} - > - <InputNumber name="incoming" label={i18n.str`Incoming`} /> - <InputNumber name="lost" label={i18n.str`Lost`} /> - </FormProvider> - - {/* <div class="field is-horizontal"> - <div class="field-label is-normal" /> - <div class="field-body is-flex-grow-3"> - <div class="field"> - {stockUpdateDescription} - </div> - </div> - </div> */} - </Fragment> - ) : ( - <InputNumber<Entity> - name="current" - label={i18n.str`Current`} - side={ - <button - class="button is-danger" - data-tooltip={i18n.str`remove stock control for this product`} - onClick={(): void => { - valueHandler(undefined as any); - }} - > - <span> - <i18n.Translate>without stock</i18n.Translate> - </span> - </button> - } - /> - )} - - <InputDate<Entity> - name="nextRestock" - label={i18n.str`Next restock`} - withTimestampSupport - /> - - <InputGroup<Entity> name="address" label={i18n.str`Warehouse address`}> - <InputLocation name="address" /> - </InputGroup> - </FormProvider> - </div> - </div> - </Fragment> - ); -} -// ( diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx deleted file mode 100644 index 1cd88d31a..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { h, VNode } from "preact"; -import { InputProps, useField } from "./useField.js"; - -interface Props<T> extends InputProps<T> { - readonly?: boolean; - expand?: boolean; - values: any[]; - toStr?: (v?: any) => string; - fromStr?: (s: string) => any; -} - -const defaultToString = (f?: any): string => f || ""; -const defaultFromString = (v: string): any => v as any; - -export function InputTab<T>({ - name, - readonly, - expand, - placeholder, - tooltip, - label, - help, - values, - fromStr = defaultFromString, - toStr = defaultToString, -}: Props<keyof T>): VNode { - const { error, value, onChange, required } = useField<T>(name); - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field has-icons-right"> - <p class={expand ? "control is-expanded " : "control "}> - <div class="tabs is-toggle is-fullwidth is-small"> - <ul> - {values.map((v, i) => { - return ( - <li key={i} class={value === v ? "is-active" : ""} - onClick={(e) => { onChange(v) }} - > - <a style={{ cursor: "initial" }}> - <span>{toStr(v)}</span> - </a> - </li> - ); - })} - </ul> - </div> - {help} - </p> - {required && ( - <span class="icon has-text-danger is-right" style={{ height: "2.5em" }}> - <i class="mdi mdi-alert" /> - </span> - )} - {error && <p class="help is-danger">{error}</p>} - </div> - </div> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx deleted file mode 100644 index 984b496e7..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useCallback, useState } from "preact/hooks"; -import * as yup from "yup"; -import { MerchantBackend } from "../../declaration.js"; -import { TaxSchema as schema } from "../../schemas/index.js"; -import { FormErrors, FormProvider } from "./FormProvider.js"; -import { Input } from "./Input.js"; -import { InputGroup } from "./InputGroup.js"; -import { InputProps, useField } from "./useField.js"; - -export interface Props<T> extends InputProps<T> { - isValid?: (e: any) => boolean; -} - -type Entity = MerchantBackend.Tax; -export function InputTaxes<T>({ - name, - readonly, - label, -}: Props<keyof T>): VNode { - const { value: taxes, onChange } = useField<T>(name); - - const [value, valueHandler] = useState<Partial<Entity>>({}); - // const [errors, setErrors] = useState<FormErrors<Entity>>({}) - - let errors: FormErrors<Entity> = {}; - - try { - schema.validateSync(value, { abortEarly: false }); - } catch (err) { - if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as yup.ValidationError[]; - errors = yupErrors.reduce( - (prev, cur) => - !cur.path ? prev : { ...prev, [cur.path]: cur.message }, - {}, - ); - } - } - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submit = useCallback((): void => { - onChange([value as any, ...taxes] as any); - valueHandler({}); - }, [value]); - - const { i18n } = useTranslationContext(); - - //FIXME: translating plural singular - return ( - <InputGroup - name="tax" - label={label} - alternative={ - taxes.length > 0 && ( - <p>This product has {taxes.length} applicable taxes configured.</p> - ) - } - > - <FormProvider<Entity> - name="tax" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <div class="field is-horizontal"> - <div class="field-label is-normal" /> - <div class="field-body" style={{ display: "block" }}> - {taxes.map((v: any, i: number) => ( - <div - key={i} - class="tags has-addons mt-3 mb-0 mr-3" - style={{ flexWrap: "nowrap" }} - > - <span - class="tag is-medium is-info mb-0" - style={{ maxWidth: "90%" }} - > - <b>{v.tax}</b>: {v.name} - </span> - <a - class="tag is-medium is-danger is-delete mb-0" - onClick={() => { - onChange(taxes.filter((f: any) => f !== v) as any); - valueHandler(v); - }} - /> - </div> - ))} - {!taxes.length && i18n.str`No taxes configured for this product.`} - </div> - </div> - - <Input<Entity> - name="tax" - label={i18n.str`Amount`} - tooltip={i18n.str`Taxes can be in currencies that differ from the main currency used by the merchant.`} - > - <i18n.Translate> - Enter currency and value separated with a colon, e.g. - "USD:2.3". - </i18n.Translate> - </Input> - - <Input<Entity> - name="name" - label={i18n.str`Description`} - tooltip={i18n.str`Legal name of the tax, e.g. VAT or import duties.`} - /> - - <div class="buttons is-right mt-5"> - <button - class="button is-info" - data-tooltip={i18n.str`add tax to the tax list`} - disabled={hasErrors} - onClick={submit} - > - <i18n.Translate>Add</i18n.Translate> - </button> - </div> - </FormProvider> - </InputGroup> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/form/TextField.tsx b/packages/auditor-backoffice-ui/src/components/form/TextField.tsx deleted file mode 100644 index 8f897c2d8..000000000 --- a/packages/auditor-backoffice-ui/src/components/form/TextField.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { ComponentChildren, h, VNode } from "preact"; -import { useField, InputProps } from "./useField.js"; - -interface Props<T> extends InputProps<T> { - inputType?: "text" | "number" | "multiline" | "password"; - expand?: boolean; - side?: ComponentChildren; - children: ComponentChildren; -} - -export function TextField<T>({ - name, - tooltip, - label, - expand, - help, - children, - side, -}: Props<keyof T>): VNode { - const { error } = useField<T>(name); - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && ( - <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span> - )} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p - class={ - expand - ? "control is-expanded has-icons-right" - : "control has-icons-right" - } - > - {children} - {help} - </p> - {error && <p class="help is-danger">{error}</p>} - </div> - {side} - </div> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/index.stories.ts b/packages/auditor-backoffice-ui/src/components/index.stories.ts deleted file mode 100644 index f96defc09..000000000 --- a/packages/auditor-backoffice-ui/src/components/index.stories.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -export * as payto from "./form/InputPaytoForm.stories.js"; diff --git a/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx deleted file mode 100644 index e36549e76..000000000 --- a/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useBackendContext } from "../../context/backend.js"; -import { Entity } from "../../paths/admin/create/CreatePage.js"; -import { Input } from "../form/Input.js"; -import { InputDuration } from "../form/InputDuration.js"; -import { InputGroup } from "../form/InputGroup.js"; -import { InputImage } from "../form/InputImage.js"; -import { InputLocation } from "../form/InputLocation.js"; -import { InputSelector } from "../form/InputSelector.js"; -import { InputToggle } from "../form/InputToggle.js"; -import { InputWithAddon } from "../form/InputWithAddon.js"; - -export function DefaultInstanceFormFields({ - readonlyId, - showId, -}: { - readonlyId?: boolean; - showId: boolean; -}): VNode { - const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - return ( - <Fragment> - {showId && ( - <InputWithAddon<Entity> - name="id" - addonBefore={`${backendURL}/instances/`} - readonly={readonlyId} - label={i18n.str`Identifier`} - tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`} - /> - )} - - <Input<Entity> - name="name" - label={i18n.str`Business name`} - tooltip={i18n.str`Legal name of the business represented by this instance.`} - /> - - <InputSelector<Entity> - name="user_type" - label={i18n.str`Type`} - tooltip={i18n.str`Different type of account can have different rules and requirements.`} - values={["business", "individual"]} - /> - - <Input<Entity> - name="email" - label={i18n.str`Email`} - tooltip={i18n.str`Contact email`} - /> - - <Input<Entity> - name="website" - label={i18n.str`Website URL`} - tooltip={i18n.str`URL.`} - /> - - <InputImage<Entity> - name="logo" - label={i18n.str`Logo`} - tooltip={i18n.str`Logo image.`} - /> - - <InputToggle<Entity> - name="use_stefan" - label={i18n.str`Pay transaction fee`} - tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`} - /> - - <InputGroup - name="address" - label={i18n.str`Address`} - tooltip={i18n.str`Physical location of the merchant.`} - > - <InputLocation name="address" /> - </InputGroup> - - <InputGroup - name="jurisdiction" - label={i18n.str`Jurisdiction`} - tooltip={i18n.str`Jurisdiction for legal disputes with the merchant.`} - > - <InputLocation name="jurisdiction" /> - </InputGroup> - - <InputDuration<Entity> - name="default_pay_delay" - label={i18n.str`Default payment delay`} - withForever - tooltip={i18n.str`Time customers have to pay an order before the offer expires by default.`} - /> - - <InputDuration<Entity> - name="default_wire_transfer_delay" - label={i18n.str`Default wire transfer delay`} - tooltip={i18n.str`Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.`} - withForever - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/menu/index.tsx b/packages/auditor-backoffice-ui/src/components/menu/index.tsx index 5a415ef4b..e411939c7 100644 --- a/packages/auditor-backoffice-ui/src/components/menu/index.tsx +++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx @@ -1,237 +1,242 @@ /* - 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. +This file is part of GNU Taler +(C) 2021-2024 Taler Systems S.A. - 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. +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. - 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/> - */ +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. -import { ComponentChildren, Fragment, h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { AdminPaths } from "../../AdminRoutes.js"; -import { InstancePaths } from "../../InstanceRoutes.js"; -import { Notification } from "../../utils/types.js"; -import { NavigationBar } from "./NavigationBar.js"; -import { Sidebar } from "./SideBar.js"; - -function getInstanceTitle(path: string, id: string): string { - switch (path) { - case InstancePaths.settings: - return `${id}: Settings`; - case InstancePaths.inventory_list: - return `${id}: Inventory`; - case InstancePaths.deposit_confirmation_list: - return `${id}: Deposit Confirmation`; - case InstancePaths.inventory_new: - return `${id}: New product`; - case InstancePaths.inventory_update: - return `${id}: Update product`; - case InstancePaths.interface: - return `${id}: Interface`; - default: - return ""; - } -} +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/> + +*/ -function getAdminTitle(path: string, instance: string) { - if (path === AdminPaths.new_instance) return `New instance`; - if (path === AdminPaths.list_instances) return `Instances`; - return getInstanceTitle(path, instance); +/** + * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel + */ + +import {ComponentChildren, Fragment, h, VNode} from "preact"; +import {useEffect, useState} from "preact/hooks"; +import {Paths} from "../../InstanceRoutes.js"; +import {Notification} from "../../utils/types.js"; +import {NavigationBar} from "./NavigationBar.js"; +import {Sidebar} from "./SideBar.js"; + +function getInstanceTitle(path: string): string { + switch (path) { + case Paths.key_figures: + return 'Key figures'; + case Paths.critical_errors: + return 'Critical errors'; + case Paths.operating_status: + return 'Operating status'; + case Paths.detail_view: + return 'Inconsistencies'; + case Paths.amount_arithmethic_inconsistency_list: + return `Amount arithmetic inconsistencies`; + case Paths.bad_sig_losses_list: + return `Bad sig losses`; + case Paths.balance_list: + return `Balances`; + case Paths.closure_lag_list: + return `Closure Lags`; + case Paths.coin_inconsistency_list: + return `Coin inconsistencies`; + case Paths.denomination_key_validity_withdraw_inconsistency_list: + return `Denomination key validity withdraw inconsistency`; + case Paths.denomination_pending_list: + return `Denominations pending`; + case Paths.denomination_without_sig_list: + return `Denominations without sigs`; + case Paths.deposit_confirmation_list: + return `Deposit confirmations`; + case Paths.deposit_confirmation_update: + return `Update deposit confirmation`; + case Paths.emergency_list: + return `Emergencies`; + case Paths.emergency_by_count_list: + return `Emergencies by count`; + case Paths.exchange_signkey_list: + return `Exchange signkeys`; + case Paths.fee_time_inconsistency_list: + return `Fee time inconsistencies`; + case Paths.historic_denomination_revenue_list: + return `Historic denomination revenue`; + case Paths.misattribution_in_inconsistency_list: + return `Misattribution in inconsistencies`; + case Paths.progress_list: + return `Progress`; + case Paths.purse_not_closed_inconsistency_list: + return `Purse not closed inconsistencies`; + case Paths.purse_list: + return `Purses`; + case Paths.refresh_hanging_list: + return `Refreshes hanging`; + case Paths.reserve_balance_insufficient_inconsistency_list: + return `Reserve balance insufficient inconsistencies`; + case Paths.reserve_balance_summary_wrong_inconsistency_list: + return `Reserve balance summary wrong inconsistencies`; + case Paths.reserve_in_inconsistency_list: + return `Reserves in inconsistencies`; + case Paths.reserve_not_closed_inconsistency_list: + return `Reserves not closed inconsistencies`; + case Paths.row_inconsistency_list: + return `Row inconsistencies`; + case Paths.row_minor_inconsistency_list: + return `Row minor inconsistencies`; + case Paths.wire_format_inconsistency_list: + return `Wire format inconsistencies`; + case Paths.wire_out_inconsistency_list: + return `Wire out inconsistencies`; + case Paths.settings: + return `Settings`; + default: + return ""; + } } interface MenuProps { - title?: string; - path: string; - instance: string; - admin?: boolean; - onLogout?: () => void; - onShowSettings: () => void; - setInstanceName: (s: string) => void; - isPasswordOk: boolean; + title?: string; + path: string; + onShowSettings: () => void; } function WithTitle({ - title, - children, -}: { - title: string; - children: ComponentChildren; + title, + children, + }: { + title: string; + children: ComponentChildren; }): VNode { - useEffect(() => { - document.title = `Taler Backoffice: ${title}`; - }, [title]); - return <Fragment>{children}</Fragment>; + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + return <Fragment>{children}</Fragment>; } 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; - return ( - <WithTitle title={titleWithSubtitle}> - <div - class={mobileOpen ? "has-aside-mobile-expanded" : ""} - onClick={() => setMobileOpen(false)} - > - <NavigationBar - onMobileMenu={() => setMobileOpen(!mobileOpen)} - title={titleWithSubtitle} - /> - - {onLogout && ( - <Sidebar - onShowSettings={onShowSettings} - onLogout={onLogout} - admin={admin} - mimic={mimic} - instance={instance} - mobile={mobileOpen} - isPasswordOk={isPasswordOk} - /> - )} - - {mimic && ( - <nav class="level" style={{ - zIndex: 100, - position: "fixed", - width: "50%", - marginLeft: "20%" - }}> - <div class="level-item has-text-centered has-background-warning"> - <p class="is-size-5"> - You are viewing the instance <b>"{instance}"</b>.{" "} - <a - href="#/instances" - onClick={(e) => { - setInstanceName("default"); - }} - > - go back - </a> - </p> + onShowSettings, + title, + path, + }: MenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + const titleWithSubtitle = getInstanceTitle(path.replace("app/#", "")); + return ( + <WithTitle title={titleWithSubtitle}> + <div + class={mobileOpen ? "has-aside-mobile-expanded" : ""} + onClick={() => setMobileOpen(false)} + > + <NavigationBar + onMobileMenu={() => setMobileOpen(!mobileOpen)} + title={titleWithSubtitle} + /> + + <Sidebar + onShowSettings={onShowSettings} + mobile={mobileOpen} + /> </div> - </nav> - )} - </div> - </WithTitle> - ); + </WithTitle> + ); } interface NotYetReadyAppMenuProps { - title: string; - onShowSettings: () => void; - onLogout?: () => void; - isPasswordOk: boolean; + title: string; + onShowSettings: () => void; } interface NotifProps { - notification?: Notification; + notification?: Notification; } -export function NotificationCard({ - notification: n, -}: NotifProps): VNode | null { - if (!n) return null; - return ( - <div class="notification"> - <div class="columns is-vcentered"> - <div class="column is-12"> - <article - class={ - n.type === "ERROR" - ? "message is-danger" - : n.type === "WARN" - ? "message is-warning" - : "message is-info" - } - > - <div class="message-header"> - <p>{n.message}</p> + +export function NotificationCard({notification: n}: NotifProps): VNode | null { + if (!n) return null; + return ( + <div class="notification"> + <div class="columns is-vcentered"> + <div class="column is-12"> + <article + class={ + n.type === "ERROR" + ? "message is-danger" + : n.type === "WARN" + ? "message is-warning" + : "message is-info" + } + > + <div class="message-header"> + <p>{n.message}</p> + </div> + {n.description && ( + <div class="message-body"> + <div>{n.description}</div> + {n.details && <pre>{n.details}</pre>} + </div> + )} + </article> + </div> </div> - {n.description && ( - <div class="message-body"> - <div>{n.description}</div> - {n.details && <pre>{n.details}</pre>} - </div> - )} - </article> </div> - </div> - </div> - ); + ); } interface NotConnectedAppMenuProps { - title: string; + title: string; } + export function NotConnectedAppMenu({ - title, -}: NotConnectedAppMenuProps): VNode { - const [mobileOpen, setMobileOpen] = useState(false); - - useEffect(() => { - document.title = `Taler Backoffice: ${title}`; - }, [title]); - - return ( - <div - class={mobileOpen ? "has-aside-mobile-expanded" : ""} - onClick={() => setMobileOpen(false)} - > - <NavigationBar - onMobileMenu={() => setMobileOpen(!mobileOpen)} - title={title} - /> - </div> - ); + title, + }: NotConnectedAppMenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + + return ( + <div + class={mobileOpen ? "has-aside-mobile-expanded" : ""} + onClick={() => setMobileOpen(false)} + > + <NavigationBar + onMobileMenu={() => setMobileOpen(!mobileOpen)} + title={title} + /> + </div> + ); } + export function NotYetReadyAppMenu({ - onLogout, - onShowSettings, - title, - isPasswordOk -}: NotYetReadyAppMenuProps): VNode { - const [mobileOpen, setMobileOpen] = useState(false); - - useEffect(() => { - document.title = `Taler Backoffice: ${title}`; - }, [title]); - - return ( - <div - class={mobileOpen ? "has-aside-mobile-expanded" : ""} - onClick={() => setMobileOpen(false)} - > - <NavigationBar - onMobileMenu={() => setMobileOpen(!mobileOpen)} - title={title} - /> - {onLogout && ( - <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} /> - )} - </div> - ); -} + onShowSettings, + title + }: NotYetReadyAppMenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + + return ( + <div + class={mobileOpen ? "has-aside-mobile-expanded" : ""} + onClick={() => setMobileOpen(false)} + > + <NavigationBar + onMobileMenu={() => setMobileOpen(!mobileOpen)} + title={title} + /> + ( + <Sidebar onShowSettings={onShowSettings} instance="" mobile={mobileOpen}/> + ) + </div> + ); +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx deleted file mode 100644 index 5cd8a237b..000000000 --- a/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { ComponentChildren, h, VNode } from "preact"; - -interface Props { - onCreateAnother?: () => void; - onConfirm: () => void; - children: ComponentChildren; -} - -export function CreatedSuccessfully({ - children, - onConfirm, - onCreateAnother, -}: Props): VNode { - return ( - <div class="columns is-fullwidth is-vcentered mt-3"> - <div class="column" /> - <div class="column is-four-fifths"> - <div class="card"> - <header class="card-header has-background-success"> - <p class="card-header-title has-text-white-ter">Success.</p> - </header> - <div class="card-content">{children}</div> - </div> - <div class="buttons is-right"> - {onCreateAnother && ( - <button class="button is-info" onClick={onCreateAnother}> - Create another - </button> - )} - <button class="button is-info" onClick={onConfirm}> - Continue - </button> - </div> - </div> - <div class="column" /> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx b/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx deleted file mode 100644 index d75c5ced2..000000000 --- a/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h } from "preact"; -import { Notifications } from "./index.js"; - -export default { - title: "Components/Notification", - component: Notifications, - argTypes: { - removeNotification: { action: "removeNotification" }, - }, -}; - -export const Info = (a: any) => <Notifications {...a} />; -Info.args = { - notifications: [ - { - message: "Title", - description: "Some large description", - type: "INFO", - }, - ], -}; -export const Warn = (a: any) => <Notifications {...a} />; -Warn.args = { - notifications: [ - { - message: "Title", - description: "Some large description", - type: "WARN", - }, - ], -}; -export const Error = (a: any) => <Notifications {...a} />; -Error.args = { - notifications: [ - { - message: "Title", - description: "Some large description", - type: "ERROR", - }, - ], -}; diff --git a/packages/auditor-backoffice-ui/src/components/notifications/index.tsx b/packages/auditor-backoffice-ui/src/components/notifications/index.tsx deleted file mode 100644 index 0c4e0d761..000000000 --- a/packages/auditor-backoffice-ui/src/components/notifications/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode } from "preact"; -import { MessageType, Notification } from "../../utils/types.js"; - -interface Props { - notifications: Notification[]; - removeNotification?: (n: Notification) => void; -} - -function messageStyle(type: MessageType): string { - switch (type) { - case "INFO": - return "message is-info"; - case "WARN": - return "message is-warning"; - case "ERROR": - return "message is-danger"; - case "SUCCESS": - return "message is-success"; - default: - return "message"; - } -} - -export function Notifications({ - notifications, - removeNotification, -}: Props): VNode { - return ( - <div class="toast"> - {notifications.map((n, i) => ( - <article key={i} class={messageStyle(n.type)}> - <div class="message-header"> - <p>{n.message}</p> - <button - class="delete" - onClick={() => removeNotification && removeNotification(n)} - /> - </div> - {n.description && <div class="message-body">{n.description}</div>} - </article> - ))} - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx deleted file mode 100644 index d637958cb..000000000 --- a/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx +++ /dev/null @@ -1,349 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, Component } from "preact"; - -interface Props { - closeFunction?: () => void; - dateReceiver?: (d: Date) => void; - opened?: boolean; -} -interface State { - displayedMonth: number; - displayedYear: number; - selectYearMode: boolean; - currentDate: Date; -} - -// inspired by https://codepen.io/m4r1vs/pen/MOOxyE -export class DatePicker extends Component<Props, State> { - closeDatePicker() { - this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent - } - - /** - * Gets fired when a day gets clicked. - * @param {object} e The event thrown by the <span /> element clicked - */ - dayClicked(e: any) { - const element = e.target; // the actual element clicked - - if (element.innerHTML === "") return false; // don't continue if <span /> empty - - // get date from clicked element (gets attached when rendered) - const date = new Date(element.getAttribute("data-value")); - - // update the state - this.setState({ currentDate: date }); - this.passDateToParent(date); - } - - /** - * returns days in month as array - * @param {number} month the month to display - * @param {number} year the year to display - */ - getDaysByMonth(month: number, year: number) { - const calendar = []; - - const date = new Date(year, month, 1); // month to display - - const firstDay = new Date(year, month, 1).getDay(); // first weekday of month - const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month - - let day: number | null = 0; - - // the calendar is 7*6 fields big, so 42 loops - for (let i = 0; i < 42; i++) { - if (i >= firstDay && day !== null) day = day + 1; - if (day !== null && day > lastDate) day = null; - - // append the calendar Array - calendar.push({ - day: day === 0 || day === null ? null : day, // null or number - date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date() - today: - day === now.getDate() && - month === now.getMonth() && - year === now.getFullYear(), // boolean - }); - } - - return calendar; - } - - /** - * Display previous month by updating state - */ - displayPrevMonth() { - if (this.state.displayedMonth <= 0) { - this.setState({ - displayedMonth: 11, - displayedYear: this.state.displayedYear - 1, - }); - } else { - this.setState({ - displayedMonth: this.state.displayedMonth - 1, - }); - } - } - - /** - * Display next month by updating state - */ - displayNextMonth() { - if (this.state.displayedMonth >= 11) { - this.setState({ - displayedMonth: 0, - displayedYear: this.state.displayedYear + 1, - }); - } else { - this.setState({ - displayedMonth: this.state.displayedMonth + 1, - }); - } - } - - /** - * Display the selected month (gets fired when clicking on the date string) - */ - displaySelectedMonth() { - if (this.state.selectYearMode) { - this.toggleYearSelector(); - } else { - if (!this.state.currentDate) return false; - this.setState({ - displayedMonth: this.state.currentDate.getMonth(), - displayedYear: this.state.currentDate.getFullYear(), - }); - } - } - - toggleYearSelector() { - this.setState({ selectYearMode: !this.state.selectYearMode }); - } - - changeDisplayedYear(e: any) { - const element = e.target; - this.toggleYearSelector(); - this.setState({ - displayedYear: parseInt(element.innerHTML, 10), - displayedMonth: 0, - }); - } - - /** - * Pass the selected date to parent when 'OK' is clicked - */ - passSavedDateDateToParent() { - this.passDateToParent(this.state.currentDate); - } - passDateToParent(date: Date) { - if (typeof this.props.dateReceiver === "function") - this.props.dateReceiver(date); - this.closeDatePicker(); - } - - componentDidUpdate() { - if (this.state.selectYearMode) { - document.getElementsByClassName("selected")[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it - } - } - - constructor() { - super(); - - this.closeDatePicker = this.closeDatePicker.bind(this); - this.dayClicked = this.dayClicked.bind(this); - this.displayNextMonth = this.displayNextMonth.bind(this); - this.displayPrevMonth = this.displayPrevMonth.bind(this); - this.getDaysByMonth = this.getDaysByMonth.bind(this); - this.changeDisplayedYear = this.changeDisplayedYear.bind(this); - this.passDateToParent = this.passDateToParent.bind(this); - this.toggleYearSelector = this.toggleYearSelector.bind(this); - this.displaySelectedMonth = this.displaySelectedMonth.bind(this); - - this.state = { - currentDate: now, - displayedMonth: now.getMonth(), - displayedYear: now.getFullYear(), - selectYearMode: false, - }; - } - - render() { - const { currentDate, displayedMonth, displayedYear, selectYearMode } = - this.state; - - return ( - <div> - <div class={`datePicker ${this.props.opened && "datePicker--opened"}`}> - <div class="datePicker--titles"> - <h3 - style={{ - color: selectYearMode - ? "rgba(255,255,255,.87)" - : "rgba(255,255,255,.57)", - }} - onClick={this.toggleYearSelector} - > - {currentDate.getFullYear()} - </h3> - <h2 - style={{ - color: !selectYearMode - ? "rgba(255,255,255,.87)" - : "rgba(255,255,255,.57)", - }} - onClick={this.displaySelectedMonth} - > - {dayArr[currentDate.getDay()]},{" "} - {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()} - </h2> - </div> - - {!selectYearMode && ( - <nav> - <span onClick={this.displayPrevMonth} class="icon"> - <i - style={{ transform: "rotate(180deg)" }} - class="mdi mdi-forward" - /> - </span> - <h4> - {monthArrShortFull[displayedMonth]} {displayedYear} - </h4> - <span onClick={this.displayNextMonth} class="icon"> - <i class="mdi mdi-forward" /> - </span> - </nav> - )} - - <div class="datePicker--scroll"> - {!selectYearMode && ( - <div class="datePicker--calendar"> - <div class="datePicker--dayNames"> - {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => ( - <span key={i}>{day}</span> - ))} - </div> - - <div onClick={this.dayClicked} class="datePicker--days"> - {/* - Loop through the calendar object returned by getDaysByMonth(). - */} - - {this.getDaysByMonth( - this.state.displayedMonth, - this.state.displayedYear, - ).map((day) => { - let selected = false; - - if (currentDate && day.date) - selected = - currentDate.toLocaleDateString() === - day.date.toLocaleDateString(); - - return ( - <span - key={day.day} - class={ - (day.today ? "datePicker--today " : "") + - (selected ? "datePicker--selected" : "") - } - disabled={!day.date} - data-value={day.date} - > - {day.day} - </span> - ); - })} - </div> - </div> - )} - - {selectYearMode && ( - <div class="datePicker--selectYear"> - {yearArr.map((year) => ( - <span - key={year} - class={year === displayedYear ? "selected" : ""} - onClick={this.changeDisplayedYear} - > - {year} - </span> - ))} - </div> - )} - </div> - </div> - - <div - class="datePicker--background" - onClick={this.closeDatePicker} - style={{ - display: this.props.opened ? "block" : "none", - }} - /> - </div> - ); - } -} - -const monthArrShortFull = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", -]; - -const monthArrShort = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", -]; - -const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - -const now = new Date(); - -const yearArr: number[] = []; - -for (let i = 2010; i <= now.getFullYear() + 10; i++) { - yearArr.push(i); -} diff --git a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx deleted file mode 100644 index b95ab054c..000000000 --- a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, FunctionalComponent } from "preact"; -import { useState } from "preact/hooks"; -import { DurationPicker as TestedComponent } from "./DurationPicker.js"; - -export default { - title: "Components/Picker/Duration", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - goBack: { action: "goBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, { - days: true, - minutes: true, - hours: true, - seconds: true, - value: 10000000, -}); - -export const WithState = () => { - const [v, s] = useState<number>(1000000); - return <TestedComponent value={v} onChange={s} days minutes hours seconds />; -}; diff --git a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx deleted file mode 100644 index 7c1c172ac..000000000 --- a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import "../../scss/DurationPicker.scss"; - -export interface Props { - hours?: boolean; - minutes?: boolean; - seconds?: boolean; - days?: boolean; - onChange: (value: number) => void; - value: number; -} - -// inspiration taken from https://github.com/flurmbo/react-duration-picker -export function DurationPicker({ - days, - hours, - minutes, - seconds, - onChange, - value, -}: Props): VNode { - const ss = 1000; - const ms = ss * 60; - const hs = ms * 60; - const ds = hs * 24; - const { i18n } = useTranslationContext(); - - return ( - <div class="rdp-picker"> - {days && ( - <DurationColumn - unit={i18n.str`days`} - max={99} - value={Math.floor(value / ds)} - onDecrease={value >= ds ? () => onChange(value - ds) : undefined} - onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined} - onChange={(diff) => onChange(value + diff * ds)} - /> - )} - {hours && ( - <DurationColumn - unit={i18n.str`hours`} - max={23} - min={1} - value={Math.floor(value / hs) % 24} - onDecrease={value >= hs ? () => onChange(value - hs) : undefined} - onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined} - onChange={(diff) => onChange(value + diff * hs)} - /> - )} - {minutes && ( - <DurationColumn - unit={i18n.str`minutes`} - max={59} - min={1} - value={Math.floor(value / ms) % 60} - onDecrease={value >= ms ? () => onChange(value - ms) : undefined} - onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined} - onChange={(diff) => onChange(value + diff * ms)} - /> - )} - {seconds && ( - <DurationColumn - unit={i18n.str`seconds`} - max={59} - value={Math.floor(value / ss) % 60} - onDecrease={value >= ss ? () => onChange(value - ss) : undefined} - onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined} - onChange={(diff) => onChange(value + diff * ss)} - /> - )} - </div> - ); -} - -interface ColProps { - unit: string; - min?: number; - max: number; - value: number; - onIncrease?: () => void; - onDecrease?: () => void; - onChange?: (diff: number) => void; -} - -function InputNumber({ - initial, - onChange, -}: { - initial: number; - onChange: (n: number) => void; -}) { - const [value, handler] = useState<{ v: string }>({ - v: toTwoDigitString(initial), - }); - - return ( - <input - value={value.v} - onBlur={(e) => onChange(parseInt(value.v, 10))} - onInput={(e) => { - e.preventDefault(); - const n = Number.parseInt(e.currentTarget.value, 10); - if (isNaN(n)) return handler({ v: toTwoDigitString(initial) }); - return handler({ v: toTwoDigitString(n) }); - }} - style={{ - width: 50, - border: "none", - fontSize: "inherit", - background: "inherit", - }} - /> - ); -} - -function DurationColumn({ - unit, - min = 0, - max, - value, - onIncrease, - onDecrease, - onChange, -}: ColProps): VNode { - const cellHeight = 35; - return ( - <div class="rdp-column-container"> - <div class="rdp-masked-div"> - <hr class="rdp-reticule" style={{ top: cellHeight * 2 - 1 }} /> - <hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} /> - - <div class="rdp-column" style={{ top: 0 }}> - <div class="rdp-cell" key={value - 2}> - {onDecrease && ( - <button - style={{ width: "100%", textAlign: "center", margin: 5 }} - onClick={onDecrease} - > - <span class="icon"> - <i class="mdi mdi-chevron-up" /> - </span> - </button> - )} - </div> - <div class="rdp-cell" key={value - 1}> - {value > min ? toTwoDigitString(value - 1) : ""} - </div> - <div class="rdp-cell rdp-center" key={value}> - {onChange ? ( - <InputNumber - initial={value} - onChange={(n) => onChange(n - value)} - /> - ) : ( - toTwoDigitString(value) - )} - <div>{unit}</div> - </div> - - <div class="rdp-cell" key={value + 1}> - {value < max ? toTwoDigitString(value + 1) : ""} - </div> - - <div class="rdp-cell" key={value + 2}> - {onIncrease && ( - <button - style={{ width: "100%", textAlign: "center", margin: 5 }} - onClick={onIncrease} - > - <span class="icon"> - <i class="mdi mdi-chevron-down" /> - </span> - </button> - )} - </div> - </div> - </div> - </div> - ); -} - -function toTwoDigitString(n: number) { - if (n < 10) { - return `0${n}`; - } - return `${n}`; -} diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx deleted file mode 100644 index fcc97f96a..000000000 --- a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { InventoryProductForm as TestedComponent } from "./InventoryProductForm.js"; - -export default { - title: "Components/Product/Add", - component: TestedComponent, - argTypes: { - onAddProduct: { action: "onAddProduct" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const WithASimpleList = createExample(TestedComponent, { - inventory: [ - { - id: "this id", - description: "this is the description", - } as any, - ], -}); - -export const WithAProductSelected = createExample(TestedComponent, { - inventory: [], - currentProducts: { - thisid: { - quantity: 1, - product: { - id: "asd", - description: "asdsadsad", - } as any, - }, - }, -}); diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx deleted file mode 100644 index 25b1f0e2d..000000000 --- a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { MerchantBackend, WithId } from "../../declaration.js"; -import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js"; -import { FormErrors, FormProvider } from "../form/FormProvider.js"; -import { InputNumber } from "../form/InputNumber.js"; -import { InputSearchOnList } from "../form/InputSearchOnList.js"; - -type Form = { - product: MerchantBackend.Products.ProductDetail & WithId; - quantity: number; -}; - -interface Props { - currentProducts: ProductMap; - onAddProduct: ( - product: MerchantBackend.Products.ProductDetail & WithId, - quantity: number, - ) => void; - inventory: (MerchantBackend.Products.ProductDetail & WithId)[]; -} - -export function InventoryProductForm({ - currentProducts, - onAddProduct, - inventory, -}: Props): VNode { - const initialState = { quantity: 1 }; - const [state, setState] = useState<Partial<Form>>(initialState); - const [errors, setErrors] = useState<FormErrors<Form>>({}); - - const { i18n } = useTranslationContext(); - - const productWithInfiniteStock = - state.product && state.product.total_stock === -1; - - const submit = (): void => { - if (!state.product) { - setErrors({ - product: i18n.str`You must enter a valid product identifier.`, - }); - return; - } - if (productWithInfiniteStock) { - onAddProduct(state.product, 1); - } else { - if (!state.quantity || state.quantity <= 0) { - setErrors({ quantity: i18n.str`Quantity must be greater than 0!` }); - return; - } - const currentStock = - state.product.total_stock - - state.product.total_lost - - state.product.total_sold; - const p = currentProducts[state.product.id]; - if (p) { - if (state.quantity + p.quantity > currentStock) { - const left = currentStock - p.quantity; - setErrors({ - quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`, - }); - return; - } - onAddProduct(state.product, state.quantity + p.quantity); - } else { - if (state.quantity > currentStock) { - const left = currentStock; - setErrors({ - quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`, - }); - return; - } - onAddProduct(state.product, state.quantity); - } - } - - setState(initialState); - }; - - return ( - <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> - <InputSearchOnList - label={i18n.str`Search product`} - selected={state.product} - onChange={(p) => setState((v) => ({ ...v, product: p }))} - list={inventory} - withImage - /> - {state.product && ( - <div class="columns mt-5"> - <div class="column is-two-thirds"> - {!productWithInfiniteStock && ( - <InputNumber<Form> - name="quantity" - label={i18n.str`Quantity`} - tooltip={i18n.str`how many products will be added`} - /> - )} - </div> - <div class="column"> - <div class="buttons is-right"> - <button class="button is-success" onClick={submit}> - <i18n.Translate>Add from inventory</i18n.Translate> - </button> - </div> - </div> - </div> - )} - </FormProvider> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx deleted file mode 100644 index 8ddd9fa95..000000000 --- a/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import * as yup from "yup"; -import { MerchantBackend } from "../../declaration.js"; -import { useListener } from "../../hooks/listener.js"; -import { NonInventoryProductSchema as schema } from "../../schemas/index.js"; -import { FormErrors, FormProvider } from "../form/FormProvider.js"; -import { Input } from "../form/Input.js"; -import { InputCurrency } from "../form/InputCurrency.js"; -import { InputImage } from "../form/InputImage.js"; -import { InputNumber } from "../form/InputNumber.js"; -import { InputTaxes } from "../form/InputTaxes.js"; - -type Entity = MerchantBackend.Product; - -interface Props { - onAddProduct: (p: Entity) => Promise<void>; - productToEdit?: Entity; -} -export function NonInventoryProductFrom({ - productToEdit, - onAddProduct, -}: Props): VNode { - const [showCreateProduct, setShowCreateProduct] = useState(false); - - const isEditing = !!productToEdit; - - useEffect(() => { - setShowCreateProduct(isEditing); - }, [isEditing]); - - const [submitForm, addFormSubmitter] = useListener< - Partial<MerchantBackend.Product> | undefined - >((result) => { - if (result) { - setShowCreateProduct(false); - return onAddProduct({ - quantity: result.quantity || 0, - taxes: result.taxes || [], - description: result.description || "", - image: result.image || "", - price: result.price || "", - unit: result.unit || "", - }); - } - return Promise.resolve(); - }); - - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - <div class="buttons"> - <button - class="button is-success" - data-tooltip={i18n.str`describe and add a product that is not in the inventory list`} - onClick={() => setShowCreateProduct(true)} - > - <i18n.Translate>Add custom product</i18n.Translate> - </button> - </div> - {showCreateProduct && ( - <div class="modal is-active"> - <div - class="modal-background " - onClick={() => setShowCreateProduct(false)} - /> - <div class="modal-card"> - <header class="modal-card-head"> - <p class="modal-card-title">{i18n.str`Complete information of the product`}</p> - <button - class="delete " - aria-label="close" - onClick={() => setShowCreateProduct(false)} - /> - </header> - <section class="modal-card-body"> - <ProductForm - initial={productToEdit} - onSubscribe={addFormSubmitter} - /> - </section> - <footer class="modal-card-foot"> - <div class="buttons is-right" style={{ width: "100%" }}> - <button - class="button " - onClick={() => setShowCreateProduct(false)} - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - <button - class="button is-info " - disabled={!submitForm} - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </button> - </div> - </footer> - </div> - <button - class="modal-close is-large " - aria-label="close" - onClick={() => setShowCreateProduct(false)} - /> - </div> - )} - </Fragment> - ); -} - -interface ProductProps { - onSubscribe: (c?: () => Entity | undefined) => void; - initial?: Partial<Entity>; -} - -interface NonInventoryProduct { - quantity: number; - description: string; - unit: string; - price: string; - image: string; - taxes: MerchantBackend.Tax[]; -} - -export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { - const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ - taxes: [], - ...initial, - }); - let errors: FormErrors<Entity> = {}; - try { - schema.validateSync(value, { abortEarly: false }); - } catch (err) { - if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as yup.ValidationError[]; - errors = yupErrors.reduce( - (prev, cur) => - !cur.path ? prev : { ...prev, [cur.path]: cur.message }, - {}, - ); - } - } - - const submit = useCallback((): Entity | undefined => { - return value as MerchantBackend.Product; - }, [value]); - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - useEffect(() => { - onSubscribe(hasErrors ? undefined : submit); - }, [submit, hasErrors]); - - const { i18n } = useTranslationContext(); - - return ( - <div> - <FormProvider<NonInventoryProduct> - name="product" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <InputImage<NonInventoryProduct> - name="image" - label={i18n.str`Image`} - tooltip={i18n.str`photo of the product`} - /> - <Input<NonInventoryProduct> - name="description" - inputType="multiline" - label={i18n.str`Description`} - tooltip={i18n.str`full product description`} - /> - <Input<NonInventoryProduct> - name="unit" - label={i18n.str`Unit`} - tooltip={i18n.str`name of the product unit`} - /> - <InputCurrency<NonInventoryProduct> - name="price" - label={i18n.str`Price`} - tooltip={i18n.str`amount in the current currency`} - /> - - <InputNumber<NonInventoryProduct> - name="quantity" - label={i18n.str`Quantity`} - tooltip={i18n.str`how many products will be added`} - /> - - <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} /> - </FormProvider> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx deleted file mode 100644 index 47e3431e2..000000000 --- a/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h } from "preact"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import * as yup from "yup"; -import { useBackendContext } from "../../context/backend.js"; -import { MerchantBackend } from "../../declaration.js"; -import { - ProductCreateSchema as createSchema, - ProductUpdateSchema as updateSchema, -} from "../../schemas/index.js"; -import { FormErrors, FormProvider } from "../form/FormProvider.js"; -import { Input } from "../form/Input.js"; -import { InputCurrency } from "../form/InputCurrency.js"; -import { InputImage } from "../form/InputImage.js"; -import { InputNumber } from "../form/InputNumber.js"; -import { InputStock, Stock } from "../form/InputStock.js"; -import { InputTaxes } from "../form/InputTaxes.js"; -import { InputWithAddon } from "../form/InputWithAddon.js"; - -type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; - -interface Props { - onSubscribe: (c?: () => Entity | undefined) => void; - initial?: Partial<Entity>; - alreadyExist?: boolean; -} - -export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { - const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({ - address: {}, - description_i18n: {}, - taxes: [], - next_restock: { t_s: "never" }, - price: ":0", - ...initial, - stock: - !initial || initial.total_stock === -1 - ? undefined - : { - current: initial.total_stock || 0, - lost: initial.total_lost || 0, - sold: initial.total_sold || 0, - address: initial.address, - nextRestock: initial.next_restock, - }, - }); - let errors: FormErrors<Entity> = {}; - - try { - (alreadyExist ? updateSchema : createSchema).validateSync(value, { - abortEarly: false, - }); - } catch (err) { - if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as yup.ValidationError[]; - errors = yupErrors.reduce( - (prev, cur) => - !cur.path ? prev : { ...prev, [cur.path]: cur.message }, - {}, - ); - } - } - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submit = useCallback((): Entity | undefined => { - const stock: Stock = (value as any).stock; - - if (!stock) { - value.total_stock = -1; - } else { - value.total_stock = stock.current; - value.total_lost = stock.lost; - value.next_restock = - stock.nextRestock instanceof Date - ? { t_s: stock.nextRestock.getTime() / 1000 } - : stock.nextRestock; - value.address = stock.address; - } - delete (value as any).stock; - - if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) { - delete value.minimum_age; - } - - return value as MerchantBackend.Products.ProductDetail & { - product_id: string; - }; - }, [value]); - - useEffect(() => { - onSubscribe(hasErrors ? undefined : submit); - }, [submit, hasErrors]); - - const { url: backendURL } = useBackendContext() - const { i18n } = useTranslationContext(); - - return ( - <div> - <FormProvider<Entity> - name="product" - errors={errors} - object={value} - valueHandler={valueHandler} - > - {alreadyExist ? undefined : ( - <InputWithAddon<Entity> - name="product_id" - addonBefore={`${backendURL}/product/`} - label={i18n.str`ID`} - tooltip={i18n.str`product identification to use in URLs (for internal use only)`} - /> - )} - <InputImage<Entity> - name="image" - label={i18n.str`Image`} - tooltip={i18n.str`illustration of the product for customers`} - /> - <Input<Entity> - name="description" - inputType="multiline" - label={i18n.str`Description`} - tooltip={i18n.str`product description for customers`} - /> - <InputNumber<Entity> - name="minimum_age" - label={i18n.str`Age restriction`} - tooltip={i18n.str`is this product restricted for customer below certain age?`} - help={i18n.str`minimum age of the buyer`} - /> - <Input<Entity> - name="unit" - label={i18n.str`Unit name`} - tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} - help={i18n.str`exajmple: kg, items or liters`} - /> - <InputCurrency<Entity> - name="price" - label={i18n.str`Price per unit`} - tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`} - /> - <InputStock - name="stock" - label={i18n.str`Stock`} - alreadyExist={alreadyExist} - tooltip={i18n.str`inventory for products with finite supply (for internal use only)`} - /> - <InputTaxes<Entity> - name="taxes" - label={i18n.str`Taxes`} - tooltip={i18n.str`taxes included in the product price, exposed to customers`} - /> - </FormProvider> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx deleted file mode 100644 index d89c5371b..000000000 --- a/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { Amounts } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import emptyImage from "../../assets/empty.png"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { MerchantBackend } from "../../declaration.js"; - -interface Props { - list: MerchantBackend.Product[]; - actions?: { - name: string; - tooltip: string; - handler: (d: MerchantBackend.Product, index: number) => void; - }[]; -} -export function ProductList({ list, actions = [] }: Props): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>image</i18n.Translate> - </th> - <th> - <i18n.Translate>description</i18n.Translate> - </th> - <th> - <i18n.Translate>quantity</i18n.Translate> - </th> - <th> - <i18n.Translate>unit price</i18n.Translate> - </th> - <th> - <i18n.Translate>total price</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {list.map((entry, index) => { - const unitPrice = !entry.price ? "0" : entry.price; - const totalPrice = !entry.price - ? "0" - : Amounts.stringify( - Amounts.mult( - Amounts.parseOrThrow(entry.price), - entry.quantity, - ).amount, - ); - - return ( - <tr key={index}> - <td> - <img - style={{ height: 32, width: 32 }} - src={entry.image ? entry.image : emptyImage} - /> - </td> - <td>{entry.description}</td> - <td> - {entry.quantity === 0 - ? "--" - : `${entry.quantity} ${entry.unit}`} - </td> - <td>{unitPrice}</td> - <td>{totalPrice}</td> - <td class="is-actions-cell right-sticky"> - {actions.map((a, i) => { - return ( - <div key={i} class="buttons is-right"> - <button - class="button is-small is-danger has-tooltip-left" - data-tooltip={a.tooltip} - type="button" - onClick={() => a.handler(entry, index)} - > - {a.name} - </button> - </div> - ); - })} - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/context/backend.test.ts b/packages/auditor-backoffice-ui/src/context/backend.test.ts deleted file mode 100644 index 74530e750..000000000 --- a/packages/auditor-backoffice-ui/src/context/backend.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { ComponentChildren, h, VNode } from "preact"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useAdminAPI, - useInstanceAPI, - useManagementAPI, -} from "../hooks/instance.js"; -import { expect } from "chai"; -import { ApiMockEnvironment } from "../hooks/testing.js"; -import { - API_CREATE_INSTANCE, - API_NEW_LOGIN, - API_UPDATE_CURRENT_INSTANCE_AUTH, - API_UPDATE_INSTANCE_AUTH_BY_ID, -} from "../hooks/urls.js"; - -interface TestingContextProps { - children?: ComponentChildren; -} - -describe("backend context api ", () => { - it("should use new token after updating the instance token in the settings as user", async () => { - const env = new ApiMockEnvironment(); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const instance = useInstanceAPI(); - const management = useManagementAPI("default"); - const admin = useAdminAPI(); - - return { instance, management, admin }; - }, - {}, - [ - ({ instance, management, admin }) => { - env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), { - request: { - method: "token", - token: "another_token", - }, - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "another_token", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - - }); - - management.setNewAccessToken(undefined,"another_token" as AccessToken); - }, - ({ instance, management, admin }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - // auth: "another_token", - request: { - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - - admin.createInstance({ - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should use new token after updating the instance token in the settings as admin", async () => { - const env = new ApiMockEnvironment(); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const instance = useInstanceAPI(); - const management = useManagementAPI("default"); - const admin = useAdminAPI(); - - return { instance, management, admin }; - }, - {}, - [ - ({ instance, management, admin }) => { - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { - request: { - method: "token", - token: "another_token", - }, - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "another_token", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - }); - instance.setNewAccessToken(undefined, "another_token" as AccessToken); - }, - ({ instance, management, admin }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - // auth: "another_token", - request: { - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - - admin.createInstance({ - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/context/entity.ts b/packages/auditor-backoffice-ui/src/context/entity.ts index a5f87ee02..8181931c4 100644 --- a/packages/auditor-backoffice-ui/src/context/entity.ts +++ b/packages/auditor-backoffice-ui/src/context/entity.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. + (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 diff --git a/packages/auditor-backoffice-ui/src/hooks/async.ts b/packages/auditor-backoffice-ui/src/hooks/async.ts deleted file mode 100644 index 212ef2211..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/async.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useState } from "preact/hooks"; - -export interface Options { - slowTolerance: number; -} - -export interface AsyncOperationApi<T> { - request: (...a: any) => void; - cancel: () => void; - data: T | undefined; - isSlow: boolean; - isLoading: boolean; - error: string | undefined; -} - -export function useAsync<T>( - fn?: (...args: any) => Promise<T>, - { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }, -): AsyncOperationApi<T> { - const [data, setData] = useState<T | undefined>(undefined); - const [isLoading, setLoading] = useState<boolean>(false); - const [error, setError] = useState<any>(undefined); - const [isSlow, setSlow] = useState(false); - - const request = async (...args: any) => { - if (!fn) return; - setLoading(true); - - const handler = setTimeout(() => { - setSlow(true); - }, tooLong); - - try { - const result = await fn(...args); - setData(result); - } catch (error) { - setError(error); - } - setLoading(false); - setSlow(false); - clearTimeout(handler); - }; - - function cancel(): void { - setLoading(false); - setSlow(false); - } - - return { - request, - cancel, - data, - isSlow, - isLoading, - error, - }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/bank.ts b/packages/auditor-backoffice-ui/src/hooks/bank.ts deleted file mode 100644 index 9804df654..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/bank.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } 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; - -// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = { -// "hwire1": { -// h_wire: "hwire1", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe", -// }, -// "hwire2": { -// h_wire: "hwire2", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe2", -// }, -// } - -export function useBankAccountAPI(): BankAccountAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createBankAccount = async ( - data: MerchantBackend.BankAccounts.AccountAddDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_ACCOUNTS[data.h_wire] = data - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; - - const updateBankAccount = async ( - h_wire: string, - data: MerchantBackend.BankAccounts.AccountPatchDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials - // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts/${h_wire}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; - - const deleteBankAccount = async ( - h_wire: string, - ): Promise<HttpResponseOk<void>> => { - // delete MOCKED_ACCOUNTS[h_wire] - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts/${h_wire}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; - - return { - createBankAccount, - updateBankAccount, - deleteBankAccount, - }; -} - -export interface BankAccountAPI { - createBankAccount: ( - data: MerchantBackend.BankAccounts.AccountAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateBankAccount: ( - id: string, - data: MerchantBackend.BankAccounts.AccountPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; -} - -export interface InstanceBankAccountFilter { -} - -export function useInstanceBankAccounts( - args?: InstanceBankAccountFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // loadMore() { }, - // loadMorePrev() { }, - // data: { - // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({ - // ...e, - // active: true, - // })) - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/accounts`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - 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.accounts.length < totalAfter; - const isReachingStart = false; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.accounts.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.accounts[afterData.data.accounts.length - 1] - .h_wire - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - }, - }; - - const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { accounts } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { accounts }, ...pagination }; - } - return { loading: true }; -} - -export function useBankAccountDetails( - h_wire: string, -): HttpResponse< - MerchantBackend.BankAccounts.BankAccountEntry, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // data: { - // ...MOCKED_ACCOUNTS[h_wire], - // active: true, - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/accounts/${h_wire}`], 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 }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts deleted file mode 100644 index 4f6cabc9e..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts +++ /dev/null @@ -1,741 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useAdminAPI, - useBackendInstances, - useInstanceAPI, - useInstanceDetails, - useManagementAPI, -} from "./instance.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_CREATE_INSTANCE, - API_DELETE_INSTANCE, - API_GET_CURRENT_INSTANCE, - API_LIST_INSTANCES, - API_NEW_LOGIN, - API_UPDATE_CURRENT_INSTANCE, - API_UPDATE_CURRENT_INSTANCE_AUTH, - API_UPDATE_INSTANCE_BY_ID, -} from "./urls.js"; - -describe("instance api interaction with details", () => { - it("should evict cache when updating an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useInstanceAPI(); - const query = useInstanceDetails(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - }); - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { - request: { - name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, - }); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "other_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - api.updateInstance({ - name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "other_name", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when setting the instance's token", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useInstanceAPI(); - const query = useInstanceDetails(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - }, - }); - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { - request: { - method: "token", - token: "secret", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "secret", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - }); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - api.setNewAccessToken(undefined, "secret" as AccessToken); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "secret", - }, - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when clearing the instance's token", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useInstanceAPI(); - const query = useInstanceDetails(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - }); - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { - request: { - method: "external", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, - }); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "external", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - api.clearAccessToken(undefined); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "external", - }, - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - // const { result, waitForNextUpdate } = renderHook( - // () => { - // const api = useInstanceAPI(); - // const query = useInstanceDetails(); - - // return { query, api }; - // }, - // { wrapper: TestingContext } - // ); - - // expect(result.current).not.undefined; - // if (!result.current) { - // return; - // } - // expect(result.current.query.loading).true; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - - // expect(result.current?.query.ok).true; - // if (!result.current?.query.ok) return; - - // expect(result.current.query.data).equals({ - // name: 'instance_name', - // auth: { - // method: 'token', - // token: 'not-secret', - // } - // }); - - // act(async () => { - // await result.current?.api.clearToken(); - // }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - // expect(result.current.query.ok).true; - - // expect(result.current.query.data).equals({ - // name: 'instance_name', - // auth: { - // method: 'external', - // } - // }); - }); -}); - -describe("instance admin api interaction with listing", () => { - it("should evict cache when creating a new instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useAdminAPI(); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - ], - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - request: { - name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - name: "instance_name", - } as MerchantBackend.Instances.Instance, - { - name: "other_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.createInstance({ - name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - { - name: "other_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - { - id: "the_id", - name: "second_instance", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useAdminAPI(); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); - - env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {}); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.deleteInstance("the_id"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // const { result, waitForNextUpdate } = renderHook( - // () => { - // const api = useAdminAPI(); - // const query = useBackendInstances(); - - // return { query, api }; - // }, - // { wrapper: TestingContext } - // ); - - // expect(result.current).not.undefined; - // if (!result.current) { - // return; - // } - // expect(result.current.query.loading).true; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - - // expect(result.current?.query.ok).true; - // if (!result.current?.query.ok) return; - - // expect(result.current.query.data).equals({ - // instances: [{ - // id: 'default', - // name: 'instance_name' - // }, { - // id: 'the_id', - // name: 'second_instance' - // }] - // }); - - // env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {}); - - // act(async () => { - // await result.current?.api.deleteInstance('the_id'); - // }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // env.addRequestExpectation(API_LIST_INSTANCES, { - // response: { - // instances: [{ - // id: 'default', - // name: 'instance_name' - // } as MerchantBackend.Instances.Instance] - // }, - // }); - - // expect(result.current.query.loading).false; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - // expect(result.current.query.ok).true; - - // expect(result.current.query.data).equals({ - // instances: [{ - // id: 'default', - // name: 'instance_name' - // }] - // }); - }); - - it("should evict cache when deleting (purge) an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - { - id: "the_id", - name: "second_instance", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useAdminAPI(); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); - - env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), { - qparam: { - purge: "YES", - }, - }); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.purgeInstance("the_id"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("instance management api interaction with listing", () => { - it("should evict cache when updating an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "managed", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useManagementAPI("managed"); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "instance_name", - }, - ], - }); - - env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), { - request: { - name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, - }); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "managed", - name: "other_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.updateInstance({ - name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "other_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.ts b/packages/auditor-backoffice-ui/src/hooks/instance.ts deleted file mode 100644 index 352f54982..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/instance.ts +++ /dev/null @@ -1,313 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useBackendContext } from "../context/backend.js"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useBackendBaseRequest, - useBackendInstanceRequest, - useCredentialsChecker, - useMatchMutate, -} from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -interface InstanceAPI { - updateInstance: ( - data: MerchantBackend.Instances.InstanceReconfigurationMessage, - ) => Promise<void>; - deleteInstance: () => Promise<void>; - clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>; - setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>; -} - -export function useAdminAPI(): AdminAPI { - const { request } = useBackendBaseRequest(); - const mutateAll = useMatchMutate(); - - const createInstance = async ( - instance: MerchantBackend.Instances.InstanceConfigurationMessage, - ): Promise<void> => { - await request(`/management/instances`, { - method: "POST", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - - const purgeInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - params: { - purge: "YES", - }, - }); - - mutateAll(/\/management\/instances/); - }; - - return { createInstance, deleteInstance, purgeInstance }; -} - -export interface AdminAPI { - createInstance: ( - data: MerchantBackend.Instances.InstanceConfigurationMessage, - ) => Promise<void>; - deleteInstance: (id: string) => Promise<void>; - purgeInstance: (id: string) => Promise<void>; -} - -export function useManagementAPI(instanceId: string): InstanceAPI { - const mutateAll = useMatchMutate(); - const { url: backendURL } = useBackendContext() - const { updateToken } = useBackendContext(); - const { request } = useBackendBaseRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "PATCH", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); - - mutateAll(/\/management\/instances/); - }; - - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); - - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } - - mutateAll(/\/management\/instances/); - }; - - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; -} - -export function useInstanceAPI(): InstanceAPI { - const { mutate } = useSWRConfig(); - const { url: backendURL, updateToken } = useBackendContext() - - const { - token: adminToken, - } = useBackendContext(); - const { request } = useBackendInstanceRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/private/`, { - method: "PATCH", - data: instance, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/private/`, { - method: "DELETE", - // token: adminToken, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); - - mutate([`/private/`], null); - }; - - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); - - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } - - mutate([`/private/`], null); - }; - - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; -} - -export function useInstanceDetails(): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - revalidateIfStale: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -type KYCStatus = - | { type: "ok" } - | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects }; - -export function useInstanceKYCDetails(): HttpResponse< - KYCStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error } = useSWR< - HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/kyc`], fetcher, { - refreshInterval: 60 * 1000, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateIfStale: false, - revalidateOnMount: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (data) { - if (data.info?.status === 202) - return { ok: true, data: { type: "redirect", status: data.data } }; - return { ok: true, data: { type: "ok" } }; - } - if (error) return error.cause; - return { loading: true }; -} - -export function useManagedInstanceDetails( - instanceId: string, -): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/management/instances/${instanceId}`], request, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useBackendInstances(): HttpResponse< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.InstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >(["/management/instances"], request); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/listener.ts b/packages/auditor-backoffice-ui/src/hooks/listener.ts deleted file mode 100644 index f59794fd4..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/listener.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useState } from "preact/hooks"; - -/** - * This component is used when a component wants one child to have a trigger for - * an action (a button) and other child have the action implemented (like - * gathering information with a form). The difference with other approaches is - * that in this case the parent component is not holding the state. - * - * It will return a subscriber and activator. - * - * The activator may be undefined, if it is undefined it is indicating that the - * subscriber is not ready to be called. - * - * The subscriber will receive a function (the listener) that will be call when the - * activator runs. The listener must return the collected information. - * - * As a result, when the activator is triggered by a child component, the - * @action function is called receives the information from the listener defined by other - * child component - * - * @param action from <T> to <R> - * @returns activator and subscriber, undefined activator means that there is not subscriber - */ - -export function useListener<T, R = any>( - action: (r: T) => Promise<R>, -): [undefined | (() => Promise<R>), (listener?: () => T) => void] { - type RunnerHandler = { toBeRan?: () => Promise<R> }; - const [state, setState] = useState<RunnerHandler>({}); - - /** - * subscriber will receive a method that will be call when the activator runs - * - * @param listener function to be run when the activator runs - */ - const subscriber = (listener?: () => T) => { - if (listener) { - setState({ - toBeRan: () => { - const whatWeGetFromTheListener = listener(); - return action(whatWeGetFromTheListener); - }, - }); - } else { - setState({ - toBeRan: undefined, - }); - } - }; - - /** - * activator will call runner if there is someone subscribed - */ - const activator = state.toBeRan - ? async () => { - if (state.toBeRan) { - return state.toBeRan(); - } - return Promise.reject(); - } - : undefined; - - return [activator, subscriber]; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/notifications.ts b/packages/auditor-backoffice-ui/src/hooks/notifications.ts deleted file mode 100644 index 137ef5333..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/notifications.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useState } from "preact/hooks"; -import { Notification } from "../utils/types.js"; - -interface Result { - notifications: Notification[]; - pushNotification: (n: Notification) => void; - removeNotification: (n: Notification) => void; -} - -type NotificationWithDate = Notification & { since: Date }; - -export function useNotifications( - initial: Notification[] = [], - timeout = 3000, -): Result { - const [notifications, setNotifications] = useState<NotificationWithDate[]>( - initial.map((i) => ({ ...i, since: new Date() })), - ); - - const pushNotification = (n: Notification): void => { - const entry = { ...n, since: new Date() }; - setNotifications((ns) => [...ns, entry]); - if (n.type !== "ERROR") - setTimeout(() => { - setNotifications((ns) => ns.filter((x) => x.since !== entry.since)); - }, timeout); - }; - - const removeNotification = (notif: Notification) => { - setNotifications((ns: NotificationWithDate[]) => - ns.filter((n) => n !== notif), - ); - }; - return { notifications, pushNotification, removeNotification }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/order.test.ts b/packages/auditor-backoffice-ui/src/hooks/order.test.ts deleted file mode 100644 index 86f53a342..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/order.test.ts +++ /dev/null @@ -1,587 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_CREATE_ORDER, - API_DELETE_ORDER, - API_FORGET_ORDER_BY_ID, - API_GET_ORDER_BY_ID, - API_LIST_ORDERS, - API_REFUND_ORDER_BY_ID, -} from "./urls.js"; - -describe("order api interaction with listing", () => { - it("should evict cache when creating an order", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - - env.addRequestExpectation(API_CREATE_ORDER, { - request: { - order: { amount: "ARS:12", summary: "pay me" }, - }, - response: { order_id: "3" }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "1" }, { order_id: "2" } as any, { order_id: "3" } as any], - }, - }); - - api.createOrder({ - order: { amount: "ARS:12", summary: "pay me" }, - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when doing a refund", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { orders: [{ - order_id: "1", - amount: "EUR:12", - refundable: true, - } as MerchantBackend.Orders.OrderHistoryEntry] }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: true, - }, - ], - }); - env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { - request: { - reason: "double pay", - refund: "EUR:1", - }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { orders: [ - { order_id: "1", amount: "EUR:12", refundable: false } as any, - ] }, - }); - - api.refundOrder("1", { - reason: "double pay", - refund: "EUR:1", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: false, - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting an order", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - - env.addRequestExpectation(API_DELETE_ORDER("1"), {}); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "2" } as any], - }, - }); - - api.deleteOrder("1"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "2" }], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("order api interaction with details", () => { - it("should evict cache when doing a refund", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - // qparam: { delta: 0, paid: "yes" }, - response: { - summary: "description", - refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useOrderDetails("1"); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); - env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { - request: { - reason: "double pay", - refund: "EUR:1", - }, - }); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - response: { - summary: "description", - refund_amount: "EUR:1", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - api.refundOrder("1", { - reason: "double pay", - refund: "EUR:1", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:1", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when doing a forget", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - // qparam: { delta: 0, paid: "yes" }, - response: { - summary: "description", - refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useOrderDetails("1"); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); - env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), { - request: { - fields: ["$.summary"], - }, - }); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - response: { - summary: undefined, - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - api.forgetOrder("1", { - fields: ["$.summary"], - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: undefined, - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("order listing pagination", () => { - it("should not load more if has reach the end", async () => { - const env = new ApiMockEnvironment(); - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: 20, wired: "yes", date_s: 12 }, - response: { - orders: [{ order_id: "1" } as any], - }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, wired: "yes", date_s: 13 }, - response: { - orders: [{ order_id: "2" } as any], - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; - - // should not trigger new state update or query - query.loadMore(); - query.loadMorePrev(); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should load more if result brings more that PAGE_SIZE", async () => { - const env = new ApiMockEnvironment(); - - const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ - order_id: String(i), - })); - const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ - order_id: String(i + 20), - })); - const ordersFrom20to0 = [...ordersFrom0to20].reverse(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: 20, wired: "yes", date_s: 12 }, - response: { - orders: ordersFrom0to20, - }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, wired: "yes", date_s: 13 }, - response: { - orders: ordersFrom20to40, - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [...ordersFrom20to0, ...ordersFrom20to40], - }); - expect(query.isReachingEnd).false; - expect(query.isReachingStart).false; - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -40, wired: "yes", date_s: 13 }, - response: { - orders: [...ordersFrom20to40, { order_id: "41" }], - }, - }); - - query.loadMore(); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: 40, wired: "yes", date_s: 12 }, - response: { - orders: [...ordersFrom0to20, { order_id: "-1" }], - }, - }); - - query.loadMorePrev(); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { order_id: "-1" }, - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/order.ts b/packages/auditor-backoffice-ui/src/hooks/order.ts deleted file mode 100644 index efc7bdcbe..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/order.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } 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 OrderAPI { - //FIXME: add OutOfStockResponse on 410 - createOrder: ( - data: MerchantBackend.Orders.PostOrderRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>>; - forgetOrder: ( - id: string, - data: MerchantBackend.Orders.ForgetRequest, - ) => Promise<HttpResponseOk<void>>; - refundOrder: ( - id: string, - data: MerchantBackend.Orders.RefundRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>>; - deleteOrder: (id: string) => Promise<HttpResponseOk<void>>; - getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>; -} - -type YesOrNo = "yes" | "no"; - -export function useOrderAPI(): OrderAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createOrder = async ( - data: MerchantBackend.Orders.PostOrderRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => { - const res = await request<MerchantBackend.Orders.PostOrderResponse>( - `/private/orders`, - { - method: "POST", - data, - }, - ); - await mutateAll(/.*private\/orders.*/); - // mutate('') - return res; - }; - const refundOrder = async ( - orderId: string, - data: MerchantBackend.Orders.RefundRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<MerchantBackend.Orders.MerchantRefundResponse>( - `/private/orders/${orderId}/refund`, - { - method: "POST", - data, - }, - ); - - // order list returns refundable information, so we must evict everything - await mutateAll(/.*private\/orders.*/); - return res; - }; - - const forgetOrder = async ( - orderId: string, - data: MerchantBackend.Orders.ForgetRequest, - ): Promise<HttpResponseOk<void>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<void>(`/private/orders/${orderId}/forget`, { - method: "PATCH", - data, - }); - // we may be forgetting some fields that are pare of the listing, so we must evict everything - await mutateAll(/.*private\/orders.*/); - return res; - }; - const deleteOrder = async ( - orderId: string, - ): Promise<HttpResponseOk<void>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<void>(`/private/orders/${orderId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/orders.*/); - return res; - }; - - const getPaymentURL = async ( - orderId: string, - ): Promise<HttpResponseOk<string>> => { - return request<MerchantBackend.Orders.MerchantOrderStatusResponse>( - `/private/orders/${orderId}`, - { - method: "GET", - }, - ).then((res) => { - const url = - res.data.order_status === "unpaid" - ? res.data.taler_pay_uri - : res.data.contract_terms.fulfillment_url; - const response: HttpResponseOk<string> = res as any; - response.data = url || ""; - return response; - }); - }; - - return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL }; -} - -export function useOrderDetails( - oderId: string, -): HttpResponse< - MerchantBackend.Orders.MerchantOrderStatusResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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 { - paid?: YesOrNo; - refunded?: YesOrNo; - wired?: YesOrNo; - date?: 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<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - totalBefore, - ], - orderFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/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/otp.ts b/packages/auditor-backoffice-ui/src/hooks/otp.ts deleted file mode 100644 index 76ece7055..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/otp.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } 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; - -const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = { - "1": { - otp_device_description: "first device", - otp_algorithm: 1, - otp_device_id: "1", - otp_key: "123", - }, - "2": { - otp_device_description: "second device", - otp_algorithm: 0, - otp_device_id: "2", - otp_key: "456", - } -} - -export function useOtpDeviceAPI(): OtpDeviceAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createOtpDevice = async ( - data: MerchantBackend.OTP.OtpDeviceAddDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_DEVICES[data.otp_device_id] = data - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; - - const updateOtpDevice = async ( - deviceId: string, - data: MerchantBackend.OTP.OtpDevicePatchDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm - // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr - // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description - // MOCKED_DEVICES[deviceId].otp_key = data.otp_key - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices/${deviceId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; - - const deleteOtpDevice = async ( - deviceId: string, - ): Promise<HttpResponseOk<void>> => { - // delete MOCKED_DEVICES[deviceId] - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices/${deviceId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; - - return { - createOtpDevice, - updateOtpDevice, - deleteOtpDevice, - }; -} - -export interface OtpDeviceAPI { - createOtpDevice: ( - data: MerchantBackend.OTP.OtpDeviceAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateOtpDevice: ( - id: string, - data: MerchantBackend.OTP.OtpDevicePatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; -} - -export interface InstanceOtpDeviceFilter { -} - -export function useInstanceOtpDevices( - args?: InstanceOtpDeviceFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // loadMore: () => { }, - // loadMorePrev: () => { }, - // data: { - // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({ - // device_description: d.otp_device_description, - // otp_device_id: d.otp_device_id - // })) - // } - // } - - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - 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.otp_devices.length < totalAfter; - const isReachingStart = true; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1] - .otp_device_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - }, - }; - - const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { otp_devices } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { otp_devices }, ...pagination }; - } - return { loading: true }; -} - -export function useOtpDeviceDetails( - deviceId: string, -): HttpResponse< - MerchantBackend.OTP.OtpDeviceDetails, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // data: { - // device_description: MOCKED_DEVICES[deviceId].otp_device_description, - // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm, - // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices/${deviceId}`], 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 }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/product.test.ts b/packages/auditor-backoffice-ui/src/hooks/product.test.ts deleted file mode 100644 index fd2b83ecc..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/product.test.ts +++ /dev/null @@ -1,362 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { - useInstanceProducts, - useProductAPI, - useProductDetails, -} from "./product.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_CREATE_PRODUCT, - API_DELETE_PRODUCT, - API_GET_PRODUCT_BY_ID, - API_LIST_PRODUCTS, - API_UPDATE_PRODUCT_BY_ID, -} from "./urls.js"; - -describe("product api interaction with listing", () => { - it("should evict cache when creating a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceProducts(); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); - - env.addRequestExpectation(API_CREATE_PRODUCT, { - request: { - price: "ARS:23", - } as MerchantBackend.Products.ProductAddDetail, - }); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { - price: "ARS:23", - } as MerchantBackend.Products.ProductDetail, - }); - - api.createProduct({ - price: "ARS:23", - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:12", - }, - { - id: "2345", - price: "ARS:23", - }, - ]); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when updating a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceProducts(); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); - - env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { - request: { - price: "ARS:13", - } as MerchantBackend.Products.ProductPatchDetail, - }); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:13", - } as MerchantBackend.Products.ProductDetail, - }); - - api.updateProduct("1234", { - price: "ARS:13", - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:13", - }, - ]); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceProducts(); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { id: "1234", price: "ARS:12" }, - { id: "2345", price: "ARS:23" }, - ]); - - env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, - }); - api.deleteProduct("2345"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("product api interaction with details", () => { - it("should evict cache when updating a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { - response: { - description: "this is a description", - } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useProductDetails("12"); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "this is a description", - }); - - env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { - request: { - description: "other description", - } as MerchantBackend.Products.ProductPatchDetail, - }); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { - response: { - description: "other description", - } as MerchantBackend.Products.ProductDetail, - }); - - api.updateProduct("12", { - description: "other description", - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "other description", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/product.ts b/packages/auditor-backoffice-ui/src/hooks/product.ts deleted file mode 100644 index f6da7b133..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/product.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend, WithId } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export interface ProductAPI { - getProduct: ( - id: string, - ) => Promise<void>; - createProduct: ( - data: MerchantBackend.Products.ProductAddDetail, - ) => Promise<void>; - updateProduct: ( - id: string, - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise<void>; - deleteProduct: (id: string) => Promise<void>; - lockProduct: ( - id: string, - data: MerchantBackend.Products.LockRequest, - ) => Promise<void>; -} - -export function useProductAPI(): ProductAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - - const { request } = useBackendInstanceRequest(); - - const createProduct = async ( - data: MerchantBackend.Products.ProductAddDetail, - ): Promise<void> => { - const res = await request(`/private/products`, { - method: "POST", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const updateProduct = async ( - productId: string, - data: MerchantBackend.Products.ProductPatchDetail, - ): Promise<void> => { - const r = await request(`/private/products/${productId}`, { - method: "PATCH", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const deleteProduct = async (productId: string): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "DELETE", - }); - await mutate([`/private/products`]); - }; - - const lockProduct = async ( - productId: string, - data: MerchantBackend.Products.LockRequest, - ): Promise<void> => { - await request(`/private/products/${productId}/lock`, { - method: "POST", - data, - }); - - return await mutateAll(/.*"\/private\/products.*/); - }; - - const getProduct = async ( - productId: string, - ): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "GET", - }); - - return - }; - - return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct }; -} - -export function useInstanceProducts(): HttpResponse< - (MerchantBackend.Products.ProductDetail & WithId)[], - MerchantBackend.ErrorDetail -> { - const { fetcher, multiFetcher } = useBackendInstanceRequest(); - - const { data: list, error: listError } = useSWR< - HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/products`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - const paths = (list?.data.products || []).map( - (p) => `/private/products/${p.product_id}`, - ); - const { data: products, error: productError } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>[], - RequestError<MerchantBackend.ErrorDetail> - >([paths], multiFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (listError) return listError.cause; - if (productError) return productError.cause; - - if (products) { - const dataWithId = products.map((d) => { - //take the id from the queried url - return { - ...d.data, - id: d.info?.url.replace(/.*\/private\/products\//, "") || "", - }; - }); - return { ok: true, data: dataWithId }; - } - return { loading: true }; -} - -export function useProductDetails( - productId: string, -): HttpResponse< - MerchantBackend.Products.ProductDetail, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/products/${productId}`], 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 }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts b/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts deleted file mode 100644 index 57ad5514c..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { - useInstanceReserves, - useReserveDetails, - useReservesAPI, - useRewardDetails, -} from "./reserves.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_AUTHORIZE_REWARD, - API_AUTHORIZE_REWARD_FOR_RESERVE, - API_CREATE_RESERVE, - API_DELETE_RESERVE, - API_GET_RESERVE_BY_ID, - API_GET_REWARD_BY_ID, - API_LIST_RESERVES, -} from "./urls.js"; -import * as tests from "@gnu-taler/web-util/testing"; - -describe("reserve api interaction with listing", () => { - it("should evict cache when creating a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "11" }], - }); - - env.addRequestExpectation(API_CREATE_RESERVE, { - request: { - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }, - response: { - reserve_pub: "22", - accounts: [], - }, - }); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.createReserve({ - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [ - { reserve_pub: "11" }, - { reserve_pub: "22" }, - { reserve_pub: "33" }, - ], - }); - - env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.deleteReserve("11"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "22" }, { reserve_pub: "33" }], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with details", () => { - it("should evict cache when adding a reward for a specific reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeRewardReserve("11", { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when adding a reward for a random reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD, { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeReward({ - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with reward details", () => { - it("should list rewards", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), { - response: { - total_picked_up: "USD:12", - reason: "not", - } as MerchantBackend.Rewards.RewardDetails, - qparam: { - pickups: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useRewardDetails("11"); - return { query }; - }, - {}, - [ - ({ query }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - total_picked_up: "USD:12", - reason: "not", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/reserves.ts b/packages/auditor-backoffice-ui/src/hooks/reserves.ts deleted file mode 100644 index 716b81ad4..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/reserves.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useReservesAPI(): ReserveMutateAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - const { request } = useBackendInstanceRequest(); - - const createReserve = async ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ): Promise< - HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation> - > => { - const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>( - `/private/reserves`, - { - method: "POST", - data, - }, - ); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - const authorizeRewardReserve = async ( - pub: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/reserves/${pub}/authorize-reward`, - { - method: "POST", - data, - }, - ); - - //evict reserve details query - await mutate([`/private/reserves/${pub}`]); - - return res; - }; - - const authorizeReward = async ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/rewards`, - { - method: "POST", - data, - }, - ); - - //evict all details query - await mutateAll(/.*private\/reserves\/.*/); - - return res; - }; - - const deleteReserve = async ( - pub: string, - ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => { - const res = await request<void>(`/private/reserves/${pub}`, { - method: "DELETE", - }); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve }; -} - -export interface ReserveMutateAPI { - createReserve: ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>; - authorizeRewardReserve: ( - id: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - authorizeReward: ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - deleteReserve: ( - id: string, - ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>; -} - -export function useInstanceReserves(): HttpResponse< - MerchantBackend.Rewards.RewardReserveStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves`], fetcher); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useReserveDetails( - reserveId: string, -): HttpResponse< - MerchantBackend.Rewards.ReserveDetail, - MerchantBackend.ErrorDetail -> { - const { reserveDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves/${reserveId}`], reserveDetailFetcher, { - 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 function useRewardDetails( - rewardId: string, -): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> { - const { rewardsDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, { - 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 }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/templates.ts b/packages/auditor-backoffice-ui/src/hooks/templates.ts deleted file mode 100644 index 96671452e..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/templates.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } 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 function useTemplateAPI(): TemplateAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createTemplate = async ( - data: MerchantBackend.Template.TemplateAddDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const updateTemplate = async ( - templateId: string, - data: MerchantBackend.Template.TemplatePatchDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const deleteTemplate = async ( - templateId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const createOrderFromTemplate = async ( - templateId: string, - data: MerchantBackend.Template.UsingTemplateDetails, - ): Promise< - HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse> - > => { - const res = await request<MerchantBackend.Template.UsingTemplateResponse>( - `/templates/${templateId}`, - { - method: "POST", - data, - }, - ); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const testTemplateExist = async ( - templateId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", }); - return res; - }; - - - return { - createTemplate, - updateTemplate, - deleteTemplate, - testTemplateExist, - createOrderFromTemplate, - }; -} - -export interface TemplateAPI { - createTemplate: ( - data: MerchantBackend.Template.TemplateAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateTemplate: ( - id: string, - data: MerchantBackend.Template.TemplatePatchDetails, - ) => Promise<HttpResponseOk<void>>; - testTemplateExist: ( - id: string - ) => Promise<HttpResponseOk<void>>; - deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>; - createOrderFromTemplate: ( - id: string, - data: MerchantBackend.Template.UsingTemplateDetails, - ) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>; -} - -export interface InstanceTemplateFilter { - //FIXME: add filter to the template list - position?: string; -} - -export function useInstanceTemplates( - args?: InstanceTemplateFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? 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<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail>>( - [ - `/private/templates`, - args?.position, - totalBefore, - ], - templateFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/templates`, args?.position, -totalAfter], templateFetcher); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - 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.templates.length < totalAfter; - const isReachingStart = args?.position === undefined - || - (beforeData && beforeData.data.templates.length < totalBefore); - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.templates.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.templates[afterData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.templates.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; - - // const templates = !afterData ? [] : (afterData || lastAfter).data.templates; - const templates = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.templates - .slice() - .reverse() - .concat((afterData || lastAfter).data.templates); - if (loadingAfter || loadingBefore) - return { loading: true, data: { templates } }; - if (beforeData && afterData) { - return { ok: true, data: { templates }, ...pagination }; - } - return { loading: true }; -} - -export function useTemplateDetails( - templateId: string, -): HttpResponse< - MerchantBackend.Template.TemplateDetails, - MerchantBackend.ErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/templates/${templateId}`], templateFetcher, { - 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 }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/testing.tsx b/packages/auditor-backoffice-ui/src/hooks/testing.tsx deleted file mode 100644 index 9d63e78db..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/testing.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { MockEnvironment } from "@gnu-taler/web-util/testing"; -import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; -import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; -import { SWRConfig } from "swr"; -import { ApiContextProvider } from "@gnu-taler/web-util/browser"; -import { BackendContextProvider } from "../context/backend.js"; -import { InstanceContextProvider } from "../context/instance.js"; -import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser"; -import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util"; - -export class ApiMockEnvironment extends MockEnvironment { - constructor(debug = false) { - super(debug); - } - - mockApiIfNeeded(): void { - null; // do nothing - } - - public buildTestingContext(): FunctionalComponent<{ - children: ComponentChildren; - }> { - const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE = - this.saveRequestAndGetMockedResponse.bind(this); - - return function TestingContext({ - children, - }: { - children: ComponentChildren; - }): VNode { - - async function request<T>( - base: string, - path: string, - options: RequestOptions = {}, - ): Promise<HttpResponseOk<T>> { - const _url = new URL(`${base}${path}`); - // Object.entries(options.params ?? {}).forEach(([key, value]) => { - // _url.searchParams.set(key, String(value)); - // }); - - const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE( - { - method: options.method ?? "GET", - url: _url.href, - }, - { - qparam: options.params, - auth: options.token, - request: options.data, - }, - ); - const status = mocked.expectedQuery?.query.code ?? 200; - const requestPayload = mocked.expectedQuery?.params?.request; - const responsePayload = mocked.expectedQuery?.params?.response; - - return { - ok: true, - data: responsePayload as T, - loading: false, - clientError: false, - serverError: false, - info: { - hasToken: !!options.token, - status, - url: _url.href, - payload: options.data, - options: {}, - }, - }; - } - const SC: any = SWRConfig; - - const mockHttpClient = new class implements HttpRequestLibrary { - async fetch(url: string, options?: HttpRequestOptions | undefined): Promise<HttpResponse> { - const _url = new URL(url); - const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE( - { - method: options?.method ?? "GET", - url: _url.href, - }, - { - qparam: _url.searchParams, - auth: options as any, - request: options?.body as any, - }, - ); - const status = mocked.expectedQuery?.query.code ?? 200; - const requestPayload = mocked.expectedQuery?.params?.request; - const responsePayload = mocked.expectedQuery?.params?.response; - - // FIXME: complete this implementation to mock any query - const resp: HttpResponse = { - requestUrl: _url.href, - status: status, - headers: {} as any, - requestMethod: options?.method ?? "GET", - json: async () => responsePayload, - text: async () => responsePayload as any as string, - bytes: async () => responsePayload as ArrayBuffer, - }; - return resp - } - get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { - return this.fetch(url, { - method: "GET", - ...opt, - }); - } - - postJson( - url: string, - body: any, - opt?: HttpRequestOptions, - ): Promise<HttpResponse> { - return this.fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - ...opt, - }); - } - - } - const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient) - const bankIntegration = new TalerBankIntegrationHttpClient(bankCore.getIntegrationAPI().href, mockHttpClient) - const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, mockHttpClient) - const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient) - - return ( - <BackendContextProvider defaultUrl="http://backend"> - <InstanceContextProvider - value={{ - token: undefined, - id: "default", - admin: true, - changeToken: () => null, - }} - > - <ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}> - <SC - value={{ - loadingTimeout: 0, - dedupingInterval: 0, - shouldRetryOnError: false, - errorRetryInterval: 0, - errorRetryCount: 0, - provider: () => new Map(), - }} - > - {children} - </SC> - </ApiContextProvider> - </InstanceContextProvider> - </BackendContextProvider> - ); - }; - } -} diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts deleted file mode 100644 index 2fd11f02e..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { useInstanceTransfers, useTransferAPI } from "./transfer.js"; - -describe("transfer api interaction with listing", () => { - it("should evict cache when informing a transfer", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20 }, - response: { - transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], - }, - }); - - const moveCursor = (d: string) => { - console.log("new position", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceTransfers({}, moveCursor); - const api = useTransferAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }], - }); - - env.addRequestExpectation(API_INFORM_TRANSFERS, { - request: { - wtid: "3", - credit_amount: "EUR:1", - exchange_url: "exchange.url", - payto_uri: "payto://", - }, - response: { total: "" } as any, - }); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20 }, - response: { - transfers: [{ wtid: "3" } as any, { wtid: "2" } as any], - }, - }); - - api.informTransfer({ - wtid: "3", - credit_amount: "EUR:1", - exchange_url: "exchange.url", - payto_uri: "payto://", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - transfers: [{ wtid: "3" }, { wtid: "2" }], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("transfer listing pagination", () => { - it("should not load more if has reach the end", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20, payto_uri: "payto://" }, - response: { - transfers: [{ wtid: "2" }, { wtid: "1" } as any], - }, - }); - - const moveCursor = (d: string) => { - console.log("new position", d); - }; - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor); - }, - {}, - [ - (query) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - (query) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }, { wtid: "1" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; - - //check that this button won't trigger more updates since - //has reach end and start - query.loadMore(); - query.loadMorePrev(); - }, - ], - env.buildTestingContext(), - ); - - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - expect(hookBehavior).deep.eq({ result: "ok" }); - }); - - it("should load more if result brings more that PAGE_SIZE", async () => { - const env = new ApiMockEnvironment(); - - const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ - wtid: String(i), - })); - const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ - wtid: String(i + 20), - })); - const transfersFrom20to0 = [...transfersFrom0to20].reverse(); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: 20, payto_uri: "payto://", offset: "1" }, - response: { - transfers: transfersFrom0to20, - }, - }); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20, payto_uri: "payto://", offset: "1" }, - response: { - transfers: transfersFrom20to40, - }, - }); - - const moveCursor = (d: string) => { - console.log("new position", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - return useInstanceTransfers( - { payto_uri: "payto://", position: "1" }, - moveCursor, - ); - }, - {}, - [ - (result) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(result.loading).true; - }, - (result) => { - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [...transfersFrom20to0, ...transfersFrom20to40], - }); - expect(result.isReachingEnd).false; - expect(result.isReachingStart).false; - - //query more - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -40, payto_uri: "payto://", offset: "1" }, - response: { - transfers: [...transfersFrom20to40, { wtid: "41" }], - }, - }); - result.loadMore(); - }, - (result) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(result.loading).true; - }, - (result) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [ - ...transfersFrom20to0, - ...transfersFrom20to40, - { wtid: "41" }, - ], - }); - expect(result.isReachingEnd).true; - expect(result.isReachingStart).false; - }, - ], - env.buildTestingContext(), - ); - - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - expect(hookBehavior).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.ts deleted file mode 100644 index 924bd202f..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/transfer.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } 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 function useTransferAPI(): TransferAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const informTransfer = async ( - data: MerchantBackend.Transfers.TransferInformation, - ): Promise<HttpResponseOk<{}>> => { - const res = await request<{}>(`/private/transfers`, { - method: "POST", - data, - }); - - await mutateAll(/.*private\/transfers.*/); - return res; - }; - - return { informTransfer }; -} - -export interface TransferAPI { - informTransfer: ( - data: MerchantBackend.Transfers.TransferInformation, - ) => Promise<HttpResponseOk<{}>>; -} - -export interface InstanceTransferFilter { - payto_uri?: string; - verified?: "yes" | "no"; - position?: string; -} - -export function useInstanceTransfers( - args?: InstanceTransferFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail -> { - const { transferFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position !== undefined ? 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<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - totalBefore, - ], - transferFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - -totalAfter, - ], - transferFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - 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.transfers.length < totalAfter; - const isReachingStart = - args?.position === undefined || - (beforeData && beforeData.data.transfers.length < totalBefore); - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.transfers.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${ - afterData.data.transfers[afterData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.transfers.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${ - beforeData.data.transfers[beforeData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; - - const transfers = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.transfers - .slice() - .reverse() - .concat((afterData || lastAfter).data.transfers); - if (loadingAfter || loadingBefore) - return { loading: true, data: { transfers } }; - if (beforeData && afterData) { - return { ok: true, data: { transfers }, ...pagination }; - } - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/urls.ts b/packages/auditor-backoffice-ui/src/hooks/urls.ts deleted file mode 100644 index 76c117224..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/urls.ts +++ /dev/null @@ -1,303 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { Query } from "@gnu-taler/web-util/testing"; -import { MerchantBackend } from "../declaration.js"; - -//////////////////// -// ORDER -//////////////////// - -export const API_CREATE_ORDER: Query< - MerchantBackend.Orders.PostOrderRequest, - MerchantBackend.Orders.PostOrderResponse -> = { - method: "POST", - url: "http://backend/instances/default/private/orders", -}; - -export const API_GET_ORDER_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({ - method: "GET", - url: `http://backend/instances/default/private/orders/${id}`, -}); - -export const API_LIST_ORDERS: Query< - unknown, - MerchantBackend.Orders.OrderHistory -> = { - method: "GET", - url: "http://backend/instances/default/private/orders", -}; - -export const API_REFUND_ORDER_BY_ID = ( - id: string, -): Query< - MerchantBackend.Orders.RefundRequest, - MerchantBackend.Orders.MerchantRefundResponse -> => ({ - method: "POST", - url: `http://backend/instances/default/private/orders/${id}/refund`, -}); - -export const API_FORGET_ORDER_BY_ID = ( - id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ - method: "PATCH", - url: `http://backend/instances/default/private/orders/${id}/forget`, -}); - -export const API_DELETE_ORDER = ( - id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/orders/${id}`, -}); - -//////////////////// -// TRANSFER -//////////////////// - -export const API_LIST_TRANSFERS: Query< - unknown, - MerchantBackend.Transfers.TransferList -> = { - method: "GET", - url: "http://backend/instances/default/private/transfers", -}; - -export const API_INFORM_TRANSFERS: Query< - MerchantBackend.Transfers.TransferInformation, - {} -> = { - method: "POST", - url: "http://backend/instances/default/private/transfers", -}; - -//////////////////// -// PRODUCT -//////////////////// - -export const API_CREATE_PRODUCT: Query< - MerchantBackend.Products.ProductAddDetail, - unknown -> = { - method: "POST", - url: "http://backend/instances/default/private/products", -}; - -export const API_LIST_PRODUCTS: Query< - unknown, - MerchantBackend.Products.InventorySummaryResponse -> = { - method: "GET", - url: "http://backend/instances/default/private/products", -}; - -export const API_GET_PRODUCT_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ - method: "GET", - url: `http://backend/instances/default/private/products/${id}`, -}); - -export const API_UPDATE_PRODUCT_BY_ID = ( - id: string, -): Query< - MerchantBackend.Products.ProductPatchDetail, - MerchantBackend.Products.InventorySummaryResponse -> => ({ - method: "PATCH", - url: `http://backend/instances/default/private/products/${id}`, -}); - -export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/products/${id}`, -}); - -//////////////////// -// RESERVES -//////////////////// - -export const API_CREATE_RESERVE: Query< - MerchantBackend.Rewards.ReserveCreateRequest, - MerchantBackend.Rewards.ReserveCreateConfirmation -> = { - method: "POST", - url: "http://backend/instances/default/private/reserves", -}; -export const API_LIST_RESERVES: Query< - unknown, - MerchantBackend.Rewards.RewardReserveStatus -> = { - method: "GET", - url: "http://backend/instances/default/private/reserves", -}; - -export const API_GET_RESERVE_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({ - method: "GET", - url: `http://backend/instances/default/private/reserves/${pub}`, -}); - -export const API_GET_REWARD_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({ - method: "GET", - url: `http://backend/instances/default/private/rewards/${pub}`, -}); - -export const API_AUTHORIZE_REWARD_FOR_RESERVE = ( - pub: string, -): Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> => ({ - method: "POST", - url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`, -}); - -export const API_AUTHORIZE_REWARD: Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> = { - method: "POST", - url: `http://backend/instances/default/private/rewards`, -}; - -export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/reserves/${id}`, -}); - -//////////////////// -// INSTANCE ADMIN -//////////////////// - -export const API_CREATE_INSTANCE: Query< - MerchantBackend.Instances.InstanceConfigurationMessage, - unknown -> = { - method: "POST", - url: "http://backend/management/instances", -}; - -export const API_GET_INSTANCE_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({ - method: "GET", - url: `http://backend/management/instances/${id}`, -}); - -export const API_GET_INSTANCE_KYC_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({ - method: "GET", - url: `http://backend/management/instances/${id}/kyc`, -}); - -export const API_LIST_INSTANCES: Query< - unknown, - MerchantBackend.Instances.InstancesResponse -> = { - method: "GET", - url: "http://backend/management/instances", -}; - -export const API_UPDATE_INSTANCE_BY_ID = ( - id: string, -): Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, - unknown -> => ({ - method: "PATCH", - url: `http://backend/management/instances/${id}`, -}); - -export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( - id: string, -): Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, - unknown -> => ({ - method: "POST", - url: `http://backend/management/instances/${id}/auth`, -}); - -export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/management/instances/${id}`, -}); - -//////////////////// -// AUTH -//////////////////// - -export const API_NEW_LOGIN: Query< - MerchantBackend.Instances.LoginTokenRequest, - unknown -> = ({ - method: "POST", - url: `http://backend/private/token`, -}); - -//////////////////// -// INSTANCE -//////////////////// - -export const API_GET_CURRENT_INSTANCE: Query< - unknown, - MerchantBackend.Instances.QueryInstancesResponse -> = { - method: "GET", - url: `http://backend/instances/default/private/`, -}; - -export const API_GET_CURRENT_INSTANCE_KYC: Query< - unknown, - MerchantBackend.KYC.AccountKycRedirects -> = { - method: "GET", - url: `http://backend/instances/default/private/kyc`, -}; - -export const API_UPDATE_CURRENT_INSTANCE: Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, - unknown -> = { - method: "PATCH", - url: `http://backend/instances/default/private/`, -}; - -export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, - unknown -> = { - method: "POST", - url: `http://backend/instances/default/private/auth`, -}; - -export const API_DELETE_CURRENT_INSTANCE: Query<unknown, unknown> = { - method: "DELETE", - url: `http://backend/instances/default/private`, -}; diff --git a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts deleted file mode 100644 index 994bfdbb0..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } 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 function useWebhookAPI(): WebhookAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createWebhook = async ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; - - const updateWebhook = async ( - webhookId: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; - - const deleteWebhook = async ( - webhookId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; - - return { createWebhook, updateWebhook, deleteWebhook }; -} - -export interface WebhookAPI { - createWebhook: ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateWebhook: ( - id: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>; -} - -export interface InstanceWebhookFilter { - //FIXME: add filter to the webhook list - position?: string; -} - -export function useInstanceWebhooks( - args?: InstanceWebhookFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; - - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); - - if (afterError) return afterError.cause; - - const isReachingEnd = - afterData && afterData.data.webhooks.length < totalAfter; - const isReachingStart = true; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.webhooks.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${ - afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - return; - }, - }; - - const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks; - - if (loadingAfter) return { loading: true, data: { webhooks } }; - if (afterData) { - return { ok: true, data: { webhooks }, ...pagination }; - } - return { loading: true }; -} - -export function useWebhookDetails( - webhookId: string, -): HttpResponse< - MerchantBackend.Webhooks.WebhookDetails, - MerchantBackend.ErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/webhooks/${webhookId}`], webhookFetcher, { - 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 }; -} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx deleted file mode 100644 index b3d0bdb18..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Instance/Create", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - goBack: { action: "goBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => ( - <ConfigContextProvider - value={{ - currency: "ARS", - version: "1", - }} - > - <Component {...args} /> - </ConfigContextProvider> - ); - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, {}); -// export const Example = (a: any): VNode => <CreatePage {...a} />; -// Example.args = { -// isLoading: false -// } diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx deleted file mode 100644 index b13b0e5dd..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx +++ /dev/null @@ -1,257 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../components/form/FormProvider.js"; -import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { MerchantBackend } from "../../../declaration.js"; -import { INSTANCE_ID_REGEX } from "../../../utils/constants.js"; -import { undefinedIfEmpty } from "../../../utils/table.js"; -import { SetTokenNewInstanceModal } from "../../../components/modal/index.js"; -import { Duration } from "@gnu-taler/taler-util"; - -export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceConfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { - auth_token?: string; - default_pay_delay: Duration, - default_wire_transfer_delay: Duration, -}; - -interface Props { - onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>; - onBack?: () => void; - forceId?: string; -} - -function with_defaults(id?: string): Partial<Entity> { - return { - id, - // accounts: [], - user_type: "business", - use_stefan: true, - default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours - default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days - }; -} - -export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { - const [value, valueHandler] = useState(with_defaults(forceId)); - const [isTokenSet, updateIsTokenSet] = useState<boolean>(false); - const [isTokenDialogActive, updateIsTokenDialogActive] = - useState<boolean>(false); - - const { i18n } = useTranslationContext(); - - const errors: FormErrors<Entity> = { - id: !value.id - ? i18n.str`required` - : !INSTANCE_ID_REGEX.test(value.id) - ? i18n.str`is not valid` - : undefined, - name: !value.name ? i18n.str`required` : undefined, - - user_type: !value.user_type - ? i18n.str`required` - : value.user_type !== "business" && value.user_type !== "individual" - ? i18n.str`should be business or individual` - : undefined, - // accounts: - // !value.accounts || !value.accounts.length - // ? i18n.str`required` - // : undefinedIfEmpty( - // value.accounts.map((p) => { - // return !PAYTO_REGEX.test(p.payto_uri) - // ? i18n.str`is not valid` - // : undefined; - // }), - // ), - default_pay_delay: !value.default_pay_delay - ? i18n.str`required` - : !!value.default_wire_transfer_delay && - value.default_wire_transfer_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? - i18n.str`pay delay can't be greater than wire transfer delay` : undefined, - default_wire_transfer_delay: !value.default_wire_transfer_delay - ? i18n.str`required` - : undefined, - address: undefinedIfEmpty({ - address_lines: - value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` - : undefined, - }), - jurisdiction: undefinedIfEmpty({ - address_lines: - value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` - : undefined, - }), - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submit = (): Promise<void> => { - // use conversion instead of this - const newToken = value.auth_token; - value.auth_token = undefined; - value.auth = newToken === null || newToken === undefined - ? { method: "external" } - : { method: "token", token: `secret-token:${newToken}` }; - if (!value.address) value.address = {}; - if (!value.jurisdiction) value.jurisdiction = {}; - // remove above use conversion - // schema.validateSync(value, { abortEarly: false }) - value.default_pay_delay = Duration.toTalerProtocolDuration(value.default_pay_delay!) as any - value.default_wire_transfer_delay = Duration.toTalerProtocolDuration(value.default_wire_transfer_delay!) as any - // delete value.default_pay_delay; - // delete value.default_wire_transfer_delay; - - return onCreate(value as any as MerchantBackend.Instances.InstanceConfigurationMessage); - }; - - function updateToken(token: string | null) { - valueHandler((old) => ({ - ...old, - auth_token: token === null ? undefined : token, - })); - } - - return ( - <div> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - {isTokenDialogActive && ( - <SetTokenNewInstanceModal - onCancel={() => { - updateIsTokenDialogActive(false); - updateIsTokenSet(false); - }} - onClear={() => { - updateToken(null); - updateIsTokenDialogActive(false); - updateIsTokenSet(true); - }} - onConfirm={(newToken) => { - updateToken(newToken); - updateIsTokenDialogActive(false); - updateIsTokenSet(true); - }} - /> - )} - </div> - <div class="column" /> - </div> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider<Entity> - errors={errors} - object={value} - valueHandler={valueHandler} - > - <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} /> - </FormProvider> - - <div class="level"> - <div class="level-item has-text-centered"> - <h1 class="title"> - <button - class={ - !isTokenSet - ? "button is-danger has-tooltip-bottom" - : !value.auth_token - ? "button has-tooltip-bottom" - : "button is-info has-tooltip-bottom" - } - data-tooltip={i18n.str`change authorization configuration`} - onClick={() => updateIsTokenDialogActive(true)} - > - <div class="icon is-centered"> - <i class="mdi mdi-lock-reset" /> - </div> - <span> - <i18n.Translate>Set access token</i18n.Translate> - </span> - </button> - </h1> - </div> - </div> - <div class="level"> - <div class="level-item has-text-centered"> - {!isTokenSet ? ( - <p class="is-size-6"> - <i18n.Translate> - Access token is not yet configured. This instance can't be - created. - </i18n.Translate> - </p> - ) : value.auth_token === undefined ? ( - <p class="is-size-6"> - <i18n.Translate> - No access token. Authorization must be handled externally. - </i18n.Translate> - </p> - ) : ( - <p class="is-size-6"> - <i18n.Translate> - Access token is set. Authorization is handled by the - merchant backend. - </i18n.Translate> - </p> - )} - </div> - </div> - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - onClick={submit} - disabled={hasErrors || !isTokenSet} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields and choose authorization method` - : "confirm operation" - } - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx deleted file mode 100644 index 939f9b06a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { h, VNode } from "preact"; -import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully.js"; -import { Entity } from "./index.js"; - -export function InstanceCreatedSuccessfully({ - entity, - onConfirm, -}: { - entity: Entity; - onConfirm: () => void; -}): VNode { - return ( - <CreatedSuccessfully onConfirm={onConfirm}> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">ID</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={entity.id} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Business Name</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={entity.name} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Access token</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - {entity.auth.method === "external" && "external"} - {entity.auth.method === "token" && ( - <input class="input" readonly value={entity.auth.token} /> - )} - </p> - </div> - </div> - </div> - </CreatedSuccessfully> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx deleted file mode 100644 index 19f857ad9..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../components/menu/index.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; -import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js"; -import { Notification } from "../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; -import { useCredentialsChecker } from "../../../hooks/backend.js"; -import { useBackendContext } from "../../../context/backend.js"; - -interface Props { - onBack?: () => void; - onConfirm: () => void; - forceId?: string; -} -export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; - -export default function Create({ onBack, onConfirm, forceId }: Props): VNode { - const { createInstance } = useAdminAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - const { requestNewLoginToken } = useCredentialsChecker() - const { url: backendURL, updateToken } = useBackendContext() - - return ( - <Fragment> - <NotificationCard notification={notif} /> - - <CreatePage - onBack={onBack} - forceId={forceId} - onCreate={async ( - d: MerchantBackend.Instances.InstanceConfigurationMessage, - ) => { - try { - await createInstance(d) - if (d.auth.token) { - const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } - } - onConfirm(); - } catch (ex) { - if (ex instanceof Error) { - setNotif({ - message: i18n.str`Failed to create instance`, - type: "ERROR", - description: ex.message, - }); - } else { - console.error(ex) - } - } - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx deleted file mode 100644 index 4091eea42..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Instance/Create", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - goBack: { action: "goBack" }, - }, -}; - -function createExample<Props>( - Internal: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const component = (args: any) => ( - <ConfigContextProvider - value={{ - currency: "TESTKUDOS", - version: "1", - }} - > - <Internal {...(props as any)} /> - </ConfigContextProvider> - ); - return { component, props }; -} - -export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts deleted file mode 100644 index 3c16c0e57..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -// export * as list from "./list/stories.js"; -export * as create from "./create/stories.js"; diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx deleted file mode 100644 index e6530ec25..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx +++ /dev/null @@ -1,287 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../declaration.js"; - -interface Props { - instances: MerchantBackend.Instances.Instance[]; - onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - onPurge: (id: MerchantBackend.Instances.Instance) => void; - onCreate: () => void; - selected?: boolean; - setInstanceName: (s: string) => void; -} - -export function CardTable({ - instances, - onCreate, - onUpdate, - onPurge, - setInstanceName, - onDelete, - selected, -}: Props): VNode { - const [actionQueue, actionQueueHandler] = useState<Actions[]>([]); - const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - - useEffect(() => { - if ( - actionQueue.length > 0 && - !selected && - actionQueue[0].type == "DELETE" - ) { - onDelete(actionQueue[0].element); - actionQueueHandler(actionQueue.slice(1)); - } - }, [actionQueue, selected, onDelete]); - - useEffect(() => { - if ( - actionQueue.length > 0 && - !selected && - actionQueue[0].type == "UPDATE" - ) { - onUpdate(actionQueue[0].element.id); - actionQueueHandler(actionQueue.slice(1)); - } - }, [actionQueue, selected, onUpdate]); - - const { i18n } = useTranslationContext(); - - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-desktop-mac" /> - </span> - <i18n.Translate>Instances</i18n.Translate> - </p> - - <div class="card-header-icon" aria-label="more options"> - <button - class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} - type="button" - onClick={(): void => - actionQueueHandler( - buildActions(instances, rowSelection, "DELETE"), - ) - } - > - <i18n.Translate>Delete</i18n.Translate> - </button> - </div> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new instance`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {instances.length > 0 ? ( - <Table - instances={instances} - onPurge={onPurge} - onUpdate={onUpdate} - setInstanceName={setInstanceName} - onDelete={onDelete} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string[]; - instances: MerchantBackend.Instances.Instance[]; - onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - onPurge: (id: MerchantBackend.Instances.Instance) => void; - rowSelectionHandler: StateUpdater<string[]>; - setInstanceName: (s: string) => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - -function Table({ - rowSelection, - rowSelectionHandler, - setInstanceName, - instances, - onUpdate, - onDelete, - onPurge, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input - type="checkbox" - checked={rowSelection.length === instances.length} - onClick={(): void => - rowSelectionHandler( - rowSelection.length === instances.length - ? [] - : instances.map((i) => i.id), - ) - } - /> - <span class="check" /> - </label> - </th> - <th> - <i18n.Translate>ID</i18n.Translate> - </th> - <th> - <i18n.Translate>Name</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.id}> - <td class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input - type="checkbox" - checked={rowSelection.indexOf(i.id) != -1} - onClick={(): void => - rowSelectionHandler(toggleSelected(i.id)) - } - /> - <span class="check" /> - </label> - </td> - <td> - <a - href={`#/orders?instance=${i.id}`} - onClick={(e) => { - setInstanceName(i.id); - }} - > - {i.id} - </a> - </td> - <td>{i.name}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-small is-success jb-modal" - type="button" - onClick={(): void => onUpdate(i.id)} - > - <i18n.Translate>Edit</i18n.Translate> - </button> - {!i.deleted && ( - <button - class="button is-small is-danger jb-modal is-outlined" - type="button" - onClick={(): void => onDelete(i)} - > - <i18n.Translate>Delete</i18n.Translate> - </button> - )} - {i.deleted && ( - <button - class="button is-small is-danger jb-modal" - type="button" - onClick={(): void => onPurge(i)} - > - <i18n.Translate>Purge</i18n.Translate> - </button> - )} - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no instances yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} - -interface Actions { - element: MerchantBackend.Instances.Instance; - type: "DELETE" | "UPDATE"; -} - -function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { - return value !== null && value !== undefined; -} - -function buildActions( - instances: MerchantBackend.Instances.Instance[], - selected: string[], - action: "DELETE", -): Actions[] { - return selected - .map((id) => instances.find((i) => i.id === id)) - .filter(notEmpty) - .map((id) => ({ element: id, type: action })); -} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx deleted file mode 100644 index c4c0996f6..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h } from "preact"; -import { View } from "./View.js"; - -export default { - title: "Pages/Instance/List", - component: View, - argTypes: { - onSelect: { action: "onSelect" }, - }, -}; - -export const Empty = (a: any) => <View {...a} />; -Empty.args = { - instances: [], -}; - -export const WithDefaultInstance = (a: any) => <View {...a} />; -WithDefaultInstance.args = { - instances: [ - { - id: "default", - name: "the default instance", - merchant_pub: "abcdef", - payment_targets: [], - }, - ], -}; - -export const WithFiveInstance = (a: any) => <View {...a} />; -WithFiveInstance.args = { - instances: [ - { - id: "first", - name: "the first instance", - merchant_pub: "abcdefgh", - payment_targets: ["asd"], - }, - { - id: "second", - name: "the second instance", - merchant_pub: "zxczxcz", - payment_targets: ["asd"], - }, - { - id: "third", - name: "the third instance", - merchant_pub: "QWEQWEWQE", - payment_targets: ["asd"], - }, - { - id: "other", - name: "the other instance", - merchant_pub: "FHJHGJGHJ", - payment_targets: ["asd"], - }, - { - id: "another", - name: "the another instance", - merchant_pub: "abcd3423423efgh", - payment_targets: ["asd"], - }, - { - id: "last", - name: "last instance", - merchant_pub: "zxcvvbnm", - payment_targets: ["pay-to", "asd"], - }, - ], -}; diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx deleted file mode 100644 index 35b59633b..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { MerchantBackend } from "../../../declaration.js"; -import { CardTable as CardTableActive } from "./TableActive.js"; - -interface Props { - instances: MerchantBackend.Instances.Instance[]; - onCreate: () => void; - onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - onPurge: (id: MerchantBackend.Instances.Instance) => void; - selected?: boolean; - setInstanceName: (s: string) => void; -} - -export function View({ - instances, - onCreate, - onDelete, - onPurge, - onUpdate, - setInstanceName, - selected, -}: Props): VNode { - const [show, setShow] = useState<"active" | "deleted" | null>("active"); - const showIsActive = show === "active" ? "is-active" : ""; - const showIsDeleted = show === "deleted" ? "is-active" : ""; - const showAll = show === null ? "is-active" : ""; - const { i18n } = useTranslationContext(); - - const showingInstances = showIsDeleted - ? instances.filter((i) => i.deleted) - : showIsActive - ? instances.filter((i) => !i.deleted) - : instances; - - return ( - <section class="section is-main-section"> - <div class="columns"> - <div class="column is-two-thirds"> - <div class="tabs" style={{ overflow: "inherit" }}> - <ul> - <li class={showIsActive}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Only show active instances`} - > - <a onClick={() => setShow("active")}> - <i18n.Translate>Active</i18n.Translate> - </a> - </div> - </li> - <li class={showIsDeleted}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Only show deleted instances`} - > - <a onClick={() => setShow("deleted")}> - <i18n.Translate>Deleted</i18n.Translate> - </a> - </div> - </li> - <li class={showAll}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Show all instances`} - > - <a onClick={() => setShow(null)}> - <i18n.Translate>All</i18n.Translate> - </a> - </div> - </li> - </ul> - </div> - </div> - </div> - <CardTableActive - instances={showingInstances} - onDelete={onDelete} - onPurge={onPurge} - setInstanceName={setInstanceName} - onUpdate={onUpdate} - selected={selected} - onCreate={onCreate} - /> - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx deleted file mode 100644 index d01460ac9..000000000 --- a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { DeleteModal, PurgeModal } from "../../../components/modal/index.js"; -import { MerchantBackend } from "../../../declaration.js"; -import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js"; -import { Notification } from "../../../utils/types.js"; -import { View } from "./View.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -interface Props { - onCreate: () => void; - onUpdate: (id: string) => void; - instances: MerchantBackend.Instances.Instance[]; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - setInstanceName: (s: string) => void; -} - -export default function Instances({ - onUnauthorized, - onLoadError, - onNotFound, - onCreate, - onUpdate, - setInstanceName, -}: Props): VNode { - const result = useBackendInstances(); - const [deleting, setDeleting] = - useState<MerchantBackend.Instances.Instance | null>(null); - const [purging, setPurging] = - useState<MerchantBackend.Instances.Instance | null>(null); - const { deleteInstance, purgeInstance } = useAdminAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - <View - instances={result.data.instances} - onDelete={setDeleting} - onCreate={onCreate} - onPurge={setPurging} - onUpdate={onUpdate} - setInstanceName={setInstanceName} - selected={!!deleting} - /> - {deleting && ( - <DeleteModal - element={deleting} - onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - await deleteInstance(deleting.id); - // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) - setNotif({ - message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`, - type: "SUCCESS", - }); - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete instance`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - // pushNotification({ message: 'delete_error', type: 'ERROR' }) - } - setDeleting(null); - }} - /> - )} - {purging && ( - <PurgeModal - element={purging} - onCancel={() => setPurging(null)} - onConfirm={async (): Promise<void> => { - try { - await purgeInstance(purging.id); - setNotif({ - message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`, - type: "SUCCESS", - }); - } catch (error) { - setNotif({ - message: i18n.str`Failed to purge instance`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setPurging(null); - }} - /> - )} - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx deleted file mode 100644 index 50cd801d8..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Accounts/Create", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx deleted file mode 100644 index 09b1d38ec..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; - -type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string }; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -const accountAuthType = ["none", "basic"]; - -function isValidURL(s: string): boolean { - try { - const u = new URL(s) - return true; - } catch (e) { - return false; - } -} - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - - const [state, setState] = useState<Partial<Entity>>({}); - const errors: FormErrors<Entity> = { - payto_uri: !state.payto_uri ? i18n.str`required` : undefined, - - credit_facade_credentials: !state.credit_facade_credentials - ? undefined - : undefinedIfEmpty({ - username: - state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username - ? i18n.str`required` - : undefined, - password: - state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password - ? i18n.str`required` - : undefined, - }), - credit_facade_url: !state.credit_facade_url - ? undefined - : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url` - : undefined, - repeatPassword: - !state.credit_facade_credentials - ? undefined - : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword) - ? i18n.str`is not the same` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - delete state.repeatPassword - return onCreate(state as any); - }; - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputPaytoForm<Entity> - name="payto_uri" - label={i18n.str`Account`} - /> - <Input<Entity> - name="credit_facade_url" - label={i18n.str`Account info URL`} - help="https://bank.com" - expand - tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} - /> - <InputSelector - name="credit_facade_credentials.type" - label={i18n.str`Auth type`} - tooltip={i18n.str`Choose the authentication type for the account info URL`} - values={accountAuthType} - toStr={(str) => { - if (str === "none") return "Without authentication"; - return "Username and password"; - }} - /> - {state.credit_facade_credentials?.type === "basic" ? ( - <Fragment> - <Input - name="credit_facade_credentials.username" - label={i18n.str`Username`} - tooltip={i18n.str`Username to access the account information.`} - /> - <Input - name="credit_facade_credentials.password" - inputType="password" - label={i18n.str`Password`} - tooltip={i18n.str`Password to access the account information.`} - /> - <Input - name="repeatPassword" - inputType="password" - label={i18n.str`Repeat password`} - /> - </Fragment> - ) : undefined} - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx deleted file mode 100644 index ad5d025d6..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useWebhookAPI } from "../../../../hooks/webhooks.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; -import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; -import { useBankAccountAPI } from "../../../../hooks/bank.js"; - -export type Entity = MerchantBackend.BankAccounts.AccountAddDetails; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} - -export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { createBankAccount } = useBankAccountAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - return ( - <> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={(request: Entity) => { - return createBankAccount(request) - .then((d) => { - onConfirm() - }) - .catch((error) => { - setNotif({ - message: i18n.str`could not create device`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx deleted file mode 100644 index 18e762642..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { FunctionalComponent, h } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; - -export default { - title: "Pages/Accounts/List", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx deleted file mode 100644 index 3359d1a95..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { CardTable } from "./Table.js"; - -export interface Props { - devices: MerchantBackend.BankAccounts.BankAccountEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; - onCreate: () => void; - onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; - onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; -} - -export function ListPage({ - devices, - onCreate, - onDelete, - onSelect, - onLoadMoreBefore, - onLoadMoreAfter, -}: Props): VNode { - const form = { payto_uri: "" }; - - const { i18n } = useTranslationContext(); - return ( - <section class="section is-main-section"> - <CardTable - accounts={devices.map((o) => ({ - ...o, - id: String(o.h_wire), - }))} - onCreate={onCreate} - onDelete={onDelete} - onSelect={onSelect} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} - /> - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx deleted file mode 100644 index 77e820e04..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx +++ /dev/null @@ -1,385 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; -import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util"; - -type Entity = MerchantBackend.BankAccounts.BankAccountEntry; - -interface Props { - accounts: Entity[]; - onDelete: (e: Entity) => void; - onSelect: (e: Entity) => void; - onCreate: () => void; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -export function CardTable({ - accounts, - onCreate, - onDelete, - onSelect, - onLoadMoreAfter, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - - const { i18n } = useTranslationContext(); - - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-newspaper" /> - </span> - <i18n.Translate>Bank accounts</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new accounts`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {accounts.length > 0 ? ( - <Table - accounts={accounts} - onDelete={onDelete} - onSelect={onSelect} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string[]; - accounts: Entity[]; - onDelete: (e: Entity) => void; - onSelect: (e: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - -function Table({ - accounts, - onLoadMoreAfter, - onDelete, - onSelect, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], } - const accountsByType = accounts.reduce((prev, acc) => { - const parsed = parsePaytoUri(acc.payto_uri) - if (!parsed) return prev //skip - if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") { - prev["unknown"].push({ parsed, acc }) - } else { - prev[parsed.targetType].push({ parsed, acc }) - } - return prev - }, emptyList) - - const bitcoinAccounts = accountsByType["bitcoin"] - const talerbankAccounts = accountsByType["x-taler-bank"] - const ibanAccounts = accountsByType["iban"] - const unkownAccounts = accountsByType["unknown"] - - - return ( - <Fragment> - - {bitcoinAccounts.length > 0 && <div class="table-container"> - <p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Address</i18n.Translate> - </th> - <th> - <i18n.Translate>Sewgit 1</i18n.Translate> - </th> - <th> - <i18n.Translate>Sewgit 2</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {bitcoinAccounts.map(({ parsed, acc }, idx) => { - const ac = parsed as PaytoUriBitcoin - return ( - <tr key={idx}> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.targetPath} - </td> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.segwitAddrs[0]} - </td> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.segwitAddrs[1]} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} - onClick={() => onDelete(acc)} - > - Delete - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div>} - - - - {talerbankAccounts.length > 0 && <div class="table-container"> - <p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Host</i18n.Translate> - </th> - <th> - <i18n.Translate>Account name</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {talerbankAccounts.map(({ parsed, acc }, idx) => { - const ac = parsed as PaytoUriTalerBank - return ( - <tr key={idx}> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.host} - </td> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.account} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} - onClick={() => onDelete(acc)} - > - Delete - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div>} - - {ibanAccounts.length > 0 && <div class="table-container"> - <p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Account name</i18n.Translate> - </th> - <th> - <i18n.Translate>IBAN</i18n.Translate> - </th> - <th> - <i18n.Translate>BIC</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {ibanAccounts.map(({ parsed, acc }, idx) => { - const ac = parsed as PaytoUriIBAN - return ( - <tr key={idx}> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.params["receiver-name"]} - </td> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.iban} - </td> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.bic ?? ""} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} - onClick={() => onDelete(acc)} - > - Delete - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div>} - - {unkownAccounts.length > 0 && <div class="table-container"> - <p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Type</i18n.Translate> - </th> - <th> - <i18n.Translate>Path</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {unkownAccounts.map(({ parsed, acc }, idx) => { - const ac = parsed as PaytoUriUnknown - return ( - <tr key={idx}> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.targetType} - </td> - <td - onClick={(): void => onSelect(acc)} - style={{ cursor: "pointer" }} - > - {ac.targetPath} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} - onClick={() => onDelete(acc)} - > - Delete - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div>} - </Fragment> - - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no accounts yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx deleted file mode 100644 index cbe03d660..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; -import { Notification } from "../../../../utils/types.js"; -import { ListPage } from "./ListPage.js"; -import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onCreate: () => void; - onSelect: (id: string) => void; -} - -export default function ListOtpDevices({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); - const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteBankAccount } = useBankAccountAPI(); - const result = useInstanceBankAccounts({ position }, (id) => setPosition(id)); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - - <ListPage - devices={result.data.accounts} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} - onCreate={onCreate} - onSelect={(e) => { - onSelect(e.h_wire); - }} - onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) => - deleteBankAccount(e.h_wire) - .then(() => - setNotif({ - message: i18n.str`bank account delete successfully`, - type: "SUCCESS", - }), - ) - .catch((error) => - setNotif({ - message: i18n.str`could not delete the bank account`, - type: "ERROR", - description: error.message, - }), - ) - } - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx deleted file mode 100644 index 06ea9d07a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { UpdatePage as TestedComponent } from "./UpdatePage.js"; - -export default { - title: "Pages/OtpDevices/Update", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx deleted file mode 100644 index 4efb822a3..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; - -type Entity = MerchantBackend.BankAccounts.BankAccountEntry - & WithId; - -const accountAuthType = ["unedit", "none", "basic"]; -interface Props { - onUpdate: (d: MerchantBackend.BankAccounts.AccountPatchDetails) => Promise<void>; - onBack?: () => void; - account: Entity; -} - - -export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - - const [state, setState] = useState<Partial<MerchantBackend.BankAccounts.AccountPatchDetails>>(account); - - const errors: FormErrors<MerchantBackend.BankAccounts.AccountPatchDetails> = { - credit_facade_url: !state.credit_facade_url ? i18n.str`required` : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined, - credit_facade_credentials: undefinedIfEmpty({ - - username: state.credit_facade_credentials?.type !== "basic" ? undefined - : !state.credit_facade_credentials.username ? i18n.str`required` : undefined, - - password: state.credit_facade_credentials?.type !== "basic" ? undefined - : !state.credit_facade_credentials.password ? i18n.str`required` : undefined, - - repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined - : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` : - (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`doesn't match` - : undefined, - }), - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - - const creds: typeof state.credit_facade_credentials = - state.credit_facade_credentials?.type === "basic" ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } : state.credit_facade_credentials?.type === "none" ? { - type: "none" - } : undefined; - - return onUpdate({ - credit_facade_credentials: creds, - credit_facade_url: state.credit_facade_url, - }); - }; - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - Account: <b>{account.id.substring(0, 8)}...</b> - </span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputPaytoForm<Entity> - name="payto_uri" - label={i18n.str`Account`} - readonly - /> - <Input<Entity> - name="credit_facade_url" - label={i18n.str`Account info URL`} - help="https://bank.com" - expand - tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} - /> - <InputSelector - name="credit_facade_credentials.type" - label={i18n.str`Auth type`} - tooltip={i18n.str`Choose the authentication type for the account info URL`} - values={accountAuthType} - toStr={(str) => { - if (str === "none") return "Without authentication"; - if (str === "basic") return "With authentication"; - return "Do not change" - }} - /> - {state.credit_facade_credentials?.type === "basic" ? ( - <Fragment> - <Input - name="credit_facade_credentials.username" - label={i18n.str`Username`} - tooltip={i18n.str`Username to access the account information.`} - /> - <Input - name="credit_facade_credentials.password" - inputType="password" - label={i18n.str`Password`} - tooltip={i18n.str`Password to access the account information.`} - /> - <Input - name="credit_facade_credentials.repeatPassword" - inputType="password" - label={i18n.str`Repeat password`} - /> - </Fragment> - ) : undefined} - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - </div> - </section> - </section> - </div> - ); -} - -function isValidURL(s: string): boolean { - try { - const u = new URL(s) - return true; - } catch (e) { - return false; - } -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx deleted file mode 100644 index ca0b692a3..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js"; -import { Notification } from "../../../../utils/types.js"; -import { UpdatePage } from "./UpdatePage.js"; - -export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId; - -interface Props { - onBack?: () => void; - onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - bid: string; -} -export default function UpdateValidator({ - bid, - onConfirm, - onBack, - onUnauthorized, - onNotFound, - onLoadError, -}: Props): VNode { - const { updateBankAccount } = useBankAccountAPI(); - const result = useBankAccountDetails(bid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - <UpdatePage - account={{ ...result.data, id: bid }} - onBack={onBack} - onUpdate={(data) => { - return updateBankAccount(bid, data) - .then(onConfirm) - .catch((error) => { - setNotif({ - message: i18n.str`could not update account`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx deleted file mode 100644 index 22bbfe28a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Product/Create", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx deleted file mode 100644 index 6cbc26d8d..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { ProductForm } from "../../../../components/product/ProductForm.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useListener } from "../../../../hooks/listener.js"; - -type Entity = MerchantBackend.Products.ProductAddDetail & { - product_id: string; -}; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( - (result) => { - if (result) return onCreate(result); - return Promise.reject(); - }, - ); - - const { i18n } = useTranslationContext(); - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <ProductForm onSubscribe={addFormSubmitter} /> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - onClick={submitForm} - data-tooltip={ - !submitForm - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={!submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx deleted file mode 100644 index 4f7da26da..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { h, VNode } from "preact"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { Entity } from "./index.js"; -import emptyImage from "../../assets/empty.png"; - -interface Props { - entity: Entity; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -export function CreatedSuccessfully({ - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - return ( - <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Image</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Description</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Price</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - </p> - </div> - </div> - </div> - </Template> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx deleted file mode 100644 index d537f6bf0..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { AuditorBackend } from "../../../../declaration.js"; -import { useDepositConfirmationAPI } from "../../../../hooks/deposit_confirmations.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; - -export type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} -export default function CreateProduct({ onConfirm, onBack }: Props): VNode { - const { createDepositConfirmation } = useDepositConfirmationAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - <NotificationCard notification={notif} /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx deleted file mode 100644 index f4368293b..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CardTable as TestedComponent } from "./Table.js"; - -export default { - title: "Pages/Product/List", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onSelect: { action: "onSelect" }, - onDelete: { action: "onDelete" }, - onUpdate: { action: "onUpdate" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx deleted file mode 100644 index e71b1eeaa..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx +++ /dev/null @@ -1,249 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { ComponentChildren, Fragment, h, VNode } from "preact"; -import { StateUpdater, useState } from "preact/hooks"; -import emptyImage from "../../../../assets/empty.png"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { AuditorBackend, WithId } from "../../../../declaration.js"; -import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId; - -interface Props { - instances: Entity[]; - onDelete: (id: Entity) => void; - onSelect: (depositConfirmation: Entity) => void; - onUpdate: ( - id: string, - data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail, - ) => Promise<void>; - onCreate: () => void; - selected?: boolean; -} - -export function CardTable({ - instances, - onCreate, - onSelect, - onUpdate, - onDelete, -}: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string | undefined>( - undefined, - ); - const { i18n } = useTranslationContext(); - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-shopping" /> - </span> - <i18n.Translate>Deposit Confirmations</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add deposit-confirmation`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {instances.length > 0 ? ( - <Table - instances={instances} - onSelect={onSelect} - onDelete={onDelete} - onUpdate={onUpdate} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string | undefined; - instances: Entity[]; - onSelect: (id: Entity) => void; - onUpdate: ( - id: string, - data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail, - ) => Promise<void>; - onDelete: (serial_id: Entity) => void; - rowSelectionHandler: StateUpdater<string | undefined>; -} - -function Table({ - rowSelection, - rowSelectionHandler, - instances, - onSelect, - onUpdate, - onDelete, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Image</i18n.Translate> - </th> - <th> - <i18n.Translate>Description</i18n.Translate> - </th> - <th> - <i18n.Translate>Price per unit</i18n.Translate> - </th> - <th> - <i18n.Translate>Taxes</i18n.Translate> - </th> - <th> - <i18n.Translate>Sales</i18n.Translate> - </th> - <th> - <i18n.Translate>Stock</i18n.Translate> - </th> - <th> - <i18n.Translate>Sold</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - - return ( - <Fragment key={i.id}> - <tr key="info"> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - </td> - - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <span - class="has-tooltip-bottom" - data-tooltip={i18n.str`go to product update page`} - > - <button - class="button is-small is-success " - type="button" - onClick={(): void => onSelect(i)} - > - <i18n.Translate>Update</i18n.Translate> - </button> - </span> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`remove this product from the database`} - > - <button - class="button is-small is-danger" - type="button" - onClick={(): void => onDelete(i)} - > - <i18n.Translate>Delete</i18n.Translate> - </button> - </span> - </div> - </td> - </tr> - {rowSelection === i.id && ( - <tr key="form"> - <td colSpan={10}> - </td> - </tr> - )} - </Fragment> - ); - })} - </tbody> - </table> - </div> - ); -} - -interface FastProductUpdate { - incoming: number; - lost: number; - price: string; -} -interface UpdatePrice { - price: string; -} - - - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no products yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} - -function difference(price: string, tax: number) { - if (!tax) return price; - const ps = price.split(":"); - const p = parseInt(ps[1], 10); - ps[1] = `${p - tax}`; - return ps.join(":"); -}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx deleted file mode 100644 index d1dc9d540..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { UpdatePage as TestedComponent } from "./UpdatePage.js"; - -export default { - title: "Pages/Product/Update", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const WithManagedStock = createExample(TestedComponent, { - product: { - product_id: "20102-ASDAS-QWE", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10", - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: 15, - unit: "bar", - address: {}, - }, -}); - -export const WithInfiniteStock = createExample(TestedComponent, { - product: { - product_id: "20102-ASDAS-QWE", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10", - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: -1, - unit: "bar", - address: {}, - }, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx deleted file mode 100644 index 53aa9d61f..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { ProductForm } from "../../../../components/product/ProductForm.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useListener } from "../../../../hooks/listener.js"; - -type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; - -interface Props { - onUpdate: (d: Entity) => Promise<void>; - onBack?: () => void; - product: Entity; -} - -export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { - const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( - (result) => { - if (result) return onUpdate(result); - return Promise.resolve(); - }, - ); - - const { i18n } = useTranslationContext(); - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - <i18n.Translate>Product id:</i18n.Translate> - <b>{product.product_id}</b> - </span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <ProductForm - initial={product} - onSubscribe={addFormSubmitter} - alreadyExist - /> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - onClick={submitForm} - data-tooltip={ - !submitForm - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={!submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx deleted file mode 100644 index 6e9b51106..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { FormProvider } from "../../../components/form/FormProvider.js"; -import { Input } from "../../../components/form/Input.js"; -import { MerchantBackend } from "../../../declaration.js"; - -type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage; -interface Props { - onUpdate: () => void; - onDelete: () => void; - selected: MerchantBackend.Instances.QueryInstancesResponse; -} - -function convert( - from: MerchantBackend.Instances.QueryInstancesResponse, -): Entity { - const defaults = { - default_wire_fee_amortization: 1, - use_stefan: true, - default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour - default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours - }; - return { ...defaults, ...from }; -} - -export function DetailPage({ selected }: Props): VNode { - const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); - - const { i18n } = useTranslationContext(); - - return ( - <div> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title">Here goes the instance description</h1> - </div> - </div> - <div class="level-right" style="display: none;"> - <div class="level-item" /> - </div> - </div> - </div> - </section> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-6"> - <FormProvider<Entity> object={value} valueHandler={valueHandler}> - <Input<Entity> name="name" readonly label={i18n.str`Name`} /> - </FormProvider> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx deleted file mode 100644 index 13dd3a2f6..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { ErrorType, HttpError } 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 { DeleteModal } from "../../../components/modal/index.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { MerchantBackend } from "../../../declaration.js"; -import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; -import { DetailPage } from "./DetailPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onUpdate: () => void; - onNotFound: () => VNode; - onDelete: () => void; -} - -export default function Detail({ - onUpdate, - onLoadError, - onUnauthorized, - onDelete, - onNotFound, -}: Props): VNode { - const { id } = useInstanceContext(); - const result = useInstanceDetails(); - const [deleting, setDeleting] = useState<boolean>(false); - - const { deleteInstance } = useInstanceAPI(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <DetailPage - selected={result.data} - onUpdate={onUpdate} - onDelete={() => setDeleting(true)} - /> - {deleting && ( - <DeleteModal - element={{ name: result.data.name, id }} - onCancel={() => setDeleting(false)} - onConfirm={async (): Promise<void> => { - try { - await deleteInstance(); - onDelete(); - } catch (error) { - //FIXME: show message error - } - setDeleting(false); - }} - /> - )} - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx deleted file mode 100644 index fa6e9d763..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; -import { DetailPage as TestedComponent } from "./DetailPage.js"; - -export default { - title: "Pages/Instance/Detail", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Internal: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const component = (args: any) => ( - <ConfigContextProvider - value={{ - currency: "TESTKUDOS", - version: "1", - }} - > - <Internal {...(props as any)} /> - </ConfigContextProvider> - ); - return { component, props }; -} - -export const Example = createExample(TestedComponent, { - selected: { - name: "name", - auth: { method: "external" }, - address: {}, - user_type: "business", - jurisdiction: {}, - use_stefan: true, - default_pay_delay: { - d_us: 1000 * 1000, //one second - }, - default_wire_transfer_delay: { - d_us: 1000 * 1000, //one second - }, - merchant_pub: "ASDWQEKASJDKSADJ", - }, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts deleted file mode 100644 index cd8f3d11c..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -export * as details from "./details/stories.js"; -export * as kycList from "./kyc/list/ListPage.stories.js"; -export * as reserve from "./reserves/create/CreatedSuccessfully.stories.js"; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx deleted file mode 100644 index a914639e5..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; -import * as tests from "@gnu-taler/web-util/testing"; -import { MerchantBackend } from "../../../../declaration.js"; - -export default { - title: "Pages/KYC/List", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -export const Example = tests.createExample(TestedComponent, { - status: { - timeout_kycs: [], - pending_kycs: [ - { - aml_status: 0, - exchange_url: "http://exchange.taler", - payto_uri: "payto://iban/de123123123", - kyc_url: "http://exchange.taler/kyc", - }, - { - aml_status: 1, - exchange_url: "http://exchange.taler", - payto_uri: "payto://iban/de123123123", - }, - { - aml_status: 2, - exchange_url: "http://exchange.taler", - payto_uri: "payto://iban/de123123123", - }, - ], - } as MerchantBackend.KYC.AccountKycRedirects, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx deleted file mode 100644 index 2ec0137d9..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx +++ /dev/null @@ -1,208 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; - -export interface Props { - status: MerchantBackend.KYC.AccountKycRedirects; -} - -export function ListPage({ status }: Props): VNode { - const { i18n } = useTranslationContext(); - - return ( - <section class="section is-main-section"> - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-clock" /> - </span> - <i18n.Translate>Pending KYC verification</i18n.Translate> - </p> - - <div class="card-header-icon" aria-label="more options" /> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {status.pending_kycs.length > 0 ? ( - <PendingTable entries={status.pending_kycs} /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - - {status.timeout_kycs.length > 0 ? ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-clock" /> - </span> - <i18n.Translate>Timed out</i18n.Translate> - </p> - - <div class="card-header-icon" aria-label="more options" /> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {status.timeout_kycs.length > 0 ? ( - <TimedOutTable entries={status.timeout_kycs} /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ) : undefined} - </section> - ); -} -interface PendingTableProps { - entries: MerchantBackend.KYC.MerchantAccountKycRedirect[]; -} - -interface TimedOutTableProps { - entries: MerchantBackend.KYC.ExchangeKycTimeout[]; -} - -function PendingTable({ entries }: PendingTableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - <table class="table is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Exchange</i18n.Translate> - </th> - <th> - <i18n.Translate>Target account</i18n.Translate> - </th> - <th> - <i18n.Translate>Reason</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {entries.map((e, i) => { - if (e.kyc_url === undefined) { - // blocked by AML - return ( - <tr key={i}> - <td>{e.exchange_url}</td> - <td>{e.payto_uri}</td> - <td> - {e.aml_status === 1 ? ( - <i18n.Translate> - There is an anti-money laundering process pending to - complete. - </i18n.Translate> - ) : ( - <i18n.Translate> - The account is frozen due to the anti-money laundering - rules. Contact the exchange service provider for further - instructions. - </i18n.Translate> - )} - </td> - </tr> - ); - } else { - // blocked by KYC - return ( - <tr key={i}> - <td>{e.exchange_url}</td> - <td>{e.payto_uri}</td> - <td> - <a href={e.kyc_url} target="_black" rel="noreferrer"> - <i18n.Translate> - Pending KYC process, click here to complete - </i18n.Translate> - </a> - </td> - </tr> - ); - } - })} - </tbody> - </table> - </div> - ); -} - -function TimedOutTable({ entries }: TimedOutTableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - <table class="table is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Exchange</i18n.Translate> - </th> - <th> - <i18n.Translate>Code</i18n.Translate> - </th> - <th> - <i18n.Translate>Http Status</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {entries.map((e, i) => { - return ( - <tr key={i}> - <td>{e.exchange_url}</td> - <td>{e.exchange_code}</td> - <td>{e.exchange_http_status}</td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-happy mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate>No pending kyc verification!</i18n.Translate> - </p> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx deleted file mode 100644 index 664f05f66..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { Loading } from "../../../../components/exception/loading.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceKYCDetails } from "../../../../hooks/instance.js"; -import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; -} - -export default function ListKYC({ - onUnauthorized, - onLoadError, - onNotFound, -}: Props): VNode { - const result = useInstanceKYCDetails(); - if (result.loading) return <Loading />; - 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 status = result.data.type === "ok" ? undefined : result.data.status; - - if (!status) { - return <div>no kyc required</div>; - } - return <ListPage status={status} />; -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx deleted file mode 100644 index fc814b68f..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Order/Create", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - goBack: { action: "goBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, { - instanceConfig: { - default_pay_delay: { - d_us: 1000 * 1000 * 60 * 60, //one hour - }, - default_wire_transfer_delay: { - d_us: 1000 * 1000 * 60 * 60, //one hour - }, - use_stefan: true, - }, - instanceInventory: [ - { - id: "t-shirt-1", - description: "a m size t-shirt", - price: "TESTKUDOS:1", - total_stock: -1, - }, - { - id: "t-shirt-2", - price: "TESTKUDOS:1", - description: "a xl size t-shirt", - } as any, - { - id: "t-shirt-3", - price: "TESTKUDOS:1", - description: "a s size t-shirt", - } as any, - ], -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx deleted file mode 100644 index 92271f52f..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ /dev/null @@ -1,705 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format, isFuture } from "date-fns"; -import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { useEffect, 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 { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputGroup } from "../../../../components/form/InputGroup.js"; -import { InputLocation } from "../../../../components/form/InputLocation.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputToggle } from "../../../../components/form/InputToggle.js"; -import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js"; -import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js"; -import { ProductList } from "../../../../components/product/ProductList.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { useSettings } from "../../../../hooks/useSettings.js"; -import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; -import { rate } from "../../../../utils/amount.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; - -interface Props { - onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; - onBack?: () => void; - instanceConfig: InstanceConfig; - instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[]; -} -interface InstanceConfig { - use_stefan: boolean; - default_pay_delay: TalerProtocolDuration; - default_wire_transfer_delay: TalerProtocolDuration; -} - -function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> { - const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay); - const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay); - - return { - inventoryProducts: {}, - products: [], - pricing: {}, - payments: { - max_fee: undefined, - createToken: true, - pay_deadline: (defaultPayDeadline), - refund_deadline: (defaultPayDeadline), - wire_transfer_deadline: (defaultWireDeadline), - }, - shipping: {}, - extra: {}, - }; -} - -interface ProductAndQuantity { - product: MerchantBackend.Products.ProductDetail & WithId; - quantity: number; -} -export interface ProductMap { - [id: string]: ProductAndQuantity; -} - -interface Pricing { - products_price: string; - order_price: string; - summary: string; -} -interface Shipping { - delivery_date?: Date; - delivery_location?: MerchantBackend.Location; - fullfilment_url?: string; -} -interface Payments { - refund_deadline: Duration; - pay_deadline: Duration; - wire_transfer_deadline: Duration; - auto_refund_deadline: Duration; - max_fee?: string; - createToken: boolean; - minimum_age?: number; -} -interface Entity { - inventoryProducts: ProductMap; - products: MerchantBackend.Product[]; - pricing: Partial<Pricing>; - payments: Partial<Payments>; - shipping: Partial<Shipping>; - extra: Record<string, string>; -} - -const stringIsValidJSON = (value: string) => { - try { - JSON.parse(value.trim()); - return true; - } catch { - return false; - } -}; - -export function CreatePage({ - onCreate, - onBack, - instanceConfig, - instanceInventory, -}: Props): VNode { - const config = useConfigContext(); - const instance_default = with_defaults(instanceConfig, config.currency) - const [value, valueHandler] = useState(instance_default); - const zero = Amounts.zeroOfCurrency(config.currency); - const [settings, updateSettings] = useSettings() - const inventoryList = Object.values(value.inventoryProducts || {}); - const productList = Object.values(value.products || {}); - - const { i18n } = useTranslationContext(); - - const parsedPrice = !value.pricing?.order_price - ? undefined - : Amounts.parse(value.pricing.order_price); - - const errors: FormErrors<Entity> = { - pricing: undefinedIfEmpty({ - summary: !value.pricing?.summary ? i18n.str`required` : undefined, - order_price: !value.pricing?.order_price - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - }), - payments: undefinedIfEmpty({ - refund_deadline: !value.payments?.refund_deadline - ? undefined - : value.payments.pay_deadline && - Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1 - ? i18n.str`refund deadline cannot be before pay deadline` - : value.payments.wire_transfer_deadline && - Duration.cmp( - value.payments.wire_transfer_deadline, - value.payments.refund_deadline, - ) === -1 - ? i18n.str`wire transfer deadline cannot be before refund deadline` - : undefined, - pay_deadline: !value.payments?.pay_deadline - ? i18n.str`required` - : value.payments.wire_transfer_deadline && - Duration.cmp( - value.payments.wire_transfer_deadline, - value.payments.pay_deadline, - ) === -1 - ? i18n.str`wire transfer deadline cannot be before pay deadline` - : undefined, - wire_transfer_deadline: !value.payments?.wire_transfer_deadline - ? i18n.str`required` - : undefined, - auto_refund_deadline: !value.payments?.auto_refund_deadline - ? undefined - : !value.payments?.refund_deadline - ? i18n.str`should have a refund deadline` - : Duration.cmp( - value.payments.refund_deadline, - value.payments.auto_refund_deadline, - ) == -1 - ? i18n.str`auto refund cannot be after refund deadline` - : undefined, - - }), - shipping: undefinedIfEmpty({ - delivery_date: !value.shipping?.delivery_date - ? undefined - : !isFuture(value.shipping.delivery_date) - ? i18n.str`should be in the future` - : undefined, - }), - }; - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submit = (): void => { - const order = value as any; //schema.cast(value); - if (!value.payments) return; - if (!value.shipping) return; - - const request: MerchantBackend.Orders.PostOrderRequest = { - order: { - amount: order.pricing.order_price, - summary: order.pricing.summary, - products: productList, - extra: undefinedIfEmpty(value.extra), - pay_deadline: !value.payments.pay_deadline ? - i18n.str`required` : - AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline)) - ,// : undefined, - wire_transfer_deadline: value.payments.wire_transfer_deadline - ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline)) - : undefined, - refund_deadline: value.payments.refund_deadline - ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline)) - : undefined, - auto_refund: value.payments.auto_refund_deadline - ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline) - : undefined, - max_fee: value.payments.max_fee as string, - - delivery_date: value.shipping.delivery_date - ? { t_s: value.shipping.delivery_date.getTime() / 1000 } - : undefined, - delivery_location: value.shipping.delivery_location, - fulfillment_url: value.shipping.fullfilment_url, - minimum_age: value.payments.minimum_age, - }, - inventory_products: inventoryList.map((p) => ({ - product_id: p.product.id, - quantity: p.quantity, - })), - create_token: value.payments.createToken, - }; - - onCreate(request); - }; - - const addProductToTheInventoryList = ( - product: MerchantBackend.Products.ProductDetail & WithId, - quantity: number, - ) => { - valueHandler((v) => { - const inventoryProducts = { ...v.inventoryProducts }; - inventoryProducts[product.id] = { product, quantity }; - return { ...v, inventoryProducts }; - }); - }; - - const removeProductFromTheInventoryList = (id: string) => { - valueHandler((v) => { - const inventoryProducts = { ...v.inventoryProducts }; - delete inventoryProducts[id]; - return { ...v, inventoryProducts }; - }); - }; - - const addNewProduct = async (product: MerchantBackend.Product) => { - return valueHandler((v) => { - const products = v.products ? [...v.products, product] : []; - return { ...v, products }; - }); - }; - - const removeFromNewProduct = (index: number) => { - valueHandler((v) => { - const products = v.products ? [...v.products] : []; - products.splice(index, 1); - return { ...v, products }; - }); - }; - - const [editingProduct, setEditingProduct] = useState< - MerchantBackend.Product | undefined - >(undefined); - - const totalPriceInventory = inventoryList.reduce((prev, cur) => { - const p = Amounts.parseOrThrow(cur.product.price); - return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount; - }, zero); - - const totalPriceProducts = productList.reduce((prev, cur) => { - if (!cur.price) return zero; - const p = Amounts.parseOrThrow(cur.price); - return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount; - }, zero); - - const hasProducts = inventoryList.length > 0 || productList.length > 0; - const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts); - - const totalAsString = Amounts.stringify(totalPrice.amount); - const allProducts = productList.concat(inventoryList.map(asProduct)); - - const [newField, setNewField] = useState("") - - useEffect(() => { - valueHandler((v) => { - return { - ...v, - pricing: { - ...v.pricing, - products_price: hasProducts ? totalAsString : undefined, - order_price: hasProducts ? totalAsString : undefined, - }, - }; - }); - }, [hasProducts, totalAsString]); - - const discountOrRise = rate( - parsedPrice ?? Amounts.zeroOfCurrency(config.currency), - totalPrice.amount, - ); - - const minAgeByProducts = allProducts.reduce( - (cur, prev) => - !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age, - 0, - ); - - // if there is no default pay deadline - const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline - // and there is no default wire deadline - const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline - // user required to set the taler options - const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline - - - return ( - <div> - - <section class="section is-main-section"> - <div class="tabs is-toggle is-fullwidth is-small"> - <ul> - <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: false - }) - }}> - <a > - <span><i18n.Translate>Simple</i18n.Translate></span> - </a> - </li> - <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: true - }) - }}> - <a > - <span><i18n.Translate>Advanced</i18n.Translate></span> - </a> - </li> - </ul> - </div> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - {/* // FIXME: translating plural singular */} - <InputGroup - name="inventory_products" - label={i18n.str`Manage products in order`} - alternative={ - allProducts.length > 0 && ( - <p> - {allProducts.length} products with a total price of{" "} - {totalAsString}. - </p> - ) - } - tooltip={i18n.str`Manage list of products in the order.`} - > - <InventoryProductForm - currentProducts={value.inventoryProducts || {}} - onAddProduct={addProductToTheInventoryList} - inventory={instanceInventory} - /> - - {settings.advanceOrderMode && - <NonInventoryProductFrom - productToEdit={editingProduct} - onAddProduct={(p) => { - setEditingProduct(undefined); - return addNewProduct(p); - }} - /> - } - - {allProducts.length > 0 && ( - <ProductList - list={allProducts} - actions={[ - { - name: i18n.str`Remove`, - tooltip: i18n.str`Remove this product from the order.`, - handler: (e, index) => { - if (e.product_id) { - removeProductFromTheInventoryList(e.product_id); - } else { - removeFromNewProduct(index); - setEditingProduct(e); - } - }, - }, - ]} - /> - )} - </InputGroup> - - <FormProvider<Entity> - errors={errors} - object={value} - valueHandler={valueHandler as any} - > - {hasProducts ? ( - <Fragment> - <InputCurrency - name="pricing.products_price" - label={i18n.str`Total price`} - readonly - tooltip={i18n.str`total product price added up`} - /> - <InputCurrency - name="pricing.order_price" - label={i18n.str`Total price`} - addonAfter={ - discountOrRise > 0 && - (discountOrRise < 1 - ? `discount of %${Math.round( - (1 - discountOrRise) * 100, - )}` - : `rise of %${Math.round((discountOrRise - 1) * 100)}`) - } - tooltip={i18n.str`Amount to be paid by the customer`} - /> - </Fragment> - ) : ( - <InputCurrency - name="pricing.order_price" - label={i18n.str`Order price`} - tooltip={i18n.str`final order price`} - /> - )} - - <Input - name="pricing.summary" - inputType="multiline" - label={i18n.str`Summary`} - tooltip={i18n.str`Title of the order to be shown to the customer`} - /> - - {settings.advanceOrderMode && - <InputGroup - name="shipping" - label={i18n.str`Shipping and Fulfillment`} - initialActive - > - <InputDate - name="shipping.delivery_date" - label={i18n.str`Delivery date`} - tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} - /> - {value.shipping?.delivery_date && ( - <InputGroup - name="shipping.delivery_location" - label={i18n.str`Location`} - tooltip={i18n.str`address where the products will be delivered`} - > - <InputLocation name="shipping.delivery_location" /> - </InputGroup> - )} - <Input - name="shipping.fullfilment_url" - label={i18n.str`Fulfillment URL`} - tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} - /> - </InputGroup> - } - - {(settings.advanceOrderMode || requiresSomeTalerOptions) && - <InputGroup - name="payments" - label={i18n.str`Taler payment options`} - tooltip={i18n.str`Override default Taler payment settings for this order`} - > - {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDuration - name="payments.pay_deadline" - label={i18n.str`Payment time`} - help={<DeadlineHelp duration={value.payments?.pay_deadline} />} - withForever - withoutClear - tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`} - side={ - <span> - <button class="button" onClick={() => { - const c = { - ...value, - payments: { - ...(value.payments ?? {}), - pay_deadline: instance_default.payments?.pay_deadline - } - } - valueHandler(c) - }}> - <i18n.Translate>default</i18n.Translate> - </button> - </span> - } - />} - {settings.advanceOrderMode && <InputDuration - name="payments.refund_deadline" - label={i18n.str`Refund time`} - help={<DeadlineHelp duration={value.payments?.refund_deadline} />} - withForever - withoutClear - tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} - side={ - <span> - <button class="button" onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - refund_deadline: instance_default.payments?.refund_deadline - } - }) - }}> - <i18n.Translate>default</i18n.Translate> - </button> - </span> - } - />} - {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDuration - name="payments.wire_transfer_deadline" - label={i18n.str`Wire transfer time`} - help={<DeadlineHelp duration={value.payments?.wire_transfer_deadline} />} - withoutClear - withForever - tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} - side={ - <span> - <button class="button" onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline - } - }) - }}> - <i18n.Translate>default</i18n.Translate> - </button> - </span> - } - />} - {settings.advanceOrderMode && <InputDuration - name="payments.auto_refund_deadline" - label={i18n.str`Auto-refund time`} - help={<DeadlineHelp duration={value.payments?.auto_refund_deadline} />} - tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} - withForever - />} - - {settings.advanceOrderMode && <InputCurrency - name="payments.max_fee" - label={i18n.str`Maximum fee`} - tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} - />} - {settings.advanceOrderMode && <InputToggle - name="payments.createToken" - label={i18n.str`Create token`} - tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} - />} - {settings.advanceOrderMode && <InputNumber - name="payments.minimum_age" - label={i18n.str`Minimum age required`} - tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} - help={ - minAgeByProducts > 0 - ? i18n.str`Min age defined by the producs is ${minAgeByProducts}` - : i18n.str`No product with age restriction in this order` - } - />} - </InputGroup> - } - - {settings.advanceOrderMode && - <InputGroup - name="extra" - label={i18n.str`Additional information`} - tooltip={i18n.str`Custom information to be included in the contract for this order.`} - > - {Object.keys(value.extra ?? {}).map((key) => { - - return <Input - name={`extra.${key}`} - inputType="multiline" - label={key} - tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} - side={ - <button class="button" onClick={(e) => { - if (value.extra && value.extra[key] !== undefined) { - console.log(value.extra) - delete value.extra[key] - } - valueHandler({ - ...value, - }) - }}>remove</button> - } - /> - })} - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Custom field name</i18n.Translate> - <span class="icon has-tooltip-right" data-tooltip={"new extra field"}> - <i class="mdi mdi-information" /> - </span> - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} /> - </p> - </div> - </div> - <button class="button" onClick={(e) => { - setNewField("") - valueHandler({ - ...value, - extra: { - ...(value.extra ?? {}), - [newField]: "" - } - }) - }}>add</button> - </div> - </InputGroup> - } - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <button - class="button is-success" - onClick={submit} - disabled={hasErrors} - > - <i18n.Translate>Confirm</i18n.Translate> - </button> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} - -function asProduct(p: ProductAndQuantity): MerchantBackend.Product { - return { - product_id: p.product.id, - image: p.product.image, - price: p.product.price, - unit: p.product.unit, - quantity: p.quantity, - description: p.product.description, - taxes: p.product.taxes, - minimum_age: p.product.minimum_age, - }; -} - - -function DeadlineHelp({ duration }: { duration?: Duration }): VNode { - const { i18n } = useTranslationContext(); - const [now, setNow] = useState(AbsoluteTime.now()) - useEffect(() => { - const iid = setInterval(() => { - setNow(AbsoluteTime.now()) - }, 60 * 1000) - return () => { - clearInterval(iid) - } - }) - if (!duration) return <i18n.Translate>Disabled</i18n.Translate> - const when = AbsoluteTime.addDuration(now, duration) - if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate> - return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate> -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx deleted file mode 100644 index 3f7b20f52..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { useOrderAPI } from "../../../../hooks/order.js"; -import { Entity } from "./index.js"; - -interface Props { - entity: Entity; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -export function OrderCreatedSuccessfully({ - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - const { getPaymentURL } = useOrderAPI(); - const [url, setURL] = useState<string | undefined>(undefined); - const { i18n } = useTranslationContext(); - useEffect(() => { - getPaymentURL(entity.response.order_id).then((response) => { - setURL(response.data); - }); - }, [getPaymentURL, entity.response.order_id]); - - return ( - <CreatedSuccessfully - onConfirm={onConfirm} - onCreateAnother={onCreateAnother} - > - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Amount</i18n.Translate> - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={entity.request.order.amount} - /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Summary</i18n.Translate> - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={entity.request.order.summary} - /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Order ID</i18n.Translate> - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={entity.response.order_id} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Payment URL</i18n.Translate> - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={url} /> - </p> - </div> - </div> - </div> - </CreatedSuccessfully> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx deleted file mode 100644 index d2a8619ce..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ErrorType, HttpError } 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 { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceDetails } from "../../../../hooks/instance.js"; -import { useOrderAPI } from "../../../../hooks/order.js"; -import { useInstanceProducts } from "../../../../hooks/product.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -export type Entity = { - request: MerchantBackend.Orders.PostOrderRequest; - response: MerchantBackend.Orders.PostOrderResponse; -}; -interface Props { - onBack?: () => void; - onConfirm: (id: string) => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; -} -export default function OrderCreate({ - onConfirm, - onBack, - onLoadError, - onNotFound, - onUnauthorized, -}: Props): VNode { - const { createOrder } = useOrderAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const detailsResult = useInstanceDetails(); - const inventoryResult = useInstanceProducts(); - - if (detailsResult.loading) return <Loading />; - if (inventoryResult.loading) return <Loading />; - - if (!detailsResult.ok) { - if ( - detailsResult.type === ErrorType.CLIENT && - detailsResult.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - detailsResult.type === ErrorType.CLIENT && - detailsResult.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(detailsResult); - } - - if (!inventoryResult.ok) { - if ( - inventoryResult.type === ErrorType.CLIENT && - inventoryResult.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - inventoryResult.type === ErrorType.CLIENT && - inventoryResult.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(inventoryResult); - } - - return ( - <Fragment> - <NotificationCard notification={notif} /> - - <CreatePage - onBack={onBack} - onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => { - createOrder(request) - .then((r) => { - return onConfirm(r.data.order_id) - }) - .catch((error) => { - setNotif({ - message: "could not create order", - type: "ERROR", - description: error.message, - }); - }); - }} - instanceConfig={detailsResult.data} - instanceInventory={inventoryResult.data} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx deleted file mode 100644 index 22c6944b3..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { addDays } from "date-fns"; -import { h, VNode, FunctionalComponent } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { DetailPage as TestedComponent } from "./DetailPage.js"; - -export default { - title: "Pages/Order/Detail", - component: TestedComponent, - argTypes: { - onRefund: { action: "onRefund" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -const defaultContractTerm = { - amount: "TESTKUDOS:10", - timestamp: { - t_s: new Date().getTime() / 1000, - }, - auditors: [], - exchanges: [], - max_fee: "TESTKUDOS:1", - merchant: {} as any, - merchant_base_url: "http://merchant.url/", - order_id: "2021.165-03GDFC26Y1NNG", - products: [], - summary: "text summary", - wire_transfer_deadline: { - t_s: "never", - }, - refund_deadline: { t_s: "never" }, - merchant_pub: "ASDASDASDSd", - nonce: "QWEQWEQWE", - pay_deadline: { - t_s: "never", - }, - wire_method: "x-taler-bank", - h_wire: "asd", -} as MerchantBackend.ContractTerms; - -// contract_terms: defaultContracTerm, -export const Claimed = createExample(TestedComponent, { - id: "2021.165-03GDFC26Y1NNG", - selected: { - order_status: "claimed", - contract_terms: defaultContractTerm, - }, -}); - -export const PaidNotRefundable = createExample(TestedComponent, { - id: "2021.165-03GDFC26Y1NNG", - selected: { - order_status: "paid", - contract_terms: defaultContractTerm, - refunded: false, - deposit_total: "TESTKUDOS:10", - exchange_ec: 0, - order_status_url: "http://merchant.backend/status", - exchange_hc: 0, - refund_amount: "TESTKUDOS:0", - refund_details: [], - refund_pending: false, - wire_details: [], - wire_reports: [], - wired: false, - }, -}); - -export const PaidRefundable = createExample(TestedComponent, { - id: "2021.165-03GDFC26Y1NNG", - selected: { - order_status: "paid", - contract_terms: { - ...defaultContractTerm, - refund_deadline: { - t_s: addDays(new Date(), 2).getTime() / 1000, - }, - }, - refunded: false, - deposit_total: "TESTKUDOS:10", - exchange_ec: 0, - order_status_url: "http://merchant.backend/status", - exchange_hc: 0, - refund_amount: "TESTKUDOS:0", - refund_details: [], - refund_pending: false, - wire_details: [], - wire_reports: [], - wired: false, - }, -}); - -export const Unpaid = createExample(TestedComponent, { - id: "2021.165-03GDFC26Y1NNG", - selected: { - order_status: "unpaid", - order_status_url: "http://merchant.backend/status", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - summary: "text summary", - taler_pay_uri: "pay uri", - total_amount: "TESTKUDOS:10", - }, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx deleted file mode 100644 index abcddb38a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx +++ /dev/null @@ -1,770 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format, formatDistance } from "date-fns"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { FormProvider } from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputGroup } from "../../../../components/form/InputGroup.js"; -import { InputLocation } from "../../../../components/form/InputLocation.js"; -import { TextField } from "../../../../components/form/TextField.js"; -import { ProductList } from "../../../../components/product/ProductList.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; -import { mergeRefunds } from "../../../../utils/amount.js"; -import { RefundModal } from "../list/Table.js"; -import { Event, Timeline } from "./Timeline.js"; - -type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; -type CT = MerchantBackend.ContractTerms; - -interface Props { - onBack: () => void; - selected: Entity; - id: string; - onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; -} - -type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & { - refund_taken: string; -}; -type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse; -type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse; - -function ContractTerms({ value }: { value: CT }) { - const { i18n } = useTranslationContext(); - - return ( - <InputGroup name="contract_terms" label={i18n.str`Contract Terms`}> - <FormProvider<CT> object={value} valueHandler={null}> - <Input<CT> - readonly - name="summary" - label={i18n.str`Summary`} - tooltip={i18n.str`human-readable description of the whole purchase`} - /> - <InputCurrency<CT> - readonly - name="amount" - label={i18n.str`Amount`} - tooltip={i18n.str`total price for the transaction`} - /> - {value.fulfillment_url && ( - <Input<CT> - readonly - name="fulfillment_url" - label={i18n.str`Fulfillment URL`} - tooltip={i18n.str`URL for this purchase`} - /> - )} - <Input<CT> - readonly - name="max_fee" - label={i18n.str`Max fee`} - tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`} - /> - <InputDate<CT> - readonly - name="timestamp" - label={i18n.str`Created at`} - tooltip={i18n.str`time when this contract was generated`} - /> - <InputDate<CT> - readonly - name="refund_deadline" - label={i18n.str`Refund deadline`} - tooltip={i18n.str`after this deadline has passed no refunds will be accepted`} - /> - <InputDate<CT> - readonly - name="pay_deadline" - label={i18n.str`Payment deadline`} - tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`} - /> - <InputDate<CT> - readonly - name="wire_transfer_deadline" - label={i18n.str`Wire transfer deadline`} - tooltip={i18n.str`transfer deadline for the exchange`} - /> - <InputDate<CT> - readonly - name="delivery_date" - label={i18n.str`Delivery date`} - tooltip={i18n.str`time indicating when the order should be delivered`} - /> - {value.delivery_date && ( - <InputGroup - name="delivery_location" - label={i18n.str`Location`} - tooltip={i18n.str`where the order will be delivered`} - > - <InputLocation name="payments.delivery_location" /> - </InputGroup> - )} - <InputDuration<CT> - readonly - name="auto_refund" - label={i18n.str`Auto-refund delay`} - tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`} - /> - <Input<CT> - readonly - name="extra" - label={i18n.str`Extra info`} - tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`} - /> - </FormProvider> - </InputGroup> - ); -} - -function ClaimedPage({ - id, - order, -}: { - id: string; - order: MerchantBackend.Orders.CheckPaymentClaimedResponse; -}) { - const events: Event[] = []; - if (order.contract_terms.timestamp.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.timestamp.t_s * 1000), - description: "order created", - type: "start", - }); - } - if (order.contract_terms.pay_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.pay_deadline.t_s * 1000), - description: "pay deadline", - type: "deadline", - }); - } - if (order.contract_terms.refund_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.refund_deadline.t_s * 1000), - description: "refund deadline", - type: "deadline", - }); - } - if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), - description: "wire deadline", - type: "deadline", - }); - } - if ( - order.contract_terms.delivery_date && - order.contract_terms.delivery_date.t_s !== "never" - ) { - events.push({ - when: new Date(order.contract_terms.delivery_date?.t_s * 1000), - description: "delivery", - type: "delivery", - }); - } - - const [value, valueHandler] = useState<Partial<Claimed>>(order); - const { i18n } = useTranslationContext(); - const [settings] = useSettings() - - return ( - <div> - <section class="section"> - <div class="columns"> - <div class="column" /> - <div class="column is-10"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <i18n.Translate>Order</i18n.Translate> #{id} - <div class="tag is-info ml-4"> - <i18n.Translate>claimed</i18n.Translate> - </div> - </div> - </div> - </div> - - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title">{order.contract_terms.amount}</h1> - </div> - </div> - </div> - - <div class="level"> - <div class="level-left" style={{ maxWidth: "100%" }}> - <div class="level-item" style={{ maxWidth: "100%" }}> - <div - class="content" - style={{ - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }} - > - <p> - <b> - <i18n.Translate>claimed at</i18n.Translate>: - </b>{" "} - {format( - new Date(order.contract_terms.timestamp.t_s * 1000), - datetimeFormatForSettings(settings) - )} - </p> - </div> - </div> - </div> - </div> - </div> - </section> - - <section class="section"> - <div class="columns"> - <div class="column is-4"> - <div class="title"> - <i18n.Translate>Timeline</i18n.Translate> - </div> - <Timeline events={events} /> - </div> - <div class="column is-8"> - <div class="title"> - <i18n.Translate>Payment details</i18n.Translate> - </div> - <FormProvider<Claimed> - object={value} - valueHandler={valueHandler} - > - <Input - name="contract_terms.summary" - readonly - inputType="multiline" - label={i18n.str`Summary`} - /> - <InputCurrency - name="contract_terms.amount" - readonly - label={i18n.str`Amount`} - /> - <Input<Claimed> - name="order_status" - readonly - label={i18n.str`Order status`} - /> - </FormProvider> - </div> - </div> - </section> - - {order.contract_terms.products.length ? ( - <Fragment> - <div class="title"> - <i18n.Translate>Product list</i18n.Translate> - </div> - <ProductList list={order.contract_terms.products} /> - </Fragment> - ) : undefined} - - {value.contract_terms && ( - <ContractTerms value={value.contract_terms} /> - )} - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} -function PaidPage({ - id, - order, - onRefund, -}: { - id: string; - order: MerchantBackend.Orders.CheckPaymentPaidResponse; - onRefund: (id: string) => void; -}) { - const events: Event[] = []; - if (order.contract_terms.timestamp.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.timestamp.t_s * 1000), - description: "order created", - type: "start", - }); - } - if (order.contract_terms.pay_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.pay_deadline.t_s * 1000), - description: "pay deadline", - type: "deadline", - }); - } - if (order.contract_terms.refund_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.refund_deadline.t_s * 1000), - description: "refund deadline", - type: "deadline", - }); - } - if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), - description: "wire deadline", - type: "deadline", - }); - } - if ( - order.contract_terms.delivery_date && - order.contract_terms.delivery_date.t_s !== "never" - ) { - if (order.contract_terms.delivery_date) - events.push({ - when: new Date(order.contract_terms.delivery_date?.t_s * 1000), - description: "delivery", - type: "delivery", - }); - } - order.refund_details.reduce(mergeRefunds, []).forEach((e) => { - if (e.timestamp.t_s !== "never") { - events.push({ - when: new Date(e.timestamp.t_s * 1000), - description: `refund: ${e.amount}: ${e.reason}`, - type: e.pending ? "refund" : "refund-taken", - }); - } - }); - if (order.wire_details && order.wire_details.length) { - if (order.wire_details.length > 1) { - let last: MerchantBackend.Orders.TransactionWireTransfer | null = null; - let first: MerchantBackend.Orders.TransactionWireTransfer | null = null; - let total: AmountJson | null = null; - - order.wire_details.forEach((w) => { - if (last === null || last.execution_time.t_s < w.execution_time.t_s) { - last = w; - } - if (first === null || first.execution_time.t_s > w.execution_time.t_s) { - first = w; - } - total = - total === null - ? Amounts.parseOrThrow(w.amount) - : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount; - }); - const last_time = last!.execution_time.t_s; - if (last_time !== "never") { - events.push({ - when: new Date(last_time * 1000), - description: `wired ${Amounts.stringify(total!)}`, - type: "wired-range", - }); - } - const first_time = first!.execution_time.t_s; - if (first_time !== "never") { - events.push({ - when: new Date(first_time * 1000), - description: `wire transfer started...`, - type: "wired-range", - }); - } - } else { - order.wire_details.forEach((e) => { - if (e.execution_time.t_s !== "never") { - events.push({ - when: new Date(e.execution_time.t_s * 1000), - description: `wired ${e.amount}`, - type: "wired", - }); - } - }); - } - } - - const now = new Date() - const nextEvent = events.find((e) => { - return e.when.getTime() > now.getTime() - }) - - const [value, valueHandler] = useState<Partial<Paid>>(order); - const { url: backendURL } = useBackendContext() - const refundurl = stringifyRefundUri({ - merchantBaseUrl: backendURL, - orderId: order.contract_terms.order_id - }) - const refundable = - new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000; - const { i18n } = useTranslationContext(); - - const amount = Amounts.parseOrThrow(order.contract_terms.amount); - const refund_taken = order.refund_details.reduce((prev, cur) => { - if (cur.pending) return prev; - return Amounts.add(prev, Amounts.parseOrThrow(cur.amount)).amount; - }, Amounts.zeroOfCurrency(amount.currency)); - value.refund_taken = Amounts.stringify(refund_taken); - - return ( - <div> - <section class="section"> - <div class="columns"> - <div class="column" /> - <div class="column is-10"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <i18n.Translate>Order</i18n.Translate> #{id} - <div class="tag is-success ml-4"> - <i18n.Translate>paid</i18n.Translate> - </div> - {order.wired ? ( - <div class="tag is-success ml-4"> - <i18n.Translate>wired</i18n.Translate> - </div> - ) : null} - {order.refunded ? ( - <div class="tag is-danger ml-4"> - <i18n.Translate>refunded</i18n.Translate> - </div> - ) : null} - </div> - </div> - </div> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title">{order.contract_terms.amount}</h1> - </div> - </div> - <div class="level-right"> - <div class="level-item"> - <h1 class="title"> - <div class="buttons"> - <span - class="has-tooltip-left" - data-tooltip={ - refundable - ? i18n.str`refund order` - : i18n.str`not refundable` - } - > - <button - class="button is-danger" - disabled={!refundable} - onClick={() => onRefund(id)} - > - <i18n.Translate>refund</i18n.Translate> - </button> - </span> - </div> - </h1> - </div> - </div> - </div> - - <div class="level"> - <div class="level-left" style={{ maxWidth: "100%" }}> - <div class="level-item" style={{ maxWidth: "100%" }}> - <div - class="content" - style={{ - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }} - > - {nextEvent && - <p> - <i18n.Translate>Next event in </i18n.Translate> {formatDistance( - nextEvent.when, - new Date(), - // "yyyy/MM/dd HH:mm:ss", - )} - </p> - } - </div> - </div> - </div> - </div> - </div> - </section> - - <section class="section"> - <div class="columns"> - <div class="column is-4"> - <div class="title"> - <i18n.Translate>Timeline</i18n.Translate> - </div> - <Timeline events={events} /> - </div> - <div class="column is-8"> - <div class="title"> - <i18n.Translate>Payment details</i18n.Translate> - </div> - <FormProvider<Paid> - object={value} - valueHandler={valueHandler} - > - {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n.str`Deposit total`} /> */} - {order.refunded && ( - <InputCurrency<Paid> - name="refund_amount" - readonly - label={i18n.str`Refunded amount`} - /> - )} - {order.refunded && ( - <InputCurrency<Paid> - name="refund_taken" - readonly - label={i18n.str`Refund taken`} - /> - )} - <Input<Paid> - name="order_status" - readonly - label={i18n.str`Order status`} - /> - <TextField<Paid> - name="order_status_url" - label={i18n.str`Status URL`} - > - <a - target="_blank" - rel="noreferrer" - href={order.order_status_url} - > - {order.order_status_url} - </a> - </TextField> - {order.refunded && ( - <TextField<Paid> - name="order_status_url" - label={i18n.str`Refund URI`} - > - <a target="_blank" rel="noreferrer" href={refundurl}> - {refundurl} - </a> - </TextField> - )} - </FormProvider> - </div> - </div> - </section> - - {order.contract_terms.products.length ? ( - <Fragment> - <div class="title"> - <i18n.Translate>Product list</i18n.Translate> - </div> - <ProductList list={order.contract_terms.products} /> - </Fragment> - ) : undefined} - - {value.contract_terms && ( - <ContractTerms value={value.contract_terms} /> - )} - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} - -function UnpaidPage({ - id, - order, -}: { - id: string; - order: MerchantBackend.Orders.CheckPaymentUnpaidResponse; -}) { - const [value, valueHandler] = useState<Partial<Unpaid>>(order); - const { i18n } = useTranslationContext(); - const [settings] = useSettings() - return ( - <div> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title"> - <i18n.Translate>Order</i18n.Translate> #{id} - </h1> - </div> - <div class="tag is-dark"> - <i18n.Translate>unpaid</i18n.Translate> - </div> - </div> - </div> - - <div class="level"> - <div class="level-left" style={{ maxWidth: "100%" }}> - <div class="level-item" style={{ maxWidth: "100%" }}> - <div - class="content" - style={{ - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }} - > - <p> - <b> - <i18n.Translate>pay at</i18n.Translate>: - </b>{" "} - <a - href={order.order_status_url} - rel="nofollow" - target="new" - > - {order.order_status_url} - </a> - </p> - <p> - <b> - <i18n.Translate>created at</i18n.Translate>: - </b>{" "} - {order.creation_time.t_s === "never" - ? "never" - : format( - new Date(order.creation_time.t_s * 1000), - datetimeFormatForSettings(settings) - )} - </p> - </div> - </div> - </div> - </div> - </div> - </section> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider<Unpaid> object={value} valueHandler={valueHandler}> - <Input<Unpaid> - readonly - name="summary" - label={i18n.str`Summary`} - tooltip={i18n.str`human-readable description of the whole purchase`} - /> - <InputCurrency<Unpaid> - readonly - name="total_amount" - label={i18n.str`Amount`} - tooltip={i18n.str`total price for the transaction`} - /> - <Input<Unpaid> - name="order_status" - readonly - label={i18n.str`Order status`} - /> - <Input<Unpaid> - name="order_status_url" - readonly - label={i18n.str`Order status URL`} - /> - <TextField<Unpaid> - name="taler_pay_uri" - label={i18n.str`Payment URI`} - > - <a target="_blank" rel="noreferrer" href={value.taler_pay_uri}> - {value.taler_pay_uri} - </a> - </TextField> - </FormProvider> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} - -export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { - const [showRefund, setShowRefund] = useState<string | undefined>(undefined); - const { i18n } = useTranslationContext(); - const DetailByStatus = function () { - switch (selected.order_status) { - case "claimed": - return <ClaimedPage id={id} order={selected} />; - case "paid": - return <PaidPage id={id} order={selected} onRefund={setShowRefund} />; - case "unpaid": - return <UnpaidPage id={id} order={selected} />; - default: - return ( - <div> - <i18n.Translate> - Unknown order status. This is an error, please contact the - administrator. - </i18n.Translate> - </div> - ); - } - }; - - return ( - <Fragment> - {DetailByStatus()} - {showRefund && ( - <RefundModal - order={selected} - onCancel={() => setShowRefund(undefined)} - onConfirm={(value) => { - onRefund(showRefund, value); - setShowRefund(undefined); - }} - /> - )} - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <div class="buttons is-right mt-5"> - <button class="button" onClick={onBack}> - <i18n.Translate>Back</i18n.Translate> - </button> - </div> - </div> - <div class="column" /> - </div> - </Fragment> - ); -} - -async function copyToClipboard(text: string) { - return navigator.clipboard.writeText(text); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx deleted file mode 100644 index 1aae4da21..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { format } from "date-fns"; -import { h } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -interface Props { - events: Event[]; -} - -export function Timeline({ events: e }: Props) { - const events = [...e]; - events.push({ - when: new Date(), - description: "now", - type: "now", - }); - - events.sort((a, b) => a.when.getTime() - b.when.getTime()); - const [settings] = useSettings(); - const [state, setState] = useState(events); - useEffect(() => { - const handle = setTimeout(() => { - const eventsWithoutNow = state.filter((e) => e.type !== "now"); - eventsWithoutNow.push({ - when: new Date(), - description: "now", - type: "now", - }); - setState(eventsWithoutNow); - }, 1000); - return () => { - clearTimeout(handle); - }; - }); - return ( - <div class="timeline"> - {events.map((e, i) => { - return ( - <div key={i} class="timeline-item"> - {(() => { - switch (e.type) { - case "deadline": - return ( - <div class="timeline-marker is-icon "> - <i class="mdi mdi-flag" /> - </div> - ); - case "delivery": - return ( - <div class="timeline-marker is-icon "> - <i class="mdi mdi-delivery" /> - </div> - ); - case "start": - return ( - <div class="timeline-marker is-icon"> - <i class="mdi mdi-flag " /> - </div> - ); - case "wired": - return ( - <div class="timeline-marker is-icon is-success"> - <i class="mdi mdi-cash" /> - </div> - ); - case "wired-range": - return ( - <div class="timeline-marker is-icon is-success"> - <i class="mdi mdi-cash" /> - </div> - ); - case "refund": - return ( - <div class="timeline-marker is-icon is-danger"> - <i class="mdi mdi-cash" /> - </div> - ); - case "refund-taken": - return ( - <div class="timeline-marker is-icon is-success"> - <i class="mdi mdi-cash" /> - </div> - ); - case "now": - return ( - <div class="timeline-marker is-icon is-info"> - <i class="mdi mdi-clock" /> - </div> - ); - } - })()} - <div class="timeline-content"> - {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>} - <p>{e.description}</p> - </div> - </div> - ); - })} - </div> - ); -} -export interface Event { - when: Date; - description: string; - type: - | "start" - | "refund" - | "refund-taken" - | "wired" - | "wired-range" - | "deadline" - | "delivery" - | "now"; -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx deleted file mode 100644 index dfeaa4447..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { - useTranslationContext, - HttpError, - ErrorType, -} 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 { MerchantBackend } from "../../../../declaration.js"; -import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js"; -import { Notification } from "../../../../utils/types.js"; -import { DetailPage } from "./DetailPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -export interface Props { - oid: string; - - onBack: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; -} - -export default function Update({ - oid, - onBack, - onLoadError, - onNotFound, - onUnauthorized, -}: Props): VNode { - const { refundOrder } = useOrderAPI(); - const result = useOrderDetails(oid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - - <DetailPage - onBack={onBack} - id={oid} - onRefund={(id, value) => - refundOrder(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, - }), - ) - } - selected={result.data} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx deleted file mode 100644 index 9df006083..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - 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/instance/orders/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx deleted file mode 100644 index 187a1d0b4..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 ( - <Fragment> - <div class="columns"> - <div class="column is-two-thirds"> - <div class="tabs" style={{ overflow: "inherit" }}> - <ul> - <li class={isNotPaidActive}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`only show paid orders`} - > - <a onClick={onShowNotPaid}> - <i18n.Translate>New</i18n.Translate> - </a> - </div> - </li> - <li class={isPaidActive}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`only show paid orders`} - > - <a onClick={onShowPaid}> - <i18n.Translate>Paid</i18n.Translate> - </a> - </div> - </li> - <li class={isRefundedActive}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`only show orders with refunds`} - > - <a onClick={onShowRefunded}> - <i18n.Translate>Refunded</i18n.Translate> - </a> - </div> - </li> - <li class={isNotWiredActive}> - <div - class="has-tooltip-left" - data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} - > - <a onClick={onShowNotWired}> - <i18n.Translate>Not wired</i18n.Translate> - </a> - </div> - </li> - <li class={isWiredActive}> - <div - class="has-tooltip-left" - data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} - > - <a onClick={onShowWired}> - <i18n.Translate>Completed</i18n.Translate> - </a> - </div> - </li> - <li class={isAllActive}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`remove all filters`} - > - <a onClick={onShowAll}> - <i18n.Translate>All</i18n.Translate> - </a> - </div> - </li> - </ul> - </div> - </div> - <div class="column "> - <div class="buttons is-right"> - <div class="field has-addons"> - {jumpToDate && ( - <div class="control"> - <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}> - <span - class="icon" - data-tooltip={i18n.str`clear date filter`} - > - <i class="mdi mdi-close" /> - </span> - </a> - </div> - )} - <div class="control"> - <span class="has-tooltip-top" data-tooltip={dateTooltip}> - <input - class="input" - type="text" - readonly - value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))} - placeholder={i18n.str`date (${dateFormatForSettings(settings)})`} - onClick={() => { - setPickDate(true); - }} - /> - </span> - </div> - <div class="control"> - <span class="has-tooltip-left" data-tooltip={dateTooltip}> - <a - class="button is-fullwidth" - onClick={() => { - setPickDate(true); - }} - > - <span class="icon"> - <i class="mdi mdi-calendar" /> - </span> - </a> - </span> - </div> - </div> - </div> - </div> - </div> - - <DatePicker - opened={pickDate} - closeFunction={() => setPickDate(false)} - dateReceiver={onSelectDate} - /> - - <CardTable - orders={orders} - onCreate={onCreate} - onCopyURL={onCopyURL} - onSelect={onSelectOrder} - onRefund={onRefundOrder} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx deleted file mode 100644 index cbe8331ca..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx +++ /dev/null @@ -1,417 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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<string[]>([]); - - const { i18n } = useTranslationContext(); - - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash-register" /> - </span> - <i18n.Translate>Orders</i18n.Translate> - </p> - - <div class="card-header-icon" aria-label="more options" /> - - <div class="card-header-icon" aria-label="more options"> - <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {orders.length > 0 ? ( - <Table - instances={orders} - onSelect={onSelect} - onRefund={onRefund} - onCopyURL={(o) => onCopyURL(o.id)} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string[]; - instances: Entity[]; - onRefund: (id: Entity) => void; - onCopyURL: (id: Entity) => void; - onSelect: (id: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; - 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 ( - <div class="table-container"> - {hasMoreBefore && ( - <button - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>load newer orders</i18n.Translate> - </button> - )} - <table class="table is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th style={{ minWidth: 100 }}> - <i18n.Translate>Date</i18n.Translate> - </th> - <th style={{ minWidth: 100 }}> - <i18n.Translate>Amount</i18n.Translate> - </th> - <th style={{ minWidth: 400 }}> - <i18n.Translate>Summary</i18n.Translate> - </th> - <th style={{ minWidth: 50 }} /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.timestamp.t_s === "never" - ? "never" - : format( - new Date(i.timestamp.t_s * 1000), - datetimeFormatForSettings(settings), - )} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.amount} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.summary} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - {i.refundable && ( - <button - class="button is-small is-danger jb-modal" - type="button" - onClick={(): void => onRefund(i)} - > - <i18n.Translate>Refund</i18n.Translate> - </button> - )} - {!i.paid && ( - <button - class="button is-small is-info jb-modal" - type="button" - onClick={(): void => onCopyURL(i)} - > - <i18n.Translate>copy url</i18n.Translate> - </button> - )} - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - {hasMoreAfter && ( - <button - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>load older orders</i18n.Translate> - </button> - )} - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - No orders have been found matching your query! - </i18n.Translate> - </p> - </div> - ); -} - -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<State>({}); - const [settings] = useSettings(); - const { i18n } = useTranslationContext(); - // const [errors, setErrors] = useState<FormErrors<State>>({}); - - 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<State> = { - 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 ( - <ConfirmModal - description="refund" - danger - active - disabled={!isRefundable || hasErrors} - onCancel={onCancel} - onConfirm={validateAndConfirm} - > - {refunds.length > 0 && ( - <div class="columns"> - <div class="column is-12"> - <InputGroup - name="asd" - label={`${Amounts.stringify(totalRefunded)} was already refunded`} - > - <table class="table is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>date</i18n.Translate> - </th> - <th> - <i18n.Translate>amount</i18n.Translate> - </th> - <th> - <i18n.Translate>reason</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {refunds.map((r) => { - return ( - <tr key={r.timestamp.t_s}> - <td> - {r.timestamp.t_s === "never" - ? "never" - : format( - new Date(r.timestamp.t_s * 1000), - datetimeFormatForSettings(settings), - )} - </td> - <td>{r.amount}</td> - <td>{r.reason}</td> - </tr> - ); - })} - </tbody> - </table> - </InputGroup> - </div> - </div> - )} - - {isRefundable && ( - <FormProvider<State> - errors={errors} - object={form} - valueHandler={(d) => setValue(d as any)} - > - <InputCurrency<State> - name="refund" - label={i18n.str`Refund`} - tooltip={i18n.str`amount to be refunded`} - > - <i18n.Translate>Max refundable:</i18n.Translate>{" "} - {Amounts.stringify(totalRefundable)} - </InputCurrency> - <InputSelector - name="mainReason" - label={i18n.str`Reason`} - values={[ - i18n.str`Choose one...`, - duplicatedText, - i18n.str`requested by the customer`, - i18n.str`other`, - ]} - tooltip={i18n.str`why this order is being refunded`} - /> - {form.mainReason && form.mainReason !== duplicatedText ? ( - <Input<State> - label={i18n.str`Description`} - name="description" - tooltip={i18n.str`more information to give context`} - /> - ) : undefined} - </FormProvider> - )} - </ConfirmModal> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx deleted file mode 100644 index 369ef8c8b..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx +++ /dev/null @@ -1,231 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { MerchantBackend } from "../../../../declaration.js"; -import { - InstanceOrderFilter, - useInstanceOrders, - useOrderAPI, - useOrderDetails, -} from "../../../../hooks/order.js"; -import { Notification } from "../../../../utils/types.js"; -import { ListPage } from "./ListPage.js"; -import { RefundModal } from "./Table.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onSelect: (id: string) => void; - onCreate: () => void; -} - -export default function OrderList({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const [filter, setFilter] = useState<InstanceOrderFilter>({ 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<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <section class="section is-main-section"> - <NotificationCard notification={notif} /> - - <JumpToElementById - testIfExist={getPaymentURL} - onSelect={onSelect} - description={i18n.str`jump to order with the given product ID`} - placeholder={i18n.str`order id`} - /> - - <ListPage - orders={result.data.orders.map((o) => ({ ...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 && ( - <RefundModalForTable - id={orderToBeRefunded.order_id} - onCancel={() => 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 <div />; - }} - onUnauthorized={onUnauthorized} - onNotFound={() => { - setNotif({ - message: i18n.str`could not get the order to refund`, - type: "ERROR", - // description: error.message - }); - setOrderToBeRefunded(undefined); - return <div />; - }} - /> - )} - </section> - ); -} - -interface RefundProps { - id: string; - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => 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 <Loading />; - 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 ( - <RefundModal - order={result.data} - onCancel={onCancel} - onConfirm={onConfirm} - /> - ); -} - -async function copyToClipboard(text: string): Promise<void> { - return navigator.clipboard.writeText(text); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx deleted file mode 100644 index 36b31ebe8..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/OtpDevices/Create", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx deleted file mode 100644 index e3ecb3243..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx +++ /dev/null @@ -1,179 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { isRfc3548Base32Charset, randomRfc3548Base32Key } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -const algorithms = [0, 1, 2]; -const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const backend = useBackendContext(); - - const [state, setState] = useState<Partial<Entity>>({}); - - const [showKey, setShowKey] = useState(false); - - const errors: FormErrors<Entity> = { - otp_device_id: !state.otp_device_id - ? i18n.str`required` - : !/[a-zA-Z0-9]*/.test(state.otp_device_id) - ? i18n.str`no valid. only characters and numbers` - : undefined, - otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined, - otp_key: !state.otp_key - ? i18n.str`required` - : !isRfc3548Base32Charset(state.otp_key) - ? i18n.str`just letters and numbers from 2 to 7` - : state.otp_key.length !== 32 - ? i18n.str`size of the key should be 32` - : undefined, - otp_device_description: !state.otp_device_description - ? i18n.str`required` - : !/[a-zA-Z0-9]*/.test(state.otp_device_description) - ? i18n.str`no valid. only characters and numbers` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onCreate(state as any); - }; - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <Input<Entity> - name="otp_device_id" - label={i18n.str`ID`} - tooltip={i18n.str`Internal id on the system`} - /> - <Input<Entity> - name="otp_device_description" - label={i18n.str`Descripiton`} - tooltip={i18n.str`Useful to identify the device physically`} - /> - <InputSelector<Entity> - name="otp_algorithm" - label={i18n.str`Verification algorithm`} - tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} - values={algorithms} - toStr={(v) => algorithmsNames[v]} - fromStr={(v) => Number(v)} - /> - {state.otp_algorithm && state.otp_algorithm > 0 ? ( - <Fragment> - <InputWithAddon<Entity> - expand - name="otp_key" - label={i18n.str`Device key`} - inputType={showKey ? "text" : "password"} - help="Be sure to be very hard to guess or use the random generator" - tooltip={i18n.str`Your device need to have exactly the same value`} - fromStr={(v) => v.toUpperCase()} - addonAfterAction={() => { - setShowKey(!showKey); - }} - addonAfter={ - <span class="icon"> - {showKey ? ( - <i class="mdi mdi-eye" /> - ) : ( - <i class="mdi mdi-eye-off" /> - )} - </span> - } - side={ - <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-3" - onClick={(e) => { - setState((s) => ({ - ...s, - otp_key: randomRfc3548Base32Key(), - })); - }} - > - <i18n.Translate>random</i18n.Translate> - </button> - } - /> - </Fragment> - ) : undefined} - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx deleted file mode 100644 index cf60925a4..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { QR } from "../../../../components/exception/QR.js"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { useInstanceContext } from "../../../../context/instance.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useBackendContext } from "../../../../context/backend.js"; - -type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; - -interface Props { - entity: Entity; - onConfirm: () => void; -} - -function isNotUndefined<X>(x: X | undefined): x is X { - return !!x; -} - -export function CreatedSuccessfully({ - entity, - onConfirm, -}: Props): VNode { - const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backendURL).hostname; - const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; - const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; - - return ( - <Template onConfirm={onConfirm} > - <p class="is-size-5"> - <i18n.Translate> - You can scan the next QR code with your device or safe the key before continue. - </i18n.Translate> - </p> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">ID</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - readonly - class="input" - value={entity.otp_device_id} - /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"><i18n.Translate>Description</i18n.Translate></label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={entity.otp_device_description} - /> - </p> - </div> - </div> - </div> - <QR - text={qrText} - /> - <div - style={{ - color: "grey", - fontSize: "small", - width: 200, - textAlign: "center", - margin: "auto", - wordBreak: "break-all", - }} - > - {qrTextSafe} - </div> - </Template> - ); -} - diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx deleted file mode 100644 index f0b419f68..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useWebhookAPI } from "../../../../hooks/webhooks.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; -import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; -import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; - -export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} - -export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { createOtpDevice } = useOtpDeviceAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null) - - if (created) { - return <CreatedSuccessfully entity={created} onConfirm={onConfirm} /> - } - - return ( - <> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={(request: Entity) => { - return createOtpDevice(request) - .then((d) => { - setCreated(request) - }) - .catch((error) => { - setNotif({ - message: i18n.str`could not create device`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx deleted file mode 100644 index 49032c80e..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { FunctionalComponent, h } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; - -export default { - title: "Pages/OtpDevices/List", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx deleted file mode 100644 index f3b5a2088..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { CardTable } from "./Table.js"; - -export interface Props { - devices: MerchantBackend.OTP.OtpDeviceEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; - onCreate: () => void; - onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; - onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; -} - -export function ListPage({ - devices, - onCreate, - onDelete, - onSelect, - onLoadMoreBefore, - onLoadMoreAfter, -}: Props): VNode { - const form = { payto_uri: "" }; - - const { i18n } = useTranslationContext(); - return ( - <section class="section is-main-section"> - <CardTable - devices={devices.map((o) => ({ - ...o, - id: String(o.otp_device_id), - }))} - onCreate={onCreate} - onDelete={onDelete} - onSelect={onSelect} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} - /> - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx deleted file mode 100644 index d085d1c44..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.OTP.OtpDeviceEntry; - -interface Props { - devices: Entity[]; - onDelete: (e: Entity) => void; - onSelect: (e: Entity) => void; - onCreate: () => void; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -export function CardTable({ - devices, - onCreate, - onDelete, - onSelect, - onLoadMoreAfter, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - - const { i18n } = useTranslationContext(); - - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-newspaper" /> - </span> - <i18n.Translate>OTP Devices</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new devices`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {devices.length > 0 ? ( - <Table - instances={devices} - onDelete={onDelete} - onSelect={onSelect} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string[]; - instances: Entity[]; - onDelete: (e: Entity) => void; - onSelect: (e: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - -function Table({ - instances, - onLoadMoreAfter, - onDelete, - onSelect, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - {hasMoreBefore && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more devices before the first one`} - onClick={onLoadMoreBefore} - > - <i18n.Translate>load newer devices</i18n.Translate> - </button> - )} - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>ID</i18n.Translate> - </th> - <th> - <i18n.Translate>Description</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.otp_device_id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.otp_device_id} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.otp_device_id} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected devices from the database`} - onClick={() => onDelete(i)} - > - Delete - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - {hasMoreAfter && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more devices after the last one`} - onClick={onLoadMoreAfter} - > - <i18n.Translate>load older devices</i18n.Translate> - </button> - )} - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no devices yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx deleted file mode 100644 index a3299326c..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; -import { Notification } from "../../../../utils/types.js"; -import { ListPage } from "./ListPage.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onCreate: () => void; - onSelect: (id: string) => void; -} - -export default function ListOtpDevices({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); - const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteOtpDevice } = useOtpDeviceAPI(); - const result = useInstanceOtpDevices({ position }, (id) => setPosition(id)); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - - <ListPage - devices={result.data.otp_devices} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} - onCreate={onCreate} - onSelect={(e) => { - onSelect(e.otp_device_id); - }} - onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) => - deleteOtpDevice(e.otp_device_id) - .then(() => - setNotif({ - message: i18n.str`validator delete successfully`, - type: "SUCCESS", - }), - ) - .catch((error) => - setNotif({ - message: i18n.str`could not delete the validator`, - type: "ERROR", - description: error.message, - }), - ) - } - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx deleted file mode 100644 index 06ea9d07a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { UpdatePage as TestedComponent } from "./UpdatePage.js"; - -export default { - title: "Pages/OtpDevices/Update", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx deleted file mode 100644 index 310810576..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { randomRfc3548Base32Key } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; - -type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; - -interface Props { - onUpdate: (d: Entity) => Promise<void>; - onBack?: () => void; - device: Entity; -} -const algorithms = [0, 1, 2]; -const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; -export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - - const [state, setState] = useState<Partial<Entity>>(device); - const [showKey, setShowKey] = useState(false); - - const errors: FormErrors<Entity> = {}; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onUpdate(state as any); - }; - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - Device: <b>{device.id}</b> - </span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <Input<Entity> - name="otp_device_description" - label={i18n.str`Description`} - tooltip={i18n.str`Useful to identify the device physically`} - /> - <InputSelector<Entity> - name="otp_algorithm" - label={i18n.str`Verification algorithm`} - tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} - values={algorithms} - toStr={(v) => algorithmsNames[v]} - fromStr={(v) => Number(v)} - /> - {state.otp_algorithm && state.otp_algorithm > 0 ? ( - <Fragment> - <InputWithAddon<Entity> - name="otp_key" - label={i18n.str`Device key`} - readonly={state.otp_key === undefined} - inputType={showKey ? "text" : "password"} - help={ - state.otp_key === undefined - ? "Not modified" - : "Be sure to be very hard to guess or use the random generator" - } - tooltip={i18n.str`Your device need to have exactly the same value`} - fromStr={(v) => v.toUpperCase()} - addonAfterAction={() => { - setShowKey(!showKey); - }} - addonAfter={ - <span - class="icon" - onClick={() => { - setShowKey(!showKey); - }} - > - {showKey ? ( - <i class="mdi mdi-eye" /> - ) : ( - <i class="mdi mdi-eye-off" /> - )} - </span> - } - side={ - state.otp_key === undefined ? ( - <button - onClick={(e) => { - setState((s) => ({ ...s, otp_key: "" })); - }} - class="button" - > - change key - </button> - ) : ( - <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-3" - onClick={(e) => { - setState((s) => ({ - ...s, - otp_key: randomRfc3548Base32Key(), - })); - }} - > - <i18n.Translate>random</i18n.Translate> - </button> - ) - } - /> - </Fragment> - ) : undefined}{" "} - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - </div> - </section> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx deleted file mode 100644 index 3128608e1..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { MerchantBackend, WithId } from "../../../../declaration.js"; -import { Notification } from "../../../../utils/types.js"; -import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js"; - -export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; - -interface Props { - onBack?: () => void; - onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - vid: string; -} -export default function UpdateValidator({ - vid, - onConfirm, - onBack, - onUnauthorized, - onNotFound, - onLoadError, -}: Props): VNode { - const { updateOtpDevice } = useOtpDeviceAPI(); - const result = useOtpDeviceDetails(vid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - <UpdatePage - device={{ - id: vid, - otp_algorithm: result.data.otp_algorithm, - otp_device_description: result.data.device_description, - otp_key: undefined, - otp_ctr: result.data.otp_ctr - }} - onBack={onBack} - onUpdate={(data) => { - return updateOtpDevice(vid, data) - .then(onConfirm) - .catch((error) => { - setNotif({ - message: i18n.str`could not update template`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx deleted file mode 100644 index 22bbfe28a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Product/Create", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx deleted file mode 100644 index 6cbc26d8d..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { ProductForm } from "../../../../components/product/ProductForm.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useListener } from "../../../../hooks/listener.js"; - -type Entity = MerchantBackend.Products.ProductAddDetail & { - product_id: string; -}; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( - (result) => { - if (result) return onCreate(result); - return Promise.reject(); - }, - ); - - const { i18n } = useTranslationContext(); - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <ProductForm onSubscribe={addFormSubmitter} /> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - onClick={submitForm} - data-tooltip={ - !submitForm - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={!submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx deleted file mode 100644 index 2b6ebed45..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { h, VNode } from "preact"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { Entity } from "./index.js"; -import emptyImage from "../../assets/empty.png"; - -interface Props { - entity: Entity; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -export function CreatedSuccessfully({ - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - return ( - <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Image</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <img src={entity.image} style={{ width: 200, height: 200 }} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Description</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <textarea class="input" readonly value={entity.description} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Price</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={entity.price} /> - </p> - </div> - </div> - </div> - </Template> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx deleted file mode 100644 index 0322f2796..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { AuditorBackend, MerchantBackend } from "../../../../declaration.js"; -import { useProductAPI } from "../../../../hooks/product.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; - -export type Entity = MerchantBackend.Products.ProductDetail; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} -export default function CreateProduct({ onConfirm, onBack }: Props): VNode { - const { createProduct } = useProductAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - <NotificationCard notification={notif} /> - - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx deleted file mode 100644 index a2f996221..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CardTable as TestedComponent } from "./Table.js"; - -export default { - title: "Pages/Product/List", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onSelect: { action: "onSelect" }, - onDelete: { action: "onDelete" }, - onUpdate: { action: "onUpdate" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, { - instances: [ - { - id: "orderid", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10", - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: 15, - unit: "bar", - address: {}, - }, - ], -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx deleted file mode 100644 index b02d2f23a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx +++ /dev/null @@ -1,496 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { ComponentChildren, Fragment, h, VNode } from "preact"; -import { StateUpdater, useState } from "preact/hooks"; -import emptyImage from "../../../../assets/empty.png"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Products.ProductDetail & WithId; - -interface Props { - instances: Entity[]; - onDelete: (id: Entity) => void; - onSelect: (product: Entity) => void; - onUpdate: ( - id: string, - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise<void>; - onCreate: () => void; - selected?: boolean; -} - -export function CardTable({ - instances, - onCreate, - onSelect, - onUpdate, - onDelete, -}: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string | undefined>( - undefined, - ); - const { i18n } = useTranslationContext(); - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-shopping" /> - </span> - <i18n.Translate>Inventory</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add product to inventory`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {instances.length > 0 ? ( - <Table - instances={instances} - onSelect={onSelect} - onDelete={onDelete} - onUpdate={onUpdate} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string | undefined; - instances: Entity[]; - onSelect: (id: Entity) => void; - onUpdate: ( - id: string, - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise<void>; - onDelete: (id: Entity) => void; - rowSelectionHandler: StateUpdater<string | undefined>; -} - -function Table({ - rowSelection, - rowSelectionHandler, - instances, - onSelect, - onUpdate, - onDelete, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Image</i18n.Translate> - </th> - <th> - <i18n.Translate>Description</i18n.Translate> - </th> - <th> - <i18n.Translate>Price per unit</i18n.Translate> - </th> - <th> - <i18n.Translate>Taxes</i18n.Translate> - </th> - <th> - <i18n.Translate>Sales</i18n.Translate> - </th> - <th> - <i18n.Translate>Stock</i18n.Translate> - </th> - <th> - <i18n.Translate>Sold</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - const restStockInfo = !i.next_restock - ? "" - : i.next_restock.t_s === "never" - ? "never" - : `restock at ${format( - new Date(i.next_restock.t_s * 1000), - dateFormatForSettings(settings), - )}`; - let stockInfo: ComponentChildren = ""; - if (i.total_stock < 0) { - stockInfo = "infinite"; - } else { - const totalStock = i.total_stock - i.total_lost - i.total_sold; - stockInfo = ( - <label title={restStockInfo}> - {totalStock} {i.unit} - </label> - ); - } - - const isFree = Amounts.isZero(Amounts.parseOrThrow(i.price)); - - return ( - <Fragment key={i.id}> - <tr key="info"> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - <img - src={i.image ? i.image : emptyImage} - style={{ - border: "solid black 1px", - maxHeight: "2em", - width: "auto", - height: "auto", - }} - /> - </td> - <td - class="has-tooltip-right" - data-tooltip={i.description} - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description} - </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`} - </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {sum(i.taxes)} - </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {difference(i.price, sum(i.taxes))} - </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {stockInfo} - </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - <span style={{"whiteSpace":"nowrap"}}> - - {i.total_sold} {i.unit} - </span> - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <span - class="has-tooltip-bottom" - data-tooltip={i18n.str`go to product update page`} - > - <button - class="button is-small is-success " - type="button" - onClick={(): void => onSelect(i)} - > - <i18n.Translate>Update</i18n.Translate> - </button> - </span> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`remove this product from the database`} - > - <button - class="button is-small is-danger" - type="button" - onClick={(): void => onDelete(i)} - > - <i18n.Translate>Delete</i18n.Translate> - </button> - </span> - </div> - </td> - </tr> - {rowSelection === i.id && ( - <tr key="form"> - <td colSpan={10}> - <FastProductUpdateForm - product={i} - onUpdate={(prod) => - onUpdate(i.id, prod).then((r) => - rowSelectionHandler(undefined), - ) - } - onCancel={() => rowSelectionHandler(undefined)} - /> - </td> - </tr> - )} - </Fragment> - ); - })} - </tbody> - </table> - </div> - ); -} - -interface FastProductUpdateFormProps { - product: Entity; - onUpdate: ( - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise<void>; - onCancel: () => void; -} -interface FastProductUpdate { - incoming: number; - lost: number; - price: string; -} -interface UpdatePrice { - price: string; -} - -function FastProductWithInfiniteStockUpdateForm({ - product, - onUpdate, - onCancel, -}: FastProductUpdateFormProps) { - const [value, valueHandler] = useState<UpdatePrice>({ price: product.price }); - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - <FormProvider<FastProductUpdate> - name="added" - object={value} - valueHandler={valueHandler as any} - > - <InputCurrency<FastProductUpdate> - name="price" - label={i18n.str`Price`} - tooltip={i18n.str`update the product with new price`} - /> - </FormProvider> - - <div class="buttons is-expanded"> - - <div class="buttons mt-5"> - - <button class="button mt-5" onClick={onCancel}> - <i18n.Translate>Clone</i18n.Translate> - </button> - </div> - <div class="buttons is-right mt-5"> - <button class="button" onClick={onCancel}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`update product with new price`} - > - <button - class="button is-info" - onClick={() => - onUpdate({ - ...product, - price: value.price, - }) - } - > - <i18n.Translate>Confirm update</i18n.Translate> - </button> - </span> - </div> - </div> - </Fragment> - ); -} - -function FastProductWithManagedStockUpdateForm({ - product, - onUpdate, - onCancel, -}: FastProductUpdateFormProps) { - const [value, valueHandler] = useState<FastProductUpdate>({ - incoming: 0, - lost: 0, - price: product.price, - }); - - const currentStock = - product.total_stock - product.total_sold - product.total_lost; - - const errors: FormErrors<FastProductUpdate> = { - lost: - currentStock + value.incoming < value.lost - ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming - })` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - <FormProvider<FastProductUpdate> - name="added" - errors={errors} - object={value} - valueHandler={valueHandler as any} - > - <InputNumber<FastProductUpdate> - name="incoming" - label={i18n.str`Incoming`} - tooltip={i18n.str`add more elements to the inventory`} - /> - <InputNumber<FastProductUpdate> - name="lost" - label={i18n.str`Lost`} - tooltip={i18n.str`report elements lost in the inventory`} - /> - <InputCurrency<FastProductUpdate> - name="price" - label={i18n.str`Price`} - tooltip={i18n.str`new price for the product`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - <button class="button" onClick={onCancel}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - <span - class="has-tooltip-left" - data-tooltip={ - hasErrors - ? i18n.str`the are value with errors` - : i18n.str`update product with new stock and price` - } - > - <button - class="button is-info" - disabled={hasErrors} - onClick={() => - onUpdate({ - ...product, - total_stock: product.total_stock + value.incoming, - total_lost: product.total_lost + value.lost, - price: value.price, - }) - } - > - <i18n.Translate>Confirm</i18n.Translate> - </button> - </span> - </div> - </Fragment> - ); -} - -function FastProductUpdateForm(props: FastProductUpdateFormProps) { - return props.product.total_stock === -1 ? ( - <FastProductWithInfiniteStockUpdateForm {...props} /> - ) : ( - <FastProductWithManagedStockUpdateForm {...props} /> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no products yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} - -function difference(price: string, tax: number) { - if (!tax) return price; - const ps = price.split(":"); - const p = parseInt(ps[1], 10); - ps[1] = `${p - tax}`; - return ps.join(":"); -} -function sum(taxes: MerchantBackend.Tax[]) { - return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx deleted file mode 100644 index 6adc221da..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { - useInstanceProducts, - useProductAPI, -} from "../../../../hooks/product.js"; -import { Notification } from "../../../../utils/types.js"; -import { CardTable } from "./Table.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; -import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; - -interface Props { - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onCreate: () => void; - onSelect: (id: string) => void; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; -} -export default function ProductList({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const result = useInstanceProducts(); - const { deleteProduct, updateProduct, getProduct } = useProductAPI(); - const [deleting, setDeleting] = - useState<MerchantBackend.Products.ProductDetail & WithId | null>(null); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <section class="section is-main-section"> - <NotificationCard notification={notif} /> - - <JumpToElementById - testIfExist={getProduct} - onSelect={onSelect} - description={i18n.str`jump to product with the given product ID`} - placeholder={i18n.str`product id`} - /> - - <CardTable - instances={result.data} - onCreate={onCreate} - onUpdate={(id, prod) => - updateProduct(id, prod) - .then(() => - setNotif({ - message: i18n.str`product updated successfully`, - type: "SUCCESS", - }), - ) - .catch((error) => - setNotif({ - message: i18n.str`could not update the product`, - type: "ERROR", - description: error.message, - }), - ) - } - onSelect={(product) => onSelect(product.id)} - onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) => - setDeleting(prod) - } - /> - - {deleting && ( - <ConfirmModal - label={`Delete product`} - description={`Delete the product "${deleting.description}"`} - danger - active - onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - await deleteProduct(deleting.id); - setNotif({ - message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`, - type: "SUCCESS", - }); - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete product`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setDeleting(null); - }} - > - <p> - If you delete the product named <b>"{deleting.description}"</b> (ID:{" "} - <b>{deleting.id}</b>), the stock and related information will be lost - </p> - <p class="warning"> - Deleting an product <b>cannot be undone</b>. - </p> - </ConfirmModal> - )} - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx deleted file mode 100644 index d1dc9d540..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { UpdatePage as TestedComponent } from "./UpdatePage.js"; - -export default { - title: "Pages/Product/Update", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const WithManagedStock = createExample(TestedComponent, { - product: { - product_id: "20102-ASDAS-QWE", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10", - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: 15, - unit: "bar", - address: {}, - }, -}); - -export const WithInfiniteStock = createExample(TestedComponent, { - product: { - product_id: "20102-ASDAS-QWE", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10", - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: -1, - unit: "bar", - address: {}, - }, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx deleted file mode 100644 index 53aa9d61f..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { ProductForm } from "../../../../components/product/ProductForm.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useListener } from "../../../../hooks/listener.js"; - -type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; - -interface Props { - onUpdate: (d: Entity) => Promise<void>; - onBack?: () => void; - product: Entity; -} - -export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { - const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( - (result) => { - if (result) return onUpdate(result); - return Promise.resolve(); - }, - ); - - const { i18n } = useTranslationContext(); - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - <i18n.Translate>Product id:</i18n.Translate> - <b>{product.product_id}</b> - </span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <ProductForm - initial={product} - onSubscribe={addFormSubmitter} - alreadyExist - /> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - onClick={submitForm} - data-tooltip={ - !submitForm - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={!submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx deleted file mode 100644 index 3b6ad5fb2..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Reserve/Create", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx deleted file mode 100644 index 2ab12687e..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx +++ /dev/null @@ -1,277 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { HttpError, RequestError, useApiContext, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - PAYTO_WIRE_METHOD_LOOKUP, - URL_REGEX, -} from "../../../../utils/constants.js"; -import { useBackendBaseRequest } from "../../../../hooks/backend.js"; -import { parsePaytoUri } from "@gnu-taler/taler-util"; - -type Entity = MerchantBackend.Rewards.ReserveCreateRequest; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -enum Steps { - EXCHANGE, - WIRE_METHOD, -} - -interface ViewProps { - step: Steps; - setCurrentStep: (s: Steps) => void; - reserve: Partial<Entity>; - onBack?: () => void; - submitForm: () => Promise<void>; - setReserve: StateUpdater<Partial<Entity>>; -} -function ViewStep({ - step, - setCurrentStep, - reserve, - onBack, - submitForm, - setReserve, -}: ViewProps): VNode { - const { i18n } = useTranslationContext(); - const {request} = useApiContext() - const [wireMethods, setWireMethods] = useState<Array<string>>([]); - const [exchangeQueryError, setExchangeQueryError] = useState< - string | undefined - >(undefined); - - useEffect(() => { - setExchangeQueryError(undefined); - }, [reserve.exchange_url]); - - switch (step) { - case Steps.EXCHANGE: { - const errors: FormErrors<Entity> = { - initial_balance: !reserve.initial_balance - ? "cannot be empty" - : !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0) - ? i18n.str`it should be greater than 0` - : undefined, - exchange_url: !reserve.exchange_url - ? i18n.str`cannot be empty` - : !URL_REGEX.test(reserve.exchange_url) - ? i18n.str`must be a valid URL` - : !exchangeQueryError - ? undefined - : exchangeQueryError, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - return ( - <Fragment> - <FormProvider<Entity> - object={reserve} - errors={errors} - valueHandler={setReserve} - > - <InputCurrency<Entity> - name="initial_balance" - label={i18n.str`Initial balance`} - tooltip={i18n.str`balance prior to deposit`} - /> - <Input<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - tooltip={i18n.str`URL of exchange`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - class="has-tooltip-left" - onClick={() => { - if (!reserve.exchange_url) { - return Promise.resolve(); - } - - return request<any>(reserve.exchange_url, "keys") - .then((r) => { - console.log(r) - if (r.loading) return; - if (r.ok) { - const wireMethods = r.data.accounts.map((a: any) => { - const p = parsePaytoUri(a.payto_uri); - const r = p?.targetType - return r - }).filter((x:any) => !!x); - setWireMethods(Array.from(new Set(wireMethods))); - } - setCurrentStep(Steps.WIRE_METHOD); - return; - }) - .catch((r: RequestError<{}>) => { - console.log(r.cause) - setExchangeQueryError(r.cause.message); - }); - }} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={hasErrors} - > - <i18n.Translate>Next</i18n.Translate> - </AsyncButton> - </div> - </Fragment> - ); - } - - case Steps.WIRE_METHOD: { - const errors: FormErrors<Entity> = { - wire_method: !reserve.wire_method - ? i18n.str`cannot be empty` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - return ( - <Fragment> - <FormProvider<Entity> - object={reserve} - errors={errors} - valueHandler={setReserve} - > - <InputCurrency<Entity> - name="initial_balance" - label={i18n.str`Initial balance`} - tooltip={i18n.str`balance prior to deposit`} - readonly - /> - <Input<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - tooltip={i18n.str`URL of exchange`} - readonly - /> - <InputSelector<Entity> - name="wire_method" - label={i18n.str`Wire method`} - tooltip={i18n.str`method to use for wire transfer`} - values={wireMethods} - placeholder={i18n.str`Select one wire method`} - /> - </FormProvider> - <div class="buttons is-right mt-5"> - {onBack && ( - <button - class="button" - onClick={() => setCurrentStep(Steps.EXCHANGE)} - > - <i18n.Translate>Back</i18n.Translate> - </button> - )} - <AsyncButton - onClick={submitForm} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={hasErrors} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </Fragment> - ); - } - } -} - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const [reserve, setReserve] = useState<Partial<Entity>>({}); - - const submitForm = () => { - return onCreate(reserve as Entity); - }; - - const [currentStep, setCurrentStep] = useState(Steps.EXCHANGE); - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <div class="tabs is-toggle is-fullwidth is-small"> - <ul> - <li class={currentStep === Steps.EXCHANGE ? "is-active" : ""}> - <a style={{ cursor: "initial" }}> - <span>Step 1: Specify exchange</span> - </a> - </li> - <li - class={currentStep === Steps.WIRE_METHOD ? "is-active" : ""} - > - <a style={{ cursor: "initial" }}> - <span>Step 2: Select wire method</span> - </a> - </li> - </ul> - </div> - - <ViewStep - step={currentStep} - reserve={reserve} - setCurrentStep={setCurrentStep} - setReserve={setReserve} - submitForm={submitForm} - onBack={onBack} - /> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx deleted file mode 100644 index 9f32a26b8..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatedSuccessfully as TestedComponent } from "./CreatedSuccessfully.js"; -import * as tests from "@gnu-taler/web-util/testing"; - -export default { - title: "Pages/Reserve/CreatedSuccessfully", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onBack: { action: "onBack" }, - }, -}; - -export const OneBankAccount = tests.createExample(TestedComponent, { - entity: { - request: { - exchange_url: "http://exchange.taler/", - initial_balance: "TESTKUDOS:1", - wire_method: "x-taler-bank", - }, - response: { - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - ], - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - }, -}); - -export const ThreeBankAccount = tests.createExample(TestedComponent, { - entity: { - request: { - exchange_url: "http://exchange.taler/", - initial_balance: "TESTKUDOS:1", - wire_method: "x-taler-bank", - }, - response: { - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - { - payto_uri: "payto://x-taler-bank/bank1.taler:8080/asd", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - { - payto_uri: "payto://x-taler-bank/bank2.taler:8080/qwe", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - ], - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - }, -}); - -export const NoBankAccount = tests.createExample(TestedComponent, { - entity: { - request: { - exchange_url: "http://exchange.taler/", - initial_balance: "TESTKUDOS:1", - wire_method: "x-taler-bank", - }, - response: { - accounts: [ - { - payto_uri: "payo://x-talr-bank/bank.taler:8080/exchange_account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - { - payto_uri: "payto://x-taler-bank", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - ], - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - }, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx deleted file mode 100644 index 336e52b5e..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { QR } from "../../../../components/exception/QR.js"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { MerchantBackend, WireAccount } from "../../../../declaration.js"; - -type Entity = { - request: MerchantBackend.Rewards.ReserveCreateRequest; - response: MerchantBackend.Rewards.ReserveCreateConfirmation; -}; - -interface Props { - entity: Entity; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -function isNotUndefined<X>(x: X | undefined): x is X { - return !!x; -} - -export function CreatedSuccessfully({ - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - const { i18n } = useTranslationContext(); - return ( - <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Amount</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - readonly - class="input" - value={entity.request.initial_balance} - /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Wire transfer subject</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={entity.response.reserve_pub} - /> - </p> - </div> - </div> - </div> - <ShowAccountsOfReserveAsQRWithLink - accounts={entity.response.accounts ?? []} - message={entity.response.reserve_pub} - amount={entity.request.initial_balance} - /> - </Template> - ); -} - -export function ShowAccountsOfReserveAsQRWithLink({ - accounts, - message, - amount, -}: { - accounts: WireAccount[]; - message: string; - amount: string; -}): VNode { - const { i18n } = useTranslationContext(); - const accountsInfo = !accounts - ? [] - : accounts - .map((acc) => { - const p = parsePaytoUri(acc.payto_uri); - if (p) { - p.params["message"] = message; - p.params["amount"] = amount; - } - return p; - }) - .filter(isNotUndefined); - - const links = accountsInfo.map((a) => stringifyPaytoUri(a)); - - if (links.length === 0) { - return ( - <Fragment> - <p class="is-size-5"> - The reserve have invalid accounts. List of invalid payto URIs below: - </p> - <ul> - {accounts.map((a, idx) => { - return <li key={idx}>{a.payto_uri}</li>; - })} - </ul> - </Fragment> - ); - } - - if (links.length === 1) { - return ( - <Fragment> - <p class="is-size-5"> - <i18n.Translate> - To complete the setup of the reserve, you must now initiate a wire - transfer using the given wire transfer subject and crediting the - specified amount to the indicated account of the exchange. - </i18n.Translate> - </p> - <p style={{ margin: 10 }}> - <b>Exchange bank account</b> - </p> - <QR text={links[0]} /> - <p class="is-size-5"> - <i18n.Translate> - If your system supports RFC 8905, you can do this by opening this - URI: - </i18n.Translate> - </p> - <pre> - <a target="_blank" rel="noreferrer" href={links[0]}> - {links[0]} - </a> - </pre> - </Fragment> - ); - } - - return ( - <div> - <p class="is-size-5"> - <i18n.Translate> - To complete the setup of the reserve, you must now initiate a wire - transfer using the given wire transfer subject and crediting the - specified amount to one of the indicated account of the exchange. - </i18n.Translate> - </p> - - <p style={{ margin: 10 }}> - <b>Exchange bank accounts</b> - </p> - <p class="is-size-5"> - <i18n.Translate> - If your system supports RFC 8905, you can do this by clicking on the - URI below the QR code: - </i18n.Translate> - </p> - {links.map((link) => { - return ( - <Fragment> - <QR text={link} /> - <pre> - <a target="_blank" rel="noreferrer" href={link}> - {link} - </a> - </pre> - </Fragment> - ); - })} - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx deleted file mode 100644 index 13ea2398c..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useReservesAPI } from "../../../../hooks/reserves.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; -import { CreatePage } from "./CreatePage.js"; -interface Props { - onBack: () => void; - onConfirm: () => void; -} -export default function CreateReserve({ onBack, onConfirm }: Props): VNode { - const { createReserve } = useReservesAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - const [createdOk, setCreatedOk] = useState< - | { - request: MerchantBackend.Rewards.ReserveCreateRequest; - response: MerchantBackend.Rewards.ReserveCreateConfirmation; - } - | undefined - >(undefined); - - if (createdOk) { - return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />; - } - - return ( - <Fragment> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => { - return createReserve(request) - .then((r) => setCreatedOk({ request, response: r.data })) - .catch((error) => { - setNotif({ - message: i18n.str`could not create reserve`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx deleted file mode 100644 index d5698e6c7..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx +++ /dev/null @@ -1,266 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { - Amounts, - parsePaytoUri, - stringifyPaytoUri, -} from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { QR } from "../../../../components/exception/QR.js"; -import { FormProvider } from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDate } from "../../../../components/form/InputDate.js"; -import { TextField } from "../../../../components/form/TextField.js"; -import { SimpleModal } from "../../../../components/modal/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useRewardDetails } from "../../../../hooks/reserves.js"; -import { RewardInfo } from "./RewardInfo.js"; -import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.ReserveDetail; -type CT = MerchantBackend.ContractTerms; - -interface Props { - onBack: () => void; - selected: Entity; - id: string; -} - -export function DetailPage({ id, selected, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const didExchangeAckTransfer = Amounts.isNonZero( - Amounts.parseOrThrow(selected.exchange_initial_amount), - ); - - return ( - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <div class="section main-section"> - <FormProvider object={{ ...selected, id }} valueHandler={null}> - <InputDate<Entity> - name="creation_time" - label={i18n.str`Created at`} - readonly - /> - <InputDate<Entity> - name="expiration_time" - label={i18n.str`Valid until`} - readonly - /> - <InputCurrency<Entity> - name="merchant_initial_amount" - label={i18n.str`Created balance`} - readonly - /> - <TextField<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - readonly - > - <a target="_blank" rel="noreferrer" href={selected.exchange_url}> - {selected.exchange_url} - </a> - </TextField> - - {didExchangeAckTransfer && ( - <Fragment> - <InputCurrency<Entity> - name="exchange_initial_amount" - label={i18n.str`Exchange balance`} - readonly - /> - <InputCurrency<Entity> - name="pickup_amount" - label={i18n.str`Picked up`} - readonly - /> - <InputCurrency<Entity> - name="committed_amount" - label={i18n.str`Committed`} - readonly - /> - </Fragment> - )} - <Input name="id" label={i18n.str`Subject`} readonly /> - </FormProvider> - - {didExchangeAckTransfer ? ( - <Fragment> - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash-register" /> - </span> - <i18n.Translate>Rewards</i18n.Translate> - </p> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {selected.rewards && selected.rewards.length > 0 ? ( - <Table rewards={selected.rewards} /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - </Fragment> - ) : selected.accounts ? ( - <ShowAccountsOfReserveAsQRWithLink - accounts={selected.accounts} - amount={selected.merchant_initial_amount} - message={id} - /> - ) : undefined} - - <div class="buttons is-right mt-5"> - <button class="button" onClick={onBack}> - <i18n.Translate>Back</i18n.Translate> - </button> - </div> - </div> - </div> - <div class="column" /> - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - No reward has been authorized from this reserve - </i18n.Translate> - </p> - </div> - ); -} - -interface TableProps { - rewards: MerchantBackend.Rewards.RewardStatusEntry[]; -} - -function Table({ rewards }: TableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Authorized</i18n.Translate> - </th> - <th> - <i18n.Translate>Picked up</i18n.Translate> - </th> - <th> - <i18n.Translate>Reason</i18n.Translate> - </th> - <th> - <i18n.Translate>Expiration</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {rewards.map((t, i) => { - return <RewardRow id={t.reward_id} key={i} entry={t} />; - })} - </tbody> - </table> - </div> - ); -} - -function RewardRow({ - id, - entry, -}: { - id: string; - entry: MerchantBackend.Rewards.RewardStatusEntry; -}) { - const [selected, setSelected] = useState(false); - const result = useRewardDetails(id); - const [settings] = useSettings(); - if (result.loading) { - return ( - <tr> - <td>...</td> - <td>...</td> - <td>...</td> - <td>...</td> - </tr> - ); - } - if (!result.ok) { - return ( - <tr> - <td>...</td> {/* authorized */} - <td>{entry.total_amount}</td> - <td>{entry.reason}</td> - <td>...</td> {/* expired */} - </tr> - ); - } - const info = result.data; - function onSelect() { - setSelected(true); - } - return ( - <Fragment> - {selected && ( - <SimpleModal - description="reward" - active - onCancel={() => setSelected(false)} - > - <RewardInfo id={id} amount={info.total_authorized} entity={info} /> - </SimpleModal> - )} - <tr> - <td onClick={onSelect}>{info.total_authorized}</td> - <td onClick={onSelect}>{info.total_picked_up}</td> - <td onClick={onSelect}>{info.reason}</td> - <td onClick={onSelect}> - {info.expiration.t_s === "never" - ? "never" - : format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))} - </td> - </tr> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx deleted file mode 100644 index e79b78027..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { DetailPage as TestedComponent } from "./DetailPage.js"; - -export default { - title: "Pages/Reserve/Detail", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Funded = createExample(TestedComponent, { - id: "THISISTHERESERVEID", - selected: { - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "", - }, - ], - exchange_url: "http://exchange.taler/", - }, -}); - -export const NotYetFunded = createExample(TestedComponent, { - id: "THISISTHERESERVEID", - selected: { - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:0", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "", - }, - ], - exchange_url: "http://exchange.taler/", - }, -}); - -export const FundedWithEmptyRewards = createExample(TestedComponent, { - id: "THISISTHERESERVEID", - selected: { - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "", - }, - ], - exchange_url: "http://exchange.taler/", - rewards: [ - { - reason: "asdasd", - reward_id: "123", - total_amount: "TESTKUDOS:1", - }, - ], - }, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx deleted file mode 100644 index 01f2bf250..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - datetimeFormatForSettings, - useSettings, -} from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.RewardDetails; - -interface Props { - id: string; - entity: Entity; - amount: string; -} - -export function RewardInfo({ - id: merchantRewardId, - amount, - entity, -}: Props): VNode { - const { url: backendURL } = useBackendContext(); - const [settings] = useSettings(); - const rewardURL = "not-supported"; - return ( - <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Amount</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={amount} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">URL</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field" style={{ overflowWrap: "anywhere" }}> - <p class="control"> - <a target="_blank" rel="noreferrer" href={rewardURL}> - {rewardURL} - </a> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Valid until</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={ - !entity.expiration || entity.expiration.t_s === "never" - ? "never" - : format( - entity.expiration.t_s * 1000, - datetimeFormatForSettings(settings), - ) - } - /> - </p> - </div> - </div> - </div> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx deleted file mode 100644 index 5146545ac..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { Loading } from "../../../../components/exception/loading.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useReserveDetails } from "../../../../hooks/reserves.js"; -import { DetailPage } from "./DetailPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -interface Props { - rid: string; - - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onDelete: () => void; - onBack: () => void; -} -export default function DetailReserve({ - rid, - onUnauthorized, - onLoadError, - onNotFound, - onBack, - onDelete, -}: Props): VNode { - const result = useReserveDetails(rid); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <DetailPage selected={result.data} onBack={onBack} id={rid} /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx deleted file mode 100644 index 4b688d932..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import * as yup from "yup"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { - ConfirmModal, - ContinueModal, -} from "../../../../components/modal/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { AuthorizeRewardSchema } from "../../../../schemas/index.js"; -import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; - -interface AuthorizeRewardModalProps { - onCancel: () => void; - onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void; - rewardAuthorized?: { - response: MerchantBackend.Rewards.RewardCreateConfirmation; - request: MerchantBackend.Rewards.RewardCreateRequest; - }; -} - -export function AuthorizeRewardModal({ - onCancel, - onConfirm, - rewardAuthorized, -}: AuthorizeRewardModalProps): VNode { - // const result = useOrderDetails(id) - type State = MerchantBackend.Rewards.RewardCreateRequest; - const [form, setValue] = useState<Partial<State>>({}); - const { i18n } = useTranslationContext(); - - // const [errors, setErrors] = useState<FormErrors<State>>({}) - let errors: FormErrors<State> = {}; - try { - AuthorizeRewardSchema.validateSync(form, { abortEarly: false }); - } catch (err) { - if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as any[]; - errors = yupErrors.reduce( - (prev, cur) => - !cur.path ? prev : { ...prev, [cur.path]: cur.message }, - {}, - ); - } - } - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const validateAndConfirm = () => { - onConfirm(form as State); - }; - if (rewardAuthorized) { - return ( - <ContinueModal description="reward" active onConfirm={onCancel}> - <CreatedSuccessfully - entity={rewardAuthorized.response} - request={rewardAuthorized.request} - onConfirm={onCancel} - /> - </ContinueModal> - ); - } - - return ( - <ConfirmModal - description="New reward" - active - onCancel={onCancel} - disabled={hasErrors} - onConfirm={validateAndConfirm} - > - <FormProvider<State> - errors={errors} - object={form} - valueHandler={setValue} - > - <InputCurrency<State> - name="amount" - label={i18n.str`Amount`} - tooltip={i18n.str`amount of reward`} - /> - <Input<State> - name="justification" - label={i18n.str`Justification`} - inputType="multiline" - tooltip={i18n.str`reason for the reward`} - /> - <Input<State> - name="next_url" - label={i18n.str`URL after reward`} - tooltip={i18n.str`URL to visit after reward payment`} - /> - </FormProvider> - </ConfirmModal> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx deleted file mode 100644 index bd6d64a71..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.RewardCreateConfirmation; - -interface Props { - entity: Entity; - request: MerchantBackend.Rewards.RewardCreateRequest; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -export function CreatedSuccessfully({ - request, - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - const [settings] = useSettings(); - return ( - <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Amount</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={request.amount} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Justification</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={request.justification} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">URL</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={entity.reward_status_url} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Valid until</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={ - !entity.reward_expiration || - entity.reward_expiration.t_s === "never" - ? "never" - : format( - entity.reward_expiration.t_s * 1000, - datetimeFormatForSettings(settings), - ) - } - /> - </p> - </div> - </div> - </div> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx deleted file mode 100644 index 18e86f401..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CardTable as TestedComponent } from "./Table.js"; - -export default { - title: "Pages/Reserve/List", - component: TestedComponent, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const AllFunded = createExample(TestedComponent, { - instances: [ - { - id: "reseverId", - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - { - id: "reseverId2", - active: true, - committed_amount: "TESTKUDOS:13", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - ], -}); - -export const Empty = createExample(TestedComponent, { - instances: [], -}); - -export const OneNotYetFunded = createExample(TestedComponent, { - instances: [ - { - id: "reseverId", - active: true, - committed_amount: "TESTKUDOS:0", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:0", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - ], -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx deleted file mode 100644 index fbb30956c..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx +++ /dev/null @@ -1,320 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId; - -interface Props { - instances: Entity[]; - onNewReward: (id: Entity) => void; - onSelect: (id: Entity) => void; - onDelete: (id: Entity) => void; - onCreate: () => void; -} - -export function CardTable({ - instances, - onCreate, - onSelect, - onNewReward, - onDelete, -}: Props): VNode { - const [withoutFunds, withFunds] = instances.reduce((prev, current) => { - const amount = current.exchange_initial_amount; - if (amount.endsWith(":0")) { - prev[0] = prev[0].concat(current); - } else { - prev[1] = prev[1].concat(current); - } - return prev; - }, new Array<Array<Entity>>([], [])); - - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - {withoutFunds.length > 0 && ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash" /> - </span> - <i18n.Translate>Reserves not yet funded</i18n.Translate> - </p> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - <TableWithoutFund - instances={withoutFunds} - onNewReward={onNewReward} - onSelect={onSelect} - onDelete={onDelete} - /> - </div> - </div> - </div> - </div> - )} - - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash" /> - </span> - <i18n.Translate>Reserves ready</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options" /> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new reserve`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {withFunds.length > 0 ? ( - <Table - instances={withFunds} - onNewReward={onNewReward} - onSelect={onSelect} - onDelete={onDelete} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - </Fragment> - ); -} -interface TableProps { - instances: Entity[]; - onNewReward: (id: Entity) => void; - onDelete: (id: Entity) => void; - onSelect: (id: Entity) => void; -} - -function Table({ instances, onNewReward, onSelect, onDelete }: TableProps): VNode { - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Created at</i18n.Translate> - </th> - <th> - <i18n.Translate>Expires at</i18n.Translate> - </th> - <th> - <i18n.Translate>Initial</i18n.Translate> - </th> - <th> - <i18n.Translate>Picked up</i18n.Translate> - </th> - <th> - <i18n.Translate>Committed</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.creation_time.t_s === "never" - ? "never" - : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.expiration_time.t_s === "never" - ? "never" - : format( - i.expiration_time.t_s * 1000, - datetimeFormatForSettings(settings), - )} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.exchange_initial_amount} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.pickup_amount} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.committed_amount} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-small is-danger has-tooltip-left" - data-tooltip={i18n.str`delete selected reserve from the database`} - type="button" - onClick={(): void => onDelete(i)} - > - Delete - </button> - <button - class="button is-small is-info has-tooltip-left" - data-tooltip={i18n.str`authorize new reward from selected reserve`} - type="button" - onClick={(): void => onNewReward(i)} - > - New Reward - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no ready reserves yet, add more pressing the + sign or fund - them - </i18n.Translate> - </p> - </div> - ); -} - -function TableWithoutFund({ - instances, - onSelect, - onDelete, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Created at</i18n.Translate> - </th> - <th> - <i18n.Translate>Expires at</i18n.Translate> - </th> - <th> - <i18n.Translate>Expected Balance</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.creation_time.t_s === "never" - ? "never" - : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.expiration_time.t_s === "never" - ? "never" - : format( - i.expiration_time.t_s * 1000, - datetimeFormatForSettings(settings), - )} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.merchant_initial_amount} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-small is-danger jb-modal has-tooltip-left" - type="button" - data-tooltip={i18n.str`delete selected reserve from the database`} - onClick={(): void => onDelete(i)} - > - Delete - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx deleted file mode 100644 index c64d28502..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - useInstanceReserves, - useReservesAPI, -} from "../../../../hooks/reserves.js"; -import { Notification } from "../../../../utils/types.js"; -import { AuthorizeRewardModal } from "./AutorizeRewardModal.js"; -import { CardTable } from "./Table.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ConfirmModal } from "../../../../components/modal/index.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onSelect: (id: string) => void; - onNotFound: () => VNode; - onCreate: () => void; -} - -interface RewardConfirmation { - response: MerchantBackend.Rewards.RewardCreateConfirmation; - request: MerchantBackend.Rewards.RewardCreateRequest; -} - -export default function ListRewards({ - onUnauthorized, - onLoadError, - onNotFound, - onSelect, - onCreate, -}: Props): VNode { - const result = useInstanceReserves(); - const { deleteReserve, authorizeRewardReserve } = useReservesAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - const [reserveForReward, setReserveForReward] = useState<string | undefined>( - undefined, - ); - const [deleting, setDeleting] = - useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null); - const [rewardAuthorized, setRewardAuthorized] = useState< - RewardConfirmation | undefined - >(undefined); - - if (result.loading) return <Loading />; - 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 ( - <section class="section is-main-section"> - <NotificationCard notification={notif} /> - - {reserveForReward && ( - <AuthorizeRewardModal - onCancel={() => { - setReserveForReward(undefined); - setRewardAuthorized(undefined); - }} - rewardAuthorized={rewardAuthorized} - onConfirm={async (request) => { - try { - const response = await authorizeRewardReserve( - reserveForReward, - request, - ); - setRewardAuthorized({ - request, - response: response.data, - }); - } catch (error) { - setNotif({ - message: i18n.str`could not create the reward`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - setReserveForReward(undefined); - } - }} - /> - )} - - <CardTable - instances={result.data.reserves - .filter((r) => r.active) - .map((o) => ({ ...o, id: o.reserve_pub }))} - onCreate={onCreate} - onDelete={(reserve) => { - setDeleting(reserve) - }} - onSelect={(reserve) => onSelect(reserve.id)} - onNewReward={(reserve) => setReserveForReward(reserve.id)} - /> - - {deleting && ( - <ConfirmModal - label={`Delete reserve`} - description={`Delete the reserve`} - danger - active - onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - await deleteReserve(deleting.reserve_pub); - setNotif({ - message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`, - type: "SUCCESS", - }); - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete reserve`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setDeleting(null); - }} - > - <p> - If you delete the reserve for <b>"{deleting.merchant_initial_amount}"</b> you won't be able to create more rewards. <br /> - Reserve ID: <b>{deleting.reserve_pub}</b> - </p> - <p class="warning"> - Deleting an template <b>cannot be undone</b>. - </p> - </ConfirmModal> - )} - - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx deleted file mode 100644 index 53025f153..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Templates/Create", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx deleted file mode 100644 index 44d023f36..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ /dev/null @@ -1,270 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; - -enum Steps { - BOTH_FIXED, - FIXED_PRICE, - FIXED_SUMMARY, - NON_FIXED, -} - -type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps }; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext(); - const devices = useInstanceOtpDevices(); - - const [state, setState] = useState<Partial<Entity>>({ - template_contract: { - minimum_age: 0, - pay_duration: { - d_us: 1000 * 1000 * 60 * 30, //30 min - }, - }, - type: Steps.NON_FIXED, - }); - - const parsedPrice = !state.template_contract?.amount - ? undefined - : Amounts.parse(state.template_contract?.amount); - - const errors: FormErrors<Entity> = { - template_id: !state.template_id - ? i18n.str`should not be empty` - : !/[a-zA-Z0-9]*/.test(state.template_id) - ? i18n.str`no valid. only characters and numbers` - : undefined, - template_description: !state.template_description - ? i18n.str`should not be empty` - : undefined, - template_contract: !state.template_contract - ? undefined - : undefinedIfEmpty({ - amount: !( - state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED - ) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !( - state.type === Steps.FIXED_SUMMARY || - state.type === Steps.BOTH_FIXED - ) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" - ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second - ? i18n.str`to short` - : undefined, - } as Partial<TalerMerchantApi.TemplateContractDetails>), - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - if (state.template_contract) { - if (state.type === Steps.NON_FIXED) { - delete state.template_contract.amount; - delete state.template_contract.summary; - } else if (state.type === Steps.FIXED_SUMMARY) { - delete state.template_contract.amount; - } else if (state.type === Steps.FIXED_PRICE) { - delete state.template_contract.summary; - } - } - delete state.type; - return onCreate(state as any); - }; - - const deviceList = !devices.ok ? [] : devices.data.otp_devices; - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputWithAddon<Entity> - name="template_id" - help={`${backendURL}/templates/${state.template_id ?? ""}`} - label={i18n.str`Identifier`} - tooltip={i18n.str`Name of the template in URLs.`} - /> - <Input<Entity> - name="template_description" - label={i18n.str`Description`} - help="" - tooltip={i18n.str`Describe what this template stands for`} - /> - <InputTab - name="type" - label={i18n.str`Type`} - help={(() => { - switch (state.type) { - case Steps.NON_FIXED: - return i18n.str`User will be able to input price and summary before payment.`; - case Steps.FIXED_PRICE: - return i18n.str`User will be able to add a summary before payment.`; - case Steps.FIXED_SUMMARY: - return i18n.str`User will be able to set the price before payment.`; - case Steps.BOTH_FIXED: - return i18n.str`User will not be able to change the price or the summary.`; - } - })()} - tooltip={i18n.str`Define what the user be allowed to modify`} - values={[ - Steps.NON_FIXED, - Steps.FIXED_PRICE, - Steps.FIXED_SUMMARY, - Steps.BOTH_FIXED, - ]} - toStr={(v: Steps): string => { - switch (v) { - case Steps.NON_FIXED: - return i18n.str`Simple`; - case Steps.FIXED_PRICE: - return i18n.str`With price`; - case Steps.FIXED_SUMMARY: - return i18n.str`With summary`; - case Steps.BOTH_FIXED: - return i18n.str`With price and summary`; - } - }} - /> - {state.type === Steps.BOTH_FIXED || - state.type === Steps.FIXED_SUMMARY ? ( - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - ) : undefined} - {state.type === Steps.BOTH_FIXED || - state.type === Steps.FIXED_PRICE ? ( - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} - /> - ) : undefined} - <InputNumber - name="template_contract.minimum_age" - label={i18n.str`Minimum age`} - help="" - tooltip={i18n.str`Is this contract restricted to some age?`} - /> - <InputDuration - name="template_contract.pay_duration" - label={i18n.str`Payment timeout`} - help="" - tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} - /> - <Input<Entity> - name="otp_id" - label={i18n.str`OTP device`} - readonly - tooltip={i18n.str`Use to verify transaction in offline mode.`} - /> - <InputSearchOnList - label={i18n.str`Search device`} - onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))} - list={deviceList.map((e) => ({ - description: e.device_description, - id: e.otp_device_id, - }))} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx deleted file mode 100644 index f76146812..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTemplateAPI } from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; - -export type Entity = MerchantBackend.Transfers.TransferInformation; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} - -export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { createTemplate } = useTemplateAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - return ( - <> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => { - return createTemplate(request) - .then(() => onConfirm()) - .catch((error) => { - setNotif({ - message: i18n.str`could not inform template`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx deleted file mode 100644 index 707324d40..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { FunctionalComponent, h } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; - -export default { - title: "Pages/Templates/List", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx deleted file mode 100644 index c21f64776..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { CardTable } from "./Table.js"; - -export interface Props { - templates: MerchantBackend.Template.TemplateEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; - onCreate: () => void; - onDelete: (e: MerchantBackend.Template.TemplateEntry) => void; - onSelect: (e: MerchantBackend.Template.TemplateEntry) => void; - onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void; - onQR: (e: MerchantBackend.Template.TemplateEntry) => void; -} - -export function ListPage({ - templates, - onCreate, - onDelete, - onSelect, - onNewOrder, - onQR, - onLoadMoreBefore, - onLoadMoreAfter, -}: Props): VNode { - const form = { payto_uri: "" }; - - const { i18n } = useTranslationContext(); - return ( - <CardTable - templates={templates.map((o) => ({ - ...o, - id: String(o.template_id), - }))} - onQR={onQR} - onCreate={onCreate} - onDelete={onDelete} - onSelect={onSelect} - onNewOrder={onNewOrder} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} - /> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx deleted file mode 100644 index 42758962d..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx +++ /dev/null @@ -1,235 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.Template.TemplateEntry; - -interface Props { - templates: Entity[]; - onDelete: (e: Entity) => void; - onSelect: (e: Entity) => void; - onNewOrder: (e: Entity) => void; - onQR: (e: Entity) => void; - onCreate: () => void; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -export function CardTable({ - templates, - onCreate, - onDelete, - onSelect, - onQR, - onNewOrder, - onLoadMoreAfter, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - - const { i18n } = useTranslationContext(); - - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-newspaper" /> - </span> - <i18n.Translate>Templates</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new templates`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {templates.length > 0 ? ( - <Table - instances={templates} - onDelete={onDelete} - onSelect={onSelect} - onNewOrder={onNewOrder} - onQR={onQR} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string[]; - instances: Entity[]; - onDelete: (e: Entity) => void; - onNewOrder: (e: Entity) => void; - onQR: (e: Entity) => void; - onSelect: (e: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - -function Table({ - instances, - onLoadMoreAfter, - onDelete, - onNewOrder, - onQR, - onSelect, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - {hasMoreBefore && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more templates before the first one`} - onClick={onLoadMoreBefore} - > - <i18n.Translate>load newer templates</i18n.Translate> - </button> - )} - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>ID</i18n.Translate> - </th> - <th> - <i18n.Translate>Description</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.template_id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.template_id} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.template_description} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected templates from the database`} - onClick={() => onDelete(i)} - > - Delete - </button> - <button - class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`use template to create new order`} - onClick={() => onNewOrder(i)} - > - Use template - </button> - <button - class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`create qr code for the template`} - onClick={() => onQR(i)} - > - QR - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - {hasMoreAfter && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more templates after the last one`} - onClick={onLoadMoreAfter} - > - <i18n.Translate>load older templates</i18n.Translate> - </button> - )} - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no templates yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx deleted file mode 100644 index bdb7faf9d..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { MerchantBackend } from "../../../../declaration.js"; -import { - useInstanceTemplates, - useTemplateAPI, -} from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; -import { ListPage } from "./ListPage.js"; -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; -import { ConfirmModal } from "../../../../components/modal/index.js"; -import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onCreate: () => void; - onSelect: (id: string) => void; - onNewOrder: (id: string) => void; - onQR: (id: string) => void; -} - -export default function ListTemplates({ - onUnauthorized, - onLoadError, - onCreate, - onQR, - onSelect, - onNewOrder, - onNotFound, -}: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); - const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteTemplate, testTemplateExist } = useTemplateAPI(); - const result = useInstanceTemplates({ position }, (id) => setPosition(id)); - const [deleting, setDeleting] = - useState<MerchantBackend.Template.TemplateEntry | null>(null); - - if (result.loading) return <Loading />; - 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 ( - <section class="section is-main-section"> - <NotificationCard notification={notif} /> - - <JumpToElementById - testIfExist={testTemplateExist} - onSelect={onSelect} - description={i18n.str`jump to template with the given template ID`} - placeholder={i18n.str`template id`} - /> - - <ListPage - templates={result.data.templates} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} - onCreate={onCreate} - onSelect={(e) => { - onSelect(e.template_id); - }} - onNewOrder={(e) => { - onNewOrder(e.template_id); - }} - onQR={(e) => { - onQR(e.template_id); - }} - onDelete={(e: MerchantBackend.Template.TemplateEntry) => { - setDeleting(e) - } - } - /> - - {deleting && ( - <ConfirmModal - label={`Delete template`} - description={`Delete the template "${deleting.template_description}"`} - danger - active - onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - await deleteTemplate(deleting.template_id); - setNotif({ - message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, - type: "SUCCESS", - }); - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete template`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setDeleting(null); - }} - > - <p> - If you delete the template <b>"{deleting.template_description}"</b> (ID:{" "} - <b>{deleting.template_id}</b>) you may loose information - </p> - <p class="warning"> - Deleting an template <b>cannot be undone</b>. - </p> - </ConfirmModal> - )} - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx deleted file mode 100644 index c0059c7bc..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { QrPage as TestedComponent } from "./QrPage.js"; - -export default { - title: "Pages/Templates/QR", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx deleted file mode 100644 index 108ec617f..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ /dev/null @@ -1,172 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { QR } from "../../../../components/exception/QR.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { useInstanceContext } from "../../../../context/instance.js"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.Template.UsingTemplateDetails; - -interface Props { - contract: MerchantBackend.Template.TemplateContractDetails; - id: string; - onBack?: () => void; -} - -export function QrPage({ contract, id: templateId, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const { id: instanceId } = useInstanceContext(); - const config = useConfigContext(); - - const [state, setState] = useState<Partial<Entity>>({ - amount: contract.amount, - summary: contract.summary, - }); - - const errors: FormErrors<Entity> = {}; - - const fixedAmount = !!contract.amount; - const fixedSummary = !!contract.summary; - - const templateParams: Record<string, string> = {} - if (!fixedAmount) { - if (state.amount) { - templateParams.amount = state.amount - } else { - templateParams.amount = config.currency - } - } - - if (!fixedSummary) { - templateParams.summary = state.summary ?? "" - } - - const merchantBaseUrl = new URL(backendURL).href; - - const payTemplateUri = stringifyPayTemplateUri({ - merchantBaseUrl, - templateId, - //templateParams - }) - - const issuer = encodeURIComponent( - `${new URL(backendURL).host}/${instanceId}`, - ); - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <p class="is-size-5 mt-5 mb-5"> - <i18n.Translate> - Here you can specify a default value for fields that are not - fixed. Default values can be edited by the customer before the - payment. - </i18n.Translate> - </p> - - <p></p> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputCurrency<Entity> - name="amount" - label={ - fixedAmount - ? i18n.str`Fixed amount` - : i18n.str`Default amount` - } - readonly={fixedAmount} - tooltip={i18n.str`Amount of the order`} - /> - <Input<Entity> - name="summary" - inputType="multiline" - readonly={fixedSummary} - label={ - fixedSummary - ? i18n.str`Fixed summary` - : i18n.str`Default summary` - } - tooltip={i18n.str`Title of the order to be shown to the customer`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <button - class="button is-info" - onClick={() => saveAsPDF(templateId)} - > - <i18n.Translate>Print</i18n.Translate> - </button> - </div> - </div> - <div class="column" /> - </div> - </section> - <section id="printThis"> - <QR text={payTemplateUri} /> - <pre style={{ textAlign: "center" }}> - <a href={payTemplateUri}>{payTemplateUri}</a> - </pre> - </section> - </div> - ); -} - -function saveAsPDF(name: string): void { - const printWindow = window.open("", "", "height=400,width=800"); - if (!printWindow) return; - const divContents = document.getElementById("printThis"); - if (!divContents) return; - printWindow.document.write( - `<html><head><title>Order template for ${name}</title><style>`, - ); - printWindow.document.write("</style></head><body> </body></html>"); - printWindow.document.close(); - printWindow.document.body.appendChild(divContents.cloneNode(true)); - printWindow.addEventListener("load", () => { - printWindow.print(); - printWindow.close(); - }); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx deleted file mode 100644 index 303d17b72..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { UpdatePage as TestedComponent } from "./UpdatePage.js"; - -export default { - title: "Pages/Templates/Update", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx deleted file mode 100644 index 3aa3f6ddd..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; - -enum Steps { - BOTH_FIXED, - FIXED_PRICE, - FIXED_SUMMARY, - NON_FIXED, -} - -type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; - -interface Props { - onUpdate: (d: Entity) => Promise<void>; - onBack?: () => void; - template: Entity; -} - -export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext(); - - const intialStep = - template.template_contract?.amount === undefined && - template.template_contract?.summary === undefined - ? Steps.NON_FIXED - : template.template_contract?.summary === undefined - ? Steps.FIXED_PRICE - : template.template_contract?.amount === undefined - ? Steps.FIXED_SUMMARY - : Steps.BOTH_FIXED; - - const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ - ...template, - type: intialStep, - }); - - const parsedPrice = !state.template_contract?.amount - ? undefined - : Amounts.parse(state.template_contract?.amount); - - const errors: FormErrors<Entity> = { - template_description: !state.template_description - ? i18n.str`should not be empty` - : undefined, - template_contract: !state.template_contract - ? undefined - : undefinedIfEmpty({ - amount: !( - state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED - ) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !( - state.type === Steps.FIXED_SUMMARY || - state.type === Steps.BOTH_FIXED - ) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" - ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second - ? i18n.str`to short` - : undefined, - } as Partial<TalerMerchantApi.TemplateContractDetails>), - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - if (state.template_contract) { - if (state.type === Steps.NON_FIXED) { - delete state.template_contract.amount; - delete state.template_contract.summary; - } else if (state.type === Steps.FIXED_SUMMARY) { - delete state.template_contract.amount; - } else if (state.type === Steps.FIXED_PRICE) { - delete state.template_contract.summary; - } - } - delete state.type; - return onUpdate(state as any); - }; - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - {backendURL}/templates/{template.id} - </span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputWithAddon<Entity> - name="id" - addonBefore={`templates/`} - readonly - label={i18n.str`Identifier`} - tooltip={i18n.str`Name of the template in URLs.`} - /> - - <Input<Entity> - name="template_description" - label={i18n.str`Description`} - help="" - tooltip={i18n.str`Describe what this template stands for`} - /> - <InputTab - name="type" - label={i18n.str`Type`} - help={(() => { - switch (state.type) { - case Steps.NON_FIXED: - return i18n.str`User will be able to input price and summary before payment.`; - case Steps.FIXED_PRICE: - return i18n.str`User will be able to add a summary before payment.`; - case Steps.FIXED_SUMMARY: - return i18n.str`User will be able to set the price before payment.`; - case Steps.BOTH_FIXED: - return i18n.str`User will not be able to change the price or the summary.`; - } - })()} - tooltip={i18n.str`Define what the user be allowed to modify`} - values={[ - Steps.NON_FIXED, - Steps.FIXED_PRICE, - Steps.FIXED_SUMMARY, - Steps.BOTH_FIXED, - ]} - toStr={(v: Steps): string => { - switch (v) { - case Steps.NON_FIXED: - return i18n.str`Simple`; - case Steps.FIXED_PRICE: - return i18n.str`With price`; - case Steps.FIXED_SUMMARY: - return i18n.str`With summary`; - case Steps.BOTH_FIXED: - return i18n.str`With price and summary`; - } - }} - /> - {state.type === Steps.BOTH_FIXED || - state.type === Steps.FIXED_SUMMARY ? ( - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - ) : undefined} - {state.type === Steps.BOTH_FIXED || - state.type === Steps.FIXED_PRICE ? ( - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} - /> - ) : undefined} - <InputNumber - name="template_contract.minimum_age" - label={i18n.str`Minimum age`} - help="" - tooltip={i18n.str`Is this contract restricted to some age?`} - /> - <InputDuration - name="template_contract.pay_duration" - label={i18n.str`Payment timeout`} - help="" - tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - </div> - </section> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx deleted file mode 100644 index 035b22da9..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { MerchantBackend, WithId } from "../../../../declaration.js"; -import { - useTemplateAPI, - useTemplateDetails, -} from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; -import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; - -interface Props { - onBack?: () => void; - onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - tid: string; -} -export default function UpdateTemplate({ - tid, - onConfirm, - onBack, - onUnauthorized, - onNotFound, - onLoadError, -}: Props): VNode { - const { updateTemplate } = useTemplateAPI(); - const result = useTemplateDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - <UpdatePage - template={{ ...result.data, id: tid }} - onBack={onBack} - onUpdate={(data) => { - return updateTemplate(tid, data) - .then(onConfirm) - .catch((error) => { - setNotif({ - message: i18n.str`could not update template`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx deleted file mode 100644 index d91888b97..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { UsePage as TestedComponent } from "./UsePage.js"; - -export default { - title: "Pages/Templates/Create", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx deleted file mode 100644 index 93f303537..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.Template.UsingTemplateDetails; - -interface Props { - id: string; - template: MerchantBackend.Template.TemplateDetails; - onCreateOrder: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - - const [state, setState] = useState<Partial<Entity>>({ - amount: template.template_contract.amount, - summary: template.template_contract.summary, - }); - - const errors: FormErrors<Entity> = { - amount: - !template.template_contract.amount && !state.amount - ? i18n.str`Amount is required` - : undefined, - summary: - !template.template_contract.summary && !state.summary - ? i18n.str`Order summary is required` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - if (template.template_contract.amount) { - delete state.amount; - } - if (template.template_contract.summary) { - delete state.summary; - } - return onCreateOrder(state as any); - }; - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - <i18n.Translate>New order for template</i18n.Translate>:{" "} - <b>{id}</b> - </span> - </div> - </div> - </div> - </div> - </section> - </section> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputCurrency<Entity> - name="amount" - label={i18n.str`Amount`} - readonly={!!template.template_contract.amount} - tooltip={i18n.str`Amount of the order`} - /> - <Input<Entity> - name="summary" - inputType="multiline" - label={i18n.str`Order summary`} - readonly={!!template.template_contract.summary} - tooltip={i18n.str`Title of the order to be shown to the customer`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx deleted file mode 100644 index 441a5c5e0..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { MerchantBackend } from "../../../../declaration.js"; -import { - useTemplateAPI, - useTemplateDetails, -} from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; -import { UsePage } from "./UsePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -export type Entity = MerchantBackend.Transfers.TransferInformation; -interface Props { - onBack?: () => void; - onOrderCreated: (id: string) => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - tid: string; -} - -export default function TemplateUsePage({ - tid, - onOrderCreated, - onBack, - onLoadError, - onNotFound, - onUnauthorized, -}: Props): VNode { - const { createOrderFromTemplate } = useTemplateAPI(); - const result = useTemplateDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <> - <NotificationCard notification={notif} /> - <UsePage - template={result.data} - id={tid} - onBack={onBack} - onCreateOrder={( - request: MerchantBackend.Template.UsingTemplateDetails, - ) => { - return createOrderFromTemplate(tid, request) - .then((res) => onOrderCreated(res.data.order_id)) - .catch((error) => { - setNotif({ - message: i18n.str`could not create order from template`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx deleted file mode 100644 index 1e9186624..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../components/exception/AsyncButton.js"; -import { FormProvider } from "../../../components/form/FormProvider.js"; -import { Input } from "../../../components/form/Input.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { AccessToken } from "../../../declaration.js"; -import { NotificationCard } from "../../../components/menu/index.js"; - -interface Props { - instanceId: string; - hasToken: boolean | undefined; - onClearToken: (c: AccessToken | undefined) => void; - onNewToken: (c: AccessToken | undefined, s: AccessToken) => void; - onBack?: () => void; -} - -export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode { - type State = { old_token: string; new_token: string; repeat_token: string }; - const [form, setValue] = useState<Partial<State>>({ - old_token: "", - new_token: "", - repeat_token: "", - }); - const { i18n } = useTranslationContext(); - - const errors = { - old_token: hasToken && !form.old_token - ? i18n.str`you need your access token to perform the operation` - : undefined, - new_token: !form.new_token - ? i18n.str`cannot be empty` - : form.new_token === form.old_token - ? i18n.str`cannot be the same as the old token` - : undefined, - repeat_token: - form.new_token !== form.repeat_token - ? i18n.str`is not the same` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const instance = useInstanceContext(); - - const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`; - - async function submitForm() { - if (hasErrors) return; - const oldToken = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined; - const newToken = `secret-token:${form.new_token}` as AccessToken; - onNewToken(oldToken, newToken) - } - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - {text} - </span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - {!hasToken && - <NotificationCard - notification={{ - message: i18n.str`This instance doesn't have authentication token.`, - description: i18n.str`You can leave it empty if there is another layer of security.`, - type: "WARN", - }} - /> - } - - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider errors={errors} object={form} valueHandler={setValue}> - <Fragment> - {hasToken && ( - <Fragment> - <Input<State> - name="old_token" - label={i18n.str`Current access token`} - tooltip={i18n.str`access token currently in use`} - inputType="password" - /> - <p> - <i18n.Translate> - Clearing the access token will mean public access to the instance. - </i18n.Translate> - </p> - <div class="buttons is-right mt-5"> - <button - class="button" - onClick={() => { - if (hasToken) { - const oldToken = `secret-token:${form.old_token}` as AccessToken; - onClearToken(oldToken) - } else { - onClearToken(undefined) - } - }} - > - <i18n.Translate>Clear token</i18n.Translate> - </button> - </div> - </Fragment> - )} - - - <Input<State> - name="new_token" - label={i18n.str`New access token`} - tooltip={i18n.str`next access token to be used`} - inputType="password" - /> - <Input<State> - name="repeat_token" - label={i18n.str`Repeat access token`} - tooltip={i18n.str`confirm the same access token`} - inputType="password" - /> - </Fragment> - </FormProvider> - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm change</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx deleted file mode 100644 index 13642ec22..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { Loading } from "../../../components/exception/loading.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; -import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; -import { DetailPage } from "./DetailPage.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../components/menu/index.js"; -import { Notification } from "../../../utils/types.js"; -import { useBackendContext } from "../../../context/backend.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onChange: () => void; - onNotFound: () => VNode; - onCancel: () => void; -} - -export default function Token({ - onLoadError, - onChange, - onUnauthorized, - onNotFound, - onCancel, -}: Props): VNode { - const { i18n } = useTranslationContext(); - - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { clearAccessToken, setNewAccessToken } = useInstanceAPI(); - const { id } = useInstanceContext(); - const result = useInstanceDetails() - - if (result.loading) return <Loading />; - 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 hasToken = result.data.auth.method === "token" - - return ( - <Fragment> - <NotificationCard notification={notif} /> - <DetailPage - instanceId={id} - onBack={onCancel} - hasToken={hasToken} - onClearToken={async (currentToken): Promise<void> => { - try { - await clearAccessToken(currentToken); - onChange(); - } catch (error) { - if (error instanceof Error) { - setNotif({ - message: i18n.str`Failed to clear token`, - type: "ERROR", - description: error.message, - }); - } - } - }} - onNewToken={async (currentToken, newToken): Promise<void> => { - try { - await setNewAccessToken(currentToken, newToken); - onChange(); - } catch (error) { - if (error instanceof Error) { - setNotif({ - message: i18n.str`Failed to set new token`, - type: "ERROR", - description: error.message, - }); - } - } - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx deleted file mode 100644 index 581828657..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { DetailPage as TestedComponent } from "./DetailPage.js"; - -export default { - title: "Pages/Token", - component: TestedComponent, -}; - diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx deleted file mode 100644 index ca38defc3..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Transfer/Create", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, { - accounts: ["payto://x-taler-bank/account1", "payto://x-taler-bank/account2"], -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx deleted file mode 100644 index eb25045a0..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - CROCKFORD_BASE32_REGEX, - URL_REGEX, -} from "../../../../utils/constants.js"; - -type Entity = MerchantBackend.Transfers.TransferInformation; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; - accounts: string[]; -} - -export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const { currency } = useConfigContext(); - - const [state, setState] = useState<Partial<Entity>>({ - wtid: "", - // payto_uri: , - // exchange_url: 'http://exchange.taler:8081/', - credit_amount: ``, - }); - - const errors: FormErrors<Entity> = { - wtid: !state.wtid - ? i18n.str`cannot be empty` - : !CROCKFORD_BASE32_REGEX.test(state.wtid) - ? i18n.str`check the id, does not look valid` - : state.wtid.length !== 52 - ? i18n.str`should have 52 characters, current ${state.wtid.length}` - : undefined, - payto_uri: !state.payto_uri ? i18n.str`cannot be empty` : undefined, - credit_amount: !state.credit_amount ? i18n.str`cannot be empty` : undefined, - exchange_url: !state.exchange_url - ? i18n.str`cannot be empty` - : !URL_REGEX.test(state.exchange_url) - ? i18n.str`URL doesn't have the right format` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onCreate(state as any); - }; - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputSelector - name="payto_uri" - label={i18n.str`Credited bank account`} - values={accounts} - placeholder={i18n.str`Select one account`} - tooltip={i18n.str`Bank account of the merchant where the payment was received`} - /> - <Input<Entity> - name="wtid" - label={i18n.str`Wire transfer ID`} - help="" - tooltip={i18n.str`unique identifier of the wire transfer used by the exchange, must be 52 characters long`} - /> - <Input<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - tooltip={i18n.str`Base URL of the exchange that made the transfer, should have been in the wire transfer subject`} - help="http://exchange.taler:8081/" - /> - <InputCurrency<Entity> - name="credit_amount" - label={i18n.str`Amount credited`} - tooltip={i18n.str`Actual amount that was wired to the merchant's bank account`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx deleted file mode 100644 index 77a8c65fb..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceDetails } from "../../../../hooks/instance.js"; -import { useTransferAPI } from "../../../../hooks/transfer.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; -import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js"; - -export type Entity = MerchantBackend.Transfers.TransferInformation; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} - -export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { informTransfer } = useTransferAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - const instance = useInstanceBankAccounts(); - const accounts = !instance.ok - ? [] - : instance.data.accounts.map((a) => a.payto_uri); - - return ( - <> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - accounts={accounts} - onCreate={(request: MerchantBackend.Transfers.TransferInformation) => { - return informTransfer(request) - .then(() => onConfirm()) - .catch((error) => { - setNotif({ - message: i18n.str`could not inform transfer`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx deleted file mode 100644 index ba22cb7d5..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; - -export default { - title: "Pages/Transfer/List", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onDelete: { action: "onDelete" }, - onLoadMoreBefore: { action: "onLoadMoreBefore" }, - onLoadMoreAfter: { action: "onLoadMoreAfter" }, - onShowAll: { action: "onShowAll" }, - onShowVerified: { action: "onShowVerified" }, - onShowUnverified: { action: "onShowUnverified" }, - onChangePayTo: { action: "onChangePayTo" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, { - transfers: [ - { - exchange_url: "http://exchange.url/", - credit_amount: "TESTKUDOS:10", - payto_uri: "payto//x-taler-bank/bank:8080/account", - transfer_serial_id: 123123123, - wtid: "!@KJELQKWEJ!L@K#!J@", - confirmed: true, - execution_time: { - t_s: new Date().getTime() / 1000, - }, - verified: false, - }, - { - exchange_url: "http://exchange.url/", - credit_amount: "TESTKUDOS:10", - payto_uri: "payto//x-taler-bank/bank:8080/account", - transfer_serial_id: 123123123, - wtid: "!@KJELQKWEJ!L@K#!J@", - confirmed: true, - execution_time: { - t_s: new Date().getTime() / 1000, - }, - verified: false, - }, - { - exchange_url: "http://exchange.url/", - credit_amount: "TESTKUDOS:10", - payto_uri: "payto//x-taler-bank/bank:8080/account", - transfer_serial_id: 123123123, - wtid: "!@KJELQKWEJ!L@K#!J@", - confirmed: true, - execution_time: { - t_s: new Date().getTime() / 1000, - }, - verified: false, - }, - ], - accounts: ["payto://x-taler-bank/bank/some_account"], -}); -export const Empty = createExample(TestedComponent, { - transfers: [], - accounts: [], -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx deleted file mode 100644 index 794d37fe6..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { FormProvider } from "../../../../components/form/FormProvider.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { CardTable } from "./Table.js"; - -export interface Props { - transfers: MerchantBackend.Transfers.TransferDetails[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; - onShowAll: () => void; - onShowVerified: () => void; - onShowUnverified: () => void; - isVerifiedTransfers?: boolean; - isNonVerifiedTransfers?: boolean; - isAllTransfers?: boolean; - accounts: string[]; - onChangePayTo: (p?: string) => void; - payTo?: string; - onCreate: () => void; - onDelete: () => void; -} - -export function ListPage({ - payTo, - onChangePayTo, - transfers, - onCreate, - onDelete, - accounts, - onLoadMoreBefore, - onLoadMoreAfter, - isAllTransfers, - isNonVerifiedTransfers, - isVerifiedTransfers, - onShowAll, - onShowUnverified, - onShowVerified, -}: Props): VNode { - const form = { payto_uri: payTo }; - - const { i18n } = useTranslationContext(); - return ( - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-10"> - <FormProvider - object={form} - valueHandler={(updater) => onChangePayTo(updater(form).payto_uri)} - > - <InputSelector - name="payto_uri" - label={i18n.str`Account URI`} - values={accounts} - placeholder={i18n.str`Select one account`} - tooltip={i18n.str`filter by account address`} - /> - </FormProvider> - </div> - <div class="column" /> - </div> - <div class="tabs"> - <ul> - <li class={isAllTransfers ? "is-active" : ""}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`remove all filters`} - > - <a onClick={onShowAll}> - <i18n.Translate>All</i18n.Translate> - </a> - </div> - </li> - <li class={isVerifiedTransfers ? "is-active" : ""}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`} - > - <a onClick={onShowVerified}> - <i18n.Translate>Verified</i18n.Translate> - </a> - </div> - </li> - <li class={isNonVerifiedTransfers ? "is-active" : ""}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`only show wire transfers claimed by the exchange`} - > - <a onClick={onShowUnverified}> - <i18n.Translate>Unverified</i18n.Translate> - </a> - </div> - </li> - </ul> - </div> - <CardTable - transfers={transfers.map((o) => ({ - ...o, - id: String(o.transfer_serial_id), - }))} - accounts={accounts} - onCreate={onCreate} - onDelete={onDelete} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} - /> - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx deleted file mode 100644 index 642666c4f..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -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 { MerchantBackend, WithId } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Transfers.TransferDetails & WithId; - -interface Props { - transfers: Entity[]; - onDelete: (id: Entity) => void; - onCreate: () => void; - accounts: string[]; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -export function CardTable({ - transfers, - onCreate, - onDelete, - onLoadMoreAfter, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - - const { i18n } = useTranslationContext(); - - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-arrow-left-right" /> - </span> - <i18n.Translate>Transfers</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new transfer`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {transfers.length > 0 ? ( - <Table - instances={transfers} - onDelete={onDelete} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string[]; - instances: Entity[]; - onDelete: (id: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - -function Table({ - instances, - onLoadMoreAfter, - onDelete, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - return ( - <div class="table-container"> - {hasMoreBefore && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more transfers before the first one`} - onClick={onLoadMoreBefore} - > - <i18n.Translate>load newer transfers</i18n.Translate> - </button> - )} - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>ID</i18n.Translate> - </th> - <th> - <i18n.Translate>Credit</i18n.Translate> - </th> - <th> - <i18n.Translate>Address</i18n.Translate> - </th> - <th> - <i18n.Translate>Exchange URL</i18n.Translate> - </th> - <th> - <i18n.Translate>Confirmed</i18n.Translate> - </th> - <th> - <i18n.Translate>Verified</i18n.Translate> - </th> - <th> - <i18n.Translate>Executed at</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.id}> - <td>{i.id}</td> - <td>{i.credit_amount}</td> - <td>{i.payto_uri}</td> - <td>{i.exchange_url}</td> - <td>{i.confirmed ? i18n.str`yes` : i18n.str`no`}</td> - <td>{i.verified ? i18n.str`yes` : i18n.str`no`}</td> - <td> - {i.execution_time - ? i.execution_time.t_s == "never" - ? i18n.str`never` - : format( - i.execution_time.t_s * 1000, - datetimeFormatForSettings(settings), - ) - : i18n.str`unknown`} - </td> - <td> - {i.verified === undefined ? ( - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected transfer from the database`} - onClick={() => onDelete(i)} - > - Delete - </button> - ) : undefined} - </td> - </tr> - ); - })} - </tbody> - </table> - {hasMoreAfter && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more transfer after the last one`} - onClick={onLoadMoreAfter} - > - <i18n.Translate>load older transfers</i18n.Translate> - </button> - )} - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no transfer yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx deleted file mode 100644 index 1402df4c1..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { Loading } from "../../../../components/exception/loading.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceDetails } from "../../../../hooks/instance.js"; -import { useInstanceTransfers } from "../../../../hooks/transfer.js"; -import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onCreate: () => void; -} -interface Form { - verified?: "yes" | "no"; - payto_uri?: string; -} - -export default function ListTransfer({ - onUnauthorized, - onLoadError, - onCreate, - onNotFound, -}: Props): VNode { - const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s }); - - const [position, setPosition] = useState<string | undefined>(undefined); - - const instance = useInstanceBankAccounts(); - const accounts = !instance.ok - ? [] - : instance.data.accounts.map((a) => a.payto_uri); - const [form, setForm] = useState<Form>({ payto_uri: "" }); - - const shoulUseDefaultAccount = accounts.length === 1 - useEffect(() => { - if (shoulUseDefaultAccount) { - setForm({...form, payto_uri: accounts[0]}) - } - }, [shoulUseDefaultAccount]) - - const isVerifiedTransfers = form.verified === "yes"; - const isNonVerifiedTransfers = form.verified === "no"; - const isAllTransfers = form.verified === undefined; - - const result = useInstanceTransfers( - { - position, - payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, - verified: form.verified, - }, - (id) => setPosition(id), - ); - - if (result.loading) return <Loading />; - 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 ( - <ListPage - accounts={accounts} - transfers={result.data.transfers} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} - onCreate={onCreate} - onDelete={() => { - null; - }} - // position={position} setPosition={setPosition} - onShowAll={() => setFilter(undefined)} - onShowUnverified={() => setFilter("no")} - onShowVerified={() => setFilter("yes")} - isAllTransfers={isAllTransfers} - isVerifiedTransfers={isVerifiedTransfers} - isNonVerifiedTransfers={isNonVerifiedTransfers} - payTo={form.payto_uri} - onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))} - /> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx deleted file mode 100644 index 2accb7685..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { UpdatePage as TestedComponent } from "./UpdatePage.js"; - -export default { - title: "Pages/Instance/Update", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, { - selected: { - name: "name", - auth: { method: "external" }, - address: {}, - user_type: "business", - use_stefan: true, - jurisdiction: {}, - default_pay_delay: { - d_us: 1000 * 1000, //one second - }, - default_wire_transfer_delay: { - d_us: 1000 * 1000, //one second - }, - merchant_pub: "ASDWQEKASJDKSADJ", - }, -}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx deleted file mode 100644 index ff0d55d2d..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../components/form/FormProvider.js"; -import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { MerchantBackend } from "../../../declaration.js"; -import { undefinedIfEmpty } from "../../../utils/table.js"; -import { Duration } from "@gnu-taler/taler-util"; - -export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { - default_pay_delay: Duration, - default_wire_transfer_delay: Duration, -}; - -//MerchantBackend.Instances.InstanceAuthConfigurationMessage -interface Props { - onUpdate: (d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void; - selected: MerchantBackend.Instances.QueryInstancesResponse; - isLoading: boolean; - onBack: () => void; -} - -function convert( - from: MerchantBackend.Instances.QueryInstancesResponse, -): Entity { - const { default_pay_delay, default_wire_transfer_delay, ...rest } = from; - - const defaults = { - use_stefan: false, - default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay), - default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay), - }; - return { ...defaults, ...rest }; -} - -export function UpdatePage({ - onUpdate, - selected, - onBack, -}: Props): VNode { - const { id } = useInstanceContext(); - - const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); - - const { i18n } = useTranslationContext(); - - const errors: FormErrors<Entity> = { - name: !value.name ? i18n.str`required` : undefined, - user_type: !value.user_type - ? i18n.str`required` - : value.user_type !== "business" && value.user_type !== "individual" - ? i18n.str`should be business or individual` - : undefined, - default_pay_delay: !value.default_pay_delay - ? i18n.str`required` - : !!value.default_wire_transfer_delay && - value.default_wire_transfer_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? - i18n.str`pay delay can't be greater than wire transfer delay` : undefined, - default_wire_transfer_delay: !value.default_wire_transfer_delay - ? i18n.str`required` - : undefined, - address: undefinedIfEmpty({ - address_lines: - value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` - : undefined, - }), - jurisdiction: undefinedIfEmpty({ - address_lines: - value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` - : undefined, - }), - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submit = async (): Promise<void> => { - const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>; - const result: MerchantBackend.Instances.InstanceReconfigurationMessage = { - default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay), - default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay), - ...rest, - } - await onUpdate(result); - }; - // const [active, setActive] = useState(false); - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - <i18n.Translate>Instance id</i18n.Translate>: <b>{id}</b> - </span> - </div> - </div> - </div> - </div> - </section> - - <hr /> - - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider<Entity> - errors={errors} - object={value} - valueHandler={valueHandler} - > - <DefaultInstanceFormFields showId={false} /> - </FormProvider> - - <div class="buttons is-right mt-4"> - <button - class="button" - onClick={onBack} - data-tooltip="cancel operation" - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - - <AsyncButton - onClick={submit} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={hasErrors} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx deleted file mode 100644 index be3793ac3..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpError, - HttpResponse, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { Loading } from "../../../components/exception/loading.js"; -import { NotificationCard } from "../../../components/menu/index.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; -import { - useInstanceAPI, - useInstanceDetails, - useManagedInstanceDetails, - useManagementAPI, -} from "../../../hooks/instance.js"; -import { Notification } from "../../../utils/types.js"; -import { UpdatePage } from "./UpdatePage.js"; - -export interface Props { - onBack: () => void; - onConfirm: () => void; - - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onUpdateError: (e: HttpError<MerchantBackend.ErrorDetail>) => void; -} - -export default function Update(props: Props): VNode { - const { updateInstance } = useInstanceAPI(); - const result = useInstanceDetails(); - return CommonUpdate(props, result, updateInstance, ); -} - -export function AdminUpdate(props: Props & { instanceId: string }): VNode { - const { updateInstance } = useManagementAPI( - props.instanceId, - ); - const result = useManagedInstanceDetails(props.instanceId); - return CommonUpdate(props, result, updateInstance, ); -} - -function CommonUpdate( - { - onBack, - onConfirm, - onLoadError, - onNotFound, - onUpdateError, - onUnauthorized, - }: Props, - result: HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail - >, - updateInstance: any, -): VNode { - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - <UpdatePage - onBack={onBack} - isLoading={false} - selected={result.data} - onUpdate={( - d: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - return updateInstance(d) - .then(onConfirm) - .catch((error: Error) => - setNotif({ - message: i18n.str`Failed to create instance`, - type: "ERROR", - description: error.message, - }), - ); - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx deleted file mode 100644 index 2e2a58b33..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Webhooks/Create", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx deleted file mode 100644 index b89e5e6bf..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; - -type Entity = MerchantBackend.Webhooks.WebhookAddDetails; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"]; - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - - const [state, setState] = useState<Partial<Entity>>({}); - - const errors: FormErrors<Entity> = { - webhook_id: !state.webhook_id ? i18n.str`required` : undefined, - event_type: !state.event_type ? i18n.str`required` - : state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"` - : undefined, - http_method: !state.http_method - ? i18n.str`required` - : !validMethod.includes(state.http_method) - ? i18n.str`should be one of '${validMethod.join(", ")}'` - : undefined, - url: !state.url ? i18n.str`required` : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onCreate(state as any); - }; - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <Input<Entity> - name="webhook_id" - label={i18n.str`ID`} - tooltip={i18n.str`Webhook ID to use`} - /> - <InputSelector - name="event_type" - label={i18n.str`Event`} - values={[ - i18n.str`Choose one...`, - i18n.str`pay`, - i18n.str`refund`, - ]} - tooltip={i18n.str`The event of the webhook: why the webhook is used`} - /> - <InputSelector - name="http_method" - label={i18n.str`Method`} - values={[ - i18n.str`Choose one...`, - i18n.str`GET`, - i18n.str`POST`, - i18n.str`PUT`, - i18n.str`PATCH`, - i18n.str`HEAD`, - ]} - tooltip={i18n.str`Method used by the webhook`} - /> - - <Input<Entity> - name="url" - label={i18n.str`URL`} - tooltip={i18n.str`URL of the webhook where the customer will be redirected`} - /> - - <p> - The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string - between <pre style={{ display: "inline", padding: 0 }}>{{</pre> and <pre style={{ display: "inline", padding: 0 }}>}}</pre> will - be replaced with replaced with the value of the corresponding variable. - </p> - <p> - For example <pre style={{ display: "inline", padding: 0 }}>{{contract_terms.amount}}</pre> will be replaced - with the the order's price - </p> - <p> - The short list of variables are: - </p> - <div class="menu"> - - <ul class="menu-list" style={{ listStyleType: "disc", marginLeft: 20 }}> - <li><b>contract_terms.summary:</b> order's description </li> - <li><b>contract_terms.amount:</b> order's price </li> - <li><b>order_id:</b> order's unique identification </li> - {state.event_type === "refund" && <Fragment> - <li><b>refund_amout:</b> the amount that was being refunded</li> - <li><b>reason:</b> the reason entered by the merchant staff for granting the refund</li> - <li><b>timestamp:</b> time of the refund in nanoseconds since 1970</li> - </Fragment>} - </ul> - </div> - {/* <Input<Entity> - name="header_template" - label={i18n.str`Http header`} - inputType="multiline" - tooltip={i18n.str`Header template of the webhook`} - /> */} - <Input<Entity> - name="body_template" - inputType="multiline" - label={i18n.str`Http body`} - tooltip={i18n.str`Body template by the webhook`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx deleted file mode 100644 index e1b8aa88e..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useWebhookAPI } from "../../../../hooks/webhooks.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatePage } from "./CreatePage.js"; - -export type Entity = MerchantBackend.Webhooks.WebhookAddDetails; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} - -export default function CreateWebhook({ onConfirm, onBack }: Props): VNode { - const { createWebhook } = useWebhookAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - return ( - <> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => { - return createWebhook(request) - .then(() => onConfirm()) - .catch((error) => { - setNotif({ - message: i18n.str`could not inform template`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx deleted file mode 100644 index 707324d40..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { FunctionalComponent, h } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; - -export default { - title: "Pages/Templates/List", - component: TestedComponent, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx deleted file mode 100644 index be7c575a0..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { CardTable } from "./Table.js"; - -export interface Props { - webhooks: MerchantBackend.Webhooks.WebhookEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; - onCreate: () => void; - onDelete: (e: MerchantBackend.Webhooks.WebhookEntry) => void; - onSelect: (e: MerchantBackend.Webhooks.WebhookEntry) => void; -} - -export function ListPage({ - webhooks, - onCreate, - onDelete, - onSelect, - onLoadMoreBefore, - onLoadMoreAfter, -}: Props): VNode { - const form = { payto_uri: "" }; - - const { i18n } = useTranslationContext(); - return ( - <section class="section is-main-section"> - <CardTable - webhooks={webhooks.map((o) => ({ - ...o, - id: String(o.webhook_id), - }))} - onCreate={onCreate} - onDelete={onDelete} - onSelect={onSelect} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} - /> - </section> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx deleted file mode 100644 index daa984382..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx +++ /dev/null @@ -1,218 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.Webhooks.WebhookEntry; - -interface Props { - webhooks: Entity[]; - onDelete: (e: Entity) => void; - onSelect: (e: Entity) => void; - onCreate: () => void; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -export function CardTable({ - webhooks, - onCreate, - onDelete, - onSelect, - onLoadMoreAfter, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - - const { i18n } = useTranslationContext(); - - return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-newspaper" /> - </span> - <i18n.Translate>Webhooks</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new webhooks`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {webhooks.length > 0 ? ( - <Table - instances={webhooks} - onDelete={onDelete} - onSelect={onSelect} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - ); -} -interface TableProps { - rowSelection: string[]; - instances: Entity[]; - onDelete: (e: Entity) => void; - onSelect: (e: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - -function Table({ - instances, - onLoadMoreAfter, - onDelete, - onSelect, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - {hasMoreBefore && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more webhooks before the first one`} - onClick={onLoadMoreBefore} - > - <i18n.Translate>load newer webhooks</i18n.Translate> - </button> - )} - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>ID</i18n.Translate> - </th> - <th> - <i18n.Translate>Event type</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.webhook_id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.webhook_id} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.event_type} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected webhook from the database`} - onClick={() => onDelete(i)} - > - Delete - </button> - {/* <button - class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`test webhook`} - onClick={() => onNewOrder(i)} - > - Test - </button> */} - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - {hasMoreAfter && ( - <button - class="button is-fullwidth" - data-tooltip={i18n.str`load more webhooks after the last one`} - onClick={onLoadMoreAfter} - > - <i18n.Translate>load older webhooks</i18n.Translate> - </button> - )} - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no webhooks yet, add more pressing the + sign - </i18n.Translate> - </p> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx deleted file mode 100644 index c4b773669..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { MerchantBackend } from "../../../../declaration.js"; -import { - useInstanceWebhooks, - useWebhookAPI, -} from "../../../../hooks/webhooks.js"; -import { Notification } from "../../../../utils/types.js"; -import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onCreate: () => void; - onSelect: (id: string) => void; -} - -export default function ListWebhooks({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); - const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteWebhook } = useWebhookAPI(); - const result = useInstanceWebhooks({ position }, (id) => setPosition(id)); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - - <ListPage - webhooks={result.data.webhooks} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} - onCreate={onCreate} - onSelect={(e) => { - onSelect(e.webhook_id); - }} - onDelete={(e: MerchantBackend.Webhooks.WebhookEntry) => - deleteWebhook(e.webhook_id) - .then(() => - setNotif({ - message: i18n.str`webhook delete successfully`, - type: "SUCCESS", - }), - ) - .catch((error) => - setNotif({ - message: i18n.str`could not delete the webhook`, - type: "ERROR", - description: error.message, - }), - ) - } - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx deleted file mode 100644 index 303d17b72..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { UpdatePage as TestedComponent } from "./UpdatePage.js"; - -export default { - title: "Pages/Templates/Update", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx deleted file mode 100644 index 304ac90f3..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; - -type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId; - -interface Props { - onUpdate: (d: Entity) => Promise<void>; - onBack?: () => void; - webhook: Entity; -} -const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"]; - -export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - - const [state, setState] = useState<Partial<Entity>>(webhook); - - const errors: FormErrors<Entity> = { - event_type: !state.event_type ? i18n.str`required` : undefined, - http_method: !state.http_method - ? i18n.str`required` - : !validMethod.includes(state.http_method) - ? i18n.str`should be one of '${validMethod.join(", ")}'` - : undefined, - url: !state.url ? i18n.str`required` : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onUpdate(state as any); - }; - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4"> - Webhook: <b>{webhook.id}</b> - </span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <Input<Entity> - name="event_type" - label={i18n.str`Event`} - tooltip={i18n.str`The event of the webhook: why the webhook is used`} - /> - <Input<Entity> - name="http_method" - label={i18n.str`Method`} - tooltip={i18n.str`Method used by the webhook`} - /> - <Input<Entity> - name="url" - label={i18n.str`URL`} - tooltip={i18n.str`URL of the webhook where the customer will be redirected`} - /> - <Input<Entity> - name="header_template" - label={i18n.str`Header`} - inputType="multiline" - tooltip={i18n.str`Header template of the webhook`} - /> - <Input<Entity> - name="body_template" - inputType="multiline" - label={i18n.str`Body`} - tooltip={i18n.str`Body template by the webhook`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </div> - </div> - </section> - </section> - </div> - ); -} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx deleted file mode 100644 index 12374d82a..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @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 { MerchantBackend, WithId } from "../../../../declaration.js"; -import { - useWebhookAPI, - useWebhookDetails, -} from "../../../../hooks/webhooks.js"; -import { Notification } from "../../../../utils/types.js"; -import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -export type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId; - -interface Props { - onBack?: () => void; - onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - tid: string; -} -export default function UpdateWebhook({ - tid, - onConfirm, - onBack, - onUnauthorized, - onNotFound, - onLoadError, -}: Props): VNode { - const { updateWebhook } = useWebhookAPI(); - const result = useWebhookDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); - - if (result.loading) return <Loading />; - 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 ( - <Fragment> - <NotificationCard notification={notif} /> - <UpdatePage - webhook={{ ...result.data, id: tid }} - onBack={onBack} - onUpdate={(data) => { - return updateWebhook(tid, data) - .then(onConfirm) - .catch((error) => { - setNotif({ - message: i18n.str`could not update template`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </Fragment> - ); -} diff --git a/packages/auditor-backoffice-ui/src/stories.test.ts b/packages/auditor-backoffice-ui/src/stories.test.ts deleted file mode 100644 index 6ce88b916..000000000 --- a/packages/auditor-backoffice-ui/src/stories.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { setupI18n } from "@gnu-taler/taler-util"; -import * as tests from "@gnu-taler/web-util/testing"; -import { parseGroupImport } from "@gnu-taler/web-util/browser"; -import * as admin from "./paths/admin/index.stories.js"; -import * as instance from "./paths/instance/index.stories.js"; - -setupI18n("en", { en: {} }); - -describe("All the examples:", () => { - const cms = parseGroupImport({ admin, instance }); - cms.forEach((group) => { - describe(`Example for group: ${group.title}`, () => { - group.list.forEach((component) => { - describe(`Component: ${component.name}`, () => { - component.examples.forEach((example) => { - it(`should render example: ${example.name}`, () => { - tests.renderUI(example.render); - }); - }); - }); - }); - }); - }); -}); |