diff options
Diffstat (limited to 'packages/merchant-backoffice-ui')
14 files changed, 343 insertions, 71 deletions
diff --git a/packages/merchant-backoffice-ui/dev.mjs b/packages/merchant-backoffice-ui/dev.mjs index 2f31d8e79..6cf7f0954 100755 --- a/packages/merchant-backoffice-ui/dev.mjs +++ b/packages/merchant-backoffice-ui/dev.mjs @@ -25,6 +25,7 @@ serve({ folder: './dist', port: 8080, source: './src', + insecure: true, development: true, onUpdate: async () => esbuild.build(buildConfig) }) diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index 84536396d..ed1fc69f3 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -23,7 +23,7 @@ import { TranslationProvider, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { route } from "preact-router"; import { useMemo } from "preact/hooks"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; @@ -70,24 +70,24 @@ function ApplicationStatusRoutes(): VNode { if (!triedToLog) { return ( - <div id="app"> + <Fragment> <NotYetReadyAppMenu title="Welcome!" /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> - </div> + </Fragment> ); } if (result.clientError && result.isUnauthorized) return ( - <div id="app"> + <Fragment> <NotYetReadyAppMenu title="Login" /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> - </div> + </Fragment> ); if (result.clientError && result.isNotfound) return ( - <div id="app"> + <Fragment> <NotYetReadyAppMenu title="Error" /> <NotificationCard notification={{ @@ -97,12 +97,12 @@ function ApplicationStatusRoutes(): VNode { }} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> - </div> + </Fragment> ); if (result.serverError) return ( - <div id="app"> + <Fragment> <NotYetReadyAppMenu title="Error" /> <NotificationCard notification={{ @@ -112,14 +112,14 @@ function ApplicationStatusRoutes(): VNode { }} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> - </div> + </Fragment> ); if (result.loading) return <Loading />; if (!result.ok) return ( - <div id="app"> + <Fragment> <NotYetReadyAppMenu title="Error" /> <NotificationCard notification={{ @@ -129,7 +129,7 @@ function ApplicationStatusRoutes(): VNode { }} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> - </div> + </Fragment> ); return ( diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index c8f22f583..b911483a7 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -52,6 +52,7 @@ import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; import ReservesListPage from "./paths/instance/reserves/list/index.js"; import TemplateCreatePage from "./paths/instance/templates/create/index.js"; import TemplateUsePage from "./paths/instance/templates/use/index.js"; +import TemplateQrPage from "./paths/instance/templates/qr/index.js"; import TemplateListPage from "./paths/instance/templates/list/index.js"; import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; @@ -94,6 +95,7 @@ export enum InstancePaths { templates_update = "/templates/:tid/update", templates_new = "/templates/new", templates_use = "/templates/:tid/use", + templates_qr = "/templates/:tid/qr", webhooks_list = "/webhooks", webhooks_update = "/webhooks/:tid/update", @@ -465,6 +467,9 @@ export function InstanceRoutes({ onNewOrder={(id: string) => { route(InstancePaths.templates_use.replace(":tid", id)); }} + onQR={(id: string) => { + route(InstancePaths.templates_qr.replace(":tid", id)); + }} onSelect={(id: string) => { route(InstancePaths.templates_update.replace(":tid", id)); }} @@ -505,6 +510,16 @@ export function InstanceRoutes({ route(InstancePaths.templates_list); }} /> + <Route + path={InstancePaths.templates_qr} + component={TemplateQrPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> {/** * reserves pages diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx index 552e76ed6..9a0411642 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx @@ -42,6 +42,14 @@ function normalizeToken(r: string | undefined): string | undefined { return r ? `secret-token:${encodeURIComponent(r)}` : undefined; } +function cleanUp(s: string): string { + let result = s; + if (result.indexOf("webui/") !== -1) { + result = result.substring(0, result.indexOf("webui/")); + } + return result; +} + export function LoginModal({ onConfirm, withMessage }: Props): VNode { const { url: backendUrl, token: baseToken } = useBackendContext(); const { admin, token: instanceToken } = useInstanceContext(); @@ -50,11 +58,11 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { ); const [token, setToken] = useState(currentToken); - const [url, setURL] = useState(backendUrl); + const [url, setURL] = useState(cleanUp(backendUrl)); const { i18n } = useTranslationContext(); return ( - <div class="columns is-centered"> + <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <div class="modal-card" style={{ width: "100%", margin: 0 }}> <header diff --git a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx index 8bd85d0ef..9624a2c38 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx @@ -61,7 +61,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode { class="navbar-start is-justify-content-center is-flex-grow-1" href="https://taler.net" > - <img src={logo} style={{ height: 50, margin: 10 }} /> + <img src={logo} style={{ height: 35, margin: 10 }} /> </a> <div class="navbar-end"> <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx index 7376a88cb..fd1b8dc05 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -57,56 +57,54 @@ export function View({ : instances; return ( - <div id="app"> - <section class="section is-main-section"> - <div class="columns"> - <div class="column is-two-thirds"> - <div class="tabs" style={{ overflow: "inherit" }}> - <ul> - <li class={showIsActive}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Only show active instances`} - > - <a onClick={() => setShow("active")}> - <i18n.Translate>Active</i18n.Translate> - </a> - </div> - </li> - <li class={showIsDeleted}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Only show deleted instances`} - > - <a onClick={() => setShow("deleted")}> - <i18n.Translate>Deleted</i18n.Translate> - </a> - </div> - </li> - <li class={showAll}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Show all instances`} - > - <a onClick={() => setShow(null)}> - <i18n.Translate>All</i18n.Translate> - </a> - </div> - </li> - </ul> - </div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-two-thirds"> + <div class="tabs" style={{ overflow: "inherit" }}> + <ul> + <li class={showIsActive}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Only show active instances`} + > + <a onClick={() => setShow("active")}> + <i18n.Translate>Active</i18n.Translate> + </a> + </div> + </li> + <li class={showIsDeleted}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Only show deleted instances`} + > + <a onClick={() => setShow("deleted")}> + <i18n.Translate>Deleted</i18n.Translate> + </a> + </div> + </li> + <li class={showAll}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Show all instances`} + > + <a onClick={() => setShow(null)}> + <i18n.Translate>All</i18n.Translate> + </a> + </div> + </li> + </ul> </div> </div> - <CardTableActive - instances={showingInstances} - onDelete={onDelete} - onPurge={onPurge} - setInstanceName={setInstanceName} - onUpdate={onUpdate} - selected={selected} - onCreate={onCreate} - /> - </section> - </div> + </div> + <CardTableActive + instances={showingInstances} + onDelete={onDelete} + onPurge={onPurge} + setInstanceName={setInstanceName} + onUpdate={onUpdate} + selected={selected} + onCreate={onCreate} + /> + </section> ); } 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 b23c52362..22f86002a 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 @@ -114,13 +114,13 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <Input name="template_contract.summary" inputType="multiline" - label={i18n.str`Order summary`} - tooltip={i18n.str`Title of the order to be shown to the customer`} + label={i18n.str`Fixed summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} /> <InputCurrency name="template_contract.amount" - label={i18n.str`Order price`} - tooltip={i18n.str`Order price`} + label={i18n.str`Fixed price`} + tooltip={i18n.str`If specified, this template will create order with the same price`} /> <InputNumber name="template_contract.minimum_age" 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 index 8482f7f52..708720818 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx @@ -32,6 +32,7 @@ export interface Props { onDelete: (e: MerchantBackend.Template.TemplateEntry) => void; onSelect: (e: MerchantBackend.Template.TemplateEntry) => void; onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void; + onQR: (e: MerchantBackend.Template.TemplateEntry) => void; } export function ListPage({ @@ -40,6 +41,7 @@ export function ListPage({ onDelete, onSelect, onNewOrder, + onQR, onLoadMoreBefore, onLoadMoreAfter, }: Props): VNode { @@ -53,6 +55,7 @@ export function ListPage({ ...o, id: String(o.template_id), }))} + onQR={onQR} onCreate={onCreate} onDelete={onDelete} onSelect={onSelect} 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 57d328d39..700c332d7 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 @@ -31,6 +31,7 @@ interface Props { onDelete: (e: Entity) => void; onSelect: (e: Entity) => void; onNewOrder: (e: Entity) => void; + onQR: (e: Entity) => void; onCreate: () => void; onLoadMoreBefore?: () => void; hasMoreBefore?: boolean; @@ -43,6 +44,7 @@ export function CardTable({ onCreate, onDelete, onSelect, + onQR, onNewOrder, onLoadMoreAfter, onLoadMoreBefore, @@ -84,6 +86,7 @@ export function CardTable({ onDelete={onDelete} onSelect={onSelect} onNewOrder={onNewOrder} + onQR={onQR} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} @@ -105,6 +108,7 @@ interface TableProps { instances: Entity[]; onDelete: (e: Entity) => void; onNewOrder: (e: Entity) => void; + onQR: (e: Entity) => void; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; @@ -123,6 +127,7 @@ function Table({ onLoadMoreAfter, onDelete, onNewOrder, + onQR, onSelect, onLoadMoreBefore, hasMoreAfter, @@ -185,6 +190,13 @@ function Table({ > New order </button> + <button + class="button is-info is-small has-tooltip-left" + data-tooltip={i18n.str`create qr code for the template`} + onClick={() => onQR(i)} + > + QR + </button> </div> </td> </tr> 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 0b7c191bd..ea8f4e7e3 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 @@ -42,12 +42,14 @@ interface Props { onCreate: () => void; onSelect: (id: string) => void; onNewOrder: (id: string) => void; + onQR: (id: string) => void; } export default function ListTemplates({ onUnauthorized, onLoadError, onCreate, + onQR, onSelect, onNewOrder, onNotFound, @@ -80,6 +82,9 @@ export default function ListTemplates({ onNewOrder={(e) => { onNewOrder(e.template_id); }} + onQR={(e) => { + onQR(e.template_id); + }} onDelete={(e: MerchantBackend.Template.TemplateEntry) => deleteTemplate(e.template_id) .then(() => diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx new file mode 100644 index 000000000..eb853c8ff --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx @@ -0,0 +1,27 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { QrPage as TestedComponent } from "./QrPage.js"; + +export default { + title: "Pages/Templates/QR", + component: TestedComponent, +}; 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 new file mode 100644 index 000000000..756909d15 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -0,0 +1,133 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { buildPayto, classifyTalerUri } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { QR } from "../../../../components/exception/QR.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { useConfigContext } from "../../../../context/config.js"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.Template.UsingTemplateDetails; + +interface Props { + template: MerchantBackend.Template.TemplateDetails; + id: string; + onBack?: () => void; +} + +export function QrPage({ template, id: templateId, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { url: backendUrl } = useBackendContext(); + const config = useConfigContext(); + + const [state, setState] = useState<Partial<Entity>>({ + amount: template.template_contract.amount, + summary: template.template_contract.summary, + }); + + const errors: FormErrors<Entity> = {}; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const fixedAmount = !!template.template_contract.amount; + const fixedSummary = !!template.template_contract.summary; + + const params = new URLSearchParams(); + if (!fixedAmount) { + if (state.amount) { + params.append("amount", state.amount); + } else { + params.append("amount", config.currency); + } + } + if (!fixedSummary) { + params.append("summary", state.summary ?? ""); + } + + const paramsStr = fixedAmount && fixedSummary ? "" : "?" + params.toString(); + const merchantURL = new URL(backendUrl); + + const talerProto = + merchantURL.protocol === "http:" ? "taler+http:" : "taler:"; + + const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`; + + 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} + > + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + readonly={fixedAmount} + tooltip={i18n.str`Amount of the order`} + /> + <Input<Entity> + name="summary" + inputType="multiline" + readonly={fixedSummary} + label={i18n.str`Order summary`} + tooltip={i18n.str`Title of the order to be shown to the customer`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <button class="button is-info" onClick={onBack}> + <i18n.Translate>Print</i18n.Translate> + </button> + </div> + </div> + <div class="column" /> + </div> + </section> + <section> + <pre> + <a href={payTemplateUri}>{payTemplateUri}</a> + </pre> + <QR text={payTemplateUri} /> + </section> + </div> + ); +} 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 new file mode 100644 index 000000000..97d25b700 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/lib/index.browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + useTemplateAPI, + useTemplateDetails, +} from "../../../../hooks/templates.js"; +import { Notification } from "../../../../utils/types.js"; +import { QrPage } from "./QrPage.js"; + +export type Entity = MerchantBackend.Transfers.TransferInformation; +interface Props { + onBack?: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + tid: string; +} + +export default function TemplateQrPage({ + tid, + onBack, + onLoadError, + onNotFound, + onUnauthorized, +}: Props): VNode { + const { createOrderFromTemplate } = useTemplateAPI(); + const result = useTemplateDetails(tid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return <Loading />; + if (!result.ok) return onLoadError(result); + + return ( + <> + <NotificationCard notification={notif} /> + <QrPage template={result.data} 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 5bd9bd38d..eba212517 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 @@ -123,13 +123,13 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <Input name="template_contract.summary" inputType="multiline" - label={i18n.str`Order summary`} - tooltip={i18n.str`Title of the order to be shown to the customer`} + label={i18n.str`Fixed summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} /> <InputCurrency name="template_contract.amount" - label={i18n.str`Order price`} - tooltip={i18n.str`total product price added up`} + label={i18n.str`Fixed price`} + tooltip={i18n.str`If specified, this template will create order with the same price`} /> <InputNumber name="template_contract.minimum_age" |