diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance')
72 files changed, 1893 insertions, 1474 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx index 61f62e631..953e412fe 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -21,16 +21,15 @@ import { HttpStatusCode, - PaytoString, PaytoUri, TalerError, TalerMerchantApi, TranslatedString, assertUnreachable, - parsePaytoUri, + parsePaytoUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -40,6 +39,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; import { CompareAccountsModal, ImportingAccountModal, @@ -47,7 +47,6 @@ import { import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { testRevenueAPI } from "./index.js"; -import { InputToggle } from "../../../../components/form/InputToggle.js"; type Entity = TalerMerchantApi.AccountAddDetails & { verified?: boolean }; @@ -73,7 +72,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { undefined, ); const errors: FormErrors<Entity> = { - payto_uri: !state.payto_uri ? i18n.str`required` : undefined, + payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, credit_facade_credentials: !state.credit_facade_credentials ? undefined @@ -81,12 +80,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { username: state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username - ? i18n.str`required` + ? i18n.str`Required` : undefined, password: state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password - ? i18n.str`required` + ? i18n.str`Required` : undefined, }), credit_facade_url: !state.credit_facade_url @@ -94,11 +93,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : !facadeURL ? i18n.str`Invalid url` : !facadeURL.href.endsWith("/") - ? i18n.str`URL should end with a '/'` + ? i18n.str`URL must end with a '/'` : facadeURL.searchParams.size > 0 - ? i18n.str`URL should not contain params` + ? i18n.str`URL must not contain params` : facadeURL.hash - ? i18n.str`URL should not hash param` + ? i18n.str`URL must not hash param` : undefined, }; @@ -223,7 +222,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <Input<Entity> name="credit_facade_url" label={i18n.str`Endpoint URL`} - help="https://bank.demo.taler.net/accounts/_username_/taler-revenue/" + help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/" expand tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} /> @@ -273,7 +272,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={i18n.str`Compare info from server with account form`} disabled={!state.credit_facade_url} onClick={async () => { - const result = await testAccountInfo(); + await testAccountInfo(); }} > <i18n.Translate>Test</i18n.Translate> @@ -302,7 +301,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx index 6994c579c..ac48e65ea 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -28,8 +28,7 @@ import { TalerError, TalerMerchantApi, TalerRevenueHttpClient, - assertUnreachable, - opFixedSuccess, + opFixedSuccess } from "@gnu-taler/taler-util"; import { BrowserFetchHttpLib, @@ -49,10 +48,9 @@ interface Props { } export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { lib: api } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const [tested, setTested] = useState(false); + // const [tested, setTested] = useState(false); const { i18n } = useTranslationContext(); return ( @@ -61,12 +59,12 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { <CreatePage onBack={onBack} onCreate={async (request: Entity) => { - return api.instance + return lib.instance .addBankAccount(state.token, request) .then((created) => { if (created.type === "fail") { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", description: created.detail.hint, }); @@ -76,9 +74,9 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { }) .catch((error) => { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx deleted file mode 100644 index 7e0b89f39..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { CardTable } from "./Table.js"; - -export interface Props { - devices: TalerMerchantApi.BankAccountEntry[]; - // onLoadMoreBefore?: () => void; - // onLoadMoreAfter?: () => void; - onCreate: () => void; - onDelete: (e: TalerMerchantApi.BankAccountEntry) => void; - onSelect: (e: TalerMerchantApi.BankAccountEntry) => void; -} - -export function ListPage({ - devices, - onCreate, - onDelete, - onSelect, - // onLoadMoreBefore, - // onLoadMoreAfter, -}: Props): VNode { - return ( - <section class="section is-main-section"> - <CardTable - accounts={devices.map((o) => ({ - ...o, - id: String(o.h_wire), - }))} - onCreate={onCreate} - onDelete={onDelete} - onSelect={onSelect} - // onLoadMoreBefore={onLoadMoreBefore} - // hasMoreBefore={!onLoadMoreBefore} - // onLoadMoreAfter={onLoadMoreAfter} - // hasMoreAfter={!onLoadMoreAfter} - /> - </section> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx index 0e813f4d2..8ab782cf4 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -64,7 +64,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new accounts`} + data-tooltip={i18n.str`Add new account`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -126,14 +126,14 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { const bitcoinAccounts = accountsByType["bitcoin"]; const talerbankAccounts = accountsByType["x-taler-bank"]; const ibanAccounts = accountsByType["iban"]; - const unkownAccounts = accountsByType["unknown"]; + const unknownAccounts = accountsByType["unknown"]; return ( <Fragment> {bitcoinAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>Bitcoin type accounts</i18n.Translate> + <i18n.Translate>Wire method: Bitcoin</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -177,10 +177,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> @@ -195,7 +195,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { {talerbankAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>Taler type accounts</i18n.Translate> + <i18n.Translate>Wire method: x-taler-bank</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -230,10 +230,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> @@ -248,7 +248,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { {ibanAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>IBAN type accounts</i18n.Translate> + <i18n.Translate>Wire method: IBAN</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -283,10 +283,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> @@ -298,10 +298,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { </div> )} - {unkownAccounts.length > 0 && ( + {unknownAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>Other type accounts</i18n.Translate> + <i18n.Translate>Other accounts</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -316,7 +316,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { </tr> </thead> <tbody> - {unkownAccounts.map(({ parsed, acc }, idx) => { + {unknownAccounts.map(({ parsed, acc }, idx) => { const ac = parsed as PaytoUriUnknown; return ( <tr key={idx}> @@ -336,10 +336,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx index 9d09473bc..2bb1144f5 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -36,7 +36,7 @@ import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; -import { ListPage } from "./ListPage.js"; +import { CardTable } from "./Table.js"; interface Props { onCreate: () => void; @@ -46,8 +46,7 @@ interface Props { export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { lib: api } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const result = useInstanceBankAccounts(); if (!result) return <Loading />; @@ -80,34 +79,43 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { }} /> )} - <ListPage - devices={result.body.accounts} - // onLoadMoreBefore={ - // result.isFirstPage ? undefined: result.loadFirst - // } - // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} - onCreate={onCreate} - onSelect={(e) => { - onSelect(e.h_wire); - }} - onDelete={(e: TalerMerchantApi.BankAccountEntry) => { - return api.instance - .deleteBankAccount(state.token, e.h_wire) - .then(() => - setNotif({ - message: i18n.str`bank account delete successfully`, - type: "SUCCESS", - }), - ) - .catch((error) => - setNotif({ - message: i18n.str`could not delete the bank account`, - type: "ERROR", - description: error.message, - }), - ); - }} - /> + <section class="section is-main-section"> + <CardTable + accounts={result.body.accounts.map((o) => ({ + ...o, + id: String(o.h_wire), + }))} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.h_wire); + }} + onDelete={async (e: TalerMerchantApi.BankAccountEntry) => { + return lib.instance + .deleteBankAccount(state.token, e.h_wire) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Bank account delete successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not delete the bank account`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) + .catch((error) => + setNotif({ + message: i18n.str`Could not delete the bank account`, + type: "ERROR", + description: error instanceof Error ? error.message : String(error), + }), + ); + }} + /> + </section> </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx index 73fe43026..e6dcfec7b 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -77,7 +77,7 @@ export function UpdatePage({ payto_uri: account.payto_uri, credit_facade_url: account.credit_facade_url, credit_facade_credentials: { - // @ts-ignore + // @ts-expect-error unofficial unedited value type: "unedit", }, }); @@ -95,48 +95,46 @@ export function UpdatePage({ const facadeURL = safeConvertURL(state.credit_facade_url); - const errors: FormErrors<FormType> = { - payto_uri: !state.payto_uri ? i18n.str`required` : undefined, + const errors = undefinedIfEmpty<FormErrors<FormType>>({ + payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, credit_facade_url: !state.credit_facade_url ? undefined : !facadeURL ? i18n.str`Invalid url` : !facadeURL.href.endsWith("/") - ? i18n.str`URL should end with a '/'` + ? i18n.str`URL must end with a '/'` : facadeURL.searchParams.size > 0 - ? i18n.str`URL should not contain params` + ? i18n.str`URL must not contain params` : facadeURL.hash - ? i18n.str`URL should not hash param` + ? i18n.str`URL must not hash param` : undefined, credit_facade_credentials: !state.credit_facade_credentials ? undefined : undefinedIfEmpty({ type: replacingAccountId && - // @ts-ignore + // @ts-expect-error unedit is not in facade creds state.credit_facade_credentials?.type === "unedit" - ? i18n.str`required` + ? i18n.str`Required` : undefined, username: state.credit_facade_credentials?.type !== "basic" ? undefined : !state.credit_facade_credentials.username - ? i18n.str`required` + ? i18n.str`Required` : undefined, password: state.credit_facade_credentials?.type !== "basic" ? undefined : !state.credit_facade_credentials.password - ? i18n.str`required` + ? i18n.str`Required` : undefined, }), - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); @@ -150,7 +148,7 @@ export function UpdatePage({ credit_facade_url == undefined || state.credit_facade_credentials === undefined ? undefined - : // @ts-ignore + : // @ts-expect-error unedit is not in facade creds state.credit_facade_credentials.type === "unedit" ? undefined : state.credit_facade_credentials.type === "basic" @@ -164,14 +162,12 @@ export function UpdatePage({ }; if (replacingAccountId) { - console.log("======== REPLACE"); return onReplace(account, { payto_uri: state.payto_uri!, credit_facade_credentials, credit_facade_url, }); } else { - console.log("======== UPDATE"); return onUpdate({ credit_facade_credentials, credit_facade_url }); } }; @@ -283,7 +279,7 @@ export function UpdatePage({ <Input<Entity> name="credit_facade_url" label={i18n.str`Endpoint URL`} - help="https://bank.demo.taler.net/accounts/_username_/taler-revenue/" + help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/" expand tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} /> @@ -293,9 +289,9 @@ export function UpdatePage({ tooltip={i18n.str`Choose the authentication type for the account info URL`} values={accountAuthType} toStr={(str) => { - if (str === "none") return "Without authentication"; - if (str === "basic") return "With authentication"; - return "Do not change"; + if (str === "none") return i18n.str`Without authentication`; + if (str === "basic") return i18n.str`With authentication`; + return i18n.str`Do not change`; }} /> {state.credit_facade_credentials?.type === "basic" ? ( @@ -334,7 +330,7 @@ export function UpdatePage({ data-tooltip={i18n.str`Compare info from server with account form`} disabled={!state.credit_facade_url} onClick={async () => { - const result = await testAccountInfo(); + await testAccountInfo(); }} > <i18n.Translate>Test</i18n.Translate> @@ -354,7 +350,7 @@ export function UpdatePage({ data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx index 60dad7257..aca8aeb8e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -51,8 +51,7 @@ export default function UpdateValidator({ onConfirm, onBack, }: Props): VNode { - const { lib: api } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const result = useBankAccountDetails(bid); const [notif, setNotif] = useState<Notification | undefined>(undefined); @@ -83,12 +82,12 @@ export default function UpdateValidator({ account={{ ...result.body, id: bid }} onBack={onBack} onUpdate={async (request) => { - return api.instance + return lib.instance .updateBankAccount(state.token, bid, request) .then((updated) => { if (updated.type === "fail") { setNotif({ - message: i18n.str`could not update account`, + message: i18n.str`Could not update account`, type: "ERROR", description: updated.detail.hint, }); @@ -98,52 +97,52 @@ export default function UpdateValidator({ }) .catch((error) => { setNotif({ - message: i18n.str`could not update account`, + message: i18n.str`Could not update account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} onReplace={async (prev, next) => { try { - const created = await api.instance.addBankAccount( + const created = await lib.instance.addBankAccount( state.token, next, ); if (created.type === "fail") { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", description: created.detail.hint, }); return; } - } catch (error: any) { + } catch (error) { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); return; } try { - const deleted = await api.instance.deleteBankAccount( + const deleted = await lib.instance.deleteBankAccount( state.token, prev.h_wire, ); if (deleted.type === "fail") { setNotif({ - message: i18n.str`could not delete account`, + message: i18n.str`Could not delete account`, type: "ERROR", description: deleted.detail.hint, }); return; } - } catch (error: any) { + } catch (error) { setNotif({ - message: i18n.str`could not delete account`, + message: i18n.str`Could not delete account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); return; } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/Create.stories.tsx index 18e762642..36b31ebe8 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/Create.stories.tsx @@ -19,10 +19,10 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { FunctionalComponent, h } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; export default { - title: "Pages/Accounts/List", + title: "Pages/OtpDevices/Create", component: TestedComponent, }; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx new file mode 100644 index 000000000..af8cf1c0b --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx @@ -0,0 +1,105 @@ +/* + 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 { + TalerMerchantApi +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +type Entity = TalerMerchantApi.CategoryCreateRequest; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({name_i18n: {}}); + + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + name: !state.name + ? i18n.str`Required` + : !/[a-zA-Z0-9]*/.test(state.name) + ? i18n.str`Invalid. Only characters and numbers` + : undefined, + }); + + const hasErrors = errors !== undefined; + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onCreate(state as Entity); + }; + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="name" + label={i18n.str`Name`} + tooltip={i18n.str`Category name`} + />{" "} + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : i18n.str`Confirm operation` + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx new file mode 100644 index 000000000..0eb8055da --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx @@ -0,0 +1,77 @@ +/* + 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 { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; + +type Entity = TalerMerchantApi.CategoryCreateRequest; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateCategory({ onConfirm, onBack }: Props): VNode { + const { state, lib } = useSessionContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={async (request: Entity) => { + return lib.instance + .addCategory(state.token, request) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Category added successfully`, + type: "SUCCESS", + }); + onConfirm() + } else { + setNotif({ + message: i18n.str`Could not add category`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) + .catch((error) => { + setNotif({ + message: i18n.str`Could not add category`, + type: "ERROR", + description: + error instanceof Error ? error.message : String(error), + }); + }); + }} + /> + </> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx new file mode 100644 index 000000000..876d035df --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx @@ -0,0 +1,205 @@ +/* + 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 { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; + +type Entity = TalerMerchantApi.CategoryListEntry; + +interface Props { + devices: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + devices, + onCreate, + onDelete, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-label" /> + </span> + <i18n.Translate>Categories</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`Add new devices`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {devices.length > 0 ? ( + <Table + instances={devices} + onDelete={onDelete} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {onLoadMoreBefore && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`Load more devices before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>Load first page</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Name</i18n.Translate> + </th> + <th> + <i18n.Translate>Total products</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.category_id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.category_id} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.name} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.product_count} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`Delete selected category from the database`} + onClick={() => onDelete(i)} + > + <i18n.Translate>Delete</i18n.Translate> + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {onLoadMoreAfter && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`Load more devices after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>Load next page</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-magnify mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no categories yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx new file mode 100644 index 000000000..732ab5201 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx @@ -0,0 +1,113 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceCategories } from "../../../../hooks/category.js"; +import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { CardTable } from "./Table.js"; + +interface Props { + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListCategories({ onCreate, onSelect }: Props): VNode { + // const [position, setPosition] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); + const result = useInstanceCategories(); + + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <section class="section is-main-section"> + <CardTable + devices={result.body.categories} + onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + onCreate={onCreate} + onSelect={(e) => { + onSelect(String(e.category_id)); + }} + onDelete={async (e: TalerMerchantApi.CategoryListEntry) => { + return lib.instance + .deleteCategory(state.token, String(e.category_id)) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Category delete successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not delete the category`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) + .catch((error) => + setNotif({ + message: i18n.str`Could not delete the category`, + type: "ERROR", + description: error instanceof Error ? error.message : String(error), + }), + ); + }} + /> + </section> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/Update.stories.tsx index 707324d40..06ea9d07a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/Update.stories.tsx @@ -19,10 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { FunctionalComponent, h } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; export default { - title: "Pages/Templates/List", + title: "Pages/OtpDevices/Update", component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, }; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx new file mode 100644 index 000000000..a08189192 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx @@ -0,0 +1,162 @@ +/* + 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 { TalerError, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputArray } from "../../../../components/form/InputArray.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; +import { + useInstanceProducts +} from "../../../../hooks/product.js"; + +type Entity = TalerMerchantApi.CategoryProductList & WithId; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + category: Entity; +} +export function UpdatePage({ category, onUpdate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { + state: { token }, + lib, + } = useSessionContext(); + // FIXME: if the product list is big the will bring a lot of info + const inventoryResult = useInstanceProducts(); + + const inventory = + !inventoryResult || + inventoryResult instanceof TalerError || + inventoryResult.type === "fail" + ? [] + : inventoryResult.body; + + const [state, setState] = useState< + Partial<Entity & { product_map: { id: string; description: string }[] }> + >({ + ...category, + product_map: [], + }); + + useEffect(() => { + if (!category || !category?.products) return; + console.log(category.products); + const ps = category.products.map((prod) => { + return lib.instance + .getProductDetails(token, String(prod.product_id)) + .then((res) => { + return res.type === "fail" + ? undefined + : { id: String(prod.product_id), description: res.body.description }; + }); + }); + Promise.all(ps).then((all) => { + const product_map = all.filter(notEmpty); + setState({ ...state, product_map }); + }); + }, []); + + const submitForm = () => { + const pids = state.product_map?.map((p) => { + return { product_id: p.id }; + }); + state.products = pids; + delete state.product_map; + return onUpdate(state as Entity); + }; + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>Id:</i18n.Translate> + + <b>{category.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider object={state} valueHandler={setState}> + <Input<Entity> + name="name" + label={i18n.str`Name`} + tooltip={i18n.str`Name of the category`} + /> + <InputArray + name="product_map" + label={i18n.str`Products`} + getSuggestion={async () => { + return inventory.map((prod) => { + return { + description: prod.description, + id: prod.id, + }; + }); + }} + help={i18n.str`Search by product description or id`} + tooltip={i18n.str`Products that this category will list.`} + unique + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={false} + data-tooltip={i18n.str`Confirm operation`} + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} +function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx new file mode 100644 index 000000000..19352ca3e --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx @@ -0,0 +1,114 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useCategoryDetails } from "../../../../hooks/category.js"; +import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { UpdatePage } from "./UpdatePage.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + cid: string; +} +export default function UpdateCategory({ + cid, + onConfirm, + onBack, +}: Props): VNode { + const result = useCategoryDetails(cid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); + + const { i18n } = useTranslationContext(); + + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + category={{ + ...result.body, + id: cid, + }} + onBack={onBack} + onUpdate={async (newInfo) => { + return lib.instance + .updateCategory(state.token, cid, newInfo) + .then((d) => { + if (d.type === "ok") { + onConfirm(); + } else { + switch (d.case) { + case HttpStatusCode.NotFound: { + setNotif({ + message: i18n.str`Could not update category`, + type: "ERROR", + description: i18n.str`Category id is unknown`, + }); + break; + } + } + } + }) + .catch((error) => { + setNotif({ + message: i18n.str`Could not update category`, + type: "ERROR", + description: + error instanceof Error ? error.message : String(error), + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx deleted file mode 100644 index 3168c7cc4..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { FormProvider } from "../../../components/form/FormProvider.js"; -import { Input } from "../../../components/form/Input.js"; -import { TalerMerchantApi } from "@gnu-taler/taler-util"; - -type Entity = TalerMerchantApi.InstanceReconfigurationMessage; -interface Props { - onUpdate: () => void; - onDelete: () => void; - selected: TalerMerchantApi.QueryInstancesResponse; -} - -function convert( - from: TalerMerchantApi.QueryInstancesResponse, -): Entity { - const defaults = { - default_wire_fee_amortization: 1, - use_stefan: true, - default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour - default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours - }; - return { ...defaults, ...from }; -} - -export function DetailPage({ selected }: Props): VNode { - const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); - - const { i18n } = useTranslationContext(); - - return ( - <div> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title">Here goes the instance description</h1> - </div> - </div> - <div class="level-right" style="display: none;"> - <div class="level-item" /> - </div> - </div> - </div> - </section> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-6"> - <FormProvider<Entity> object={value} valueHandler={valueHandler}> - <Input<Entity> name="name" readonly label={i18n.str`Name`} /> - </FormProvider> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx deleted file mode 100644 index e1a7f87f0..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; -import { Loading } from "../../../components/exception/loading.js"; -import { DeleteModal } from "../../../components/modal/index.js"; -import { useSessionContext } from "../../../context/session.js"; -import { useInstanceDetails } from "../../../hooks/instance.js"; -import { LoginPage } from "../../login/index.js"; -import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; -import { DetailPage } from "./DetailPage.js"; - -interface Props { - onUpdate: () => void; - onDelete: () => void; -} - -export default function Detail({ - onUpdate, - onDelete, -}: Props): VNode { - const { state } = useSessionContext(); - const result = useInstanceDetails(); - const [deleting, setDeleting] = useState<boolean>(false); - - // const { deleteInstance } = useInstanceAPI(); - const { lib } = useSessionContext(); - - if (!result) return <Loading /> - if (result instanceof TalerError) { - return <ErrorLoadingMerchant error={result} /> - } - if (result.type === "fail") { - switch(result.case) { - case HttpStatusCode.Unauthorized: { - return <LoginPage /> - } - case HttpStatusCode.NotFound: { - return <NotFoundPageOrAdminCreate />; - } - default: { - assertUnreachable(result) - } - } - } - - - return ( - <Fragment> - <DetailPage - selected={result.body} - onUpdate={onUpdate} - onDelete={() => setDeleting(true)} - /> - {deleting && ( - <DeleteModal - element={{ name: result.body.name, id: state.instance }} - onCancel={() => setDeleting(false)} - onConfirm={async (): Promise<void> => { - if (state.status !== "loggedIn") { - return - } - try { - await lib.instance.deleteCurrentInstance(state.token); - onDelete(); - } catch (error) { - //FIXME: show message error - } - setDeleting(false); - }} - /> - )} - </Fragment> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx deleted file mode 100644 index 42cb1cb02..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser"; -import { FunctionalComponent, h } from "preact"; -import { DetailPage as TestedComponent } from "./DetailPage.js"; - -export default { - title: "Pages/Instance/Detail", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Internal: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const component = (args: any) => ( - <MerchantApiProviderTesting - value={{ - cancelRequest: () => { }, - changeBackend: () => { }, - config: { - currency: "ARS", - version: "1", - currencies: { - "ASD": { - name: "testkudos", - alt_unit_names: {}, - num_fractional_input_digits: 1, - num_fractional_normal_digits: 1, - num_fractional_trailing_zero_digits: 1, - } - }, - exchanges: [], - name: "taler-merchant" - }, - hints: [], - lib: {} as any, - onActivity: (() => { }) as any, - url: new URL("asdasd"), - }} - > - <Internal {...(props as any)} /> - </MerchantApiProviderTesting> - ); - return { component, props }; -} - -export const Example = createExample(TestedComponent, { - selected: { - name: "name", - auth: { method: "external" }, - address: {}, - user_type: "business", - jurisdiction: {}, - use_stefan: true, - default_pay_delay: { - d_us: 1000 * 1000, //one second - }, - default_wire_transfer_delay: { - d_us: 1000 * 1000, //one second - }, - merchant_pub: "ASDWQEKASJDKSADJ", - }, -}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts index 8f06937df..a21ba918e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts +++ b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts @@ -14,5 +14,4 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -export * as details from "./details/stories.js"; export * as kycList from "./kyc/list/ListPage.stories.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx index ed0e1220f..ff63dc64e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -45,12 +45,7 @@ export default function ListKYC(_p: Props): VNode { return <div /> } case HttpStatusCode.BadGateway: { - const status = result.body; - - if (!status) { - return <div>no kyc required</div>; - } - return <ListPage status={status} />; + return <ListPage status={result.body} />; } case HttpStatusCode.ServiceUnavailable: { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx index 151905b5e..3adf79b04 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -27,9 +27,7 @@ import { TalerMerchantApi, TalerProtocolDuration, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format, isFuture } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -149,15 +147,15 @@ export function CreatePage({ ? undefined : Amounts.parse(value.pricing.order_price); - const errors: FormErrors<Entity> = { + const errors = undefinedIfEmpty<FormErrors<Entity>>({ pricing: undefinedIfEmpty({ - summary: !value.pricing?.summary ? i18n.str`required` : undefined, + summary: !value.pricing?.summary ? i18n.str`Required` : undefined, order_price: !value.pricing?.order_price - ? i18n.str`required` + ? i18n.str`Required` : !parsedPrice - ? i18n.str`not valid` + ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` + ? i18n.str`Must be greater than 0` : undefined, }), payments: undefinedIfEmpty({ @@ -168,58 +166,59 @@ export function CreatePage({ value.payments.refund_deadline, value.payments.pay_deadline, ) === -1 - ? i18n.str`refund deadline cannot be before pay deadline` + ? i18n.str`Refund deadline can't be before pay deadline` : value.payments.wire_transfer_deadline && Duration.cmp( value.payments.wire_transfer_deadline, value.payments.refund_deadline, ) === -1 - ? i18n.str`wire transfer deadline cannot be before refund deadline` + ? i18n.str`Wire transfer deadline can't be before refund deadline` : undefined, pay_deadline: !value.payments?.pay_deadline - ? i18n.str`required` + ? i18n.str`Required` : value.payments.wire_transfer_deadline && Duration.cmp( value.payments.wire_transfer_deadline, value.payments.pay_deadline, ) === -1 - ? i18n.str`wire transfer deadline cannot be before pay deadline` + ? i18n.str`Wire transfer deadline can't be before pay deadline` : undefined, wire_transfer_deadline: !value.payments?.wire_transfer_deadline - ? i18n.str`required` + ? i18n.str`Required` : undefined, auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined : !value.payments?.refund_deadline - ? i18n.str`should have a refund deadline` + ? i18n.str`Must have a refund deadline` : Duration.cmp( value.payments.refund_deadline, value.payments.auto_refund_deadline, ) == -1 - ? i18n.str`auto refund cannot be after refund deadline` + ? i18n.str`Auto refund can't be after refund deadline` : undefined, }), shipping: undefinedIfEmpty({ delivery_date: !value.shipping?.delivery_date ? undefined : !isFuture(value.shipping.delivery_date) - ? i18n.str`should be in the future` + ? i18n.str`Must be in the future` : undefined, }), - }; - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + }); + const hasErrors = errors !== undefined; const submit = (): void => { - const order = value as any; //schema.cast(value); + const order = value; + const price = order.pricing?.order_price as AmountString | undefined; + const summary = order.pricing?.summary; if (!value.payments) return; if (!value.shipping) return; + if (!price || ! summary) return; const request: TalerMerchantApi.PostOrderRequest = { order: { - amount: order.pricing.order_price, - summary: order.pricing.summary, + amount: price, + summary: summary, products: productList, extra: undefinedIfEmpty(value.extra), pay_deadline: AbsoluteTime.toProtocolTimestamp( @@ -340,7 +339,9 @@ export function CreatePage({ const minAgeByProducts = inventoryList.reduce( (cur, prev) => - !prev.product.minimum_age || cur > prev.product.minimum_age ? cur : prev.product.minimum_age, + !prev.product.minimum_age || cur > prev.product.minimum_age + ? cur + : prev.product.minimum_age, 0, ); @@ -396,8 +397,10 @@ export function CreatePage({ alternative={ allProducts.length > 0 && ( <p> - {allProducts.length} products with a total price of{" "} - {totalAsString}. + <i18n.Translate> + {allProducts.length} products with a total price of{" "} + {totalAsString}. + </i18n.Translate> </p> ) } @@ -451,7 +454,7 @@ export function CreatePage({ name="pricing.products_price" label={i18n.str`Total price`} readonly - tooltip={i18n.str`total product price added up`} + tooltip={i18n.str`Total product price added up`} /> <InputCurrency name="pricing.order_price" @@ -471,7 +474,7 @@ export function CreatePage({ <InputCurrency name="pricing.order_price" label={i18n.str`Order price`} - tooltip={i18n.str`final order price`} + tooltip={i18n.str`Final order price`} /> )} @@ -497,7 +500,7 @@ export function CreatePage({ <InputGroup name="shipping.delivery_location" label={i18n.str`Location`} - tooltip={i18n.str`address where the products will be delivered`} + tooltip={i18n.str`Address where the products will be delivered`} > <InputLocation name="shipping.delivery_location" /> </InputGroup> @@ -542,7 +545,7 @@ export function CreatePage({ valueHandler(c); }} > - <i18n.Translate>default</i18n.Translate> + <i18n.Translate>Default</i18n.Translate> </button> </span> } @@ -575,7 +578,7 @@ export function CreatePage({ }); }} > - <i18n.Translate>default</i18n.Translate> + <i18n.Translate>Default</i18n.Translate> </button> </span> } @@ -609,7 +612,7 @@ export function CreatePage({ }); }} > - <i18n.Translate>default</i18n.Translate> + <i18n.Translate>Default</i18n.Translate> </button> </span> } @@ -731,7 +734,7 @@ export function CreatePage({ e.preventDefault(); }} > - add + <i18n.Translate>Add</i18n.Translate> </button> </div> </InputGroup> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx index 04f0b2482..37cb4ffdc 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -37,6 +37,7 @@ import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CreatePage } from "./CreatePage.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; export type Entity = { request: TalerMerchantApi.PostOrderRequest; @@ -47,11 +48,12 @@ interface Props { onConfirm: (id: string) => void; } export default function OrderCreate({ onConfirm, onBack }: Props): VNode { - const { lib } = useSessionContext(); + const { state, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { state } = useSessionContext(); const detailsResult = useInstanceDetails(); + // FIXME: if the product list is big the will bring a lot of info const inventoryResult = useInstanceProducts(); + const { i18n } = useTranslationContext(); if (!detailsResult) return <Loading />; if (detailsResult instanceof TalerError) { @@ -102,16 +104,21 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { return onConfirm(r.body.order_id); } else { setNotif({ - message: "could not create order", + message: i18n.str`Could not create order`, type: "ERROR", + description: + r.case === HttpStatusCode.Gone + ? i18n.str`No more stock for product with id "${r.body.product_id}".` + : r.detail.hint, }); } }) .catch((error) => { setNotif({ - message: "could not create order", + message: i18n.str`Could not create order`, type: "ERROR", - description: error.message, + description: + error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx index 498ea83e3..2ffb4203d 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -75,13 +75,13 @@ function ContractTerms({ value }: { value: CT }) { readonly name="summary" label={i18n.str`Summary`} - tooltip={i18n.str`human-readable description of the whole purchase`} + tooltip={i18n.str`Human-readable description of the whole purchase`} /> <InputCurrency<CT> readonly name="amount" label={i18n.str`Amount`} - tooltip={i18n.str`total price for the transaction`} + tooltip={i18n.str`Total price for the transaction`} /> {value.fulfillment_url && ( <Input<CT> @@ -95,43 +95,43 @@ function ContractTerms({ value }: { value: CT }) { readonly name="max_fee" label={i18n.str`Max fee`} - tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`} + tooltip={i18n.str`Maximum total deposit fee accepted by the merchant for this contract`} /> <InputDate<CT> readonly name="timestamp" label={i18n.str`Created at`} - tooltip={i18n.str`time when this contract was generated`} + tooltip={i18n.str`Time when this contract was generated`} /> <InputDate<CT> readonly name="refund_deadline" label={i18n.str`Refund deadline`} - tooltip={i18n.str`after this deadline has passed no refunds will be accepted`} + tooltip={i18n.str`After this deadline has passed no refunds will be accepted`} /> <InputDate<CT> readonly name="pay_deadline" label={i18n.str`Payment deadline`} - tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`} + tooltip={i18n.str`After this deadline, the merchant won't accept payments for the contract`} /> <InputDate<CT> readonly name="wire_transfer_deadline" label={i18n.str`Wire transfer deadline`} - tooltip={i18n.str`transfer deadline for the exchange`} + tooltip={i18n.str`Transfer deadline for the exchange`} /> <InputDate<CT> readonly name="delivery_date" label={i18n.str`Delivery date`} - tooltip={i18n.str`time indicating when the order should be delivered`} + tooltip={i18n.str`Time indicating when the order should be delivered`} /> {value.delivery_date && ( <InputGroup name="delivery_location" label={i18n.str`Location`} - tooltip={i18n.str`where the order will be delivered`} + tooltip={i18n.str`Where the order will be delivered`} > <InputLocation name="payments.delivery_location" /> </InputGroup> @@ -140,13 +140,13 @@ function ContractTerms({ value }: { value: CT }) { readonly name="auto_refund" label={i18n.str`Auto-refund delay`} - tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`} + tooltip={i18n.str`How long the wallet should try to get an automatic refund for the purchase`} /> <Input<CT> readonly name="extra" label={i18n.str`Extra info`} - tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`} + tooltip={i18n.str`Extra data that is only interpreted by the merchant frontend`} /> </FormProvider> </InputGroup> @@ -221,7 +221,7 @@ function ClaimedPage({ <div class="level-item"> <i18n.Translate>Order</i18n.Translate> #{id} <div class="tag is-info ml-4"> - <i18n.Translate>claimed</i18n.Translate> + <i18n.Translate>Claimed</i18n.Translate> </div> </div> </div> @@ -248,7 +248,7 @@ function ClaimedPage({ > <p> <b> - <i18n.Translate>claimed at</i18n.Translate>: + <i18n.Translate>Claimed at</i18n.Translate>: </b>{" "} {order.contract_terms.timestamp.t_s === "never" ? "never" @@ -458,16 +458,16 @@ function PaidPage({ <div class="level-item"> <i18n.Translate>Order</i18n.Translate> #{id} <div class="tag is-success ml-4"> - <i18n.Translate>paid</i18n.Translate> + <i18n.Translate>Paid</i18n.Translate> </div> {order.wired ? ( <div class="tag is-success ml-4"> - <i18n.Translate>wired</i18n.Translate> + <i18n.Translate>Wired</i18n.Translate> </div> ) : null} {order.refunded ? ( <div class="tag is-danger ml-4"> - <i18n.Translate>refunded</i18n.Translate> + <i18n.Translate>Refunded</i18n.Translate> </div> ) : null} </div> @@ -487,8 +487,8 @@ function PaidPage({ class="has-tooltip-left" data-tooltip={ refundable - ? i18n.str`refund order` - : i18n.str`not refundable` + ? i18n.str`Refund order` + : i18n.str`Not refundable` } > <button @@ -496,7 +496,7 @@ function PaidPage({ disabled={!refundable} onClick={() => onRefund(id)} > - <i18n.Translate>refund</i18n.Translate> + <i18n.Translate>Refund</i18n.Translate> </button> </span> </div> @@ -638,7 +638,7 @@ function UnpaidPage({ </h1> </div> <div class="tag is-dark"> - <i18n.Translate>unpaid</i18n.Translate> + <i18n.Translate>Unpaid</i18n.Translate> </div> </div> </div> @@ -656,7 +656,7 @@ function UnpaidPage({ > <p> <b> - <i18n.Translate>pay at</i18n.Translate>: + <i18n.Translate>Pay at</i18n.Translate>: </b>{" "} <a href={order.order_status_url} @@ -668,7 +668,7 @@ function UnpaidPage({ </p> <p> <b> - <i18n.Translate>created at</i18n.Translate>: + <i18n.Translate>Created at</i18n.Translate>: </b>{" "} {order.creation_time.t_s === "never" ? "never" @@ -693,13 +693,13 @@ function UnpaidPage({ readonly name="summary" label={i18n.str`Summary`} - tooltip={i18n.str`human-readable description of the whole purchase`} + tooltip={i18n.str`Human-readable description of the whole purchase`} /> <InputCurrency<Unpaid> readonly name="total_amount" label={i18n.str`Amount`} - tooltip={i18n.str`total price for the transaction`} + tooltip={i18n.str`Total price for the transaction`} /> <Input<Unpaid> name="order_status" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx index b28e59b29..7d5acd07f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -18,9 +18,7 @@ import { TalerError, assertUnreachable, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -41,8 +39,7 @@ export interface Props { export default function Update({ oid, onBack }: Props): VNode { const result = useOrderDetails(oid); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { lib: api } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -64,7 +61,7 @@ export default function Update({ oid, onBack }: Props): VNode { ); } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { assertUnreachable(result); @@ -83,19 +80,27 @@ export default function Update({ oid, onBack }: Props): VNode { if (state.status !== "loggedIn") { return; } - api.instance + lib.instance .addRefund(state.token, id, value) - .then(() => - setNotif({ - message: i18n.str`refund created successfully`, - type: "SUCCESS", - }), - ) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Refund created successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not create the refund`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => setNotif({ - message: i18n.str`could not create the refund`, + message: i18n.str`Could not create the refund`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx index 4681f9943..fd1c9fa30 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -80,7 +80,7 @@ export function ListPage({ isWiredActive, }: ListPageProps): VNode { const { i18n } = useTranslationContext(); - const dateTooltip = i18n.str`select date to show nearby orders`; + const dateTooltip = i18n.str`Select date to show nearby orders`; const [pickDate, setPickDate] = useState(false); const [settings] = usePreference(); @@ -93,7 +93,7 @@ export function ListPage({ <li class={isNotPaidActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show paid orders`} + data-tooltip={i18n.str`Only show paid orders`} > <a onClick={onShowNotPaid}> <i18n.Translate>New</i18n.Translate> @@ -103,7 +103,7 @@ export function ListPage({ <li class={isPaidActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show paid orders`} + data-tooltip={i18n.str`Only show paid orders`} > <a onClick={onShowPaid}> <i18n.Translate>Paid</i18n.Translate> @@ -113,7 +113,7 @@ export function ListPage({ <li class={isRefundedActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show orders with refunds`} + data-tooltip={i18n.str`Only show orders with refunds`} > <a onClick={onShowRefunded}> <i18n.Translate>Refunded</i18n.Translate> @@ -123,7 +123,7 @@ export function ListPage({ <li class={isNotWiredActive}> <div class="has-tooltip-left" - data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} + data-tooltip={i18n.str`Only show orders where customers paid, but wire payments from payment provider are still pending`} > <a onClick={onShowNotWired}> <i18n.Translate>Not wired</i18n.Translate> @@ -133,7 +133,7 @@ export function ListPage({ <li class={isWiredActive}> <div class="has-tooltip-left" - data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} + data-tooltip={i18n.str`Only show orders where customers paid, but wire payments from payment provider are still pending`} > <a onClick={onShowWired}> <i18n.Translate>Completed</i18n.Translate> @@ -143,7 +143,7 @@ export function ListPage({ <li class={isAllActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`remove all filters`} + data-tooltip={i18n.str`Remove all filters`} > <a onClick={onShowAll}> <i18n.Translate>All</i18n.Translate> @@ -161,7 +161,7 @@ export function ListPage({ <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}> <span class="icon" - data-tooltip={i18n.str`clear date filter`} + data-tooltip={i18n.str`Clear date filter`} > <i class="mdi mdi-close" /> </span> @@ -175,7 +175,7 @@ export function ListPage({ type="text" readonly value={!jumpToDate || jumpToDate.t_ms === "never" ? "" : format(jumpToDate.t_ms, dateFormatForSettings(settings))} - placeholder={i18n.str`date (${dateFormatForSettings(settings)})`} + placeholder={i18n.str`Jump to date (${dateFormatForSettings(settings)})`} onClick={() => { setPickDate(true); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx index 7b9691fdd..74bfe7939 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -80,7 +80,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options" /> <div class="card-header-icon" aria-label="more options"> - <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}> + <span class="has-tooltip-left" data-tooltip={i18n.str`Create order`}> <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> <i class="mdi mdi-plus mdi-36px" /> @@ -137,7 +137,7 @@ function Table({ <div class="table-container"> {onLoadMoreBefore && ( <button class="button is-fullwidth" onClick={onLoadMoreBefore}> - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-striped is-hoverable is-fullwidth"> @@ -211,9 +211,9 @@ function Table({ </table> {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more orders after the last one`} + data-tooltip={i18n.str`Load more orders after the last one`} onClick={onLoadMoreAfter}> - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> @@ -277,20 +277,20 @@ export function RefundModal({ : orderPrice; const isRefundable = Amounts.isNonZero(totalRefundable); - const duplicatedText = i18n.str`duplicated`; + const duplicatedText = i18n.str`Duplicated`; const errors: FormErrors<State> = { - mainReason: !form.mainReason ? i18n.str`required` : undefined, + mainReason: !form.mainReason ? i18n.str`Required` : undefined, description: !form.description && form.mainReason !== duplicatedText - ? i18n.str`required` + ? i18n.str`Required` : undefined, refund: !form.refund - ? i18n.str`required` + ? i18n.str`Required` : !Amounts.parse(form.refund) - ? i18n.str`invalid format` + ? i18n.str`Invalid` : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1 - ? i18n.str`this value exceed the refundable amount` + ? i18n.str`This value exceed the refundable amount` : undefined, }; const hasErrors = Object.keys(errors).some( @@ -335,13 +335,13 @@ export function RefundModal({ <thead> <tr> <th> - <i18n.Translate>date</i18n.Translate> + <i18n.Translate>Date</i18n.Translate> </th> <th> - <i18n.Translate>amount</i18n.Translate> + <i18n.Translate>Amount</i18n.Translate> </th> <th> - <i18n.Translate>reason</i18n.Translate> + <i18n.Translate>Reason</i18n.Translate> </th> </tr> </thead> @@ -378,7 +378,7 @@ export function RefundModal({ <InputCurrency<State> name="refund" label={i18n.str`Refund`} - tooltip={i18n.str`amount to be refunded`} + tooltip={i18n.str`Amount to be refunded`} > <i18n.Translate>Max refundable:</i18n.Translate>{" "} {Amounts.stringify(totalRefundable)} @@ -389,16 +389,16 @@ export function RefundModal({ values={[ i18n.str`Choose one...`, duplicatedText, - i18n.str`requested by the customer`, - i18n.str`other`, + i18n.str`Requested by the customer`, + i18n.str`Other`, ]} - tooltip={i18n.str`why this order is being refunded`} + tooltip={i18n.str`Why this order is being refunded`} /> {form.mainReason && form.mainReason !== duplicatedText ? ( <Input<State> label={i18n.str`Description`} name="description" - tooltip={i18n.str`more information to give context`} + tooltip={i18n.str`More information to give context`} /> ) : undefined} </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx index 8a1f85b1c..3d1892b47 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -26,9 +26,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -64,27 +62,26 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { const result = useInstanceOrders(filter, (d) => setFilter({ ...filter, position: d }), ); - const { lib } = useSessionContext(); + const { state, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const { state } = useSessionContext(); if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; } if (result.type === "fail") { - switch(result.case) { + switch (result.case) { case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { - assertUnreachable(result) + assertUnreachable(result); } } } @@ -113,8 +110,8 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { return resp.type === "ok"; }} onSelect={onSelect} - description={i18n.str`jump to order with the given product ID`} - placeholder={i18n.str`order id`} + description={i18n.str`Jump to order with the given product ID`} + placeholder={i18n.str`Order id`} /> <ListPage @@ -160,17 +157,25 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { onConfirm={(value) => { lib.instance .addRefund(state.token, orderToBeRefunded.order_id, value) - .then(() => - setNotif({ - message: i18n.str`refund created successfully`, - type: "SUCCESS", - }), - ) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Refund created successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not create the refund`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => setNotif({ - message: i18n.str`could not create the refund`, + message: i18n.str`Could not create the refund`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ) .then(() => setOrderToBeRefunded(undefined)); @@ -208,7 +213,7 @@ function RefundModalForTable({ id, onConfirm, onCancel }: RefundProps): VNode { ); } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx index a16817bab..bd024fb38 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx @@ -35,6 +35,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.OtpDeviceAddDetails; @@ -53,34 +54,32 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const [showKey, setShowKey] = useState(false); - const errors: FormErrors<Entity> = { + const errors = undefinedIfEmpty<FormErrors<Entity>>({ otp_device_id: !state.otp_device_id - ? i18n.str`required` + ? i18n.str`Required` : !/[a-zA-Z0-9]*/.test(state.otp_device_id) - ? i18n.str`no valid. only characters and numbers` + ? i18n.str`Invalid. Only characters and numbers` : undefined, - otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined, + otp_algorithm: !state.otp_algorithm ? i18n.str`Required` : undefined, otp_key: !state.otp_key - ? i18n.str`required` + ? i18n.str`Required` : !isRfc3548Base32Charset(state.otp_key) - ? i18n.str`just letters and numbers from 2 to 7` + ? i18n.str`Just letters and numbers from 2 to 7` : state.otp_key.length !== 32 - ? i18n.str`size of the key should be 32` + ? i18n.str`Size of the key must be 32` : undefined, otp_device_description: !state.otp_device_description - ? i18n.str`required` + ? i18n.str`Required` : !/[a-zA-Z0-9]*/.test(state.otp_device_description) - ? i18n.str`no valid. only characters and numbers` + ? i18n.str`Invalid. Only characters and numbers` : undefined, - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); - return onCreate(state as any); + return onCreate(state as TalerMerchantApi.OtpDeviceAddDetails); }; return ( @@ -112,14 +111,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { toStr={(v) => algorithmsNames[v]} fromStr={(v) => Number(v)} /> - {state.otp_algorithm ? ( + {state.otp_algorithm ? ( <Fragment> <InputWithAddon<Entity> expand name="otp_key" label={i18n.str`Device key`} inputType={showKey ? "text" : "password"} - help="Be sure to be very hard to guess or use the random generator" + help={i18n.str`Be sure to be very hard to guess or use the random generator`} tooltip={i18n.str`Your device need to have exactly the same value`} fromStr={(v) => v.toUpperCase()} addonAfterAction={() => { @@ -136,7 +135,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { } side={ <button - data-tooltip={i18n.str`generate random secret key`} + data-tooltip={i18n.str`Generate random secret key`} class="button is-info mr-3" onClick={(e) => { setState((s) => ({ @@ -146,7 +145,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { e.preventDefault(); }} > - <i18n.Translate>random</i18n.Translate> + <i18n.Translate>Random</i18n.Translate> </button> } /> @@ -165,7 +164,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx index 8ab0e1f26..adb417a09 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx @@ -36,14 +36,14 @@ interface Props { } export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { lib: api } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const [created, setCreated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null) + const [created, setCreated] = + useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null); if (created) { - return <CreatedSuccessfully entity={created} onConfirm={onConfirm} /> + return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />; } return ( @@ -51,16 +51,29 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: Entity) => { - return api.instance.addOtpDevice(state.token, request) - .then((d) => { - setCreated(request) + onCreate={async (request: Entity) => { + return lib.instance + .addOtpDevice(state.token, request) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Device added successfully`, + type: "SUCCESS", + }); + setCreated(request); + } else { + setNotif({ + message: i18n.str`Could not add device`, + type: "ERROR", + description: resp.detail.hint, + }); + } }) .catch((error) => { setNotif({ - message: i18n.str`could not create device`, + message: i18n.str`Could not add device`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx deleted file mode 100644 index 49032c80e..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { FunctionalComponent, h } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; - -export default { - title: "Pages/OtpDevices/List", - component: TestedComponent, -}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx deleted file mode 100644 index 8ca0a9c58..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { CardTable } from "./Table.js"; - -export interface Props { - devices: TalerMerchantApi.OtpDeviceEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; - onCreate: () => void; - onDelete: (e: TalerMerchantApi.OtpDeviceEntry) => void; - onSelect: (e: TalerMerchantApi.OtpDeviceEntry) => void; -} - -export function ListPage({ - devices, - onCreate, - onDelete, - onSelect, - onLoadMoreBefore, - onLoadMoreAfter, -}: Props): VNode { - - return ( - <section class="section is-main-section"> - <CardTable - devices={devices.map((o) => ({ - ...o, - id: String(o.otp_device_id), - }))} - onCreate={onCreate} - onDelete={onDelete} - onSelect={onSelect} - onLoadMoreBefore={onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - /> - </section> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx index e4206ff7d..cdba229f6 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx @@ -59,7 +59,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new devices`} + data-tooltip={i18n.str`Add new devices`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -114,10 +114,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more devices before the first one`} + data-tooltip={i18n.str`Load more devices before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -152,10 +152,10 @@ function Table({ <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected devices from the database`} + data-tooltip={i18n.str`Delete selected devices from the database`} onClick={() => onDelete(i)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> @@ -167,10 +167,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more devices after the last one`} + data-tooltip={i18n.str`Load more devices after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx index b6a077863..d6f2bfe4d 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx @@ -23,11 +23,9 @@ import { HttpStatusCode, TalerError, TalerMerchantApi, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -38,7 +36,7 @@ import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; -import { ListPage } from "./ListPage.js"; +import { CardTable } from "./Table.js"; interface Props { onCreate: () => void; @@ -49,8 +47,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { // const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const result = useInstanceOtpDevices(); if (!result) return <Loading />; @@ -63,7 +60,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { assertUnreachable(result); @@ -75,32 +72,42 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { <Fragment> <NotificationCard notification={notif} /> - <ListPage - devices={result.body.otp_devices} - onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} - onCreate={onCreate} - onSelect={(e) => { - onSelect(e.otp_device_id); - }} - onDelete={(e: TalerMerchantApi.OtpDeviceEntry) => { - return lib.instance - .deleteOtpDevice(state.token, e.otp_device_id) - .then(() => - setNotif({ - message: i18n.str`validator delete successfully`, - type: "SUCCESS", - }), - ) - .catch((error) => - setNotif({ - message: i18n.str`could not delete the validator`, - type: "ERROR", - description: error.message, - }), - ); - }} - /> + <section class="section is-main-section"> + <CardTable + devices={result.body.otp_devices} + onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.otp_device_id); + }} + onDelete={async (e: TalerMerchantApi.OtpDeviceEntry) => { + return lib.instance + .deleteOtpDevice(state.token, e.otp_device_id) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Device delete successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not delete the device`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) + .catch((error) => + setNotif({ + message: i18n.str`Could not delete the device`, + type: "ERROR", + description: error instanceof Error ? error.message : String(error), + }), + ); + }} + /> + </section> </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx index 7866b9cd9..68fbf371a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx @@ -19,15 +19,15 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { randomRfc3548Base32Key, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + randomRfc3548Base32Key, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; @@ -48,15 +48,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>(device); const [showKey, setShowKey] = useState(false); - const errors: FormErrors<Entity> = {}; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onUpdate(state as any); + return onUpdate(state as Entity); }; return ( @@ -68,7 +61,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - Device: <b>{device.id}</b> + <i18n.Translate>Device:</i18n.Translate> + <b>{device.id}</b> </span> </div> </div> @@ -80,11 +74,7 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { <section class="section is-main-section"> <div class="columns"> <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > + <FormProvider object={state} valueHandler={setState}> <Input<Entity> name="otp_device_description" label={i18n.str`Description`} @@ -107,8 +97,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { inputType={showKey ? "text" : "password"} help={ state.otp_key === undefined - ? "Not modified" - : "Be sure to be very hard to guess or use the random generator" + ? i18n.str`Not modified` + : i18n.str`Be sure to be very hard to guess or use the random generator` } tooltip={i18n.str`Your device need to have exactly the same value`} fromStr={(v) => v.toUpperCase()} @@ -132,25 +122,25 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { side={ state.otp_key === undefined ? ( <button - onClick={(e) => { + onClick={() => { setState((s) => ({ ...s, otp_key: "" })); }} class="button" > - change key + <i18n.Translate>Change key</i18n.Translate> </button> ) : ( <button - data-tooltip={i18n.str`generate random secret key`} + data-tooltip={i18n.str`Generate random secret key`} class="button is-info mr-3" - onClick={(e) => { + onClick={() => { setState((s) => ({ ...s, otp_key: randomRfc3548Base32Key(), })); }} > - <i18n.Translate>random</i18n.Translate> + <i18n.Translate>Random</i18n.Translate> </button> ) } @@ -166,12 +156,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { </button> )} <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } + disabled={false} + data-tooltip={i18n.str`Confirm operation`} onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx index 2fe3abaae..8f9997cfb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx @@ -58,8 +58,7 @@ export default function UpdateValidator({ const [notif, setNotif] = useState<Notification | undefined>(undefined); const [keyUpdated, setKeyUpdated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null); - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -136,9 +135,9 @@ export default function UpdateValidator({ }) .catch((error) => { setNotif({ - message: i18n.str`could not update template`, + message: i18n.str`Could not update template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx deleted file mode 100644 index 2b6ebed45..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { h, VNode } from "preact"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { Entity } from "./index.js"; -import emptyImage from "../../assets/empty.png"; - -interface Props { - entity: Entity; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -export function CreatedSuccessfully({ - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - return ( - <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Image</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <img src={entity.image} style={{ width: 200, height: 200 }} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Description</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <textarea class="input" readonly value={entity.description} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Price</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={entity.price} /> - </p> - </div> - </div> - </div> - </Template> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx index 9de5cae78..5352fd792 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx @@ -34,8 +34,7 @@ interface Props { onConfirm: () => void; } export default function CreateProduct({ onConfirm, onBack }: Props): VNode { - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -45,13 +44,28 @@ export default function CreateProduct({ onConfirm, onBack }: Props): VNode { <CreatePage onBack={onBack} onCreate={(request: TalerMerchantApi.ProductAddDetail) => { - return lib.instance.addProduct(state.token, request) - .then(() => onConfirm()) + return lib.instance + .addProduct(state.token, request) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Product created successfully`, + type: "SUCCESS", + }); + onConfirm(); + } else { + setNotif({ + message: i18n.str`Could not create product`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not create product`, + message: i18n.str`Could not create product`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx index 580a92cdc..ca0b62704 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx @@ -43,20 +43,20 @@ function createExample<Props>( return r; } -export const Example = createExample(TestedComponent, { - instances: [ - { - id: "orderid", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10" as AmountString, - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: 15, - unit: "bar", - address: {}, - }, - ], -}); +// export const Example = createExample(TestedComponent, { +// instances: [ +// { +// id: "orderid", +// description: "description1", +// description_i18n: {} as any, +// image: "", +// price: "TESTKUDOS:10" as AmountString, +// taxes: [], +// total_lost: 10, +// total_sold: 5, +// total_stock: 15, +// unit: "bar", +// address: {}, +// }, +// ], +// }); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx index 08d42a8c9..a4e3663ef 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -75,7 +75,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add product to inventory`} + data-tooltip={i18n.str`Add product to inventory`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -138,7 +138,7 @@ function Table({ <div class="table-container"> {onLoadMoreBefore && ( <button class="button is-fullwidth" onClick={onLoadMoreBefore}> - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -227,7 +227,7 @@ function Table({ } style={{ cursor: "pointer" }} > - {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`} + {isFree ? i18n.str`Free` : `${i.price} / ${i.unit}`} </td> <td onClick={() => @@ -268,7 +268,7 @@ function Table({ <div class="buttons is-right"> <span class="has-tooltip-bottom" - data-tooltip={i18n.str`go to product update page`} + data-tooltip={i18n.str`Go to product update page`} > <button class="button is-small is-success " @@ -280,7 +280,7 @@ function Table({ </span> <span class="has-tooltip-left" - data-tooltip={i18n.str`remove this product from the database`} + data-tooltip={i18n.str`Remove this product from the database`} > <button class="button is-small is-danger" @@ -315,9 +315,9 @@ function Table({ </table> {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more products after the last one`} + data-tooltip={i18n.str`Load more products after the last one`} onClick={onLoadMoreAfter}> - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> @@ -358,7 +358,7 @@ function FastProductWithInfiniteStockUpdateForm({ <InputCurrency<FastProductUpdate> name="price" label={i18n.str`Price`} - tooltip={i18n.str`update the product with new price`} + tooltip={i18n.str`Update the product with new price`} /> </FormProvider> @@ -370,7 +370,7 @@ function FastProductWithInfiniteStockUpdateForm({ </button> <span class="has-tooltip-left" - data-tooltip={i18n.str`update product with new price`} + data-tooltip={i18n.str`Update product with new price`} > <button class="button is-info" @@ -407,7 +407,7 @@ function FastProductWithManagedStockUpdateForm({ const errors: FormErrors<FastProductUpdate> = { lost: currentStock + value.incoming < value.lost - ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming + ? `lost can't be greater that current + incoming (max ${currentStock + value.incoming })` : undefined, }; @@ -428,17 +428,17 @@ function FastProductWithManagedStockUpdateForm({ <InputNumber<FastProductUpdate> name="incoming" label={i18n.str`Incoming`} - tooltip={i18n.str`add more elements to the inventory`} + tooltip={i18n.str`Add more elements to the inventory`} /> <InputNumber<FastProductUpdate> name="lost" label={i18n.str`Lost`} - tooltip={i18n.str`report elements lost in the inventory`} + tooltip={i18n.str`Report elements lost in the inventory`} /> <InputCurrency<FastProductUpdate> name="price" label={i18n.str`Price`} - tooltip={i18n.str`new price for the product`} + tooltip={i18n.str`New price for the product`} /> </FormProvider> @@ -450,8 +450,8 @@ function FastProductWithManagedStockUpdateForm({ class="has-tooltip-left" data-tooltip={ hasErrors - ? i18n.str`the are value with errors` - : i18n.str`update product with new stock and price` + ? i18n.str`The are value with errors` + : i18n.str`Update product with new stock and price` } > <button diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx index dc3e73850..3b0fb7ed6 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -19,10 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -31,9 +34,7 @@ import { JumpToElementById } from "../../../../components/form/JumpToElementById import { NotificationCard } from "../../../../components/menu/index.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; import { useSessionContext } from "../../../../context/session.js"; -import { - useInstanceProducts -} from "../../../../hooks/product.js"; +import { useInstanceProducts } from "../../../../hooks/product.js"; import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; @@ -44,15 +45,12 @@ interface Props { onCreate: () => void; onSelect: (id: string) => void; } -export default function ProductList({ - onCreate, - onSelect, -}: Props): VNode { +export default function ProductList({ onCreate, onSelect }: Props): VNode { const result = useInstanceProducts(); - const { lib } = useSessionContext(); - const { state } = useSessionContext(); - const [deleting, setDeleting] = - useState<TalerMerchantApi.ProductDetail & WithId | null>(null); + const { state, lib } = useSessionContext(); + const [deleting, setDeleting] = useState< + (TalerMerchantApi.ProductDetail & WithId) | null + >(null); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -67,7 +65,7 @@ export default function ProductList({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { assertUnreachable(result); @@ -85,8 +83,8 @@ export default function ProductList({ return resp.type === "ok"; }} onSelect={onSelect} - description={i18n.str`jump to product with the given product ID`} - placeholder={i18n.str`product id`} + description={i18n.str`Jump to product with the given product ID`} + placeholder={i18n.str`Product id`} /> <CardTable @@ -96,19 +94,31 @@ export default function ProductList({ onCreate={onCreate} onUpdate={async (id, prod) => { try { - await lib.instance.updateProduct(state.token, id, prod); - setNotif({ - message: i18n.str`product updated successfully`, - type: "SUCCESS", - }); + const resp = await lib.instance.updateProduct( + state.token, + id, + prod, + ); + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Product updated successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not update the product`, + type: "ERROR", + description: resp.detail.hint, + }); + } } catch (error) { setNotif({ - message: i18n.str`could not update the product`, + message: i18n.str`Could not update the product`, type: "ERROR", description: error instanceof Error ? error.message : undefined, }); } - return + return; }} onSelect={(product) => onSelect(product.id)} onDelete={(prod: TalerMerchantApi.ProductDetail & WithId) => @@ -125,14 +135,25 @@ export default function ProductList({ onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { - await lib.instance.deleteProduct(state.token, deleting.id); - setNotif({ - message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`, - type: "SUCCESS", - }); + const resp = await lib.instance.deleteProduct( + state.token, + deleting.id, + ); + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not delete the product`, + type: "ERROR", + description: resp.detail.hint, + }); + } } catch (error) { setNotif({ - message: i18n.str`Failed to delete product`, + message: i18n.str`Could not delete the product`, type: "ERROR", description: error instanceof Error ? error.message : undefined, }); @@ -141,11 +162,17 @@ export default function ProductList({ }} > <p> - If you delete the product named <b>"{deleting.description}"</b> (ID:{" "} - <b>{deleting.id}</b>), the stock and related information will be lost + <i18n.Translate> + If you delete the product named{" "} + <b>"{deleting.description}"</b> (ID:{" "} + <b>{deleting.id}</b> + ), the stock and related information will be lost + </i18n.Translate> </p> <p class="warning"> - Deleting an product <b>cannot be undone</b>. + <i18n.Translate> + Deleting an product can't be undone. + </i18n.Translate> </p> </ConfirmModal> )} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx index 7aa93b186..889ea1b26 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx @@ -41,34 +41,34 @@ function createExample<Props>( return r; } -export const WithManagedStock = createExample(TestedComponent, { - product: { - product_id: "20102-ASDAS-QWE", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10" as AmountString, - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: 15, - unit: "bar", - address: {}, - }, -}); +// export const WithManagedStock = createExample(TestedComponent, { +// product: { +// product_id: "20102-ASDAS-QWE", +// description: "description1", +// description_i18n: {} as any, +// image: "", +// price: "TESTKUDOS:10" as AmountString, +// taxes: [], +// total_lost: 10, +// total_sold: 5, +// total_stock: 15, +// unit: "bar", +// address: {}, +// }, +// }); -export const WithInfiniteStock = createExample(TestedComponent, { - product: { - product_id: "20102-ASDAS-QWE", - description: "description1", - description_i18n: {} as any, - image: "", - price: "TESTKUDOS:10" as AmountString, - taxes: [], - total_lost: 10, - total_sold: 5, - total_stock: -1, - unit: "bar", - address: {}, - }, -}); +// export const WithInfiniteStock = createExample(TestedComponent, { +// product: { +// product_id: "20102-ASDAS-QWE", +// description: "description1", +// description_i18n: {} as any, +// image: "", +// price: "TESTKUDOS:10" as AmountString, +// taxes: [], +// total_lost: 10, +// total_sold: 5, +// total_stock: -1, +// unit: "bar", +// address: {}, +// }, +// }); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx index 5e3e58d80..33db7021f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -48,9 +48,8 @@ export default function UpdateProduct({ }: Props): VNode { const result = useProductDetails(pid); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { lib } = useSessionContext(); - const { state } = useSessionContext(); - + const { state, lib } = useSessionContext(); + const { i18n } = useTranslationContext(); if (!result) return <Loading />; @@ -71,6 +70,7 @@ export default function UpdateProduct({ } } + return ( <Fragment> <NotificationCard notification={notif} /> @@ -79,12 +79,27 @@ export default function UpdateProduct({ onBack={onBack} onUpdate={(data) => { return lib.instance.updateProduct(state.token, pid, data) - .then(onConfirm) + .then(resp => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Product (ID: ${pid}) has been updated`, + type: "SUCCESS", + }); + onConfirm() + } else { + setNotif({ + message: i18n.str`Could not update product`, + type: "ERROR", + description: resp.detail.hint, + }); + + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not create product`, + message: i18n.str`Could not update product`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx index 336a336ed..c20f8edf0 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -27,9 +27,7 @@ import { TalerMerchantApi, TranslatedString, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; @@ -69,8 +67,7 @@ interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { config } = useSessionContext(); - const { state: session } = useSessionContext(); + const { config, state: session } = useSessionContext(); const devices = useInstanceOtpDevices(); const [state, setState] = useState<Partial<Entity>>({ @@ -94,28 +91,32 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const errors: FormErrors<Entity> = { id: !state.id - ? i18n.str`should not be empty` + ? i18n.str`Required` : !/[a-zA-Z0-9]*/.test(state.id) - ? i18n.str`no valid. only characters and numbers` + ? i18n.str`Invalid. only characters and numbers` : undefined, - description: !state.description ? i18n.str`should not be empty` : undefined, + description: !state.description ? i18n.str`Required` : undefined, amount: !state.amount - ? state.amount_editable ? undefined : i18n.str`required` + ? state.amount_editable + ? undefined + : i18n.str`Required` : !parsedPrice - ? i18n.str`not valid` + ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) - ? state.amount_editable ? undefined : i18n.str`must be greater than 0` + ? state.amount_editable + ? undefined + : i18n.str`Must be greater than 0` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 - ? i18n.str`should be greater that 0` + ? i18n.str`Must be greater that 0` : undefined, pay_duration: !state.pay_duration - ? i18n.str`can't be empty` + ? i18n.str`Required` : state.pay_duration.d_ms === "forever" ? undefined : state.pay_duration.d_ms < 1000 //less than one second - ? i18n.str`to short` + ? i18n.str`To short` : undefined, }; @@ -125,12 +126,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { (k) => (errors as Record<string, unknown>)[k] !== undefined, ); - const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency)) + const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency)); const submitForm = () => { if (hasErrors) return Promise.reject(); - const contract_amount = state.amount_editable ? undefined : state.amount as AmountString - const contract_summary = state.summary_editable ? undefined : state.summary + const contract_amount = state.amount_editable + ? undefined + : (state.amount as AmountString); + const contract_summary = state.summary_editable ? undefined : state.summary; const template_contract: TalerMerchantApi.TemplateContractDetails = { minimum_age: state.minimum_age!, pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), @@ -140,14 +143,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { cList.length > 1 && state.currency_editable ? undefined : config.currency, - } + }; return onCreate({ template_id: state.id!, template_description: state.description!, template_contract, editable_defaults: { - amount: !state.amount_editable ? undefined : (state.amount ?? zero), - summary: !state.summary_editable ? undefined : (state.summary ?? ""), + amount: !state.amount_editable ? undefined : state.amount ?? zero, + summary: !state.summary_editable ? undefined : state.summary ?? "", currency: cList.length === 1 || !state.currency_editable ? undefined @@ -181,7 +184,10 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <InputWithAddon<Entity> name="id" help={ - new URL(`templates/${state.id ?? ""}`, session.backendUrl.href).href + new URL( + `templates/${state.id ?? ""}`, + session.backendUrl.href, + ).href } label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} @@ -224,7 +230,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { tooltip={i18n.str`Allow the user to change currency.`} /> <TextField name="sc" label={i18n.str`Supported currencies`}> - <i18n.Translate>supported currencies: {cList.join(", ")}</i18n.Translate> + <i18n.Translate> + Supported currencies: {cList.join(", ")} + </i18n.Translate> </TextField> </Fragment> )} @@ -281,7 +289,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx index 499c7c859..cc40591d4 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -28,15 +28,13 @@ import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; onConfirm: () => void; } -export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { lib } = useSessionContext(); - const { state } = useSessionContext(); +export default function CreateTemplate({ onConfirm, onBack }: Props): VNode { + const { state, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -45,14 +43,29 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: TalerMerchantApi.TemplateAddDetails) => { - return lib.instance.addTemplate(state.token, request) - .then(() => onConfirm()) + onCreate={async (request: TalerMerchantApi.TemplateAddDetails) => { + return lib.instance + .addTemplate(state.token, request) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Template has been created`, + type: "SUCCESS", + }); + onConfirm(); + } else { + setNotif({ + message: i18n.str`Could not create template`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not inform template`, + message: i18n.str`Could not create template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx deleted file mode 100644 index 66d8a2f7e..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { CardTable } from "./Table.js"; - -export interface Props { - templates: TalerMerchantApi.TemplateEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; - onCreate: () => void; - onDelete: (e: TalerMerchantApi.TemplateEntry) => void; - onSelect: (e: TalerMerchantApi.TemplateEntry) => void; - onNewOrder: (e: TalerMerchantApi.TemplateEntry) => void; - onQR: (e: TalerMerchantApi.TemplateEntry) => void; -} - -export function ListPage({ - templates, - onCreate, - onDelete, - onSelect, - onNewOrder, - onQR, - onLoadMoreBefore, - onLoadMoreAfter, -}: Props): VNode { - - return ( - <CardTable - templates={templates.map((o) => ({ - ...o, - id: String(o.template_id), - }))} - onQR={onQR} - onCreate={onCreate} - onDelete={onDelete} - onSelect={onSelect} - onNewOrder={onNewOrder} - onLoadMoreBefore={onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - /> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx index 4c55bae2a..80c893049 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -63,7 +63,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new templates`} + data-tooltip={i18n.str`Add new templates`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -124,10 +124,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more templates before the first one`} + data-tooltip={i18n.str`Load more templates before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -162,21 +162,21 @@ function Table({ <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected templates from the database`} + data-tooltip={i18n.str`Delete selected templates from the database`} onClick={() => onDelete(i)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> <button class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`use template to create new order`} + data-tooltip={i18n.str`Use template to create new order`} onClick={() => onNewOrder(i)} > - Use template + <i18n.Translate>Use template</i18n.Translate> </button> <button class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`create qr code for the template`} + data-tooltip={i18n.str`Create qr code for the template`} onClick={() => onQR(i)} > QR @@ -191,10 +191,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more templates after the last one`} + data-tooltip={i18n.str`Load more templates after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx index 4fe11bf5c..ad4ed9363 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -19,10 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -31,13 +34,11 @@ import { JumpToElementById } from "../../../../components/form/JumpToElementById import { NotificationCard } from "../../../../components/menu/index.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; import { useSessionContext } from "../../../../context/session.js"; -import { - useInstanceTemplates -} from "../../../../hooks/templates.js"; +import { useInstanceTemplates } from "../../../../hooks/templates.js"; import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; -import { ListPage } from "./ListPage.js"; +import { CardTable } from "./Table.js"; interface Props { onCreate: () => void; @@ -59,20 +60,20 @@ export default function ListTemplates({ const [deleting, setDeleting] = useState<TalerMerchantApi.TemplateEntry | null>(null); - if (!result) return <Loading /> + if (!result) return <Loading />; if (result instanceof TalerError) { - return <ErrorLoadingMerchant error={result} /> + return <ErrorLoadingMerchant error={result} />; } if (result.type === "fail") { - switch(result.case) { + switch (result.case) { case HttpStatusCode.NotFound: { - return <NotFoundPageOrAdminCreate /> + return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { - assertUnreachable(result) + assertUnreachable(result); } } } @@ -83,25 +84,21 @@ export default function ListTemplates({ <JumpToElementById testIfExist={async (id) => { - const resp = await lib.instance.getTemplateDetails(state.token, id) - return resp.type === "ok" + const resp = await lib.instance.getTemplateDetails(state.token, id); + return resp.type === "ok"; }} onSelect={onSelect} - description={i18n.str`jump to template with the given template ID`} - placeholder={i18n.str`template id`} + description={i18n.str`Jump to template with the given template ID`} + placeholder={i18n.str`Template identification`} /> - <ListPage - // templates={result.body} - // onLoadMoreBefore={ - // result.isFirstPage ? undefined: result.loadFirst - // } - // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} - - templates={result.body.templates} + <CardTable + templates={result.body.templates.map((o) => ({ + ...o, + id: String(o.template_id), + }))} onLoadMoreBefore={undefined} onLoadMoreAfter={undefined} - onCreate={onCreate} onSelect={(e) => { onSelect(e.template_id); @@ -113,9 +110,8 @@ export default function ListTemplates({ onQR(e.template_id); }} onDelete={(e: TalerMerchantApi.TemplateEntry) => { - setDeleting(e) - } - } + setDeleting(e); + }} /> {deleting && ( @@ -127,11 +123,22 @@ export default function ListTemplates({ onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { - await lib.instance.deleteTemplate(state.token, deleting.template_id); - setNotif({ - message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, - type: "SUCCESS", - }); + const resp = await lib.instance.deleteTemplate( + state.token, + deleting.template_id, + ); + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Failed to delete template`, + type: "ERROR", + description: resp.detail.hint, + }); + } } catch (error) { setNotif({ message: i18n.str`Failed to delete template`, @@ -143,11 +150,18 @@ export default function ListTemplates({ }} > <p> - If you delete the template <b>"{deleting.template_description}"</b> (ID:{" "} - <b>{deleting.template_id}</b>) you may loose information + <i18n.Translate> + If you delete the template{" "} + <b>"{deleting.template_description}"</b> (ID:{" "} + <b>{deleting.template_id}</b>) you may loose information + </i18n.Translate> </p> <p class="warning"> - Deleting an template <b>cannot be undone</b>. + <i18n.Translate>Deleting an template </i18n.Translate> + <b> + <i18n.Translate>can't be undone</i18n.Translate> + </b> + . </p> </ConfirmModal> )} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx index 547996ea1..ec3bec184 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -21,11 +21,9 @@ import { TalerMerchantApi, - stringifyPayTemplateUri + stringifyPayTemplateUri, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { QR } from "../../../../components/exception/QR.js"; import { useSessionContext } from "../../../../context/session.js"; @@ -42,29 +40,6 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { state } = useSessionContext(); - // const [state, setState] = useState<Partial<Entity>>({ - // amount: contract.amount, - // summary: contract.summary, - // }); - - // const errors: FormErrors<Entity> = {}; - - // const fixedAmount = !!contract.amount; - // const fixedSummary = !!contract.summary; - - // const templateParams: Record<string, string> = {}; - // if (!fixedAmount) { - // if (state.amount) { - // templateParams.amount = state.amount; - // } else { - // templateParams.amount = config.currency; - // } - // } - - // if (!fixedSummary) { - // templateParams.summary = state.summary ?? ""; - // } - const merchantBaseUrl = state.backendUrl.href; const payTemplateUri = stringifyPayTemplateUri({ @@ -79,7 +54,9 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { <section id="printThis"> <QR text={payTemplateUri} /> <pre style={{ textAlign: "center" }}> - <a target="_blank" rel="noreferrer" href={payTemplateUri}>{payTemplateUri}</a> + <a target="_blank" rel="noreferrer" href={payTemplateUri}> + {payTemplateUri} + </a> </pre> </section> @@ -87,35 +64,6 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> - {/* <p class="is-size-5 mt-5 mb-5"> - <i18n.Translate> - Here you can specify a default value for fields that are not - fixed. Default values can be edited by the customer before the - payment. - </i18n.Translate> - </p> */} - - <p></p> - {/* <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputCurrency<Entity> - name="amount" - label={i18n.str`Amount`} - readonly - tooltip={i18n.str`Amount of the order`} - /> - <Input<Entity> - name="summary" - inputType="multiline" - readonly - label={i18n.str`Summary`} - tooltip={i18n.str`Title of the order to be shown to the customer`} - /> - </FormProvider> */} - <div class="buttons is-right mt-5"> {onBack && ( <button class="button" onClick={onBack}> @@ -138,18 +86,31 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { } function saveAsPDF(name: string): void { - const printWindow = window.open("", "", "height=400,width=800"); - if (!printWindow) return; + // TODO: Look into using media queries in the current page, to print the current page, instead of opening a new window + const divContents = document.getElementById("printThis"); if (!divContents) return; - printWindow.document.write( - `<html><head><title>Order template for ${name}</title><style>`, - ); - printWindow.document.write("</style></head><body> </body></html>"); - printWindow.document.close(); - printWindow.document.body.appendChild(divContents.cloneNode(true)); + + let dom = `<!DOCTYPE html> +<html> + <head> + <title>Order template for ${name}</title> + <style> + pre > a { + text-decoration: none; + } + </style> + </head> + <body> + ${divContents.outerHTML} + </body> +</html>`; + const blobUrl = URL.createObjectURL(new Blob([dom])); + const printWindow = window.open(blobUrl, "", "height=400,width=800"); + if (!printWindow) return; printWindow.addEventListener("load", () => { printWindow.print(); - // printWindow.close(); + printWindow.close(); + URL.revokeObjectURL(blobUrl); }); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx index ed809c7b3..93347f616 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -19,13 +19,16 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; +import { + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { - useTemplateDetails -} from "../../../../hooks/templates.js"; +import { useTemplateDetails } from "../../../../hooks/templates.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { QrPage } from "./QrPage.js"; import { LoginPage } from "../../../login/index.js"; @@ -36,31 +39,27 @@ interface Props { tid: string; } -export default function TemplateQrPage({ - tid, - onBack, -}: Props): VNode { +export default function TemplateQrPage({ tid, onBack }: Props): VNode { const result = useTemplateDetails(tid); - if (!result) return <Loading /> + if (!result) return <Loading />; if (result instanceof TalerError) { - return <ErrorLoadingMerchant error={result} /> + return <ErrorLoadingMerchant error={result} />; } if (result.type === "fail") { - switch(result.case) { + switch (result.case) { case HttpStatusCode.NotFound: { - return <NotFoundPageOrAdminCreate /> + return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { - assertUnreachable(result) + assertUnreachable(result); } } } - return ( - <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} /> + <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} /> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx index 113cf5baa..37f2bf898 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -68,8 +68,7 @@ interface Props { export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { config } = useSessionContext(); - const { state: session } = useSessionContext(); + const { config, state: session } = useSessionContext(); const [state, setState] = useState<Partial<Entity>>({ description: template.template_description, @@ -116,24 +115,24 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); const errors: FormErrors<Entity> = { - description: !state.description ? i18n.str`should not be empty` : undefined, + description: !state.description ? i18n.str`Required` : undefined, amount: !state.amount - ? state.amount_editable ? undefined : i18n.str`required` + ? state.amount_editable ? undefined : i18n.str`Required` : !parsedPrice - ? i18n.str`not valid` + ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) - ? state.amount_editable ? undefined : i18n.str`must be greater than 0` + ? state.amount_editable ? undefined : i18n.str`Must be greater than 0` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 - ? i18n.str`should be greater that 0` + ? i18n.str`Must be greater that 0` : undefined, pay_duration: !state.pay_duration - ? i18n.str`can't be empty` + ? i18n.str`Required` : state.pay_duration.d_ms === "forever" ? undefined : state.pay_duration.d_ms < 1000 // less than one second - ? i18n.str`to short` + ? i18n.str`Too short` : undefined, }; @@ -237,7 +236,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { /> <TextField name="sc" label={i18n.str`Supported currencies`}> <i18n.Translate> - supported currencies: {cList.join(", ")} + Supported currencies: {cList.join(", ")} </i18n.Translate> </TextField> </Fragment> @@ -295,7 +294,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx index ad804831c..a42a40c63 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -19,19 +19,20 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; -import { - useTemplateDetails, -} from "../../../../hooks/templates.js"; +import { useTemplateDetails } from "../../../../hooks/templates.js"; import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; @@ -50,27 +51,26 @@ export default function UpdateTemplate({ onConfirm, onBack, }: Props): VNode { - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const result = useTemplateDetails(tid); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (!result) return <Loading /> + if (!result) return <Loading />; if (result instanceof TalerError) { - return <ErrorLoadingMerchant error={result} /> + return <ErrorLoadingMerchant error={result} />; } if (result.type === "fail") { - switch(result.case) { + switch (result.case) { case HttpStatusCode.NotFound: { - return <NotFoundPageOrAdminCreate /> + return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { - assertUnreachable(result) + assertUnreachable(result); } } } @@ -79,16 +79,31 @@ export default function UpdateTemplate({ <Fragment> <NotificationCard notification={notif} /> <UpdatePage - template={{...result.body, id: tid}} + template={{ ...result.body, id: tid }} onBack={onBack} onUpdate={(data) => { - return lib.instance.updateTemplate(state.token, tid, data) - .then(onConfirm) + return lib.instance + .updateTemplate(state.token, tid, data) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Template (ID: ${tid}) has been updated`, + type: "SUCCESS", + }); + onConfirm(); + } else { + setNotif({ + message: i18n.str`Could not update template`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not update template`, + message: i18n.str`Could not update template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx index 00cb2b827..ebdb1a6fa 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -82,23 +82,26 @@ export default function TemplateUsePage({ onCreateOrder={( request: TalerMerchantApi.UsingTemplateDetails, ) => { - return lib.instance.useTemplateCreateOrder(tid, request) .then((res) => { if (res.type === "ok") { onOrderCreated(res.body.order_id) } else { setNotif({ - message: i18n.str`could not create order from template`, + message: i18n.str`Could not create order from template`, type: "ERROR", + description: + res.case === HttpStatusCode.Gone + ? i18n.str`No more stock for product with id "${res.body.product_id}".` + : res.detail.hint, }); } }) .catch((error) => { setNotif({ - message: i18n.str`could not create order from template`, + message: i18n.str`Could not create order from template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx index d718ffb69..be23299ff 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -28,6 +28,7 @@ import { Input } from "../../../components/form/Input.js"; import { NotificationCard } from "../../../components/menu/index.js"; import { useSessionContext } from "../../../context/session.js"; import { AccessToken, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util"; +import { undefinedIfEmpty } from "../../../utils/table.js"; interface Props { hasToken: boolean | undefined; @@ -50,25 +51,23 @@ export function DetailPage({ }); const { i18n } = useTranslationContext(); - const errors = { + const errors = undefinedIfEmpty({ old_token: hasToken && !form.old_token - ? i18n.str`you need your access token to perform the operation` + ? i18n.str`You need your access token to perform the operation` : undefined, new_token: !form.new_token - ? i18n.str`cannot be empty` + ? i18n.str`Required` : form.new_token === form.old_token - ? i18n.str`cannot be the same as the old token` + ? i18n.str`Can't be the same as the old token` : undefined, repeat_token: form.new_token !== form.repeat_token - ? i18n.str`is not the same` + ? i18n.str`Is not the same` : undefined, - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as Record<string, unknown>)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const { state } = useSessionContext(); @@ -120,7 +119,7 @@ export function DetailPage({ <Input<State> name="old_token" label={i18n.str`Current access token`} - tooltip={i18n.str`access token currently in use`} + tooltip={i18n.str`Access token currently in use`} inputType="password" /> <p> @@ -149,13 +148,13 @@ export function DetailPage({ <Input<State> name="new_token" label={i18n.str`New access token`} - tooltip={i18n.str`next access token to be used`} + 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`} + tooltip={i18n.str`Confirm the same access token`} inputType="password" /> </Fragment> @@ -171,7 +170,7 @@ export function DetailPage({ data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx index c23e5be17..def8ec821 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -18,9 +18,7 @@ import { TalerError, assertUnreachable, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; @@ -40,8 +38,7 @@ interface Props { export default function Token({ onChange, onCancel }: Props): VNode { const { i18n } = useTranslationContext(); - const { lib } = useSessionContext(); - const { logIn } = useSessionContext(); + const { logIn, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const result = useInstanceDetails(); @@ -89,13 +86,11 @@ export default function Token({ onChange, onCancel }: Props): VNode { }); } } catch (error) { - if (error instanceof Error) { - return setNotif({ - message: i18n.str`Failed to clear token`, - type: "ERROR", - description: error.message, - }); - } + return setNotif({ + message: i18n.str`Failed to clear token`, + type: "ERROR", + description: error instanceof Error ? error.message : String(error), + }); } }} onNewToken={async (currentToken, newToken): Promise<void> => { @@ -137,13 +132,11 @@ export default function Token({ onChange, onCancel }: Props): VNode { }); } } catch (error) { - if (error instanceof Error) { return setNotif({ message: i18n.str`Failed to set new token`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); - } } }} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx index 32c92cab0..34361fd56 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx @@ -23,12 +23,12 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { Notification } from "../../../../utils/types.js"; import { useSessionContext } from "../../../../context/session.js"; import { CreatePage } from "./CreatePage.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; -export type Entity = MerchantBackend.TokenFamilies.TokenFamilyAddDetail; +export type Entity = TalerMerchantApi.TokenFamilyCreateRequest; interface Props { onBack?: () => void; onConfirm: () => void; @@ -36,8 +36,7 @@ interface Props { export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode { const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); return ( <Fragment> @@ -46,12 +45,26 @@ export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode { onBack={onBack} onCreate={(request) => { return lib.instance.createTokenFamily(state.token, request) - .then(() => onConfirm()) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Token familty created successfully`, + type: "SUCCESS", + }); + onConfirm(); + } else { + setNotif({ + message: i18n.str`Could not create token family`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not create token family`, + message: i18n.str`Could not create token family`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx index 1af8a1192..28823e8a1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx @@ -23,7 +23,6 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { format } from "date-fns"; -import { MerchantBackend } from "../../../../declaration.js"; import { TalerMerchantApi } from "@gnu-taler/taler-util"; type Entity = TalerMerchantApi.TokenFamilySummary; @@ -34,7 +33,7 @@ interface Props { onSelect: (tokenFamily: Entity) => void; onUpdate: ( slug: string, - data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, + data: TalerMerchantApi.TokenFamilyUpdateRequest, ) => Promise<void>; onCreate: () => void; selected?: boolean; @@ -63,7 +62,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add token family`} + data-tooltip={i18n.str`Add token family`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -100,7 +99,7 @@ interface TableProps { onSelect: (tokenFamily: Entity) => void; onUpdate: ( slug: string, - data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, + data: TalerMerchantApi.TokenFamilyUpdateRequest, ) => Promise<void>; onDelete: (tokenFamily: Entity) => void; rowSelectionHandler: StateUpdater<string | undefined>; @@ -111,7 +110,6 @@ function Table({ rowSelectionHandler, instances, onSelect, - onUpdate, onDelete, }: TableProps): VNode { const { i18n } = useTranslationContext(); @@ -191,7 +189,7 @@ function Table({ <div class="buttons is-right"> <span class="has-tooltip-bottom" - data-tooltip={i18n.str`go to token family update page`} + data-tooltip={i18n.str`Go to token family update page`} > <button class="button is-small is-success " @@ -203,7 +201,7 @@ function Table({ </span> <span class="has-tooltip-left" - data-tooltip={i18n.str`remove this token family from the database`} + data-tooltip={i18n.str`Remove this token family from the database`} > <button class="button is-small is-danger" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx index 58d071ffc..f01d0b0d7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx @@ -20,38 +20,32 @@ */ import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - useInstanceTokenFamilies, -} from "../../../../hooks/tokenfamily.js"; -import { Notification } from "../../../../utils/types.js"; -import { CardTable } from "./Table.js"; -import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; -import { useSessionContext } from "../../../../context/session.js"; -import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceTokenFamilies } from "../../../../hooks/tokenfamily.js"; +import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { CardTable } from "./Table.js"; interface Props { onUnauthorized: () => VNode; onNotFound: () => VNode; onCreate: () => void; onSelect: (slug: string) => void; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; } -export default function TokenFamilyList({ - onCreate, - onSelect, -}: Props): VNode { +export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { const result = useInstanceTokenFamilies(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { lib, state } = useSessionContext(); @@ -70,7 +64,7 @@ export default function TokenFamilyList({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { assertUnreachable(result); @@ -87,14 +81,26 @@ export default function TokenFamilyList({ onCreate={onCreate} onUpdate={async (slug, fam) => { try { - await lib.instance.updateTokenFamily(state.token, slug, fam); - setNotif({ - message: i18n.str`token family updated successfully`, - type: "SUCCESS", - }); + const resp = await lib.instance.updateTokenFamily( + state.token, + slug, + fam, + ); + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Token family updated successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not update the token family`, + type: "ERROR", + description: resp.detail.hint, + }); + } } catch (error) { setNotif({ - message: i18n.str`could not update the token family`, + message: i18n.str`Could not update the token family`, type: "ERROR", description: error instanceof Error ? error.message : undefined, }); @@ -114,11 +120,22 @@ export default function TokenFamilyList({ onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { - await lib.instance.deleteTokenFamily(state.token, deleting.slug); - setNotif({ - message: i18n.str`Token family "${deleting.name}" (SLUG: ${deleting.slug}) has been deleted`, - type: "SUCCESS", - }); + const resp = await lib.instance.deleteTokenFamily( + state.token, + deleting.slug, + ); + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Token family "${deleting.name}" (SLUG: ${deleting.slug}) has been deleted`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Failed to delete token family`, + type: "ERROR", + description: resp.detail.hint, + }); + } } catch (error) { setNotif({ message: i18n.str`Failed to delete token family`, @@ -130,11 +147,20 @@ export default function TokenFamilyList({ }} > <p> - If you delete the <b>"{deleting.name}"</b> token family (Slug:{" "} - <b>{deleting.slug}</b>), all issued tokens will become invalid. + <i18n.Translate> + If you delete the <b>"{deleting.name}"</b> token family + (Slug: <b>{deleting.slug}</b>), all issued tokens will become + invalid. + </i18n.Translate> </p> <p class="warning"> - Deleting a token family <b>cannot be undone</b>. + <i18n.Translate> + Deleting a token family{" "} + <b> + <i18n.Translate>can't be undone</i18n.Translate> + </b> + . + </i18n.Translate> </p> </ConfirmModal> )} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx index 5641d261b..382821f8c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx @@ -23,13 +23,12 @@ import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { useState } from "preact/hooks"; -import * as yup from "yup"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputDate } from "../../../../components/form/InputDate.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { TokenFamilyUpdateSchema } from "../../../../schemas/index.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = Omit<TalerMerchantApi.TokenFamilyUpdateRequest, "duration"> & { duration: Duration, @@ -52,25 +51,16 @@ function convert(from: TalerMerchantApi.TokenFamilyUpdateRequest) { export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { const [value, valueHandler] = useState<Partial<Entity>>(convert(tokenFamily)); - let errors: FormErrors<Entity> = {}; - - try { - TokenFamilyUpdateSchema.validateSync(value, { - abortEarly: false, - }); - } catch (err) { - if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as yup.ValidationError[]; - errors = yupErrors.reduce( - (prev, cur) => - !cur.path ? prev : { ...prev, [cur.path]: cur.message }, - {}, - ); - } - } - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const { i18n } = useTranslationContext(); + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + name: !value.name ? i18n.str`Required` : undefined, + description: !value.description ? i18n.str`Required` : undefined, + valid_after: !value.valid_after ? i18n.str`Required` : undefined, + valid_before: !value.valid_before ? i18n.str`Required` : undefined, + duration: !value.duration ? i18n.str`Required` : undefined, + }); + + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); @@ -84,7 +74,6 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { return onUpdate(result); } - const { i18n } = useTranslationContext(); return ( <div> @@ -95,7 +84,7 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - Token Family: <b>{tokenFamily.name}</b> + <i18n.Translate>Token Family: <b>{tokenFamily.name}</b></i18n.Translate> </span> </div> </div> @@ -117,30 +106,30 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { name="name" inputType="text" label={i18n.str`Name`} - tooltip={i18n.str`user-readable token family name`} + tooltip={i18n.str`User-readable token family name`} /> <Input<Entity> name="description" inputType="multiline" label={i18n.str`Description`} - tooltip={i18n.str`token family description for customers`} + tooltip={i18n.str`Token family description for customers`} /> <InputDate<Entity> name="valid_after" label={i18n.str`Valid After`} - tooltip={i18n.str`token family can issue tokens after this date`} + tooltip={i18n.str`Token family can issue tokens after this date`} withTimestampSupport /> <InputDate<Entity> name="valid_before" label={i18n.str`Valid Before`} - tooltip={i18n.str`token family can issue tokens until this date`} + tooltip={i18n.str`Token family can issue tokens until this date`} withTimestampSupport /> <InputDuration<Entity> name="duration" label={i18n.str`Duration`} - tooltip={i18n.str`validity duration of a issued token`} + tooltip={i18n.str`Validity duration of a issued token`} withForever /> </FormProvider> @@ -156,7 +145,7 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx index 068235e14..287cc7a51 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx @@ -20,22 +20,25 @@ */ import { - ErrorType, - HttpError, - useTranslationContext, + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { Notification } from "../../../../utils/types.js"; -import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; -import { useTokenFamilyDetails } from "../../../../hooks/tokenfamily.js"; import { useSessionContext } from "../../../../context/session.js"; -import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { useTokenFamilyDetails } from "../../../../hooks/tokenfamily.js"; +import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { UpdatePage } from "./UpdatePage.js"; type Entity = TalerMerchantApi.TokenFamilyUpdateRequest; @@ -65,7 +68,7 @@ export default function UpdateTokenFamily({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { assertUnreachable(result); @@ -89,13 +92,28 @@ export default function UpdateTokenFamily({ tokenFamily={family} onBack={onBack} onUpdate={(data) => { - return lib.instance.updateTokenFamily(state.token, slug, data) - .then(onConfirm) + return lib.instance + .updateTokenFamily(state.token, slug, data) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Token familty updated successfully`, + type: "SUCCESS", + }); + onConfirm(); + } else { + setNotif({ + message: i18n.str`Could not update token family`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not update token family`, + message: i18n.str`Could not update token family`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx index 91aabe58e..d3a1d7832 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -35,6 +35,7 @@ import { CROCKFORD_BASE32_REGEX, URL_REGEX, } from "../../../../utils/constants.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.TransferInformation; @@ -54,26 +55,24 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { credit_amount: `` as AmountString, }); - const errors: FormErrors<Entity> = { + const errors = undefinedIfEmpty<FormErrors<Entity>>({ wtid: !state.wtid - ? i18n.str`cannot be empty` + ? i18n.str`Required` : !CROCKFORD_BASE32_REGEX.test(state.wtid) - ? i18n.str`check the id, does not look valid` - : state.wtid.length !== 52 - ? i18n.str`should have 52 characters, current ${state.wtid.length}` - : undefined, - payto_uri: !state.payto_uri ? i18n.str`cannot be empty` : undefined, - credit_amount: !state.credit_amount ? i18n.str`cannot be empty` : undefined, + ? i18n.str`Check the id, does not look valid` + : state.wtid.length !== 52 + ? i18n.str`Must have 52 characters, current ${state.wtid.length}` + : undefined, + payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, + credit_amount: !state.credit_amount ? i18n.str`Required` : undefined, exchange_url: !state.exchange_url - ? i18n.str`cannot be empty` + ? i18n.str`Required` : !URL_REGEX.test(state.exchange_url) - ? i18n.str`URL doesn't have the right format` - : undefined, - }; + ? i18n.str`URL doesn't have the right format` + : undefined, + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); @@ -102,7 +101,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { name="wtid" label={i18n.str`Wire transfer ID`} help="" - tooltip={i18n.str`unique identifier of the wire transfer used by the exchange, must be 52 characters long`} + tooltip={i18n.str`Unique identifier of the wire transfer used by the exchange, must be 52 characters long`} /> <Input<Entity> name="exchange_url" @@ -128,7 +127,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx index 428476337..2f295fcc8 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -38,8 +38,7 @@ interface Props { } export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); const instance = useInstanceBankAccounts(); @@ -57,12 +56,26 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { onCreate={(request: TalerMerchantApi.TransferInformation) => { return lib.instance .informWireTransfer(state.token, request) - .then(() => onConfirm()) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Wire transfer informed successfully`, + type: "SUCCESS", + }); + onConfirm() + } else { + setNotif({ + message: i18n.str`Could not inform transfer`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not inform transfer`, + message: i18n.str`Could not inform transfer`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx index 927e36cf7..f80c0f53f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -81,7 +81,7 @@ export function ListPage({ return d }} placeholder={i18n.str`All accounts`} - tooltip={i18n.str`filter by account address`} + tooltip={i18n.str`Filter by account address`} /> </FormProvider> </div> @@ -92,7 +92,7 @@ export function ListPage({ <li class={isAllTransfers ? "is-active" : ""}> <div class="has-tooltip-right" - data-tooltip={i18n.str`remove all filters`} + data-tooltip={i18n.str`Remove all filters`} > <a onClick={onShowAll}> <i18n.Translate>All</i18n.Translate> @@ -102,7 +102,7 @@ export function ListPage({ <li class={isVerifiedTransfers ? "is-active" : ""}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`} + data-tooltip={i18n.str`Only show wire transfers confirmed by the merchant`} > <a onClick={onShowVerified}> <i18n.Translate>Verified</i18n.Translate> @@ -112,7 +112,7 @@ export function ListPage({ <li class={isNonVerifiedTransfers ? "is-active" : ""}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show wire transfers claimed by the exchange`} + data-tooltip={i18n.str`Only show wire transfers claimed by the exchange`} > <a onClick={onShowUnverified}> <i18n.Translate>Unverified</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx index 5687d5e57..29ff69f55 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -61,7 +61,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new transfer`} + data-tooltip={i18n.str`Add new transfer`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -114,10 +114,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more transfers before the first one`} + data-tooltip={i18n.str`Load more transfers before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -163,7 +163,7 @@ function Table({ {i.verified !== true ? ( <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected transfer from the database`} + data-tooltip={i18n.str`Delete selected transfer from the database`} onClick={() => onDelete(i)} > <i18n.Translate>Delete</i18n.Translate> @@ -178,10 +178,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more transfers after the last one`} + data-tooltip={i18n.str`Load more transfers after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx deleted file mode 100644 index 719f99209..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode } from "preact"; - -export default function UpdateTransfer(): VNode { - return <div>order transfer page</div>; -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx index cde58967f..c7d38cf32 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -37,7 +37,6 @@ export type Entity = Omit<Omit<TalerMerchantApi.InstanceReconfigurationMessage, default_wire_transfer_delay: Duration, }; -//TalerMerchantApi.InstanceAuthConfigurationMessage interface Props { onUpdate: (d: TalerMerchantApi.InstanceReconfigurationMessage) => void; selected: TalerMerchantApi.QueryInstancesResponse; @@ -69,40 +68,38 @@ export function UpdatePage({ const { i18n } = useTranslationContext(); - const errors: FormErrors<Entity> = { - name: !value.name ? i18n.str`required` : undefined, + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + name: !value.name ? i18n.str`Required` : undefined, user_type: !value.user_type - ? i18n.str`required` + ? i18n.str`Required` : value.user_type !== "business" && value.user_type !== "individual" - ? i18n.str`should be business or individual` + ? i18n.str`Must be business or individual` : undefined, default_pay_delay: !value.default_pay_delay - ? i18n.str`required` + ? i18n.str`Required` : !!value.default_wire_transfer_delay && value.default_wire_transfer_delay.d_ms !== "forever" && value.default_pay_delay.d_ms !== "forever" && value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? - i18n.str`pay delay can't be greater than wire transfer delay` : undefined, + i18n.str`Pay delay can't be greater than wire transfer delay` : undefined, default_wire_transfer_delay: !value.default_wire_transfer_delay - ? i18n.str`required` + ? i18n.str`Required` : undefined, address: undefinedIfEmpty({ address_lines: value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` + ? i18n.str`Max 7 lines` : undefined, }), jurisdiction: undefinedIfEmpty({ address_lines: value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` + ? i18n.str`Max 7 lines` : undefined, }), - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submit = async (): Promise<void> => { const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>; @@ -111,7 +108,7 @@ export function UpdatePage({ default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay), ...rest, } - await onUpdate(result); + onUpdate(result); }; // const [active, setActive] = useState(false); @@ -159,7 +156,7 @@ export function UpdatePage({ data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } disabled={hasErrors} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx index 9da7f7efb..2b2327eb2 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -103,11 +103,11 @@ function CommonUpdate( } return updateInstance(state.token, d) .then(onConfirm) - .catch((error: Error) => + .catch((error) => setNotif({ message: i18n.str`Failed to update instance`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx index 8792aabea..234295174 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx @@ -30,6 +30,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.WebhookAddDetails; @@ -45,26 +46,26 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>({}); - const errors: FormErrors<Entity> = { - webhook_id: !state.webhook_id ? i18n.str`required` : undefined, - event_type: !state.event_type ? i18n.str`required` - : state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"` + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + webhook_id: !state.webhook_id ? i18n.str`Required` : undefined, + event_type: !state.event_type + ? i18n.str`Required` + : state.event_type !== "pay" && state.event_type !== "refund" + ? i18n.str`Must be "pay" or "refund"` : undefined, http_method: !state.http_method - ? i18n.str`required` + ? i18n.str`Required` : !validMethod.includes(state.http_method) - ? i18n.str`should be one of '${validMethod.join(", ")}'` + ? i18n.str`Must be one of '${validMethod.join(", ")}'` : undefined, - url: !state.url ? i18n.str`required` : undefined, - }; + url: !state.url ? i18n.str`Required` : undefined, + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); - return onCreate(state as any); + return onCreate(state as TalerMerchantApi.WebhookAddDetails); }; return ( @@ -88,8 +89,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { label={i18n.str`Event`} values={[ i18n.str`Choose one...`, - i18n.str`pay`, - i18n.str`refund`, + i18n.str`Pay`, + i18n.str`Refund`, ]} tooltip={i18n.str`The event of the webhook: why the webhook is used`} /> @@ -114,28 +115,79 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { /> <p> - The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string - between <pre style={{ display: "inline", padding: 0 }}>{{</pre> and <pre style={{ display: "inline", padding: 0 }}>}}</pre> will - be replaced with replaced with the value of the corresponding variable. + {/* prettier will add some nodes which we don't want because of i18n */} + {/* prettier-ignore */} + <i18n.Translate> + The text below support <a + target="_blank" + rel="noreferrer" + href="https://mustache.github.io/mustache.5.html" + > + mustache + </a> template engine. Any string between <pre style={{ display: "inline", padding: 0 }}> + {{ + </pre> and <pre style={{ display: "inline", padding: 0 }}> + }} + </pre> will be replaced with replaced with the value of the + corresponding variable. + </i18n.Translate> </p> <p> - For example <pre style={{ display: "inline", padding: 0 }}>{{contract_terms.amount}}</pre> will be replaced - with the the order's price + {/* prettier will add some nodes which we don't want because of i18n */} + {/* prettier-ignore */} + <i18n.Translate> + For example <pre style={{ display: "inline", padding: 0 }}> + {{contract_terms.amount}} + </pre> will be replaced with the the order's price + </i18n.Translate> </p> <p> - The short list of variables are: + <i18n.Translate> + The short list of variables are: + </i18n.Translate> </p> <div class="menu"> - - <ul class="menu-list" style={{ listStyleType: "disc", marginLeft: 20 }}> - <li><b>contract_terms.summary:</b> order's description </li> - <li><b>contract_terms.amount:</b> order's price </li> - <li><b>order_id:</b> order's unique identification </li> - {state.event_type === "refund" && <Fragment> - <li><b>refund_amout:</b> the amount that was being refunded</li> - <li><b>reason:</b> the reason entered by the merchant staff for granting the refund</li> - <li><b>timestamp:</b> time of the refund in nanoseconds since 1970</li> - </Fragment>} + <ul + class="menu-list" + style={{ listStyleType: "disc", marginLeft: 20 }} + > + <li> + <b>contract_terms.summary:</b>{" "} + <i18n.Translate>order's description</i18n.Translate> + </li> + <li> + <b>contract_terms.amount:</b>{" "} + <i18n.Translate>order's price</i18n.Translate> + </li> + <li> + <b>order_id:</b>{" "} + <i18n.Translate> + order's unique identification + </i18n.Translate> + </li> + {state.event_type === "refund" && ( + <Fragment> + <li> + <b>refund_amout:</b>{" "} + <i18n.Translate> + the amount that was being refunded + </i18n.Translate> + </li> + <li> + <b>reason:</b>{" "} + <i18n.Translate> + the reason entered by the merchant staff for granting + the refund + </i18n.Translate> + </li> + <li> + <b>timestamp:</b>{" "} + <i18n.Translate> + time of the refund in nanoseconds since 1970 + </i18n.Translate> + </li> + </Fragment> + )} </ul> </div> {/* <Input<Entity> @@ -163,7 +215,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx index 70f246ff1..6595879fb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx @@ -37,22 +37,35 @@ interface Props { export default function CreateWebhook({ onConfirm, onBack }: Props): VNode { const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); return ( <> <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: TalerMerchantApi.WebhookAddDetails) => { + onCreate={async (request: TalerMerchantApi.WebhookAddDetails) => { return lib.instance.addWebhook(state.token, request) - .then(() => onConfirm()) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Webhook create successfully`, + type: "SUCCESS", + }); + onConfirm() + } else { + setNotif({ + message: i18n.str`Could not create the webhook`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not inform template`, + message: i18n.str`Could not create webhook`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx index 877bd30e5..2d42ea6a5 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -59,7 +59,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new webhooks`} + data-tooltip={i18n.str`Add new webhooks`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -114,10 +114,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more webhooks before the first one`} + data-tooltip={i18n.str`Load more webhooks before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -152,18 +152,11 @@ function Table({ <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected webhook from the database`} + data-tooltip={i18n.str`Delete selected webhook from the database`} onClick={() => onDelete(i)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> - {/* <button - class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`test webhook`} - onClick={() => onNewOrder(i)} - > - Test - </button> */} </div> </td> </tr> @@ -174,10 +167,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more webhooks after the last one`} + data-tooltip={i18n.str`Load more webhooks after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx index 789b8d73b..0e1c57839 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx @@ -25,9 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -48,8 +46,7 @@ interface Props { export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const result = useInstanceWebhooks(); if (!result) return <Loading />; @@ -62,7 +59,7 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage />; } default: { assertUnreachable(result); @@ -85,17 +82,25 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { onDelete={(e: TalerMerchantApi.WebhookEntry) => { return lib.instance .deleteWebhook(state.token, e.webhook_id) - .then(() => - setNotif({ - message: i18n.str`webhook delete successfully`, - type: "SUCCESS", - }), - ) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Webhook delete successfully`, + type: "SUCCESS", + }); + } else { + setNotif({ + message: i18n.str`Could not delete the webhook`, + type: "ERROR", + description: resp.detail.hint, + }); + } + }) .catch((error) => setNotif({ - message: i18n.str`could not delete the webhook`, + message: i18n.str`Could not delete the webhook`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx index bcd53ffd0..1c1d0da79 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -28,8 +29,8 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { WithId } from "../../../../declaration.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; @@ -45,23 +46,21 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>(webhook); - const errors: FormErrors<Entity> = { - event_type: !state.event_type ? i18n.str`required` : undefined, + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + event_type: !state.event_type ? i18n.str`Required` : undefined, http_method: !state.http_method - ? i18n.str`required` + ? i18n.str`Required` : !validMethod.includes(state.http_method) - ? i18n.str`should be one of '${validMethod.join(", ")}'` - : undefined, - url: !state.url ? i18n.str`required` : undefined, - }; + ? i18n.str`Must be one of '${validMethod.join(", ")}'` + : undefined, + url: !state.url ? i18n.str`Required` : undefined, + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); - return onUpdate(state as any); + return onUpdate(state as Entity); }; return ( @@ -130,7 +129,7 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx index 6c0466dad..83dea45eb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx @@ -29,6 +29,7 @@ import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchan import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; import { useWebhookDetails, } from "../../../../hooks/webhooks.js"; @@ -36,7 +37,6 @@ import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -import { WithId } from "../../../../declaration.js"; export type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; @@ -50,8 +50,7 @@ export default function UpdateWebhook({ onConfirm, onBack, }: Props): VNode { - const { lib } = useSessionContext(); - const { state } = useSessionContext(); + const { state, lib } = useSessionContext(); const result = useWebhookDetails(tid); const [notif, setNotif] = useState<Notification | undefined>(undefined); @@ -81,14 +80,29 @@ export default function UpdateWebhook({ <UpdatePage webhook={{ ...result.body, id: tid }} onBack={onBack} - onUpdate={(data) => { + onUpdate={async (data) => { return lib.instance.updateWebhook(state.token, tid, data) - .then(onConfirm) + .then((resp) => { + if (resp.type === "ok") { + setNotif({ + message: i18n.str`Webhook updated`, + type: "SUCCESS", + }); + onConfirm() + } else { + setNotif({ + message: i18n.str`Could not update webhook`, + type: "ERROR", + description: resp.detail.hint, + }); + + } + }) .catch((error) => { setNotif({ - message: i18n.str`could not update template`, + message: i18n.str`Could not update webhook`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} |