diff options
Diffstat (limited to 'packages')
46 files changed, 5045 insertions, 341 deletions
diff --git a/packages/auditor-backoffice-ui/src/Application.tsx b/packages/auditor-backoffice-ui/src/Application.tsx index 36059fe1a..3b6aa8dd3 100644 --- a/packages/auditor-backoffice-ui/src/Application.tsx +++ b/packages/auditor-backoffice-ui/src/Application.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,261 +14,153 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import {Fragment, VNode, h, render} from "preact"; -import "./scss/main.scss"; -import { AuditorBackend } from "./declaration.js" - /** + * + * @author Sebastian Javier Marchano (sebasjm) * @author Nic Eigel */ +import {HttpStatusCode, LibtoolVersion} from "@gnu-taler/taler-util"; +import { + ErrorType, + TranslationProvider, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import {Fragment, VNode, h, render} from "preact"; +import {useMemo} from "preact/hooks"; +import {ApplicationReadyRoutes} from "./ApplicationReadyRoutes.js"; +import {Loading} from "./components/exception/loading.js"; +import { + NotConnectedAppMenu, + NotificationCard +} from "./components/menu/index.js"; +import { + BackendContextProvider +} from "./context/backend.js"; +import {ConfigContextProvider} from "./context/config.js"; +import {useBackendConfig} from "./hooks/backend.js"; +import { strings } from "./i18n/strings.js"; export function Application(): VNode { return ( - <MyComponent/> + <BackendContextProvider> + <TranslationProvider source={strings}> + <ApplicationStatusRoutes/> + </TranslationProvider> + </BackendContextProvider> ); } - -export class Configer { - name?: string - version?: string - implementation?: string - currency?: string - auditor_public_key?: string - exchange_master_public_key?: string - - public constructor(init?: Partial<Configer>) { - Object.assign(this, init); - } -} - -interface ApiResponse { - Data: Array<Configer> -} - -function tryConfig(): Promise<Configer> { - console.log("getting here"); - const request: RequestInfo = new Request('http://localhost:8083/config', { - method: 'GET' - }) - return fetch(request) - // the JSON body is taken from the response - //.then(res => res.json()) - //.then(res => { - // console.log(res); - // return res as AuditorBackend.Config; - .then(response => { - if (!response.ok) { - throw new Error(response.statusText) - } - return response.json() as Promise<Configer> - }) - -} - -let testers: Array<Configer> = [] - -// Implementation code where T is the returned data shape -function api<T>(url: string): Promise<T> { - return fetch(url) - .then(response => { - if (!response.ok) { - throw new Error(response.statusText) - } - return response.json() - }) - -} - -// Consumer -api<Configer>('http://localhost:8083/config') - .then((r: Configer) => { - let e = structuredClone(r) - testers.push(r) - e.name = "er" - testers.push(e) - teste() - }) - .catch(error => { - /* show error message */ - }) - - -function MyComponent() { - - return <section class="section is-main-section"> - <div> - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash-register"/> - </span> - Deposit Confirmations - </p> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - <div class="table-container"> - <table class="table is-striped is-hoverable is-fullwidth"> - <thead> - <th>Name</th> - <th>Currency</th> - <th>Version</th> - <th>Auditor Pubkey</th> - <th>Exchange Master Pub Key</th> - <th>Implementation</th> - </thead> - <Glossary/> - </table> - </div> - </div> - </div> - </div> - </div> - - <Sidebar/> - <NavigationBar title={"Deposit confirmations"}/> - - </div> - </section>; -} - -function Glossary() { - console.log(testers) - return ( - <tbody> - { - testers.map(item => ( - <Fragment key={item.name}> - <tr> - <td>{item.name}</td> - <td>{item.currency}</td> - <td>{item.version}</td> - <td>test</td> - <td>test</td> - <td>{item.implementation}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-small is-info jb-modal" - type="button" - > - Update - </button> - <button - class="button is-small is-danger jb-modal" - type="button" - > - Delete - </button> - </div> - </td> - </tr> +/** + * Check connection testing against /config + * + * @returns + */ +function ApplicationStatusRoutes(): VNode { + const result = useBackendConfig(); + const {i18n} = useTranslationContext(); + + const configData = result.ok && result.data + ? result.data + : undefined; + const ctx = useMemo(() => (configData), [configData]); + + if (!result.ok) { + if (result.loading) return <Loading/>; + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) { + return ( + <Fragment> + <NotConnectedAppMenu title="Login"/> + <NotificationCard + notification={{ + message: i18n.str`Checking the /config endpoint got authorization error`, + type: "ERROR", + description: `The /config endpoint of the backend server should be accessible`, + }} + /> </Fragment> - )) + ); } - </tbody> - ); -} - -const App = ( - <ul> - <MyComponent/> - </ul> -); - - -function teste() { - render(App, document.body); -} - - -export function Sidebar(props: any): VNode { - //const config = useConfigContext(); - //const { url: backendURL } = useBackendContext() - //const {i18n} = useTranslationContext(); - - return ( - <aside class="aside is-placed-left is-expanded" style={{overflowY: "scroll"}}> - <div class="aside-tools"> - <div class="aside-tools-label"> - <div> - <b>Taler</b> Backoffice - </div> - <div - class="is-size-7 has-text-right" - style={{lineHeight: 0, marginTop: -10}} - > - Version (0.1) - </div> - </div> - </div> - <div class="menu is-menu-main"> + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) { + return ( <Fragment> - <ul class="menu-list"> - <li> - <a href={"/deposit-confirmations"} class="has-icon"> - <span class="icon"> - <i class="mdi mdi-cash-register"/> - </span> - <span class="menu-item-label"> - Deposit confirmations - </span> - </a> - </li> - </ul> + <NotConnectedAppMenu title="Error"/> + <NotificationCard + notification={{ + message: i18n.str`Could not find /config endpoint on this URL`, + type: "ERROR", + description: `Check the URL or contact the system administrator.`, + }} + /> </Fragment> - </div> - </aside> - ); -} + ); + } + if (result.type === ErrorType.SERVER) { + <Fragment> + <NotConnectedAppMenu title="Error"/> + <NotificationCard + notification={{ + message: i18n.str`Server response with an error code`, + type: "ERROR", + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, + }} + /> + </Fragment>; + } + if (result.type === ErrorType.UNREADABLE) { + <Fragment> + <NotConnectedAppMenu title="Error"/> + <NotificationCard + notification={{ + message: i18n.str`Response from server is unreadable, http status: ${result.status}`, + type: "ERROR", + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, + }} + /> + </Fragment>; + } + return ( + <Fragment> + <NotConnectedAppMenu title="Error"/> + <NotificationCard + notification={{ + message: i18n.str`Unexpected Error`, + type: "ERROR", + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, + }} + /> + </Fragment> + ); + } -interface Props { - title: string; -} + const SUPPORTED_VERSION = "1:0:1" + if (result.data && !LibtoolVersion.compare( + SUPPORTED_VERSION, + result.data.version, + )?.compatible) { + return <Fragment> + <NotConnectedAppMenu title="Error"/> + <NotificationCard + notification={{ + message: i18n.str`Incompatible version`, + type: "ERROR", + description: i18n.str`Auditor backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`, + }} + /> + </Fragment> + } -export function NavigationBar({title}: Props): VNode { return ( - <nav - class="navbar is-fixed-top" - role="navigation" - aria-label="main navigation" - > - <div class="navbar-brand"> - <span class="navbar-item" style={{fontSize: 24, fontWeight: 900}}> - {title} - </span> - - <a - role="button" - class="navbar-burger" - aria-label="menu" - aria-expanded="false" - onClick={(e) => { - e.stopPropagation(); - }} - > - <span aria-hidden="true"/> - <span aria-hidden="true"/> - <span aria-hidden="true"/> - </a> - </div> - - <div class="navbar-menu "> - <a - class="navbar-start is-justify-content-center is-flex-grow-1" - href="https://taler.net" - > - <img style={{height: 35, margin: 10}}/> - </a> - <div class="navbar-end"> - <div class="navbar-item" style={{paddingTop: 4, paddingBottom: 4}}> - </div> - </div> - </div> - </nav> + <div class="has-navbar-fixed-top"> + <ConfigContextProvider value={ctx!}> + <ApplicationReadyRoutes/> + </ConfigContextProvider> + </div> ); }
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx new file mode 100644 index 000000000..45063de96 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx @@ -0,0 +1,88 @@ +/* + 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) + * @author Nic Eigel + */ +import {ErrorType, useTranslationContext} from "@gnu-taler/web-util/browser"; +import {createHashHistory} from "history"; +import {Fragment, VNode, h} from "preact"; +import {Route, Router, route} from "preact-router"; +import {useEffect, useErrorBoundary, useState} from "preact/hooks"; +import {InstanceRoutes} from "./InstanceRoutes.js"; +import { + NotConnectedAppMenu, + NotYetReadyAppMenu, + NotificationCard, +} from "./components/menu/index.js"; +import { useBackendContext, useBackendTokenContext } from "./context/backend.js"; +import {Settings} from "./paths/settings/index.js"; +import { useBackendConfig, useBackendToken } from "./hooks/backend.js"; +import { Loading } from "./components/exception/loading.js"; +import { LoginPage } from "./paths/login/index.js"; + +/** + * Check if admin against /management/instances + * @returns + */ +export function ApplicationReadyRoutes(): VNode { + const {i18n} = useTranslationContext(); + const [unauthorized, setUnauthorized] = useState(false) + const [backendToken, setToken] = useState(false) + const { url: backendURL} = useBackendContext(); + const { token } = useBackendTokenContext(); + + const result = useBackendToken(); + if (result.loading) return <Loading/>; + if (!result.ok) { + return ( + <LoginPage /> + ); + } + const [showSettings, setShowSettings] = useState(false) + + if (showSettings) { + return <Fragment> + <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings"/> + <Settings onClose={() => setShowSettings(false)}/> + </Fragment> + } + + const history = createHashHistory(); + return ( + <Router history={history}> + <Route + default + component={DefaultMainRoute} + /> + </Router> + ); +} + +function DefaultMainRoute({ + url, //from preact-router + }: any): VNode { + //TODO + url = "app/#" + url; + + return ( + <InstanceRoutes + path={url} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx new file mode 100644 index 000000000..c2a5707aa --- /dev/null +++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx @@ -0,0 +1,334 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nicola Eigel + */ + +import { TranslatedString } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, FunctionComponent, VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useEffect, useErrorBoundary, useMemo, useState } from "preact/hooks"; +import { Loading } from "./components/exception/loading.js"; +import { Menu, NotificationCard } from "./components/menu/index.js"; +import { EntityContextProvider } from "./context/entity.js"; +import { Notification } from "./utils/types.js"; +import NotFoundPage from "./paths/notfound/index.js"; +import { Settings } from "./paths/settings/index.js"; +import DefaultList from "./paths/default/index.js"; +import { + ClassBalance, + ClassDepositConfirmation, + ClassHelper, +} 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 Paths { + error = "/error", + settings = "/settings", + + key_figures = "/key-figures", + critical_errors = "/critical-errors", + operating_status = "/operating-status", + detail_view = "/detail-view", + + amount_arithmethic_inconsistency_list = "/amount-arithmetic-inconsistencies", + + 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", + + reserve_balance_insufficient_inconsistency_list = "/reserve-balance-insufficient-inconsistencies", + + 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", + + 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" +} + +interface TestProps { + title: string; + endpoint: string; + entity: any; +} + +function getInstanceTitle(path: string): TestProps { + switch (path) { + case Paths.key_figures: + const helper_aggregation: ClassHelper = {} as ClassHelper; + return { title: `Key figures`, endpoint: "helper", entity: helper_aggregation }; + 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: + return [`Amount arithmetic inconsistencies`, ""]; + case Paths.bad_sig_losses_list: + return `Bad sig losses`;*/ + case Paths.balance_list: + const balance: ClassBalance = {} as ClassBalance; + return { title: "Balances", endpoint: "balances", entity: balance }; + /*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: + let depositConfirmation: ClassDepositConfirmation = {} as ClassDepositConfirmation; + return { title: "Deposit Confirmations", endpoint: "deposit-confirmation", entity: depositConfirmation }; + /*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 { title: `Settings`, endpoint: "settings", entity: null }; + default: + return { title: "", endpoint: "", entity: null }; + } +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => { +}; + +export interface Props { + // id: string; + path: string; + //setInstanceName: (s: string) => void; +} + +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], + ); + + /*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={"depsits"} + 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); + } + }} + > + + <Route path="/" component={Redirect} to={Paths.balance_list} /> + + <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.balance_list} + component={DefaultList} + onNotFound={NotFoundPage} + //onLoadError={ServerErrorRedirectTo(Paths.balance_list)} + /> + {<Route + path={Paths.settings} + component={Settings} + />} + {/** + * 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/loading.tsx b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx new file mode 100644 index 000000000..11b62c124 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx @@ -0,0 +1,48 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; + +export function Loading(): VNode { + return ( + <div + class="columns is-centered is-vcentered" + style={{ + height: "calc(100% - 3rem)", + position: "absolute", + width: "100%", + }} + > + <Spinner /> + </div> + ); +} + +export function Spinner(): VNode { + return ( + <div class="lds-ring"> + <div /> + <div /> + <div /> + <div /> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/forms/FormProvider.tsx b/packages/auditor-backoffice-ui/src/components/forms/FormProvider.tsx new file mode 100644 index 000000000..0d53c4d08 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/FormProvider.tsx @@ -0,0 +1,109 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext, useMemo } from "preact/hooks"; + +type Updater<S> = (value: (prevState: S) => S) => void; + +export interface Props<T> { + object?: Partial<T>; + errors?: FormErrors<T>; + name?: string; + valueHandler: Updater<Partial<T>> | null; + children: ComponentChildren; +} + +const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s; + +export function FormProvider<T>({ + object = {}, + errors = {}, + name = "", + valueHandler, + children, +}: Props<T>): VNode { + const initialObject = useMemo(() => object, []); + const value = useMemo<FormType<T>>( + () => ({ + errors, + object, + initialObject, + valueHandler: valueHandler ? valueHandler : noUpdater, + name, + toStr: {}, + fromStr: {}, + }), + [errors, object, valueHandler], + ); + + return ( + <FormContext.Provider value={value}> + <form + class="field" + onSubmit={(e) => { + e.preventDefault(); + // if (valueHandler) valueHandler(object); + }} + > + {children} + </form> + </FormContext.Provider> + ); +} + +export interface FormType<T> { + object: Partial<T>; + initialObject: Partial<T>; + errors: FormErrors<T>; + toStr: FormtoStr<T>; + name: string; + fromStr: FormfromStr<T>; + valueHandler: Updater<Partial<T>>; +} + +const FormContext = createContext<FormType<unknown>>(null!); + +/** + * FIXME: + * USE MEMORY EVENTS INSTEAD OF CONTEXT + * @deprecated + */ + +export function useFormContext<T>() { + return useContext<FormType<T>>(FormContext); +} + +export type FormErrors<T> = { + [P in keyof T]?: string | FormErrors<T[P]>; +}; + +export type FormtoStr<T> = { + [P in keyof T]?: (f?: T[P]) => string; +}; + +export type FormfromStr<T> = { + [P in keyof T]?: (f: string) => T[P]; +}; + +export type FormUpdater<T> = { + [P in keyof T]?: (f: keyof T) => (v: T[P]) => void; +}; diff --git a/packages/auditor-backoffice-ui/src/components/forms/Input.tsx b/packages/auditor-backoffice-ui/src/components/forms/Input.tsx new file mode 100644 index 000000000..c1ddcb064 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/Input.tsx @@ -0,0 +1,116 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <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; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; + inputExtra?: any; + side?: ComponentChildren; + children?: ComponentChildren; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +const TextInput = ({ inputType, error, ...rest }: any) => + inputType === "multiline" ? ( + <textarea + {...rest} + class={error ? "textarea is-danger" : "textarea"} + rows="3" + /> + ) : ( + <input + {...rest} + class={error ? "input is-danger" : "input"} + type={inputType} + /> + ); + +export function Input<T>({ + name, + readonly, + placeholder, + tooltip, + label, + expand, + help, + children, + inputType, + inputExtra, + side, + 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"> + <p + class={ + expand + ? "control is-expanded has-icons-right" + : "control has-icons-right" + } + > + <TextInput + error={error} + {...inputExtra} + inputType={inputType} + placeholder={placeholder} + readonly={readonly} + disabled={readonly} + name={String(name)} + value={toStr(value)} + onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => + onChange(fromStr(e.currentTarget.value)) + } + /> + {help} + {children} + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + {side} + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/forms/InputCurrency.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputCurrency.tsx new file mode 100644 index 000000000..b02354d7c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/InputCurrency.tsx @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { useConfigContext } from "../../context/config.js"; +import { Amount } from "../../declaration.js"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { InputProps } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + addonAfter?: ComponentChildren; + children?: ComponentChildren; + side?: ComponentChildren; +} + +export function InputCurrency<T>({ + name, + readonly, + label, + placeholder, + help, + tooltip, + expand, + addonAfter, + children, + side, +}: Props<keyof T>): VNode { + const config = useConfigContext(); + return ( + <InputWithAddon<T> + name={name} + readonly={readonly} + addonBefore={config.currency} + side={side} + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + addonAfter={addonAfter} + inputType="number" + expand={expand} + toStr={(v?: Amount) => v?.split(":")[1] || ""} + fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)} + inputExtra={{ min: 0 }} + > + {children} + </InputWithAddon> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/forms/InputNumber.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputNumber.tsx new file mode 100644 index 000000000..3b5df1474 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/InputNumber.tsx @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h } from "preact"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { InputProps } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + readonly?: boolean; + expand?: boolean; + side?: ComponentChildren; + children?: ComponentChildren; +} + +export function InputNumber<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + expand, + children, + side, +}: Props<keyof T>) { + return ( + <InputWithAddon<T> + name={name} + readonly={readonly} + fromStr={(v) => (!v ? undefined : parseInt(v, 10))} + toStr={(v) => `${v}`} + inputType="number" + expand={expand} + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + inputExtra={{ min: 0 }} + children={children} + side={side} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/forms/InputSelector.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputSelector.tsx new file mode 100644 index 000000000..a8dad5d89 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/InputSelector.tsx @@ -0,0 +1,94 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <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 InputSelector<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 select" : "control select "}> + <select + class={error ? "select is-danger" : "select"} + name={String(name)} + disabled={readonly} + readonly={readonly} + onChange={(e) => { + onChange(fromStr(e.currentTarget.value)); + }} + > + {placeholder && <option>{placeholder}</option>} + {values.map((v, i) => { + return ( + <option key={i} value={v} selected={value === v}> + {toStr(v)} + </option> + ); + })} + </select> + + {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/forms/InputToggle.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputToggle.tsx new file mode 100644 index 000000000..f95dfcd05 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/InputToggle.tsx @@ -0,0 +1,91 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <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 InputToggle<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="toggle" style={{ marginLeft: 4, marginTop: 0 }}> + <input + type="checkbox" + class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"} + checked={toBoolean(value)} + placeholder={placeholder} + readonly={readonly} + name={String(name)} + disabled={readonly} + onChange={onCheckboxClick} + /> + <div class="toggle-switch"></div> + </label> + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/forms/InputWithAddon.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputWithAddon.tsx new file mode 100644 index 000000000..e9fd88770 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/InputWithAddon.tsx @@ -0,0 +1,116 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + inputType?: "text" | "number" | "password"; + addonBefore?: ComponentChildren; + addonAfter?: ComponentChildren; + addonAfterAction?: () => void; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; + inputExtra?: any; + children?: ComponentChildren; + side?: ComponentChildren; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +export function InputWithAddon<T>({ + name, + readonly, + addonBefore, + children, + expand, + label, + placeholder, + help, + tooltip, + inputType, + inputExtra, + side, + addonAfter, + addonAfterAction, + toStr = defaultToString, + fromStr = defaultFromString, +}: 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"> + <div class="field has-addons"> + {addonBefore && ( + <div class="control"> + <a class="button is-static">{addonBefore}</a> + </div> + )} + <p + class={`control${expand ? " is-expanded" : ""}${required ? " has-icons-right" : "" + }`} + > + <input + {...(inputExtra || {})} + class={error ? "input is-danger" : "input"} + type={inputType} + placeholder={placeholder} + readonly={readonly} + disabled={readonly} + name={String(name)} + value={toStr(value)} + onChange={(e): void => onChange(fromStr(e.currentTarget.value))} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + {children} + </p> + {addonAfter && ( + <div class="control" onClick={addonAfterAction} style={{ cursor: addonAfterAction ? "pointer" : undefined }}> + <a class="button is-static">{addonAfter}</a> + </div> + )} + </div> + {error && <p class="help is-danger">{error}</p>} + <span class="has-text-grey">{help}</span> + </div> + {expand ? <div>{side}</div> : side} + </div> + + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/forms/useField.tsx b/packages/auditor-backoffice-ui/src/components/forms/useField.tsx new file mode 100644 index 000000000..c7559faae --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/forms/useField.tsx @@ -0,0 +1,92 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ComponentChildren, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useFormContext } from "./FormProvider.js"; + +interface Use<V> { + error?: string; + required: boolean; + value: any; + initial: any; + onChange: (v: V) => void; + toStr: (f: V | undefined) => string; + fromStr: (v: string) => V; +} + +export function useField<T>(name: keyof T): Use<T[typeof name]> { + const { errors, object, initialObject, toStr, fromStr, valueHandler } = + useFormContext<T>(); + type P = typeof name; + type V = T[P]; + const [isDirty, setDirty] = useState(false); + const updateField = + (field: P) => + (value: V): void => { + setDirty(true); + return valueHandler((prev) => { + return setValueDeeper(prev, String(field).split("."), value); + }); + }; + + const defaultToString = (f?: V): string => String(!f ? "" : f); + const defaultFromString = (v: string): V => v as any; + const value = readField(object, String(name)); + const initial = readField(initialObject, String(name)); + const hasError = readField(errors, String(name)); + return { + error: isDirty ? hasError : undefined, + required: !isDirty && hasError, + value, + initial, + onChange: updateField(name) as any, + toStr: toStr[name] ? toStr[name]! : defaultToString, + fromStr: fromStr[name] ? fromStr[name]! : defaultFromString, + }; +} +/** + * read the field of an object an support accessing it using '.' + * + * @param object + * @param name + * @returns + */ +const readField = (object: any, name: string) => { + return name + .split(".") + .reduce((prev, current) => prev && prev[current], object); +}; + +const setValueDeeper = (object: any, names: string[], value: any): any => { + if (names.length === 0) return value; + const [head, ...rest] = names; + return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) }; +}; + +export interface InputProps<T> { + name: T; + label: ComponentChildren; + placeholder?: string; + tooltip?: ComponentChildren; + readonly?: boolean; + help?: ComponentChildren; +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx new file mode 100644 index 000000000..41fe1374a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx @@ -0,0 +1,92 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <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 langIcon from "../../assets/icons/languageicon.svg"; +import { strings as messages } from "../../i18n/strings.js"; + +type LangsNames = { + [P in keyof typeof messages]: string; +}; + +const names: LangsNames = { + es: "Español [es]", + en: "English [en]", + fr: "Français [fr]", + de: "Deutsch [de]", + sv: "Svenska [sv]", + it: "Italiano [it]", +}; + +function getLangName(s: keyof LangsNames | string) { + if (names[s]) return names[s]; + return s; +} + +export function LangSelector(): VNode { + const [updatingLang, setUpdatingLang] = useState(false); + const { lang, changeLanguage } = useTranslationContext(); + + return ( + <div class="dropdown is-active "> + <div class="dropdown-trigger"> + <button + class="button has-tooltip-left" + data-tooltip="change language selection" + aria-haspopup="true" + aria-controls="dropdown-menu" + onClick={() => setUpdatingLang(!updatingLang)} + > + <div class="icon is-small is-left"> + <img src={langIcon} /> + </div> + <span>{getLangName(lang)}</span> + <div class="icon is-right"> + <i class="mdi mdi-chevron-down" /> + </div> + </button> + </div> + {updatingLang && ( + <div class="dropdown-menu" id="dropdown-menu" role="menu"> + <div class="dropdown-content"> + {Object.keys(messages) + .filter((l) => l !== lang) + .map((l) => ( + <a + key={l} + class="dropdown-item" + value={l} + onClick={() => { + changeLanguage(l); + setUpdatingLang(false); + }} + > + {getLangName(l)} + </a> + ))} + </div> + </div> + )} + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx new file mode 100644 index 000000000..9f1b33893 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx @@ -0,0 +1,72 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import logo from "../../assets/logo-2021.svg"; + +interface Props { + onMobileMenu: () => void; + title: string; +} + +export function NavigationBar({ onMobileMenu, title }: Props): VNode { + return ( + <nav + class="navbar is-fixed-top" + role="navigation" + aria-label="main navigation" + > + <div class="navbar-brand"> + <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}> + {title} + </span> + + <a + role="button" + class="navbar-burger" + aria-label="menu" + aria-expanded="false" + onClick={(e) => { + onMobileMenu(); + e.stopPropagation(); + }} + > + <span aria-hidden="true" /> + <span aria-hidden="true" /> + <span aria-hidden="true" /> + </a> + </div> + + <div class="navbar-menu "> + <a + class="navbar-start is-justify-content-center is-flex-grow-1" + href="https://taler.net" + > + <img src={logo} style={{ height: 35, margin: 10 }} /> + </a> + <div class="navbar-end"> + <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> + </div> + </div> + </div> + </nav> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx index a000cd251..e4e895403 100644 --- a/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx +++ b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx @@ -22,8 +22,6 @@ import {useTranslationContext} from "@gnu-taler/web-util/browser"; import {Fragment, h, VNode} from "preact"; export function Sidebar(props: any): VNode { - //const config = useConfigContext(); - //const { url: backendURL } = useBackendContext() const {i18n} = useTranslationContext(); return ( @@ -43,18 +41,58 @@ export function Sidebar(props: any): VNode { </div> <div class="menu is-menu-main"> <Fragment> - <ul class="menu-list"> - <li> - <a href={"/deposit-confirmations"} class="has-icon"> + <ul class="menu-list"> + <li> + <a href={"/key-figures"} class="has-icon"> <span class="icon"> - <i class="mdi mdi-cash-register"/> + <i class="mdi mdi-bank" /> </span> - <span class="menu-item-label"> - <i18n.Translate>Deposit Confirmations</i18n.Translate> + <span class="menu-item-label"> + <i18n.Translate>Key figures</i18n.Translate> </span> - </a> - </li> - </ul> + </a> + </li> + <li> + <a href={"/critical-errors"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-alert" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Critical errors</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/operating-status"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-close-network" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Operating status</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/detail-view"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-format-wrap-tight" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Inconsistencies</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/settings"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-square-edit-outline" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Settings</i18n.Translate> + </span> + </a> + </li> + </ul> </Fragment> </div> </aside> diff --git a/packages/auditor-backoffice-ui/src/components/menu/index.tsx b/packages/auditor-backoffice-ui/src/components/menu/index.tsx index 4c02797ab..c3562928b 100644 --- a/packages/auditor-backoffice-ui/src/components/menu/index.tsx +++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx @@ -17,12 +17,101 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @author Nic Eigel * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel */ -/* + import {ComponentChildren, Fragment, h, VNode} from "preact"; -import {useEffect} from "preact/hooks"; +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"; + +/** + * @author Nic Eigel + * + */ + +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; + onShowSettings: () => void; +} function WithTitle({ title, @@ -38,24 +127,12 @@ function WithTitle({ } export function Menu({ - onLogout, onShowSettings, title, - instance, path, - admin, - setInstanceName, - isPasswordOk }: MenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); - - const titleWithSubtitle = title - ? title - : !admin - ? getInstanceTitle(path, instance) - : getAdminTitle(path, instance); - const adminInstance = instance === "default"; - const mimic = admin && !adminInstance; + const titleWithSubtitle = getInstanceTitle(path.replace("app/#", "")); return ( <WithTitle title={titleWithSubtitle}> <div @@ -67,41 +144,104 @@ export function Menu({ 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> - </div> - </nav> - )} + <Sidebar + onShowSettings={onShowSettings} + mobile={mobileOpen} + /> </div> </WithTitle> ); -}*/
\ No newline at end of file +} + +interface NotYetReadyAppMenuProps { + title: string; + onShowSettings: () => void; +} + +interface NotifProps { + 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> + </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; +} + +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> + ); +} + + +export function NotYetReadyAppMenu({ + 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/modal/index.tsx b/packages/auditor-backoffice-ui/src/components/modal/index.tsx new file mode 100644 index 000000000..efda6b944 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/modal/index.tsx @@ -0,0 +1,496 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useEntityContext } from "../../context/entity.js"; +import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js"; +import { Spinner } from "../exception/loading.js"; +import { FormProvider } from "../forms/FormProvider.js"; +import { Input } from "../forms/Input.js"; + +interface Props { + active?: boolean; + description?: string; + onCancel?: () => void; + onConfirm?: () => void; + label?: string; + children?: ComponentChildren; + danger?: boolean; + disabled?: boolean; +} + +export function ConfirmModal({ + active, + description, + onCancel, + onConfirm, + children, + danger, + disabled, + label = "Confirm", +}: Props): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card" style={{ maxWidth: 700 }}> + <header class="modal-card-head"> + {!description ? null : ( + <p class="modal-card-title"> + <b>{description}</b> + </p> + )} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body">{children}</section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + {onConfirm ? ( + <Fragment> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + + <button + class={danger ? "button is-danger " : "button is-info "} + disabled={disabled} + onClick={onConfirm} + > + <i18n.Translate>{label}</i18n.Translate> + </button> + </Fragment> + ) : ( + <button class="button " onClick={onCancel}> + <i18n.Translate>Close</i18n.Translate> + </button> + )} + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function ContinueModal({ + active, + description, + onCancel, + onConfirm, + children, + disabled, +}: Props): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head has-background-success"> + {!description ? null : <p class="modal-card-title">{description}</p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body">{children}</section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button + class="button is-success " + disabled={disabled} + onClick={onConfirm} + > + <i18n.Translate>Continue</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function SimpleModal({ onCancel, children }: any): VNode { + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <section class="modal-card-body is-main-section">{children}</section> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function ClearConfirmModal({ + description, + onCancel, + onClear, + onConfirm, + children, +}: Props & { onClear?: () => void }): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + {!description ? null : <p class="modal-card-title">{description}</p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body is-main-section">{children}</section> + <footer class="modal-card-foot"> + {onClear && ( + <button + class="button is-danger" + onClick={onClear} + disabled={onClear === undefined} + > + <i18n.Translate>Clear</i18n.Translate> + </button> + )} + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <button + class="button is-info" + onClick={onConfirm} + disabled={onConfirm === undefined} + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +interface DeleteModalProps { + element: { id: string; name: string }; + onCancel: () => void; + onConfirm: (id: string) => void; +} + +export function DeleteModal({ + element, + onCancel, + onConfirm, +}: DeleteModalProps): VNode { + return ( + <ConfirmModal + label={`Delete instance`} + description={`Delete the instance "${element.name}"`} + danger + active + onCancel={onCancel} + onConfirm={() => onConfirm(element.id)} + > + <p> + If you delete the instance named <b>"{element.name}"</b> (ID:{" "} + <b>{element.id}</b>), the merchant will no longer be able to process + orders or refunds + </p> + <p> + This action deletes the instance private key, but preserves all + transaction data. You can still access that data after deleting the + instance. + </p> + <p class="warning"> + Deleting an instance <b>cannot be undone</b>. + </p> + </ConfirmModal> + ); +} + +export function PurgeModal({ + element, + onCancel, + onConfirm, +}: DeleteModalProps): VNode { + return ( + <ConfirmModal + label={`Purge the instance`} + description={`Purge the instance "${element.name}"`} + danger + active + onCancel={onCancel} + onConfirm={() => onConfirm(element.id)} + > + <p> + If you purge the instance named <b>"{element.name}"</b> (ID:{" "} + <b>{element.id}</b>), you will also delete all it's transaction + data. + </p> + <p> + The instance will disappear from your list, and you will no longer be + able to access it's data. + </p> + <p class="warning"> + Purging an instance <b>cannot be undone</b>. + </p> + </ConfirmModal> + ); +} + +interface UpdateTokenModalProps { + oldToken?: string; + onCancel: () => void; + onConfirm: (value: string) => void; + onClear: () => void; +} + +//FIXME: merge UpdateTokenModal with SetTokenNewInstanceModal +export function UpdateTokenModal({ + onCancel, + onClear, + onConfirm, + oldToken, +}: UpdateTokenModalProps): 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 hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token; + const errors = { + old_token: hasInputTheCorrectOldToken + ? i18n.str`is not the same as the current access token` + : 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 = useEntityContext(); + + const text = i18n.str`You are updating the access token from instance with id `; + + return ( + <ClearConfirmModal + description={text} + onCancel={onCancel} + onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined} + onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined} + > + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider errors={errors} object={form} valueHandler={setValue}> + {oldToken && ( + <Input<State> + name="old_token" + label={i18n.str`Old access token`} + tooltip={i18n.str`access token currently in use`} + inputType="password" + /> + )} + <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" + /> + </FormProvider> + <p> + <i18n.Translate> + Clearing the access token will mean public access to the instance + </i18n.Translate> + </p> + </div> + <div class="column" /> + </div> + </ClearConfirmModal> + ); +} + +export function SetTokenNewInstanceModal({ + onCancel, + onClear, + onConfirm, +}: UpdateTokenModalProps): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; + const [form, setValue] = useState<Partial<State>>({ + new_token: "", + repeat_token: "", + }); + const { i18n } = useTranslationContext(); + + const errors = { + 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 access 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, + ); + + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">{i18n.str`You are setting the access token for the new instance`}</p> + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + errors={errors} + object={form} + valueHandler={setValue} + > + <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" + /> + </FormProvider> + <p> + <i18n.Translate> + With external authorization method no check will be done by + the merchant backend + </i18n.Translate> + </p> + </div> + <div class="column" /> + </div> + </section> + <footer class="modal-card-foot"> + {onClear && ( + <button + class="button is-danger" + onClick={onClear} + disabled={onClear === undefined} + > + <i18n.Translate>Set external authorization</i18n.Translate> + </button> + )} + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <button + class="button is-info" + onClick={() => onConfirm(form.new_token!)} + disabled={hasErrors} + > + <i18n.Translate>Set access token</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title"> + <i18n.Translate>Operation in progress...</i18n.Translate> + </p> + </header> + <section class="modal-card-body"> + <div class="columns"> + <div class="column" /> + <Spinner /> + <div class="column" /> + </div> + <p>{i18n.str`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p> + </section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/context/backend.ts b/packages/auditor-backoffice-ui/src/context/backend.ts new file mode 100644 index 000000000..cc19eef31 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/backend.ts @@ -0,0 +1,71 @@ +/* + 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) + * @author Nic Eigel + */ + +import { createContext, h, VNode } from "preact"; +import { useContext } from "preact/hooks"; +import { useBackendURL } from "../hooks/index.js"; + +//TODO: maybe add token +interface BackendContextType { + url: string, +} + +const BackendContext = createContext<BackendContextType>({ + url: "", +}); + +function useBackendContextState( + defaultUrl?: string, +): BackendContextType { + const [url] = useBackendURL(defaultUrl); + + return { + url, + }; +} + +export const BackendContextProvider = ({ + children, + defaultUrl, + }: { + children: any; + defaultUrl?: string; +}): VNode => { + const value = useBackendContextState(defaultUrl); + + return h(BackendContext.Provider, { value, children }); +}; + + + +export const useBackendContext = (): BackendContextType => + useContext(BackendContext); + +interface BackendTokenType { + token: string; +} + +const BackendTokenContext = createContext<BackendTokenType>({} as any); + +export const BackendTokenContextProvider = BackendTokenContext.Provider; + +export const useBackendTokenContext = (): BackendTokenType => useContext(BackendTokenContext);
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/context/config.ts b/packages/auditor-backoffice-ui/src/context/config.ts new file mode 100644 index 000000000..58ee5a594 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/config.ts @@ -0,0 +1,29 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; +import { AuditorBackend } from "../declaration.js"; + +const Context = createContext<AuditorBackend.VersionResponse>(null!); + +export const ConfigContextProvider = Context.Provider; +export const useConfigContext = (): AuditorBackend.VersionResponse => useContext(Context); diff --git a/packages/auditor-backoffice-ui/src/context/entity.ts b/packages/auditor-backoffice-ui/src/context/entity.ts new file mode 100644 index 000000000..8181931c4 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/entity.ts @@ -0,0 +1,47 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel + */ + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; + +interface EntityType { + title: string; + path: string; + endpoint: string; + entity: any; +} + +const EntityContext = createContext<EntityType>({} as any); + +export const EntityContextProvider = EntityContext.Provider; + +export const useEntityContext = (): EntityType => useContext(EntityContext); + +interface EntityDataType { + data: any; +} + +const EntityDataContext = createContext<EntityDataType>({} as any); + +export const EntityDataContextProvider = EntityDataContext.Provider; + +export const useEntityDataContext = (): EntityDataType => useContext(EntityDataContext); diff --git a/packages/auditor-backoffice-ui/src/custom.d.ts b/packages/auditor-backoffice-ui/src/custom.d.ts new file mode 100644 index 000000000..e693c2951 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/custom.d.ts @@ -0,0 +1,42 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +declare module "*.po" { + const content: any; + export default content; +} +declare module "jed" { + const x: any; + export = x; +} +declare module "*.jpeg" { + const content: any; + export default content; +} +declare module "*.png" { + const content: any; + export default content; +} +declare module "*.svg" { + const content: any; + export default content; +} + +declare module "*.scss" { + const content: Record<string, string>; + export default content; +} +declare const __VERSION__: string; +declare const __GIT_HASH__: string; diff --git a/packages/auditor-backoffice-ui/src/declaration.d.ts b/packages/auditor-backoffice-ui/src/declaration.d.ts index d5301c115..7f3fbebf3 100644 --- a/packages/auditor-backoffice-ui/src/declaration.d.ts +++ b/packages/auditor-backoffice-ui/src/declaration.d.ts @@ -16,30 +16,300 @@ /** * + * @author Sebastian Javier Marchano (sebasjm) * @author Nic Eigel */ +type HashCode = string; +type EddsaPublicKey = string; +type EddsaSignature = string; +type WireTransferIdentifierRawP = string; +type RelativeTime = TalerProtocolDuration; +type ImageDataUrl = string; +type AuditorUserType = "business" | "individual"; + + +export interface WithId { + id: string; +} + +interface Timestamp { + // Milliseconds since epoch, or the special + // value "forever" to represent an event that will + // never happen. + t_s: number | "never"; +} + +interface TalerProtocolDuration { + d_us: number | "forever"; +} + +interface Duration { + d_ms: number | "forever"; +} + +interface WithId { + id: string; +} + +type Amount = string; +type UUID = string; +type Integer = number; + export namespace AuditorBackend { - interface DepositConfirmation { - // identifier - serial_id: number; + interface DepositConfirmation { + // identifier + deposit_confirmation_serial_id: number; + + h_contract_terms: string; + + h_policy: string; + + h_wire: string; + + exchange_timestamp: string; + + refund_deadline: string; + + wire_deadline: string; + + total_without_fee: string; + + coin_pubs: string; + + coin_sigs: string; + + merchant_pub: string; + + merchant_sig: string; + + exchange_pub: string; + + exchange_sig: string; + + suppressed: string; + + ancient: string; + } - // amount of deposit confirmation - amount: string; + interface Config { + name: string; + version: string; + implementation: string; + currency: string; + auditor_public_key: string; + exchange_master_public_key: string; + } - // timestamp of deposit confirmation - timestamp: string; + interface ErrorDetail { + // Numeric error code unique to the condition. + // The other arguments are specific to the error value reported here. + code: number; - // account - account: string; + // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... + // Should give a human-readable hint about the error's nature. Optional, may change without notice! + hint?: string; + + // Optional detail about the specific input value that failed. May change without notice! + detail?: string; + + // Name of the parameter that was bogus (if applicable). + parameter?: string; + + // Path to the argument that was bogus (if applicable). + path?: string; + + // Offset of the argument that was bogus (if applicable). + offset?: string; + + // Index of the argument that was bogus (if applicable). + index?: string; + + // Name of the object that was bogus (if applicable). + object?: string; + + // Name of the currency than was problematic (if applicable). + currency?: string; + + // Expected type (if applicable). + type_expected?: string; + + // Type that was provided instead (if applicable). + type_actual?: string; + } + + interface VersionResponse { + // libtool-style representation of the Merchant protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + // Name of the protocol. + + name: "taler-auditor"; + version: string; + + // Default (!) currency supported by this backend. + // This is the currency that the backend should + // suggest by default to the user when entering + // amounts. See currencies for a list of + // supported currencies and how to render them. + implementation: string; + currency: string; + auditor_public_key: string; + exchange_master_public_key: string; + + // How services should render currencies supported + // by this backend. Maps + // currency codes (e.g. "EUR" or "KUDOS") to + // the respective currency specification. + // All currencies in this map are supported by + // the backend. Note that the actual currency + // specifications are a *hint* for applications + // that would like *advice* on how to render amounts. + // Applications *may* ignore the currency specification + // if they know how to render currencies that they are + // used with. + //currencies: { currency: CurrencySpecification }; + + // Array of exchanges trusted by the merchant. + // Since protocol v6. + // exchanges: ExchangeConfigInfo[]; + } + + export interface TokenResponse { + null; + } + + namespace Default { + interface ObjectResponse { + object: AnyEntry[]; } + } + + namespace DepositConfirmation { - interface Config { - name: string; - version: string; - implementation: string; - currency: string; - auditor_public_key: string; - exchange_master_public_key: string; + interface InventorySummaryResponse { + // List of products that are present in the inventory + deposit_confirmations: InventoryEntry[]; } + + interface DepositConfirmationDetail { + // identifier + deposit_confirmation_serial_id: number; + + h_contract_terms: string; + + h_policy: string; + + h_wire: string; + + exchange_timestamp: string; + + refund_deadline: string; + + wire_deadline: string; + + total_without_fee: string; + + coin_pubs: string; + + coin_sigs: string; + + merchant_pub: string; + + merchant_sig: string; + + exchange_pub: string; + + exchange_sig: string; + + suppressed: string; + + ancient: string; + } + } + + namespace Balance { + + interface InventorySummaryResponse { + // List of products that are present in the inventory + balances: InventoryEntry[]; + } + + interface BalanceDetail { + // identifier + row_id: number; + + balance_key: string; + + balance_value: string; + + suppressed: string; + } + } +} + +export class ClassDepositConfirmation { + // List of products that are present in the inventory + deposit_confirmations: DepositConfirmationDetail[]; +} + +export class DepositConfirmationDetail implements AuditorBackend.DepositConfirmation.DepositConfirmationDetail { + deposit_confirmation_serial_id: number; + + h_contract_terms: string; + + h_policy: string; + + h_wire: string; + + exchange_timestamp: string; + + refund_deadline: string; + + wire_deadline: string; + + total_without_fee: string; + + coin_pubs: string; + + coin_sigs: string; + + merchant_pub: string; + + merchant_sig: string; + + exchange_pub: string; + + exchange_sig: string; + + suppressed: string; + + ancient: string; +} + + + +export class ClassBalance { + // List of products that are present in the inventory + balances: BalanceDetail[]; +} + +class BalanceDetail implements AuditorBackend.Balance.BalanceDetail { + // identifier + row_id: number; + + balance_key: string; + + balance_value: string; + + suppressed: string; +} + +export class ClassHelper { + // List of products that are present in the inventory + balances: BalanceDetail[]; +} + +export class ClassAggregation { + balances: BalanceDetail[]; }
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts index 7f293162f..d230db88d 100644 --- a/packages/auditor-backoffice-ui/src/hooks/backend.ts +++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts @@ -1,21 +1,262 @@ -//import { HttpResponse, RequestError } from "@gnu-taler/web-util/lib/utils/request.js"; +/* + 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) + * @author Nic Eigel + */ + +import {AbsoluteTime, HttpStatusCode} from "@gnu-taler/taler-util"; +import { + HttpResponse, + HttpResponseOk, + RequestError, + RequestOptions, + useApiContext, +} from "@gnu-taler/web-util/browser"; +import {useCallback, useEffect, useState} from "preact/hooks"; +import {useSWRConfig} from "swr"; +import { useBackendContext } from "../context/backend.js"; import { AuditorBackend } from "../declaration.js"; -import Config = AuditorBackend.Config; - -export function tryConfig(): Promise<Config> { - // const request: RequestInfo = new Request('./Config.json', { - // method: 'GET', - // headers: headers - // }) - - return fetch("/config.json") - // the JSON body is taken from the response - .then(res => res.json()) - .then(res => { - // The response has an `any` type, so we need to cast - // it to the `User` type, and return it from the promise - return res as Config; - }); +export function useMatchMutate(): ( + re?: RegExp, + value?: unknown, +) => Promise<any> { + const {cache, mutate} = useSWRConfig(); + + if (!(cache instanceof Map)) { + throw new Error( + "matchMutate requires the cache provider to be a Map instance", + ); + } + + return function matchRegexMutate(re?: RegExp) { + return mutate((key) => { + // evict if no key or regex === all + if (!key || !re) return true + // match string + if (typeof key === 'string' && re.test(key)) return true + // record or object have the path at [0] + if (typeof key === 'object' && re.test(key[0])) return true + //key didn't match regex + return false + }, undefined, { + revalidate: true, + }); + }; } +const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; +const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + +export function useBackendConfig(): HttpResponse< + AuditorBackend.VersionResponse | undefined, + RequestError<AuditorBackend.ErrorDetail> +> { + const {request} = useBackendBaseRequest(); + + type Type = AuditorBackend.VersionResponse; + type State = { data: HttpResponse<Type, RequestError<AuditorBackend.ErrorDetail>>, timer: number } + const [result, setResult] = useState<State>({data: {loading: true}, timer: 0}); + + useEffect(() => { + if (result.timer) { + clearTimeout(result.timer); + } + + function tryConfig(): void { + request<Type>(`/config`) + .then((data) => { + const timer: any = setTimeout(() => { + tryConfig(); + }, CHECK_CONFIG_INTERVAL_OK); + setResult({data, timer}); + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryConfig(); + }, CHECK_CONFIG_INTERVAL_FAIL); + const data = error.cause; + setResult({data, timer}); + }); + } + + tryConfig(); + }, [request]); + + return result.data; +} + +export function useBackendToken(): HttpResponse< + AuditorBackend.VersionResponse, + RequestError<AuditorBackend.ErrorDetail> +> { + const {request} = useBackendBaseRequest(); + + type Type = AuditorBackend.VersionResponse; + type State = { data: HttpResponse<Type, RequestError<AuditorBackend.ErrorDetail>>, timer: number } + const [result, setResult] = useState<State>({data: {loading: true}, timer: 0}); + + useEffect(() => { + if (result.timer) { + clearTimeout(result.timer); + } + + function tryToken(): void { + request<Type>(`/monitoring/balances`) + .then((data) => { + const timer: any = setTimeout(() => { + tryToken(); + }, CHECK_CONFIG_INTERVAL_OK); + setResult({data, timer}); + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryToken(); + }, CHECK_CONFIG_INTERVAL_FAIL); + const data = error.cause; + setResult({data, timer}); + }); + } + + tryToken(); + }, [request]); + + return result.data; +} + +interface useBackendInstanceRequestType { + + request: <T>( + endpoint: string, + options?: RequestOptions, + ) => Promise<HttpResponseOk<T>>; + fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(params: string[]) => Promise<HttpResponseOk<T>[]>; + depositConfirmationFetcher: <T>( + params: [ + endpoint: string, + ], + ) => Promise<HttpResponseOk<T>>; +} + +interface useBackendBaseRequestType { + + request: <T>( + endpoint: string, + options?: RequestOptions + ) => Promise<HttpResponseOk<T>>; +} + +type YesOrNo = "yes" | "no"; + +/** + * + * @param root the request is intended to the base URL and no the instance URL + * @returns request handler to + */ +//TODO: Add token +export function useBackendBaseRequest(): useBackendBaseRequestType { + const {url: backend} = useBackendContext(); + const {request: requestHandler} = useApiContext(); + //const { token } = useBackendTokenContext(); + const token = "secret-token:D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"; + + + const request = useCallback( + function requestImpl<T>( + endpoint: string, + //todo: remove + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(backend, endpoint, {...options, token}).then(res => { + return res; + }).catch(err => { + throw err; + }); + }, + [backend], + ); + + return {request}; +} + + +export function useBackendRequest(): useBackendInstanceRequestType { + const {url: rootBackendUrl} = useBackendContext(); + // const {id} = useInstanceContext(); + const {request: requestHandler} = useApiContext(); + + //TODO: check + const baseUrl = "http://localhost:8083/"; + const token = "secret-token:D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"; + + + + + const request = useCallback( + function requestImpl<T>( + endpoint: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, {...options, token}); + }, + [baseUrl], + ); + + const multiFetcher = useCallback( + function multiFetcherImpl<T>( + params: string[], + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>[]> { + return Promise.all( + params.map((endpoint) => + requestHandler<T>(baseUrl, endpoint, {...options, token}), + ), + ); + }, + [baseUrl], + ); + + const fetcher = useCallback( + function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, {token}); + }, + [baseUrl], + ); + + const depositConfirmationFetcher = useCallback( + function orderFetcherImpl<T>( + args: [endpoint: string, + ], + ): Promise<HttpResponseOk<T>> { + const [endpoint] = args; + const params: any = {"token": "secret-token:D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"}; + return requestHandler<T>(baseUrl, endpoint, {params, token}); + }, + [baseUrl], + ); + + + return { + request, + fetcher, + depositConfirmationFetcher, + multiFetcher + }; +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/config.ts b/packages/auditor-backoffice-ui/src/hooks/config.ts new file mode 100644 index 000000000..a57fa15d5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/config.ts @@ -0,0 +1,24 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; +import { AuditorBackend } from "../declaration.js"; + +const Context = createContext<AuditorBackend.VersionResponse>(null!); + +export const ConfigContextProvider = Context.Provider; +export const useConfigContext = (): AuditorBackend.VersionResponse => useContext(Context); diff --git a/packages/auditor-backoffice-ui/src/hooks/critical.ts b/packages/auditor-backoffice-ui/src/hooks/critical.ts new file mode 100644 index 000000000..6a25d3037 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/critical.ts @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendRequest, 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; + +type YesOrNo = "yes" | "no"; + +export interface HelperDashboardFilter { + finance?: YesOrNo; + security?: YesOrNo; + operating?: YesOrNo; + detail?: YesOrNo; +} + +export function getCriticalData( + args?: HelperDashboardFilter, + updateFilter?: (d: Date) => void, +): HttpResponse<any, AuditorBackend.ErrorDetail> { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/fee-time-inconsistency", + "monitoring/emergency", + "monitoring/emergency-by-count", + "monitoring/reserve-balance-insufficient-inconsistency", + ]; + + + const { data: list, error: listError } = useSWR< + HttpResponseOk<any>[], RequestError<AuditorBackend.ErrorDetail> + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/entity.ts b/packages/auditor-backoffice-ui/src/hooks/entity.ts new file mode 100644 index 000000000..ae62da35e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/entity.ts @@ -0,0 +1,82 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; +import { useEntityContext } from "../context/entity.js"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +interface Props { + endpoint: string; + entity: any; +} + +export function getEntityList({ endpoint, entity }: Props): HttpResponse<any, AuditorBackend.ErrorDetail> { + const { fetcher } = useBackendRequest(); + + const { data: list, error: listError } = useSWR< + HttpResponseOk<typeof entity>, + RequestError<AuditorBackend.ErrorDetail> + >([`monitoring/` + endpoint], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list?.data != null) { + return { ok: true, data: [list?.data] }; + } + return { loading: true }; +} +export interface EntityAPI { + updateEntity: ( + id: string + ) => Promise<void>; +} + +export function useEntityAPI(): EntityAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendRequest(); + const { endpoint } = useEntityContext(); + const data = {"suppressed": true}; + + const updateEntity = async ( + id: string, + ): Promise<void> => { + const r = await request(`monitoring/${endpoint}/${id}`, { + method: "PATCH", + data, + }); + + return await mutateAll(/.*\/monitoring.*/); + }; + + return { updateEntity }; +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/finance.ts b/packages/auditor-backoffice-ui/src/hooks/finance.ts new file mode 100644 index 000000000..7d8c30fb8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/finance.ts @@ -0,0 +1,63 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; +import { useEntityContext } from "../context/entity.js"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +export function getKeyFiguresData(): HttpResponse<any, AuditorBackend.ErrorDetail> { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/misattribution-in-inconsistency", + "monitoring/coin-inconsistency", + "monitoring/reserve-in-inconsistency", + "monitoring/bad-sig-losses", + "monitoring/balances", + "monitoring/amount-arithmetic-inconsistency", + "monitoring/wire-format-inconsistency", + "monitoring/wire-out-inconsistency", + "monitoring/reserve-balance-summary-wrong-inconsistency", + + ]; + + const { data: list, error: listError } = useSWR< + HttpResponseOk<any>[], RequestError<AuditorBackend.ErrorDetail> + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts new file mode 100644 index 000000000..cf1c57771 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/index.ts @@ -0,0 +1,79 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import {StateUpdater, useState} from "preact/hooks"; +import { ValueOrFunction } from "../utils/types.js"; + +export function useBackendURL( + url?: string, +): [string, StateUpdater<string>] { + const [value, setter] = useSimpleLocalStorage( + "auditor-base-url", + url || calculateRootPath(), + ); + + const checkedSetter = (v: ValueOrFunction<string>) => { + return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); + }; + + return [value!, checkedSetter]; +} + +const calculateRootPath = () => { + const rootPath = + typeof window !== undefined + ? window.location.origin + window.location.pathname + : "/"; + + /** + * By default, auditor backend serves the html content + * from the /webui root. This should cover most of the + * cases and the rootPath will be the auditor backend + * URL where the instances are + */ + return rootPath.replace("/webui/", ""); +}; + +export function useSimpleLocalStorage( + key: string, + initialValue?: string, +): [string | undefined, StateUpdater<string | undefined>] { + const [storedValue, setStoredValue] = useState<string | undefined>( + (): string | undefined => { + return typeof window !== "undefined" + ? window.localStorage.getItem(key) || initialValue + : initialValue; + }, + ); + + const setValue = ( + value?: string | ((val?: string) => string | undefined), + ) => { + setStoredValue((p) => { + const toStore = value instanceof Function ? value(p) : value; + if (typeof window !== "undefined") { + if (!toStore) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, toStore); + } + } + return toStore; + }); + }; + + return [storedValue, setValue]; +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/operational.ts b/packages/auditor-backoffice-ui/src/hooks/operational.ts new file mode 100644 index 000000000..2f34c14cb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/operational.ts @@ -0,0 +1,83 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; +import { useEntityContext } from "../context/entity.js"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +export interface HelperDashboardFilter { + finance?: YesOrNo; + security?: YesOrNo; + operating?: YesOrNo; + detail?: YesOrNo; +} + +export function getOperationData( + args?: HelperDashboardFilter, + updateFilter?: (d: Date) => void, +): HttpResponse<any, AuditorBackend.ErrorDetail> { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/row-inconsistency", + "monitoring/purse-not-closed-inconsistencies", + "monitoring/reserve-not-closed-inconsistency", + "monitoring/denominations-without-sigs", + "monitoring/deposit-confirmation", + "monitoring/denomination-key-validity-withdraw-inconsistency", + "monitoring/refreshes-hanging", + // "monitoring/closure-lags", + // "monitoring/row-minor-inconsistencies", + // "monitoring/historic-denomination-revenue", + // "monitoring/denomination-pending", + "monitoring/historic-reserve-summary", + + ]; + + + const { data: list, error: listError } = useSWR< + HttpResponseOk<any>[], RequestError<AuditorBackend.ErrorDetail> + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} + +export interface EntityAPI { + updateEntity: ( + id: string, + ) => Promise<void>; +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/useSettings.ts b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts new file mode 100644 index 000000000..8c1ebd9f6 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts @@ -0,0 +1,73 @@ +/* + This file is part of GNU Taler + (C) 2022 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 { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { + Codec, + buildCodecForObject, + codecForBoolean, + codecForConstString, + codecForEither, + codecForString, +} from "@gnu-taler/taler-util"; + +export interface Settings { + advanceOrderMode: boolean; + dateFormat: "ymd" | "dmy" | "mdy"; +} + +const defaultSettings: Settings = { + advanceOrderMode: false, + dateFormat: "ymd", +} + +export const codecForSettings = (): Codec<Settings> => + buildCodecForObject<Settings>() + .property("advanceOrderMode", codecForBoolean()) + .property("dateFormat", codecForEither( + codecForConstString("ymd"), + codecForConstString("dmy"), + codecForConstString("mdy"), + )) + .build("Settings"); + +const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings()); + +export function useSettings(): [ + Readonly<Settings>, + (s: Settings) => void, +] { + const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings); + + // const parsed: Settings = value ?? defaultSettings; + // function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { + // const next = { ...parsed, [k]: v } + // update(next); + // } + return [value, update]; +} + +export function dateFormatForSettings(s: Settings): string { + switch (s.dateFormat) { + case "ymd": return "yyyy/MM/dd" + case "dmy": return "dd/MM/yyyy" + case "mdy": return "MM/dd/yyyy" + } +} + +export function datetimeFormatForSettings(s: Settings): string { + return dateFormatForSettings(s) + " HH:mm:ss" +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/index.tsx b/packages/auditor-backoffice-ui/src/index.tsx index 79c0edce4..fc956e8aa 100644 --- a/packages/auditor-backoffice-ui/src/index.tsx +++ b/packages/auditor-backoffice-ui/src/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,10 +14,6 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -/** - * @author Nic Eigel - */ - import { Application } from "./Application.js"; import { h, render } from "preact"; diff --git a/packages/auditor-backoffice-ui/src/paths/default/Table.tsx b/packages/auditor-backoffice-ui/src/paths/default/Table.tsx new file mode 100644 index 000000000..60e3e28c2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/default/Table.tsx @@ -0,0 +1,155 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { useEntityContext, useEntityDataContext } from "../../context/entity.js"; + +interface Props { + onSuppress: (id: any) => void; +} + +export function CardTable({onSuppress}: Props): any { + + const data = useEntityDataContext(); + const [rowSelection, rowSelectionHandler] = useState<string | undefined>( + undefined, + ); + const { i18n } = useTranslationContext(); + const { title, endpoint, entity } = useEntityContext(); + + 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>{title}</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {!!data.data[0][endpoint] ? ( + <Table + data={data.data[0][endpoint]} + onSuppress={onSuppress} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} + +interface TableProps { + data: any; + onSuppress: (id: any) => void; +} + +function Table({ + data, + onSuppress, + }: TableProps): VNode { + const { i18n } = useTranslationContext(); + const { entity } = useEntityContext(); + type Entity = typeof entity; + let count = 0; + + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + {Object.keys(data[0]).map((i: Entity) => { + const paramName = i[0].toUpperCase() + i.replace("_", " ").slice(1, i.count); + return ( + <Fragment key={count.toString() + i}> + <th> + <i18n.Translate>{paramName}</i18n.Translate> + </th> + </Fragment>); + })} + </tr> + </thead> + <tbody> + {data.map((key: Entity, value: string) => { + return ( + <tr> + {Object.keys(data[0]).map((i: Entity) => { + return ( + <Fragment> + <td> + {(key[i] == false) ? "false" : key[i]} + </td> + </Fragment> + ); + })} + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <span + class="has-tooltip-bottom" + data-tooltip={i18n.str`suppress`} + > + <button + class="button is-small is-success " + type="button" + onClick={(): void => onSuppress(key["row_id"])} + > + {<i18n.Translate>Suppress</i18n.Translate>} + </button> + </span> + </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-happy mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There are no entries yet + </i18n.Translate> + </p> + </div> + ); +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/default/index.tsx b/packages/auditor-backoffice-ui/src/paths/default/index.tsx new file mode 100644 index 000000000..1b7758190 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/default/index.tsx @@ -0,0 +1,130 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * @author Nic Eigel + * @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 { AuditorBackend, WithId } from "../../declaration.js"; +import { Notification } from "../../utils/types.js"; +import { CardTable } from "./Table.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { EntityDataContextProvider, useEntityContext } from "../../context/entity.js"; +import { getEntityList, useEntityAPI } from "../../hooks/entity.js"; +import { useMemo } from "preact/hooks"; +import { ConfirmModal, DeleteModal } from "../../components/modal/index.js"; +import { route } from "preact-router"; +import { Paths } from "../../InstanceRoutes.js"; + + +interface Props { + onNotFound: () => VNode; + onLoadError: (e: HttpError<AuditorBackend.ErrorDetail>) => VNode; +} + +export default function DefaultList({ + onLoadError, + onNotFound, + }: Props): VNode { + const { endpoint, entity } = useEntityContext(); + const result = getEntityList({ endpoint, entity }); + const { updateEntity } = useEntityAPI(); + const [suppressing, setSuppressing] = + useState<typeof entity & 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 onNotFound(); + return onLoadError(result); + } + + let data = result.data; + const value = useMemo( + () => ({ data }), + [data], + ); + + function onReturn(): void { + route(Paths.detail_view); + } + + return ( + + <section class="section is-main-section"> + <button + class="button is-fullwidth" + onClick={onReturn} + >Back + </button><br /> + + <NotificationCard notification={notif} /> + + <EntityDataContextProvider value={value}> + <CardTable + onSuppress={(e: typeof entity & WithId) => + setSuppressing(e) + } + /> + </EntityDataContextProvider> + + {suppressing && ( + <ConfirmModal + label={`Suppress row`} + description={`Suppress the row`} + danger + active + onCancel={() => setSuppressing(null)} + onConfirm={async (): Promise<void> => { + try { + await updateEntity(suppressing); + setNotif({ + message: i18n.str`Entity row with id: ${suppressing} has been suppressed`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to suppress row`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setSuppressing(null); + }} + > + <p class="warning"> + Suppressing a row <b>cannot be undone</b> in this GUI. + </p> + </ConfirmModal> + )} + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx new file mode 100644 index 000000000..495022ac7 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx @@ -0,0 +1,347 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode, Fragment } from "preact"; +import { route, Route } from "preact-router"; +import { Paths, Redirect } from "../../InstanceRoutes.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; + + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; + + onCreate: () => void; +} + +export function ListPage(): VNode { + const { i18n } = useTranslationContext(); + + function onCreate(): void { + route(Paths.balance_list); + } + + return ( + <Fragment> + + <div class="columns"> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Amount arithmetic inconsistencies + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Bad signature losses + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Closure Lags + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Coin inconsistencies + </button> + </div> + </div> + </div> + </div> + + <div class="columns"> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Denominations key validity + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Denominations without signature + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Denominations pending + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Deposit confirmations + </button> + </div> + </div> + </div> + </div> + + <div class="columns"> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Emergencies + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Emergencies by count + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Fee time inconsistencies + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Misattribution in inconsistencies + </button> + </div> + </div> + </div> + </div> + + <div class="columns"> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Purses not closed + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Purses + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Refreshes hanging + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Reserve balances insufficient + </button> + </div> + </div> + </div> + </div> + + <div class="columns"> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Reserve balances summary wrong + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Reserves in + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Reserves not closed + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Reserves + </button> + </div> + </div> + </div> + </div> + + <div class="columns"> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Row inconsistencies + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Row minor inconsistencies + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Wire out format inconsistencies + </button> + </div> + </div> + </div> + <div class="column"> + <div class="card"> + <div class="card-body"> + <button + class="button is-fullwidth" + onClick={onCreate} + >Wire out inconsistencies + </button> + </div> + </div> + </div> + </div> + + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/details/index.tsx new file mode 100644 index 000000000..0544449a2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/details/index.tsx @@ -0,0 +1,39 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../components/menu/index.js"; +import { Notification } from "../../utils/types.js"; +import { ListPage } from "./ListPage.js"; + +export default function DetailsDashboard(): VNode { + + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + <ListPage /> + </section> + ); +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx new file mode 100644 index 000000000..ea6091074 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx @@ -0,0 +1,195 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode, Fragment } from "preact"; + +export function ListPage(data: any): VNode { + const { i18n } = useTranslationContext(); + + let balances = data.data.data[0][4].data.balances; + let coinBalances = [ + "Total recoup loss", + "Coin refund fee revenue", + "Coin deposit fee revenue", + "Coin melt fee revenue", + "Coin irregular loss", + "Coins reported emergency risk by amount", + "Coins emergencies loss by count", + "Coins emergencies loss", + "Coins total arithmetic delta minus", + "Coins total arithmetic delta plus", + "Total escrowed", + "Total refresh hanging", + ]; + let reserveBalances = [ + "Total balance summary delta minus", + "Total balance reserve not closed", + "Reserves total arithmetic delta minus", + "Reserves total arithmetic delta plus", + "Reserves total bad signature loss", + "Reserves history fee revenue", + "Reserves open fee revenue", + ]; + let i = 0; + + return ( + <Fragment> + <div class="columns"> + <div class="column is-half"> + <div class="columns"> + <div class="column"> + <div class="card"> + <div class="card-content"> + <table class="table is-striped is-fullwidth is-dark"> + <tbody> + <tr> + <th>Finding</th> + <td class="has-text-right"><b>Count</b></td> + <td class="has-text-right"><b>Gain/Loss</b></td> + </tr> + { + data["data"]["data"][0].map((x: any) => { + const key = Object.keys(x.data)[0]; + let value = Object.values(x.data)[0]; + if (key != "balances") { + let gains = 0; + if (value == null) + value = 0; + else + value = Object.keys(value).length; + const paramName = key[0].toUpperCase() + key.split("_").join(" ").split("-").join(" ").slice(1, key.length); + return ( + <tr class="is-link"> + <td>{paramName}</td> + <td class="has-text-right"><p + class={value == 0 ? "text-success" : "text-danger"}>{String(value)}</p></td> + <td class="has-text-right"><p + class={gains == 0 ? "text-success" : "text-danger"}>{String(gains)}</p></td> + </tr> + ); + } + }) + } + </tbody> + </table> + </div> + </div> + <div class="card"> + <div class="card-content"> + <table class="table is-striped is-fullwidth is-dark"> + <tbody> + <tr> + <th>Summary</th> + <td class="has-text-right"><b>Value</b></td> + </tr> + <tr> + <td>Total gain/loss</td> + <td class="has-text-right">0</td> + </tr> + <tr> + <td>Pending gain/loss</td> + <td class="has-text-right">0</td> + </tr> + <tr> + <td>Transaction count</td> + <td class="has-text-right">0</td> + </tr> + <tr> + <td>Transactions pending</td> + <td class="has-text-right">0</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> + <div class="column is-half"> + <div class="card"> + <div class="card-content"> + <p class="has-text-weight-bold">Helper coin</p> + <table class="table is-striped is-fullwidth is-dark"> + <tbody> + <tr> + <th>Balance</th> + <td><b>Value</b></td> + </tr> + { + balances.map((x: any) => { + let key = x.balance_key; + let balanceName = key[0].toUpperCase() + key.split("_").join(" ").split("-").join(" ").slice(1, key.length); + + if(coinBalances.includes(balanceName)) + { + let value = balances[i].balance_value.replace(":", " "); + i=i+1; + return ( + <tr class="is-link"> + <td>{balanceName}</td> + <td><p>{value}</p></td> + </tr> + ); + } else { + return null; + } + }) + } + </tbody> + </table> + <p class="has-text-weight-bold">Helper reserve</p> + <table class="table is-striped is-fullwidth is-dark"> + <tbody> + <tr> + <th>Balance</th> + <td><b>Value</b></td> + </tr> + { + balances.map((x: any) => { + let key = x.balance_key; + let balanceName = key[0].toUpperCase() + key.split("_").join(" ").split("-").join(" ").slice(1, key.length); + + if(reserveBalances.includes(balanceName)) + { + let value = balances[i].balance_value.replace(":", " "); + i = i+1; + return ( + <tr class="is-link"> + <td>{balanceName}</td> + <td><p>{value}</p></td> + </tr> + ); + } else { + return null; + } + }) + } + </tbody> + </table> + </div> + </div> + </div> + </div> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/finance/index.tsx b/packages/auditor-backoffice-ui/src/paths/finance/index.tsx new file mode 100644 index 000000000..cce47f831 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/finance/index.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + 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 { Notification } from "../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { getKeyFiguresData } from "../../hooks/finance.js"; + + +interface Props { + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onSelect: (id: string) => void; + onCreate: () => void; +} + +export default function FinanceDashboard({ + onUnauthorized, + // onLoadError, + onCreate, + onSelect, + onNotFound, + }: Props): VNode { + + const result = getKeyFiguresData(); + + 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(); + else + return onNotFound(); + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + <ListPage data={result} /> + </section> + ); +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/login/index.tsx b/packages/auditor-backoffice-ui/src/paths/login/index.tsx new file mode 100644 index 000000000..5bcbb10c8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/login/index.tsx @@ -0,0 +1,202 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { useCallback, useState } from "preact/hooks"; +import { useBackendContext, useBackendTokenContext } from "../../context/backend.js"; +import { NotificationCard } from "../../components/menu/index.js"; +import { Notification } from "../../utils/types.js"; +import { useBackendToken } from "../../hooks/backend.js"; + +export function LoginPage(): VNode { + const [token, setToken] = useState(""); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const doLogin = useCallback(async function doLoginImpl() { + + const result = useBackendToken(); + if (!result.ok) { + } + if (result.ok) { + const { token } = useBackendTokenContext(); + } else { + setNotif({ + message: "Your password is incorrect", + type: "ERROR", + }); + } + }, [token]); + + return (<div class="columns is-centered" style={{ margin: "auto" }}> + <div class="column is-two-thirds "> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} + > + <p class="modal-card-title">{i18n.str`Token required`}</p> + </header> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + + <p> + <i18n.Translate>Need the access token for the API.</i18n.Translate> + </p> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Access Token</i18n.Translate> + </label> + </div> + <div class="field-body"> + <div class="field"> + <p class="control is-expanded"> + <input + class="input" + type="password" + placeholder={"current access token"} + name="token" + onKeyPress={(e) => + e.keyCode === 13 + ? doLogin() + : null + } + value={token} + onInput={(e): void => setToken(e?.currentTarget.value)} + /> + </p> + </div> + </div> + </div> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "flex-end", + border: "1px solid", + borderTop: 0, + }} + > + <AsyncButton + onClick={doLogin} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </footer> + </div> + </div> + </div>); + + + return (<Fragment> + <NotificationCard notification={notif} /> + <div class="columns is-centered" style={{ margin: "auto" }}> + <div class="column is-two-thirds "> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} + > + <p class="modal-card-title">{i18n.str`Login required`}</p> + </header> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + <i18n.Translate>Please enter your access token.</i18n.Translate> + + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Access Token</i18n.Translate> + </label> + </div> + <div class="field-body"> + + <div class="field"> + <p class="control is-expanded"> + <input + class="input" + type="password" + placeholder={"current access token"} + name="token" + onKeyPress={(e) => + e.keyCode === 13 + ? doLogin() + : null + } + value={token} + onInput={(e): void => setToken(e?.currentTarget.value)} + /> + </p> + </div> + </div> + </div> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + <div /> + <AsyncButton + type="is-info" + onClick={doLogin} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + + </footer> + </div> + </div> + </div> + </Fragment> + + ); +} + +function AsyncButton({ onClick, disabled, type = "", children }: { + type?: string, + disabled?: boolean, + onClick: () => Promise<void>, + children: ComponentChildren +}): VNode { + const [running, setRunning] = useState(false); + return <button class={"button " + type} disabled={disabled || running} onClick={() => { + setRunning(true); + onClick().then(() => { + setRunning(false); + }).catch(() => { + setRunning(false); + }); + }}> + {children} + </button>; +} + + diff --git a/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx new file mode 100644 index 000000000..ff7d0e25b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx @@ -0,0 +1,34 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { Link } from "preact-router"; + +export default function NotFoundPage(): VNode { + return ( + <div> + <p>That page doesn't exist.</p> + <Link href="/"> + <h4>Back to Home</h4> + </Link> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx new file mode 100644 index 000000000..73400ae20 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode, Fragment } from "preact"; + +export function ListPage(data: any): VNode { + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <div class="columns is-fullwidth"> + <div class="column is-fullwidth"> + <div class="card"> + <div class="card-content"> + <table class="table is-striped is-fullwidth"> + <tbody> + <tr> + <th>Finding</th> + <td class="has-text-right"><b>Count</b></td> + <td class="has-text-right"><b>Time difference (s)</b></td> + <td class="has-text-right"><b>Diagnostic</b></td> + </tr> + { + data["data"]["data"][0].map((x: any) => { + const key = Object.keys(x.data)[0]; + let value = Object.values(x.data)[0]; + console.log(value); + if (!!value) + value = 0; + const paramName = key[0].toUpperCase() + key.split("_").join(" ").split("-").join(" ").slice(1, key.length); + return ( + <tr class="is-link"> + <td>{paramName}</td> + <td className="has-text-right"><p + class={value == 0 ? "text-success" : "text-danger"}>{String(value)}</p></td> + <td className="has-text-right">0</td> + <td></td> + </tr> + ); + }) + } + </tbody> + </table> + </div> + </div> + </div> + </div> + </Fragment> + ); +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/operations/index.tsx b/packages/auditor-backoffice-ui/src/paths/operations/index.tsx new file mode 100644 index 000000000..b07134e52 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/operations/index.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + 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 { Notification } from "../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { getOperationData } from "../../hooks/operational.js"; + + +interface Props { + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onSelect: (id: string) => void; + onCreate: () => void; +} + +export default function OperationsDashboard({ + onUnauthorized, + // onLoadError, + onCreate, + onSelect, + onNotFound, + }: Props): VNode { + + const result = getOperationData(); + + 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(); + else + return onNotFound(); + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + <ListPage data={result} /> + </section> + ); +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx new file mode 100644 index 000000000..4f57fd3ef --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode, Fragment } from "preact"; + +export function ListPage(data: any): VNode { + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <div class="columns is-fullwidth"> + <div class="column is-fullwidth"> + <div class="card"> + <div class="card-content"> + <table class="table is-striped is-fullwidth"> + <tbody> + <tr> + <th>Finding</th> + <td class="has-text-right"><b>Count</b></td> + <td class="has-text-right"><b>Expiration dates</b></td> + </tr> + { + data["data"]["data"][0].map((x: any) => { + const key = Object.keys(x.data)[0]; + let value = Object.values(x.data)[0]; + console.log(value); + if (!!value) + value = 0; + const paramName = key[0].toUpperCase() + key.split("_").join(" ").split("-").join(" ").slice(1, key.length); + return ( + <tr class="is-link"> + <td>{paramName}</td> + <td class="has-text-right"><p + class={value == 0 ? "text-success" : "text-danger"}>{String(value)}</p></td> + <td class="has-text-right">0</td> + </tr> + ); + }) + } + </tbody> + </table> + </div> + </div> + </div> + </div> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/security/index.tsx b/packages/auditor-backoffice-ui/src/paths/security/index.tsx new file mode 100644 index 000000000..7a0d80166 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/security/index.tsx @@ -0,0 +1,78 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Nic Eigel + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + 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 { Notification } from "../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { getCriticalData } from "../../hooks/critical.js"; +interface Props { + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onSelect: (id: string) => void; + onCreate: () => void; +} + +export default function SecurityDashboard({ + onUnauthorized, + // onLoadError, + onCreate, + onSelect, + onNotFound, + }: Props): VNode { + + const result = getCriticalData(); + + 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(); + else + return onNotFound(); + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + <ListPage data={result} /> + </section> + ); +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx new file mode 100644 index 000000000..77a56a794 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx @@ -0,0 +1,107 @@ +/* + 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) + * @author Nic Eigel + */ + +import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { FormErrors, FormProvider } from "../../components/forms/FormProvider.js"; +import { LangSelector } from "../../components/menu/LangSelector.js"; +import { Settings, useSettings } from "../../hooks/useSettings.js"; + +function getBrowserLang(): string | undefined { + if (typeof window === "undefined") return undefined; + if (window.navigator.languages) return window.navigator.languages[0]; + if (window.navigator.language) return window.navigator.language; + return undefined; +} + +export function Settings({ onClose }: { onClose?: () => void }): VNode { + const { i18n } = useTranslationContext() + const borwserLang = getBrowserLang() + const { update } = useLang(undefined, {}) + + const [value, updateValue] = useSettings() + const errors: FormErrors<Settings> = { + } + + function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void { + const next = s(value) + const v: Settings = { + advanceOrderMode: next.advanceOrderMode ?? false, + dateFormat: next.dateFormat ?? "ymd" + } + updateValue(v) + } + + return <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div> + + <FormProvider<Settings> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Language</i18n.Translate> + <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}> + <i class="mdi mdi-information" /> + </span> + </label> + </div> + <div class="field field-body has-addons is-flex-grow-3"> + <LangSelector /> + + {borwserLang !== undefined && <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-2" + onClick={(e) => { + update(borwserLang.substring(0, 2)) + }} + > + <i18n.Translate>Set default</i18n.Translate> + </button>} + </div> + </div> + </FormProvider> + </div> + </div> + <div class="column" /> + </div> + </section > + {onClose && + <section class="section is-main-section"> + <button + class="button" + onClick={onClose} + > + <i18n.Translate>Close</i18n.Translate> + </button> + </section> + } + </div > +}
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index cf46a34d5..f1b8526b3 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -42,7 +42,7 @@ import { strings } from "./i18n/strings.js"; export function Application(): VNode { return ( - <BackendContextProvider> + <BackendContextProvider> <TranslationProvider source={strings}> <ApplicationStatusRoutes /> </TranslationProvider> diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts index f8a892d99..8366b3ec4 100644 --- a/packages/web-util/src/utils/request.ts +++ b/packages/web-util/src/utils/request.ts @@ -47,6 +47,10 @@ export async function defaultRequestHandler<T>( `${options.basicAuth.username}:${options.basicAuth.password}`, )}`; } + + //TODO remove + requestHeaders["Access-Control-Allow-Origin"] = "*"; + requestHeaders["Content-Type"] = !options.contentType || options.contentType === "json" ? "application/json" : "text/plain"; |