summaryrefslogtreecommitdiff
path: root/packages/auditor-backoffice-ui/src/paths
diff options
context:
space:
mode:
Diffstat (limited to 'packages/auditor-backoffice-ui/src/paths')
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx57
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx257
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx74
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx82
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx52
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts18
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx287
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx90
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx110
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx140
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx173
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx65
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx385
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx195
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx96
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx69
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx46
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx249
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx126
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx73
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx83
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx87
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts19
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx58
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx208
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx63
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx705
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx135
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx770
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx129
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx226
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx417
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx231
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx179
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx104
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx70
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx211
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx186
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx102
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx47
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx496
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx150
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx73
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx277
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx120
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx190
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx266
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx126
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx94
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx124
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx102
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx96
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx320
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx171
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx259
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx235
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx152
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx172
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx254
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx143
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx101
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx183
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx45
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx146
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx93
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx134
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx229
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx118
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx26
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx59
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx176
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx118
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx183
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx218
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx109
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx146
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/login/index.tsx202
-rw-r--r--packages/auditor-backoffice-ui/src/paths/notfound/index.tsx34
-rw-r--r--packages/auditor-backoffice-ui/src/paths/settings/index.tsx112
127 files changed, 15968 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx
new file mode 100644
index 000000000..91b6b4b56
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Instance/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "ARS",
+ version: "1",
+ }}
+ >
+ <Component {...args} />
+ </ConfigContextProvider>
+ );
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
+// export const Example = (a: any): VNode => <CreatePage {...a} />;
+// Example.args = {
+// isLoading: false
+// }
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx
new file mode 100644
index 000000000..d13b7e929
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -0,0 +1,257 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../components/form/FormProvider.js";
+import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
+import { undefinedIfEmpty } from "../../../utils/table.js";
+import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceConfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+ auth_token?: string;
+ default_pay_delay: Duration,
+ default_wire_transfer_delay: Duration,
+};
+
+interface Props {
+ onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>;
+ onBack?: () => void;
+ forceId?: string;
+}
+
+function with_defaults(id?: string): Partial<Entity> {
+ return {
+ id,
+ // accounts: [],
+ user_type: "business",
+ use_stefan: true,
+ default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours
+ default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days
+ };
+}
+
+export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
+ const [value, valueHandler] = useState(with_defaults(forceId));
+ const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
+ const [isTokenDialogActive, updateIsTokenDialogActive] =
+ useState<boolean>(false);
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ id: !value.id
+ ? i18n.str`required`
+ : !INSTANCE_ID_REGEX.test(value.id)
+ ? i18n.str`is not valid`
+ : undefined,
+ name: !value.name ? i18n.str`required` : undefined,
+
+ user_type: !value.user_type
+ ? i18n.str`required`
+ : value.user_type !== "business" && value.user_type !== "individual"
+ ? i18n.str`should be business or individual`
+ : undefined,
+ // accounts:
+ // !value.accounts || !value.accounts.length
+ // ? i18n.str`required`
+ // : undefinedIfEmpty(
+ // value.accounts.map((p) => {
+ // return !PAYTO_REGEX.test(p.payto_uri)
+ // ? i18n.str`is not valid`
+ // : undefined;
+ // }),
+ // ),
+ default_pay_delay: !value.default_pay_delay
+ ? i18n.str`required`
+ : !!value.default_wire_transfer_delay &&
+ value.default_wire_transfer_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ?
+ i18n.str`pay delay can't be greater than wire transfer delay` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n.str`required`
+ : undefined,
+ address: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ jurisdiction: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = (): Promise<void> => {
+ // use conversion instead of this
+ const newToken = value.auth_token;
+ value.auth_token = undefined;
+ value.auth = newToken === null || newToken === undefined
+ ? { method: "external" }
+ : { method: "token", token: `secret-token:${newToken}` };
+ if (!value.address) value.address = {};
+ if (!value.jurisdiction) value.jurisdiction = {};
+ // remove above use conversion
+ // schema.validateSync(value, { abortEarly: false })
+ value.default_pay_delay = Duration.toTalerProtocolDuration(value.default_pay_delay!) as any
+ value.default_wire_transfer_delay = Duration.toTalerProtocolDuration(value.default_wire_transfer_delay!) as any
+ // delete value.default_pay_delay;
+ // delete value.default_wire_transfer_delay;
+
+ return onCreate(value as any as MerchantBackend.Instances.InstanceConfigurationMessage);
+ };
+
+ function updateToken(token: string | null) {
+ valueHandler((old) => ({
+ ...old,
+ auth_token: token === null ? undefined : token,
+ }));
+ }
+
+ return (
+ <div>
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {isTokenDialogActive && (
+ <SetTokenNewInstanceModal
+ onCancel={() => {
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(false);
+ }}
+ onClear={() => {
+ updateToken(null);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ onConfirm={(newToken) => {
+ updateToken(newToken);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} />
+ </FormProvider>
+
+ <div class="level">
+ <div class="level-item has-text-centered">
+ <h1 class="title">
+ <button
+ class={
+ !isTokenSet
+ ? "button is-danger has-tooltip-bottom"
+ : !value.auth_token
+ ? "button has-tooltip-bottom"
+ : "button is-info has-tooltip-bottom"
+ }
+ data-tooltip={i18n.str`change authorization configuration`}
+ onClick={() => updateIsTokenDialogActive(true)}
+ >
+ <div class="icon is-centered">
+ <i class="mdi mdi-lock-reset" />
+ </div>
+ <span>
+ <i18n.Translate>Set access token</i18n.Translate>
+ </span>
+ </button>
+ </h1>
+ </div>
+ </div>
+ <div class="level">
+ <div class="level-item has-text-centered">
+ {!isTokenSet ? (
+ <p class="is-size-6">
+ <i18n.Translate>
+ Access token is not yet configured. This instance can't be
+ created.
+ </i18n.Translate>
+ </p>
+ ) : value.auth_token === undefined ? (
+ <p class="is-size-6">
+ <i18n.Translate>
+ No access token. Authorization must be handled externally.
+ </i18n.Translate>
+ </p>
+ ) : (
+ <p class="is-size-6">
+ <i18n.Translate>
+ Access token is set. Authorization is handled by the
+ merchant backend.
+ </i18n.Translate>
+ </p>
+ )}
+ </div>
+ </div>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submit}
+ disabled={hasErrors || !isTokenSet}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields and choose authorization method`
+ : "confirm operation"
+ }
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
new file mode 100644
index 000000000..c620c6482
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+
+export function InstanceCreatedSuccessfully({
+ entity,
+ onConfirm,
+}: {
+ entity: Entity;
+ onConfirm: () => void;
+}): VNode {
+ return (
+ <CreatedSuccessfully onConfirm={onConfirm}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.id} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Business Name</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.name} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Access token</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ {entity.auth.method === "external" && "external"}
+ {entity.auth.method === "token" && (
+ <input class="input" readonly value={entity.auth.token} />
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </CreatedSuccessfully>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx
new file mode 100644
index 000000000..23f41ecff
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useCredentialsChecker } from "../../../hooks/backend.js";
+import { useBackendContext } from "../../../context/backend.js";
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ forceId?: string;
+}
+export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage;
+
+export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
+ const { createInstance } = useAdminAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const { requestNewLoginToken } = useCredentialsChecker()
+ const { url: backendURL, updateToken } = useBackendContext()
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <CreatePage
+ onBack={onBack}
+ forceId={forceId}
+ onCreate={async (
+ d: MerchantBackend.Instances.InstanceConfigurationMessage,
+ ) => {
+ try {
+ await createInstance(d)
+ if (d.auth.token) {
+ const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+ }
+ onConfirm();
+ } catch (ex) {
+ if (ex instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: ex.message,
+ });
+ } else {
+ console.error(ex)
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx
new file mode 100644
index 000000000..0012f9b9b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx
@@ -0,0 +1,52 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Instance/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Internal: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const component = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "TESTKUDOS",
+ version: "1",
+ }}
+ >
+ <Internal {...(props as any)} />
+ </ConfigContextProvider>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
new file mode 100644
index 000000000..fdae1a24d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
@@ -0,0 +1,18 @@
+/*
+ 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/>
+ */
+
+// export * as list from "./list/stories.js";
+export * as create from "./create/stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
new file mode 100644
index 000000000..885a351d2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -0,0 +1,287 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+
+interface Props {
+ instances: MerchantBackend.Instances.Instance[];
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ onCreate: () => void;
+ selected?: boolean;
+ setInstanceName: (s: string) => void;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onUpdate,
+ onPurge,
+ setInstanceName,
+ onDelete,
+ selected,
+}: Props): VNode {
+ const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ useEffect(() => {
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "DELETE"
+ ) {
+ onDelete(actionQueue[0].element);
+ actionQueueHandler(actionQueue.slice(1));
+ }
+ }, [actionQueue, selected, onDelete]);
+
+ useEffect(() => {
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "UPDATE"
+ ) {
+ onUpdate(actionQueue[0].element.id);
+ actionQueueHandler(actionQueue.slice(1));
+ }
+ }, [actionQueue, selected, onUpdate]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-desktop-mac" />
+ </span>
+ <i18n.Translate>Instances</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options">
+ <button
+ class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"}
+ type="button"
+ onClick={(): void =>
+ actionQueueHandler(
+ buildActions(instances, rowSelection, "DELETE"),
+ )
+ }
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </div>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new instance`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onPurge={onPurge}
+ onUpdate={onUpdate}
+ setInstanceName={setInstanceName}
+ onDelete={onDelete}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: MerchantBackend.Instances.Instance[];
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ setInstanceName: (s: string) => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ setInstanceName,
+ instances,
+ onUpdate,
+ onDelete,
+ onPurge,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th class="is-checkbox-cell">
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ checked={rowSelection.length === instances.length}
+ onClick={(): void =>
+ rowSelectionHandler(
+ rowSelection.length === instances.length
+ ? []
+ : instances.map((i) => i.id),
+ )
+ }
+ />
+ <span class="check" />
+ </label>
+ </th>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Name</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td class="is-checkbox-cell">
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ checked={rowSelection.indexOf(i.id) != -1}
+ onClick={(): void =>
+ rowSelectionHandler(toggleSelected(i.id))
+ }
+ />
+ <span class="check" />
+ </label>
+ </td>
+ <td>
+ <a
+ href={`#/orders?instance=${i.id}`}
+ onClick={(e) => {
+ setInstanceName(i.id);
+ }}
+ >
+ {i.id}
+ </a>
+ </td>
+ <td>{i.name}</td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-small is-success jb-modal"
+ type="button"
+ onClick={(): void => onUpdate(i.id)}
+ >
+ <i18n.Translate>Edit</i18n.Translate>
+ </button>
+ {!i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal is-outlined"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ )}
+ {i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onPurge(i)}
+ >
+ <i18n.Translate>Purge</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no instances yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface Actions {
+ element: MerchantBackend.Instances.Instance;
+ type: "DELETE" | "UPDATE";
+}
+
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
+
+function buildActions(
+ instances: MerchantBackend.Instances.Instance[],
+ selected: string[],
+ action: "DELETE",
+): Actions[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
+ .filter(notEmpty)
+ .map((id) => ({ element: id, type: action }));
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
new file mode 100644
index 000000000..e0f5d5430
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h } from "preact";
+import { View } from "./View.js";
+
+export default {
+ title: "Pages/Instance/List",
+ component: View,
+ argTypes: {
+ onSelect: { action: "onSelect" },
+ },
+};
+
+export const Empty = (a: any) => <View {...a} />;
+Empty.args = {
+ instances: [],
+};
+
+export const WithDefaultInstance = (a: any) => <View {...a} />;
+WithDefaultInstance.args = {
+ instances: [
+ {
+ id: "default",
+ name: "the default instance",
+ merchant_pub: "abcdef",
+ payment_targets: [],
+ },
+ ],
+};
+
+export const WithFiveInstance = (a: any) => <View {...a} />;
+WithFiveInstance.args = {
+ instances: [
+ {
+ id: "first",
+ name: "the first instance",
+ merchant_pub: "abcdefgh",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "second",
+ name: "the second instance",
+ merchant_pub: "zxczxcz",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "third",
+ name: "the third instance",
+ merchant_pub: "QWEQWEWQE",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "other",
+ name: "the other instance",
+ merchant_pub: "FHJHGJGHJ",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "another",
+ name: "the another instance",
+ merchant_pub: "abcd3423423efgh",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "last",
+ name: "last instance",
+ merchant_pub: "zxcvvbnm",
+ payment_targets: ["pay-to", "asd"],
+ },
+ ],
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
new file mode 100644
index 000000000..b59112338
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+import { CardTable as CardTableActive } from "./TableActive.js";
+
+interface Props {
+ instances: MerchantBackend.Instances.Instance[];
+ onCreate: () => void;
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ selected?: boolean;
+ setInstanceName: (s: string) => void;
+}
+
+export function View({
+ instances,
+ onCreate,
+ onDelete,
+ onPurge,
+ onUpdate,
+ setInstanceName,
+ selected,
+}: Props): VNode {
+ const [show, setShow] = useState<"active" | "deleted" | null>("active");
+ const showIsActive = show === "active" ? "is-active" : "";
+ const showIsDeleted = show === "deleted" ? "is-active" : "";
+ const showAll = show === null ? "is-active" : "";
+ const { i18n } = useTranslationContext();
+
+ const showingInstances = showIsDeleted
+ ? instances.filter((i) => i.deleted)
+ : showIsActive
+ ? instances.filter((i) => !i.deleted)
+ : instances;
+
+ return (
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-two-thirds">
+ <div class="tabs" style={{ overflow: "inherit" }}>
+ <ul>
+ <li class={showIsActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Only show active instances`}
+ >
+ <a onClick={() => setShow("active")}>
+ <i18n.Translate>Active</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showIsDeleted}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Only show deleted instances`}
+ >
+ <a onClick={() => setShow("deleted")}>
+ <i18n.Translate>Deleted</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showAll}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Show all instances`}
+ >
+ <a onClick={() => setShow(null)}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <CardTableActive
+ instances={showingInstances}
+ onDelete={onDelete}
+ onPurge={onPurge}
+ setInstanceName={setInstanceName}
+ onUpdate={onUpdate}
+ selected={selected}
+ onCreate={onCreate}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
new file mode 100644
index 000000000..2f839291b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
@@ -0,0 +1,140 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { View } from "./View.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onCreate: () => void;
+ onUpdate: (id: string) => void;
+ instances: MerchantBackend.Instances.Instance[];
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ setInstanceName: (s: string) => void;
+}
+
+export default function Instances({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onCreate,
+ onUpdate,
+ setInstanceName,
+}: Props): VNode {
+ const result = useBackendInstances();
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Instances.Instance | null>(null);
+ const [purging, setPurging] =
+ useState<MerchantBackend.Instances.Instance | null>(null);
+ const { deleteInstance, purgeInstance } = useAdminAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <View
+ instances={result.data.instances}
+ onDelete={setDeleting}
+ onCreate={onCreate}
+ onPurge={setPurging}
+ onUpdate={onUpdate}
+ setInstanceName={setInstanceName}
+ selected={!!deleting}
+ />
+ {deleting && (
+ <DeleteModal
+ element={deleting}
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteInstance(deleting.id);
+ // pushNotification({ message: 'delete_success', type: 'SUCCESS' })
+ setNotif({
+ message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ // pushNotification({ message: 'delete_error', type: 'ERROR' })
+ }
+ setDeleting(null);
+ }}
+ />
+ )}
+ {purging && (
+ <PurgeModal
+ element={purging}
+ onCancel={() => setPurging(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await purgeInstance(purging.id);
+ setNotif({
+ message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to purge instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setPurging(null);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
new file mode 100644
index 000000000..3336c53a4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Accounts/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
new file mode 100644
index 000000000..6e4786a47
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -0,0 +1,173 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string };
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const accountAuthType = ["none", "basic"];
+
+function isValidURL(s: string): boolean {
+ try {
+ const u = new URL(s)
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+ const errors: FormErrors<Entity> = {
+ payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
+
+ credit_facade_credentials: !state.credit_facade_credentials
+ ? undefined
+ : undefinedIfEmpty({
+ username:
+ state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username
+ ? i18n.str`required`
+ : undefined,
+ password:
+ state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password
+ ? i18n.str`required`
+ : undefined,
+ }),
+ credit_facade_url: !state.credit_facade_url
+ ? undefined
+ : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url`
+ : undefined,
+ repeatPassword:
+ !state.credit_facade_credentials
+ ? undefined
+ : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword)
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ delete state.repeatPassword
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<Entity>
+ name="payto_uri"
+ label={i18n.str`Account`}
+ />
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Account info URL`}
+ help="https://bank.com"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
+ />
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none") return "Without authentication";
+ return "Username and password";
+ }}
+ />
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="repeatPassword"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx
new file mode 100644
index 000000000..7d33d25ce
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { useBankAccountAPI } from "../../../../hooks/bank.js";
+
+export type Entity = MerchantBackend.BankAccounts.AccountAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createBankAccount } = useBankAccountAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createBankAccount(request)
+ .then((d) => {
+ onConfirm()
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
new file mode 100644
index 000000000..6b4b63735
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Accounts/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
new file mode 100644
index 000000000..24da755b9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: MerchantBackend.BankAccounts.BankAccountEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+ onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ accounts={devices.map((o) => ({
+ ...o,
+ id: String(o.h_wire),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
new file mode 100644
index 000000000..7d6db0782
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -0,0 +1,385 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util";
+
+type Entity = MerchantBackend.BankAccounts.BankAccountEntry;
+
+interface Props {
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ accounts,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Bank accounts</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new accounts`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {accounts.length > 0 ? (
+ <Table
+ accounts={accounts}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ accounts,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], }
+ const accountsByType = accounts.reduce((prev, acc) => {
+ const parsed = parsePaytoUri(acc.payto_uri)
+ if (!parsed) return prev //skip
+ if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") {
+ prev["unknown"].push({ parsed, acc })
+ } else {
+ prev[parsed.targetType].push({ parsed, acc })
+ }
+ return prev
+ }, emptyList)
+
+ const bitcoinAccounts = accountsByType["bitcoin"]
+ const talerbankAccounts = accountsByType["x-taler-bank"]
+ const ibanAccounts = accountsByType["iban"]
+ const unkownAccounts = accountsByType["unknown"]
+
+
+ return (
+ <Fragment>
+
+ {bitcoinAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 1</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 2</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {bitcoinAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriBitcoin
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[0]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[1]}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+
+
+ {talerbankAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Host</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {talerbankAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriTalerBank
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.host}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.account}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {ibanAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>IBAN</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>BIC</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {ibanAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriIBAN
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.params["receiver-name"]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.iban}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.bic ?? ""}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {unkownAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Path</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {unkownAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriUnknown
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetType}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+ </Fragment>
+
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no accounts yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx
new file mode 100644
index 000000000..100241e22
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListOtpDevices({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteBankAccount } = useBankAccountAPI();
+ const result = useInstanceBankAccounts({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.accounts}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.h_wire);
+ }}
+ onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) =>
+ deleteBankAccount(e.h_wire)
+ .then(() =>
+ setNotif({
+ message: i18n.str`bank account delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the bank account`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
new file mode 100644
index 000000000..d6b1d65e0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
new file mode 100644
index 000000000..0d20879e8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -0,0 +1,195 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = MerchantBackend.BankAccounts.BankAccountEntry
+ & WithId;
+
+const accountAuthType = ["unedit", "none", "basic"];
+interface Props {
+ onUpdate: (d: MerchantBackend.BankAccounts.AccountPatchDetails) => Promise<void>;
+ onBack?: () => void;
+ account: Entity;
+}
+
+
+export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<MerchantBackend.BankAccounts.AccountPatchDetails>>(account);
+
+ const errors: FormErrors<MerchantBackend.BankAccounts.AccountPatchDetails> = {
+ credit_facade_url: !state.credit_facade_url ? i18n.str`required` : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined,
+ credit_facade_credentials: undefinedIfEmpty({
+
+ username: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !state.credit_facade_credentials.username ? i18n.str`required` : undefined,
+
+ password: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !state.credit_facade_credentials.password ? i18n.str`required` : undefined,
+
+ repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` :
+ (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`doesn't match`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+
+ const creds: typeof state.credit_facade_credentials =
+ state.credit_facade_credentials?.type === "basic" ? {
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ } : state.credit_facade_credentials?.type === "none" ? {
+ type: "none"
+ } : undefined;
+
+ return onUpdate({
+ credit_facade_credentials: creds,
+ credit_facade_url: state.credit_facade_url,
+ });
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Account: <b>{account.id.substring(0, 8)}...</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<Entity>
+ name="payto_uri"
+ label={i18n.str`Account`}
+ readonly
+ />
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Account info URL`}
+ help="https://bank.com"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
+ />
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none") return "Without authentication";
+ if (str === "basic") return "With authentication";
+ return "Do not change"
+ }}
+ />
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.repeatPassword"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
+
+function isValidURL(s: string): boolean {
+ try {
+ const u = new URL(s)
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx
new file mode 100644
index 000000000..44dee7651
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -0,0 +1,96 @@
+/*
+ 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 { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ bid: string;
+}
+export default function UpdateValidator({
+ bid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateBankAccount } = useBankAccountAPI();
+ const result = useBankAccountDetails(bid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ account={{ ...result.data, id: bid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateBankAccount(bid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx
new file mode 100644
index 000000000..2fc0819bb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Product/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx
new file mode 100644
index 000000000..becaf8f3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductAddDetail & {
+ product_id: string;
+};
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onCreate(result);
+ return Promise.reject();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm onSubscribe={addFormSubmitter} />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..573064aea
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+import emptyImage from "../../assets/empty.png";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Image</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Description</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Price</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ </p>
+ </div>
+ </div>
+ </div>
+ </Template>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx
new file mode 100644
index 000000000..99599cfab
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx
@@ -0,0 +1,46 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { AuditorBackend } from "../../../../declaration.js";
+import { useDepositConfirmationAPI } from "../../../../hooks/deposit_confirmations.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
+ const { createDepositConfirmation } = useDepositConfirmationAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx
new file mode 100644
index 000000000..41c297d5b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
+
+export default {
+ title: "Pages/Product/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onSelect: { action: "onSelect" },
+ onDelete: { action: "onDelete" },
+ onUpdate: { action: "onUpdate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx
new file mode 100644
index 000000000..2c97b59e8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx
@@ -0,0 +1,249 @@
+/*
+ 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 { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import emptyImage from "../../../../assets/empty.png";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { AuditorBackend, WithId } from "../../../../declaration.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId;
+
+interface Props {
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ onSelect: (depositConfirmation: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ ) => Promise<void>;
+ onCreate: () => void;
+ selected?: boolean;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <i18n.Translate>Deposit Confirmations</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add deposit-confirmation`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ onUpdate={onUpdate}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string | undefined;
+ instances: Entity[];
+ onSelect: (id: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ ) => Promise<void>;
+ onDelete: (serial_id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string | undefined>;
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Price per unit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Taxes</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sales</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Stock</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sold</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+
+ return (
+ <Fragment key={i.id}>
+ <tr key="info">
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ </td>
+
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`go to product update page`}
+ >
+ <button
+ class="button is-small is-success "
+ type="button"
+ onClick={(): void => onSelect(i)}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </span>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`remove this product from the database`}
+ >
+ <button
+ class="button is-small is-danger"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </td>
+ </tr>
+ {rowSelection === i.id && (
+ <tr key="form">
+ <td colSpan={10}>
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+interface FastProductUpdate {
+ incoming: number;
+ lost: number;
+ price: string;
+}
+interface UpdatePrice {
+ price: string;
+}
+
+
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no products yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+function difference(price: string, tax: number) {
+ if (!tax) return price;
+ const ps = price.split(":");
+ const p = parseInt(ps[1], 10);
+ ps[1] = `${p - tax}`;
+ return ps.join(":");
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx
new file mode 100644
index 000000000..a99cfd2ef
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx
@@ -0,0 +1,126 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { AuditorBackend, WithId } from "../../../../declaration.js";
+import {
+ useDepositConfirmation,
+ useDepositConfirmationAPI,
+} from "../../../../hooks/deposit_confirmations.js";
+import { Notification } from "../../../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onLoadError: (e: HttpError<AuditorBackend.ErrorDetail>) => VNode;
+}
+export default function DepositConfirmationList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const result = useDepositConfirmation();
+ const { deleteDepositConfirmation, updateDepositConfirmation, getDepositConfirmation } = useDepositConfirmationAPI();
+ const [deleting, setDeleting] =
+ useState<AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId | null>(null);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getDepositConfirmation}
+ onSelect={onSelect}
+ description={i18n.str`jump to deposit_confirmation with the given serial ID`}
+ placeholder={i18n.str`serial id`}
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete deposit-confirmation`}
+ description={`Delete the deposit-cofirmation "${deleting.serial_id}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteDepositConfirmation(deleting.serial_id);
+ setNotif({
+ message: i18n.str`Deposit-confirmation "${deleting.serial_id}" (ID: ${deleting.serial_id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete deposit-confirmation`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the deposit-confirmation (ID:{" "}
+ <b>{deleting.serial_id}</b>), the stock and related information will be lost
+ </p>
+ <p class="warning">
+ Deleting a deposit-confirmation <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx
new file mode 100644
index 000000000..a85b13b8b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Product/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithManagedStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: 15,
+ unit: "bar",
+ address: {},
+ },
+});
+
+export const WithInfiniteStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: -1,
+ unit: "bar",
+ address: {},
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx
new file mode 100644
index 000000000..97715171e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ product: Entity;
+}
+
+export function UpdatePage({ product, onUpdate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onUpdate(result);
+ return Promise.resolve();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>Product id:</i18n.Translate>
+ <b>{product.product_id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm
+ initial={product}
+ onSubscribe={addFormSubmitter}
+ alreadyExist
+ />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx
new file mode 100644
index 000000000..8e0f7647f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx
@@ -0,0 +1,95 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Products.ProductAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ pid: string;
+}
+export default function UpdateProduct({
+ pid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateProduct } = useProductAPI();
+ const result = useProductDetails(pid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ product={{ ...result.data, product_id: pid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateProduct(pid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx
new file mode 100644
index 000000000..21dadb1e3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "../../../components/form/FormProvider.js";
+import { Input } from "../../../components/form/Input.js";
+import { MerchantBackend } from "../../../declaration.js";
+
+type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage;
+interface Props {
+ onUpdate: () => void;
+ onDelete: () => void;
+ selected: MerchantBackend.Instances.QueryInstancesResponse;
+}
+
+function convert(
+ from: MerchantBackend.Instances.QueryInstancesResponse,
+): Entity {
+ const defaults = {
+ default_wire_fee_amortization: 1,
+ use_stefan: true,
+ default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour
+ default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours
+ };
+ return { ...defaults, ...from };
+}
+
+export function DetailPage({ selected }: Props): VNode {
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">Here goes the instance description</h1>
+ </div>
+ </div>
+ <div class="level-right" style="display: none;">
+ <div class="level-item" />
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-6">
+ <FormProvider<Entity> object={value} valueHandler={valueHandler}>
+ <Input<Entity> name="name" readonly label={i18n.str`Name`} />
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx
new file mode 100644
index 000000000..9b393b818
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { DeleteModal } from "../../../components/modal/index.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onUpdate: () => void;
+ onNotFound: () => VNode;
+ onDelete: () => void;
+}
+
+export default function Detail({
+ onUpdate,
+ onLoadError,
+ onUnauthorized,
+ onDelete,
+ onNotFound,
+}: Props): VNode {
+ const { id } = useInstanceContext();
+ const result = useInstanceDetails();
+ const [deleting, setDeleting] = useState<boolean>(false);
+
+ const { deleteInstance } = useInstanceAPI();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <DetailPage
+ selected={result.data}
+ onUpdate={onUpdate}
+ onDelete={() => setDeleting(true)}
+ />
+ {deleting && (
+ <DeleteModal
+ element={{ name: result.data.name, id }}
+ onCancel={() => setDeleting(false)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteInstance();
+ onDelete();
+ } catch (error) {
+ //FIXME: show message error
+ }
+ setDeleting(false);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx
new file mode 100644
index 000000000..367fabce2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Instance/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Internal: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const component = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "TESTKUDOS",
+ version: "1",
+ }}
+ >
+ <Internal {...(props as any)} />
+ </ConfigContextProvider>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {
+ selected: {
+ name: "name",
+ auth: { method: "external" },
+ address: {},
+ user_type: "business",
+ jurisdiction: {},
+ use_stefan: true,
+ default_pay_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ merchant_pub: "ASDWQEKASJDKSADJ",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
new file mode 100644
index 000000000..1d8c76ff9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
@@ -0,0 +1,19 @@
+/*
+ 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/>
+ */
+
+export * as details from "./details/stories.js";
+export * as kycList from "./kyc/list/ListPage.stories.js";
+export * as reserve from "./reserves/create/CreatedSuccessfully.stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
new file mode 100644
index 000000000..d33f64ada
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
@@ -0,0 +1,58 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { MerchantBackend } from "../../../../declaration.js";
+
+export default {
+ title: "Pages/KYC/List",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const Example = tests.createExample(TestedComponent, {
+ status: {
+ timeout_kycs: [],
+ pending_kycs: [
+ {
+ aml_status: 0,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ kyc_url: "http://exchange.taler/kyc",
+ },
+ {
+ aml_status: 1,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ },
+ {
+ aml_status: 2,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ },
+ ],
+ } as MerchantBackend.KYC.AccountKycRedirects,
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
new file mode 100644
index 000000000..338081886
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
@@ -0,0 +1,208 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+
+export interface Props {
+ status: MerchantBackend.KYC.AccountKycRedirects;
+}
+
+export function ListPage({ status }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <section class="section is-main-section">
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ <i18n.Translate>Pending KYC verification</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {status.pending_kycs.length > 0 ? (
+ <PendingTable entries={status.pending_kycs} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {status.timeout_kycs.length > 0 ? (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ <i18n.Translate>Timed out</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {status.timeout_kycs.length > 0 ? (
+ <TimedOutTable entries={status.timeout_kycs} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ ) : undefined}
+ </section>
+ );
+}
+interface PendingTableProps {
+ entries: MerchantBackend.KYC.MerchantAccountKycRedirect[];
+}
+
+interface TimedOutTableProps {
+ entries: MerchantBackend.KYC.ExchangeKycTimeout[];
+}
+
+function PendingTable({ entries }: PendingTableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Exchange</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Target account</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Reason</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {entries.map((e, i) => {
+ if (e.kyc_url === undefined) {
+ // blocked by AML
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.payto_uri}</td>
+ <td>
+ {e.aml_status === 1 ? (
+ <i18n.Translate>
+ There is an anti-money laundering process pending to
+ complete.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ The account is frozen due to the anti-money laundering
+ rules. Contact the exchange service provider for further
+ instructions.
+ </i18n.Translate>
+ )}
+ </td>
+ </tr>
+ );
+ } else {
+ // blocked by KYC
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.payto_uri}</td>
+ <td>
+ <a href={e.kyc_url} target="_black" rel="noreferrer">
+ <i18n.Translate>
+ Pending KYC process, click here to complete
+ </i18n.Translate>
+ </a>
+ </td>
+ </tr>
+ );
+ }
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function TimedOutTable({ entries }: TimedOutTableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Exchange</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Code</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Http Status</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {entries.map((e, i) => {
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.exchange_code}</td>
+ <td>{e.exchange_http_status}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-happy mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>No pending kyc verification!</i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
new file mode 100644
index 000000000..5b93ac169
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
@@ -0,0 +1,63 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+}
+
+export default function ListKYC({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+}: Props): VNode {
+ const result = useInstanceKYCDetails();
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const status = result.data.type === "ok" ? undefined : result.data.status;
+
+ if (!status) {
+ return <div>no kyc required</div>;
+ }
+ return <ListPage status={status} />;
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
new file mode 100644
index 000000000..bd9f65718
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Order/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ instanceConfig: {
+ default_pay_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ use_stefan: true,
+ },
+ instanceInventory: [
+ {
+ id: "t-shirt-1",
+ description: "a m size t-shirt",
+ price: "TESTKUDOS:1",
+ total_stock: -1,
+ },
+ {
+ id: "t-shirt-2",
+ price: "TESTKUDOS:1",
+ description: "a xl size t-shirt",
+ } as any,
+ {
+ id: "t-shirt-3",
+ price: "TESTKUDOS:1",
+ description: "a s size t-shirt",
+ } as any,
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
new file mode 100644
index 000000000..62ceaa24b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
@@ -0,0 +1,705 @@
+/*
+ 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 { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format, isFuture } from "date-fns";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputLocation } from "../../../../components/form/InputLocation.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js";
+import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js";
+import { ProductList } from "../../../../components/product/ProductList.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { useSettings } from "../../../../hooks/useSettings.js";
+import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
+import { rate } from "../../../../utils/amount.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+interface Props {
+ onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
+ onBack?: () => void;
+ instanceConfig: InstanceConfig;
+ instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
+}
+interface InstanceConfig {
+ use_stefan: boolean;
+ default_pay_delay: TalerProtocolDuration;
+ default_wire_transfer_delay: TalerProtocolDuration;
+}
+
+function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> {
+ const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay);
+ const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay);
+
+ return {
+ inventoryProducts: {},
+ products: [],
+ pricing: {},
+ payments: {
+ max_fee: undefined,
+ createToken: true,
+ pay_deadline: (defaultPayDeadline),
+ refund_deadline: (defaultPayDeadline),
+ wire_transfer_deadline: (defaultWireDeadline),
+ },
+ shipping: {},
+ extra: {},
+ };
+}
+
+interface ProductAndQuantity {
+ product: MerchantBackend.Products.ProductDetail & WithId;
+ quantity: number;
+}
+export interface ProductMap {
+ [id: string]: ProductAndQuantity;
+}
+
+interface Pricing {
+ products_price: string;
+ order_price: string;
+ summary: string;
+}
+interface Shipping {
+ delivery_date?: Date;
+ delivery_location?: MerchantBackend.Location;
+ fullfilment_url?: string;
+}
+interface Payments {
+ refund_deadline: Duration;
+ pay_deadline: Duration;
+ wire_transfer_deadline: Duration;
+ auto_refund_deadline: Duration;
+ max_fee?: string;
+ createToken: boolean;
+ minimum_age?: number;
+}
+interface Entity {
+ inventoryProducts: ProductMap;
+ products: MerchantBackend.Product[];
+ pricing: Partial<Pricing>;
+ payments: Partial<Payments>;
+ shipping: Partial<Shipping>;
+ extra: Record<string, string>;
+}
+
+const stringIsValidJSON = (value: string) => {
+ try {
+ JSON.parse(value.trim());
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+export function CreatePage({
+ onCreate,
+ onBack,
+ instanceConfig,
+ instanceInventory,
+}: Props): VNode {
+ const config = useConfigContext();
+ const instance_default = with_defaults(instanceConfig, config.currency)
+ const [value, valueHandler] = useState(instance_default);
+ const zero = Amounts.zeroOfCurrency(config.currency);
+ const [settings, updateSettings] = useSettings()
+ const inventoryList = Object.values(value.inventoryProducts || {});
+ const productList = Object.values(value.products || {});
+
+ const { i18n } = useTranslationContext();
+
+ const parsedPrice = !value.pricing?.order_price
+ ? undefined
+ : Amounts.parse(value.pricing.order_price);
+
+ const errors: FormErrors<Entity> = {
+ pricing: undefinedIfEmpty({
+ summary: !value.pricing?.summary ? i18n.str`required` : undefined,
+ order_price: !value.pricing?.order_price
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ }),
+ payments: undefinedIfEmpty({
+ refund_deadline: !value.payments?.refund_deadline
+ ? undefined
+ : value.payments.pay_deadline &&
+ Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1
+ ? i18n.str`refund deadline cannot be before pay deadline`
+ : value.payments.wire_transfer_deadline &&
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.refund_deadline,
+ ) === -1
+ ? i18n.str`wire transfer deadline cannot be before refund deadline`
+ : undefined,
+ pay_deadline: !value.payments?.pay_deadline
+ ? i18n.str`required`
+ : value.payments.wire_transfer_deadline &&
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.pay_deadline,
+ ) === -1
+ ? i18n.str`wire transfer deadline cannot be before pay deadline`
+ : undefined,
+ wire_transfer_deadline: !value.payments?.wire_transfer_deadline
+ ? i18n.str`required`
+ : undefined,
+ auto_refund_deadline: !value.payments?.auto_refund_deadline
+ ? undefined
+ : !value.payments?.refund_deadline
+ ? i18n.str`should have a refund deadline`
+ : Duration.cmp(
+ value.payments.refund_deadline,
+ value.payments.auto_refund_deadline,
+ ) == -1
+ ? i18n.str`auto refund cannot be after refund deadline`
+ : undefined,
+
+ }),
+ shipping: undefinedIfEmpty({
+ delivery_date: !value.shipping?.delivery_date
+ ? undefined
+ : !isFuture(value.shipping.delivery_date)
+ ? i18n.str`should be in the future`
+ : undefined,
+ }),
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = (): void => {
+ const order = value as any; //schema.cast(value);
+ if (!value.payments) return;
+ if (!value.shipping) return;
+
+ const request: MerchantBackend.Orders.PostOrderRequest = {
+ order: {
+ amount: order.pricing.order_price,
+ summary: order.pricing.summary,
+ products: productList,
+ extra: undefinedIfEmpty(value.extra),
+ pay_deadline: !value.payments.pay_deadline ?
+ i18n.str`required` :
+ AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline))
+ ,// : undefined,
+ wire_transfer_deadline: value.payments.wire_transfer_deadline
+ ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline))
+ : undefined,
+ refund_deadline: value.payments.refund_deadline
+ ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline))
+ : undefined,
+ auto_refund: value.payments.auto_refund_deadline
+ ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline)
+ : undefined,
+ max_fee: value.payments.max_fee as string,
+
+ delivery_date: value.shipping.delivery_date
+ ? { t_s: value.shipping.delivery_date.getTime() / 1000 }
+ : undefined,
+ delivery_location: value.shipping.delivery_location,
+ fulfillment_url: value.shipping.fullfilment_url,
+ minimum_age: value.payments.minimum_age,
+ },
+ inventory_products: inventoryList.map((p) => ({
+ product_id: p.product.id,
+ quantity: p.quantity,
+ })),
+ create_token: value.payments.createToken,
+ };
+
+ onCreate(request);
+ };
+
+ const addProductToTheInventoryList = (
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number,
+ ) => {
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ inventoryProducts[product.id] = { product, quantity };
+ return { ...v, inventoryProducts };
+ });
+ };
+
+ const removeProductFromTheInventoryList = (id: string) => {
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ delete inventoryProducts[id];
+ return { ...v, inventoryProducts };
+ });
+ };
+
+ const addNewProduct = async (product: MerchantBackend.Product) => {
+ return valueHandler((v) => {
+ const products = v.products ? [...v.products, product] : [];
+ return { ...v, products };
+ });
+ };
+
+ const removeFromNewProduct = (index: number) => {
+ valueHandler((v) => {
+ const products = v.products ? [...v.products] : [];
+ products.splice(index, 1);
+ return { ...v, products };
+ });
+ };
+
+ const [editingProduct, setEditingProduct] = useState<
+ MerchantBackend.Product | undefined
+ >(undefined);
+
+ const totalPriceInventory = inventoryList.reduce((prev, cur) => {
+ const p = Amounts.parseOrThrow(cur.product.price);
+ return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
+ }, zero);
+
+ const totalPriceProducts = productList.reduce((prev, cur) => {
+ if (!cur.price) return zero;
+ const p = Amounts.parseOrThrow(cur.price);
+ return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
+ }, zero);
+
+ const hasProducts = inventoryList.length > 0 || productList.length > 0;
+ const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts);
+
+ const totalAsString = Amounts.stringify(totalPrice.amount);
+ const allProducts = productList.concat(inventoryList.map(asProduct));
+
+ const [newField, setNewField] = useState("")
+
+ useEffect(() => {
+ valueHandler((v) => {
+ return {
+ ...v,
+ pricing: {
+ ...v.pricing,
+ products_price: hasProducts ? totalAsString : undefined,
+ order_price: hasProducts ? totalAsString : undefined,
+ },
+ };
+ });
+ }, [hasProducts, totalAsString]);
+
+ const discountOrRise = rate(
+ parsedPrice ?? Amounts.zeroOfCurrency(config.currency),
+ totalPrice.amount,
+ );
+
+ const minAgeByProducts = allProducts.reduce(
+ (cur, prev) =>
+ !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age,
+ 0,
+ );
+
+ // if there is no default pay deadline
+ const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline
+ // and there is no default wire deadline
+ const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline
+ // user required to set the taler options
+ const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline
+
+
+ return (
+ <div>
+
+ <section class="section is-main-section">
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
+ updateSettings({
+ ...settings,
+ advanceOrderMode: false
+ })
+ }}>
+ <a >
+ <span><i18n.Translate>Simple</i18n.Translate></span>
+ </a>
+ </li>
+ <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
+ updateSettings({
+ ...settings,
+ advanceOrderMode: true
+ })
+ }}>
+ <a >
+ <span><i18n.Translate>Advanced</i18n.Translate></span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {/* // FIXME: translating plural singular */}
+ <InputGroup
+ name="inventory_products"
+ label={i18n.str`Manage products in order`}
+ alternative={
+ allProducts.length > 0 && (
+ <p>
+ {allProducts.length} products with a total price of{" "}
+ {totalAsString}.
+ </p>
+ )
+ }
+ tooltip={i18n.str`Manage list of products in the order.`}
+ >
+ <InventoryProductForm
+ currentProducts={value.inventoryProducts || {}}
+ onAddProduct={addProductToTheInventoryList}
+ inventory={instanceInventory}
+ />
+
+ {settings.advanceOrderMode &&
+ <NonInventoryProductFrom
+ productToEdit={editingProduct}
+ onAddProduct={(p) => {
+ setEditingProduct(undefined);
+ return addNewProduct(p);
+ }}
+ />
+ }
+
+ {allProducts.length > 0 && (
+ <ProductList
+ list={allProducts}
+ actions={[
+ {
+ name: i18n.str`Remove`,
+ tooltip: i18n.str`Remove this product from the order.`,
+ handler: (e, index) => {
+ if (e.product_id) {
+ removeProductFromTheInventoryList(e.product_id);
+ } else {
+ removeFromNewProduct(index);
+ setEditingProduct(e);
+ }
+ },
+ },
+ ]}
+ />
+ )}
+ </InputGroup>
+
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ {hasProducts ? (
+ <Fragment>
+ <InputCurrency
+ name="pricing.products_price"
+ label={i18n.str`Total price`}
+ readonly
+ tooltip={i18n.str`total product price added up`}
+ />
+ <InputCurrency
+ name="pricing.order_price"
+ label={i18n.str`Total price`}
+ addonAfter={
+ discountOrRise > 0 &&
+ (discountOrRise < 1
+ ? `discount of %${Math.round(
+ (1 - discountOrRise) * 100,
+ )}`
+ : `rise of %${Math.round((discountOrRise - 1) * 100)}`)
+ }
+ tooltip={i18n.str`Amount to be paid by the customer`}
+ />
+ </Fragment>
+ ) : (
+ <InputCurrency
+ name="pricing.order_price"
+ label={i18n.str`Order price`}
+ tooltip={i18n.str`final order price`}
+ />
+ )}
+
+ <Input
+ name="pricing.summary"
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+
+ {settings.advanceOrderMode &&
+ <InputGroup
+ name="shipping"
+ label={i18n.str`Shipping and Fulfillment`}
+ initialActive
+ >
+ <InputDate
+ name="shipping.delivery_date"
+ label={i18n.str`Delivery date`}
+ tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
+ />
+ {value.shipping?.delivery_date && (
+ <InputGroup
+ name="shipping.delivery_location"
+ label={i18n.str`Location`}
+ tooltip={i18n.str`address where the products will be delivered`}
+ >
+ <InputLocation name="shipping.delivery_location" />
+ </InputGroup>
+ )}
+ <Input
+ name="shipping.fullfilment_url"
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
+ />
+ </InputGroup>
+ }
+
+ {(settings.advanceOrderMode || requiresSomeTalerOptions) &&
+ <InputGroup
+ name="payments"
+ label={i18n.str`Taler payment options`}
+ tooltip={i18n.str`Override default Taler payment settings for this order`}
+ >
+ {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDuration
+ name="payments.pay_deadline"
+ label={i18n.str`Payment time`}
+ help={<DeadlineHelp duration={value.payments?.pay_deadline} />}
+ withForever
+ withoutClear
+ tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`}
+ side={
+ <span>
+ <button class="button" onClick={() => {
+ const c = {
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ pay_deadline: instance_default.payments?.pay_deadline
+ }
+ }
+ valueHandler(c)
+ }}>
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />}
+ {settings.advanceOrderMode && <InputDuration
+ name="payments.refund_deadline"
+ label={i18n.str`Refund time`}
+ help={<DeadlineHelp duration={value.payments?.refund_deadline} />}
+ withForever
+ withoutClear
+ tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`}
+ side={
+ <span>
+ <button class="button" onClick={() => {
+ valueHandler({
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ refund_deadline: instance_default.payments?.refund_deadline
+ }
+ })
+ }}>
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />}
+ {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDuration
+ name="payments.wire_transfer_deadline"
+ label={i18n.str`Wire transfer time`}
+ help={<DeadlineHelp duration={value.payments?.wire_transfer_deadline} />}
+ withoutClear
+ withForever
+ tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`}
+ side={
+ <span>
+ <button class="button" onClick={() => {
+ valueHandler({
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline
+ }
+ })
+ }}>
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />}
+ {settings.advanceOrderMode && <InputDuration
+ name="payments.auto_refund_deadline"
+ label={i18n.str`Auto-refund time`}
+ help={<DeadlineHelp duration={value.payments?.auto_refund_deadline} />}
+ tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
+ withForever
+ />}
+
+ {settings.advanceOrderMode && <InputCurrency
+ name="payments.max_fee"
+ label={i18n.str`Maximum fee`}
+ tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
+ />}
+ {settings.advanceOrderMode && <InputToggle
+ name="payments.createToken"
+ label={i18n.str`Create token`}
+ tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`}
+ />}
+ {settings.advanceOrderMode && <InputNumber
+ name="payments.minimum_age"
+ label={i18n.str`Minimum age required`}
+ tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
+ help={
+ minAgeByProducts > 0
+ ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
+ : i18n.str`No product with age restriction in this order`
+ }
+ />}
+ </InputGroup>
+ }
+
+ {settings.advanceOrderMode &&
+ <InputGroup
+ name="extra"
+ label={i18n.str`Additional information`}
+ tooltip={i18n.str`Custom information to be included in the contract for this order.`}
+ >
+ {Object.keys(value.extra ?? {}).map((key) => {
+
+ return <Input
+ name={`extra.${key}`}
+ inputType="multiline"
+ label={key}
+ tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
+ side={
+ <button class="button" onClick={(e) => {
+ if (value.extra && value.extra[key] !== undefined) {
+ console.log(value.extra)
+ delete value.extra[key]
+ }
+ valueHandler({
+ ...value,
+ })
+ }}>remove</button>
+ }
+ />
+ })}
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Custom field name</i18n.Translate>
+ <span class="icon has-tooltip-right" data-tooltip={"new extra field"}>
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} />
+ </p>
+ </div>
+ </div>
+ <button class="button" onClick={(e) => {
+ setNewField("")
+ valueHandler({
+ ...value,
+ extra: {
+ ...(value.extra ?? {}),
+ [newField]: ""
+ }
+ })
+ }}>add</button>
+ </div>
+ </InputGroup>
+ }
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <button
+ class="button is-success"
+ onClick={submit}
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+function asProduct(p: ProductAndQuantity): MerchantBackend.Product {
+ return {
+ product_id: p.product.id,
+ image: p.product.image,
+ price: p.product.price,
+ unit: p.product.unit,
+ quantity: p.quantity,
+ description: p.product.description,
+ taxes: p.product.taxes,
+ minimum_age: p.product.minimum_age,
+ };
+}
+
+
+function DeadlineHelp({ duration }: { duration?: Duration }): VNode {
+ const { i18n } = useTranslationContext();
+ const [now, setNow] = useState(AbsoluteTime.now())
+ useEffect(() => {
+ const iid = setInterval(() => {
+ setNow(AbsoluteTime.now())
+ }, 60 * 1000)
+ return () => {
+ clearInterval(iid)
+ }
+ })
+ if (!duration) return <i18n.Translate>Disabled</i18n.Translate>
+ const when = AbsoluteTime.addDuration(now, duration)
+ if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate>
+ return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate>
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
new file mode 100644
index 000000000..88a984c97
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
@@ -0,0 +1,114 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { useOrderAPI } from "../../../../hooks/order.js";
+import { Entity } from "./index.js";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function OrderCreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const { getPaymentURL } = useOrderAPI();
+ const [url, setURL] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ useEffect(() => {
+ getPaymentURL(entity.response.order_id).then((response) => {
+ setURL(response.data);
+ });
+ }, [getPaymentURL, entity.response.order_id]);
+
+ return (
+ <CreatedSuccessfully
+ onConfirm={onConfirm}
+ onCreateAnother={onCreateAnother}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Amount</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.request.order.amount}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Summary</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.request.order.summary}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Order ID</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.response.order_id} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Payment URL</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={url} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </CreatedSuccessfully>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
new file mode 100644
index 000000000..2474fd042
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
@@ -0,0 +1,114 @@
+/*
+ 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 { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useOrderAPI } from "../../../../hooks/order.js";
+import { useInstanceProducts } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = {
+ request: MerchantBackend.Orders.PostOrderRequest;
+ response: MerchantBackend.Orders.PostOrderResponse;
+};
+interface Props {
+ onBack?: () => void;
+ onConfirm: (id: string) => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+export default function OrderCreate({
+ onConfirm,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { createOrder } = useOrderAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const detailsResult = useInstanceDetails();
+ const inventoryResult = useInstanceProducts();
+
+ if (detailsResult.loading) return <Loading />;
+ if (inventoryResult.loading) return <Loading />;
+
+ if (!detailsResult.ok) {
+ if (
+ detailsResult.type === ErrorType.CLIENT &&
+ detailsResult.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ detailsResult.type === ErrorType.CLIENT &&
+ detailsResult.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(detailsResult);
+ }
+
+ if (!inventoryResult.ok) {
+ if (
+ inventoryResult.type === ErrorType.CLIENT &&
+ inventoryResult.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ inventoryResult.type === ErrorType.CLIENT &&
+ inventoryResult.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(inventoryResult);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
+ createOrder(request)
+ .then((r) => {
+ return onConfirm(r.data.order_id)
+ })
+ .catch((error) => {
+ setNotif({
+ message: "could not create order",
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ instanceConfig={detailsResult.data}
+ instanceInventory={inventoryResult.data}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
new file mode 100644
index 000000000..6e73a01a5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
@@ -0,0 +1,135 @@
+/*
+ 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 { addDays } from "date-fns";
+import { h, VNode, FunctionalComponent } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Order/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onRefund: { action: "onRefund" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+const defaultContractTerm = {
+ amount: "TESTKUDOS:10",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ auditors: [],
+ exchanges: [],
+ max_fee: "TESTKUDOS:1",
+ merchant: {} as any,
+ merchant_base_url: "http://merchant.url/",
+ order_id: "2021.165-03GDFC26Y1NNG",
+ products: [],
+ summary: "text summary",
+ wire_transfer_deadline: {
+ t_s: "never",
+ },
+ refund_deadline: { t_s: "never" },
+ merchant_pub: "ASDASDASDSd",
+ nonce: "QWEQWEQWE",
+ pay_deadline: {
+ t_s: "never",
+ },
+ wire_method: "x-taler-bank",
+ h_wire: "asd",
+} as MerchantBackend.ContractTerms;
+
+// contract_terms: defaultContracTerm,
+export const Claimed = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "claimed",
+ contract_terms: defaultContractTerm,
+ },
+});
+
+export const PaidNotRefundable = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "paid",
+ contract_terms: defaultContractTerm,
+ refunded: false,
+ deposit_total: "TESTKUDOS:10",
+ exchange_ec: 0,
+ order_status_url: "http://merchant.backend/status",
+ exchange_hc: 0,
+ refund_amount: "TESTKUDOS:0",
+ refund_details: [],
+ refund_pending: false,
+ wire_details: [],
+ wire_reports: [],
+ wired: false,
+ },
+});
+
+export const PaidRefundable = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "paid",
+ contract_terms: {
+ ...defaultContractTerm,
+ refund_deadline: {
+ t_s: addDays(new Date(), 2).getTime() / 1000,
+ },
+ },
+ refunded: false,
+ deposit_total: "TESTKUDOS:10",
+ exchange_ec: 0,
+ order_status_url: "http://merchant.backend/status",
+ exchange_hc: 0,
+ refund_amount: "TESTKUDOS:0",
+ refund_details: [],
+ refund_pending: false,
+ wire_details: [],
+ wire_reports: [],
+ wired: false,
+ },
+});
+
+export const Unpaid = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "unpaid",
+ order_status_url: "http://merchant.backend/status",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ summary: "text summary",
+ taler_pay_uri: "pay uri",
+ total_amount: "TESTKUDOS:10",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
new file mode 100644
index 000000000..5ff76e37a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
@@ -0,0 +1,770 @@
+/*
+ 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 { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format, formatDistance } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputLocation } from "../../../../components/form/InputLocation.js";
+import { TextField } from "../../../../components/form/TextField.js";
+import { ProductList } from "../../../../components/product/ProductList.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+import { mergeRefunds } from "../../../../utils/amount.js";
+import { RefundModal } from "../list/Table.js";
+import { Event, Timeline } from "./Timeline.js";
+
+type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
+type CT = MerchantBackend.ContractTerms;
+
+interface Props {
+ onBack: () => void;
+ selected: Entity;
+ id: string;
+ onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
+}
+
+type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & {
+ refund_taken: string;
+};
+type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse;
+type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse;
+
+function ContractTerms({ value }: { value: CT }) {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <InputGroup name="contract_terms" label={i18n.str`Contract Terms`}>
+ <FormProvider<CT> object={value} valueHandler={null}>
+ <Input<CT>
+ readonly
+ name="summary"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
+ />
+ <InputCurrency<CT>
+ readonly
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
+ />
+ {value.fulfillment_url && (
+ <Input<CT>
+ readonly
+ name="fulfillment_url"
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL for this purchase`}
+ />
+ )}
+ <Input<CT>
+ readonly
+ name="max_fee"
+ label={i18n.str`Max fee`}
+ tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
+ />
+ <InputDate<CT>
+ readonly
+ name="timestamp"
+ label={i18n.str`Created at`}
+ tooltip={i18n.str`time when this contract was generated`}
+ />
+ <InputDate<CT>
+ readonly
+ name="refund_deadline"
+ label={i18n.str`Refund deadline`}
+ tooltip={i18n.str`after this deadline has passed no refunds will be accepted`}
+ />
+ <InputDate<CT>
+ readonly
+ name="pay_deadline"
+ label={i18n.str`Payment deadline`}
+ tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`}
+ />
+ <InputDate<CT>
+ readonly
+ name="wire_transfer_deadline"
+ label={i18n.str`Wire transfer deadline`}
+ tooltip={i18n.str`transfer deadline for the exchange`}
+ />
+ <InputDate<CT>
+ readonly
+ name="delivery_date"
+ label={i18n.str`Delivery date`}
+ tooltip={i18n.str`time indicating when the order should be delivered`}
+ />
+ {value.delivery_date && (
+ <InputGroup
+ name="delivery_location"
+ label={i18n.str`Location`}
+ tooltip={i18n.str`where the order will be delivered`}
+ >
+ <InputLocation name="payments.delivery_location" />
+ </InputGroup>
+ )}
+ <InputDuration<CT>
+ readonly
+ name="auto_refund"
+ label={i18n.str`Auto-refund delay`}
+ tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`}
+ />
+ <Input<CT>
+ readonly
+ name="extra"
+ label={i18n.str`Extra info`}
+ tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`}
+ />
+ </FormProvider>
+ </InputGroup>
+ );
+}
+
+function ClaimedPage({
+ id,
+ order,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentClaimedResponse;
+}) {
+ const events: Event[] = [];
+ if (order.contract_terms.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.timestamp.t_s * 1000),
+ description: "order created",
+ type: "start",
+ });
+ }
+ if (order.contract_terms.pay_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
+ description: "pay deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.refund_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
+ description: "refund deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
+ description: "wire deadline",
+ type: "deadline",
+ });
+ }
+ if (
+ order.contract_terms.delivery_date &&
+ order.contract_terms.delivery_date.t_s !== "never"
+ ) {
+ events.push({
+ when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
+ description: "delivery",
+ type: "delivery",
+ });
+ }
+
+ const [value, valueHandler] = useState<Partial<Claimed>>(order);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+
+ return (
+ <div>
+ <section class="section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ <div class="tag is-info ml-4">
+ <i18n.Translate>claimed</i18n.Translate>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">{order.contract_terms.amount}</h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <p>
+ <b>
+ <i18n.Translate>claimed at</i18n.Translate>:
+ </b>{" "}
+ {format(
+ new Date(order.contract_terms.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings)
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="columns">
+ <div class="column is-4">
+ <div class="title">
+ <i18n.Translate>Timeline</i18n.Translate>
+ </div>
+ <Timeline events={events} />
+ </div>
+ <div class="column is-8">
+ <div class="title">
+ <i18n.Translate>Payment details</i18n.Translate>
+ </div>
+ <FormProvider<Claimed>
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <Input
+ name="contract_terms.summary"
+ readonly
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ />
+ <InputCurrency
+ name="contract_terms.amount"
+ readonly
+ label={i18n.str`Amount`}
+ />
+ <Input<Claimed>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ </FormProvider>
+ </div>
+ </div>
+ </section>
+
+ {order.contract_terms.products.length ? (
+ <Fragment>
+ <div class="title">
+ <i18n.Translate>Product list</i18n.Translate>
+ </div>
+ <ProductList list={order.contract_terms.products} />
+ </Fragment>
+ ) : undefined}
+
+ {value.contract_terms && (
+ <ContractTerms value={value.contract_terms} />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+function PaidPage({
+ id,
+ order,
+ onRefund,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentPaidResponse;
+ onRefund: (id: string) => void;
+}) {
+ const events: Event[] = [];
+ if (order.contract_terms.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.timestamp.t_s * 1000),
+ description: "order created",
+ type: "start",
+ });
+ }
+ if (order.contract_terms.pay_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
+ description: "pay deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.refund_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
+ description: "refund deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
+ description: "wire deadline",
+ type: "deadline",
+ });
+ }
+ if (
+ order.contract_terms.delivery_date &&
+ order.contract_terms.delivery_date.t_s !== "never"
+ ) {
+ if (order.contract_terms.delivery_date)
+ events.push({
+ when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
+ description: "delivery",
+ type: "delivery",
+ });
+ }
+ order.refund_details.reduce(mergeRefunds, []).forEach((e) => {
+ if (e.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(e.timestamp.t_s * 1000),
+ description: `refund: ${e.amount}: ${e.reason}`,
+ type: e.pending ? "refund" : "refund-taken",
+ });
+ }
+ });
+ if (order.wire_details && order.wire_details.length) {
+ if (order.wire_details.length > 1) {
+ let last: MerchantBackend.Orders.TransactionWireTransfer | null = null;
+ let first: MerchantBackend.Orders.TransactionWireTransfer | null = null;
+ let total: AmountJson | null = null;
+
+ order.wire_details.forEach((w) => {
+ if (last === null || last.execution_time.t_s < w.execution_time.t_s) {
+ last = w;
+ }
+ if (first === null || first.execution_time.t_s > w.execution_time.t_s) {
+ first = w;
+ }
+ total =
+ total === null
+ ? Amounts.parseOrThrow(w.amount)
+ : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount;
+ });
+ const last_time = last!.execution_time.t_s;
+ if (last_time !== "never") {
+ events.push({
+ when: new Date(last_time * 1000),
+ description: `wired ${Amounts.stringify(total!)}`,
+ type: "wired-range",
+ });
+ }
+ const first_time = first!.execution_time.t_s;
+ if (first_time !== "never") {
+ events.push({
+ when: new Date(first_time * 1000),
+ description: `wire transfer started...`,
+ type: "wired-range",
+ });
+ }
+ } else {
+ order.wire_details.forEach((e) => {
+ if (e.execution_time.t_s !== "never") {
+ events.push({
+ when: new Date(e.execution_time.t_s * 1000),
+ description: `wired ${e.amount}`,
+ type: "wired",
+ });
+ }
+ });
+ }
+ }
+
+ const now = new Date()
+ const nextEvent = events.find((e) => {
+ return e.when.getTime() > now.getTime()
+ })
+
+ const [value, valueHandler] = useState<Partial<Paid>>(order);
+ const { url: backendURL } = useBackendContext()
+ const refundurl = stringifyRefundUri({
+ merchantBaseUrl: backendURL,
+ orderId: order.contract_terms.order_id
+ })
+ const refundable =
+ new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
+ const { i18n } = useTranslationContext();
+
+ const amount = Amounts.parseOrThrow(order.contract_terms.amount);
+ const refund_taken = order.refund_details.reduce((prev, cur) => {
+ if (cur.pending) return prev;
+ return Amounts.add(prev, Amounts.parseOrThrow(cur.amount)).amount;
+ }, Amounts.zeroOfCurrency(amount.currency));
+ value.refund_taken = Amounts.stringify(refund_taken);
+
+ return (
+ <div>
+ <section class="section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ <div class="tag is-success ml-4">
+ <i18n.Translate>paid</i18n.Translate>
+ </div>
+ {order.wired ? (
+ <div class="tag is-success ml-4">
+ <i18n.Translate>wired</i18n.Translate>
+ </div>
+ ) : null}
+ {order.refunded ? (
+ <div class="tag is-danger ml-4">
+ <i18n.Translate>refunded</i18n.Translate>
+ </div>
+ ) : null}
+ </div>
+ </div>
+ </div>
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">{order.contract_terms.amount}</h1>
+ </div>
+ </div>
+ <div class="level-right">
+ <div class="level-item">
+ <h1 class="title">
+ <div class="buttons">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={
+ refundable
+ ? i18n.str`refund order`
+ : i18n.str`not refundable`
+ }
+ >
+ <button
+ class="button is-danger"
+ disabled={!refundable}
+ onClick={() => onRefund(id)}
+ >
+ <i18n.Translate>refund</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ {nextEvent &&
+ <p>
+ <i18n.Translate>Next event in </i18n.Translate> {formatDistance(
+ nextEvent.when,
+ new Date(),
+ // "yyyy/MM/dd HH:mm:ss",
+ )}
+ </p>
+ }
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="columns">
+ <div class="column is-4">
+ <div class="title">
+ <i18n.Translate>Timeline</i18n.Translate>
+ </div>
+ <Timeline events={events} />
+ </div>
+ <div class="column is-8">
+ <div class="title">
+ <i18n.Translate>Payment details</i18n.Translate>
+ </div>
+ <FormProvider<Paid>
+ object={value}
+ valueHandler={valueHandler}
+ >
+ {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n.str`Deposit total`} /> */}
+ {order.refunded && (
+ <InputCurrency<Paid>
+ name="refund_amount"
+ readonly
+ label={i18n.str`Refunded amount`}
+ />
+ )}
+ {order.refunded && (
+ <InputCurrency<Paid>
+ name="refund_taken"
+ readonly
+ label={i18n.str`Refund taken`}
+ />
+ )}
+ <Input<Paid>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ <TextField<Paid>
+ name="order_status_url"
+ label={i18n.str`Status URL`}
+ >
+ <a
+ target="_blank"
+ rel="noreferrer"
+ href={order.order_status_url}
+ >
+ {order.order_status_url}
+ </a>
+ </TextField>
+ {order.refunded && (
+ <TextField<Paid>
+ name="order_status_url"
+ label={i18n.str`Refund URI`}
+ >
+ <a target="_blank" rel="noreferrer" href={refundurl}>
+ {refundurl}
+ </a>
+ </TextField>
+ )}
+ </FormProvider>
+ </div>
+ </div>
+ </section>
+
+ {order.contract_terms.products.length ? (
+ <Fragment>
+ <div class="title">
+ <i18n.Translate>Product list</i18n.Translate>
+ </div>
+ <ProductList list={order.contract_terms.products} />
+ </Fragment>
+ ) : undefined}
+
+ {value.contract_terms && (
+ <ContractTerms value={value.contract_terms} />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+function UnpaidPage({
+ id,
+ order,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentUnpaidResponse;
+}) {
+ const [value, valueHandler] = useState<Partial<Unpaid>>(order);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+ return (
+ <div>
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ </h1>
+ </div>
+ <div class="tag is-dark">
+ <i18n.Translate>unpaid</i18n.Translate>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <p>
+ <b>
+ <i18n.Translate>pay at</i18n.Translate>:
+ </b>{" "}
+ <a
+ href={order.order_status_url}
+ rel="nofollow"
+ target="new"
+ >
+ {order.order_status_url}
+ </a>
+ </p>
+ <p>
+ <b>
+ <i18n.Translate>created at</i18n.Translate>:
+ </b>{" "}
+ {order.creation_time.t_s === "never"
+ ? "never"
+ : format(
+ new Date(order.creation_time.t_s * 1000),
+ datetimeFormatForSettings(settings)
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Unpaid> object={value} valueHandler={valueHandler}>
+ <Input<Unpaid>
+ readonly
+ name="summary"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
+ />
+ <InputCurrency<Unpaid>
+ readonly
+ name="total_amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
+ />
+ <Input<Unpaid>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ <Input<Unpaid>
+ name="order_status_url"
+ readonly
+ label={i18n.str`Order status URL`}
+ />
+ <TextField<Unpaid>
+ name="taler_pay_uri"
+ label={i18n.str`Payment URI`}
+ >
+ <a target="_blank" rel="noreferrer" href={value.taler_pay_uri}>
+ {value.taler_pay_uri}
+ </a>
+ </TextField>
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
+ const [showRefund, setShowRefund] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const DetailByStatus = function () {
+ switch (selected.order_status) {
+ case "claimed":
+ return <ClaimedPage id={id} order={selected} />;
+ case "paid":
+ return <PaidPage id={id} order={selected} onRefund={setShowRefund} />;
+ case "unpaid":
+ return <UnpaidPage id={id} order={selected} />;
+ default:
+ return (
+ <div>
+ <i18n.Translate>
+ Unknown order status. This is an error, please contact the
+ administrator.
+ </i18n.Translate>
+ </div>
+ );
+ }
+ };
+
+ return (
+ <Fragment>
+ {DetailByStatus()}
+ {showRefund && (
+ <RefundModal
+ order={selected}
+ onCancel={() => setShowRefund(undefined)}
+ onConfirm={(value) => {
+ onRefund(showRefund, value);
+ setShowRefund(undefined);
+ }}
+ />
+ )}
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </Fragment>
+ );
+}
+
+async function copyToClipboard(text: string) {
+ return navigator.clipboard.writeText(text);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
new file mode 100644
index 000000000..8c863f386
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
@@ -0,0 +1,129 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { format } from "date-fns";
+import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+interface Props {
+ events: Event[];
+}
+
+export function Timeline({ events: e }: Props) {
+ const events = [...e];
+ events.push({
+ when: new Date(),
+ description: "now",
+ type: "now",
+ });
+
+ events.sort((a, b) => a.when.getTime() - b.when.getTime());
+ const [settings] = useSettings();
+ const [state, setState] = useState(events);
+ useEffect(() => {
+ const handle = setTimeout(() => {
+ const eventsWithoutNow = state.filter((e) => e.type !== "now");
+ eventsWithoutNow.push({
+ when: new Date(),
+ description: "now",
+ type: "now",
+ });
+ setState(eventsWithoutNow);
+ }, 1000);
+ return () => {
+ clearTimeout(handle);
+ };
+ });
+ return (
+ <div class="timeline">
+ {events.map((e, i) => {
+ return (
+ <div key={i} class="timeline-item">
+ {(() => {
+ switch (e.type) {
+ case "deadline":
+ return (
+ <div class="timeline-marker is-icon ">
+ <i class="mdi mdi-flag" />
+ </div>
+ );
+ case "delivery":
+ return (
+ <div class="timeline-marker is-icon ">
+ <i class="mdi mdi-delivery" />
+ </div>
+ );
+ case "start":
+ return (
+ <div class="timeline-marker is-icon">
+ <i class="mdi mdi-flag " />
+ </div>
+ );
+ case "wired":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "wired-range":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "refund":
+ return (
+ <div class="timeline-marker is-icon is-danger">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "refund-taken":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "now":
+ return (
+ <div class="timeline-marker is-icon is-info">
+ <i class="mdi mdi-clock" />
+ </div>
+ );
+ }
+ })()}
+ <div class="timeline-content">
+ {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>}
+ <p>{e.description}</p>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+export interface Event {
+ when: Date;
+ description: string;
+ type:
+ | "start"
+ | "refund"
+ | "refund-taken"
+ | "wired"
+ | "wired-range"
+ | "deadline"
+ | "delivery"
+ | "now";
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
new file mode 100644
index 000000000..1517a3c42
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
@@ -0,0 +1,95 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ useTranslationContext,
+ HttpError,
+ ErrorType,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
+import { Notification } from "../../../../utils/types.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export interface Props {
+ oid: string;
+
+ onBack: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+
+export default function Update({
+ oid,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { refundOrder } = useOrderAPI();
+ const result = useOrderDetails(oid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <DetailPage
+ onBack={onBack}
+ id={oid}
+ onRefund={(id, value) =>
+ refundOrder(id, value)
+ .then(() =>
+ setNotif({
+ message: i18n.str`refund created successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not create the refund`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ selected={result.data}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
new file mode 100644
index 000000000..156c577f4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Order/List",
+ component: TestedComponent,
+ argTypes: {
+ onShowAll: { action: "onShowAll" },
+ onShowPaid: { action: "onShowPaid" },
+ onShowRefunded: { action: "onShowRefunded" },
+ onShowNotWired: { action: "onShowNotWired" },
+ onCopyURL: { action: "onCopyURL" },
+ onSelectDate: { action: "onSelectDate" },
+ onLoadMoreBefore: { action: "onLoadMoreBefore" },
+ onLoadMoreAfter: { action: "onLoadMoreAfter" },
+ onSelectOrder: { action: "onSelectOrder" },
+ onRefundOrder: { action: "onRefundOrder" },
+ onSearchOrderById: { action: "onSearchOrderById" },
+ onCreate: { action: "onCreate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ orders: [
+ {
+ id: "123",
+ amount: "TESTKUDOS:10",
+ paid: false,
+ refundable: true,
+ row_id: 1,
+ summary: "summary",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "123",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: true,
+ refundable: true,
+ row_id: 2,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ {
+ id: "456",
+ amount: "TESTKUDOS:1",
+ paid: false,
+ refundable: false,
+ row_id: 3,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "456",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: false,
+ refundable: false,
+ row_id: 4,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
new file mode 100644
index 000000000..9f80719a1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
@@ -0,0 +1,226 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode, Fragment } from "preact";
+import { useState } from "preact/hooks";
+import { DatePicker } from "../../../../components/picker/DatePicker.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { CardTable } from "./Table.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+export interface ListPageProps {
+ onShowAll: () => void;
+ onShowNotPaid: () => void;
+ onShowPaid: () => void;
+ onShowRefunded: () => void;
+ onShowNotWired: () => void;
+ onShowWired: () => void;
+ onCopyURL: (id: string) => void;
+ isAllActive: string;
+ isPaidActive: string;
+ isNotPaidActive: string;
+ isRefundedActive: string;
+ isNotWiredActive: string;
+ isWiredActive: string;
+
+ jumpToDate?: Date;
+ onSelectDate: (date?: Date) => void;
+
+ orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[];
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+
+ onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onCreate: () => void;
+}
+
+export function ListPage({
+ hasMoreAfter,
+ hasMoreBefore,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ orders,
+ isAllActive,
+ onSelectOrder,
+ onRefundOrder,
+ jumpToDate,
+ onCopyURL,
+ onShowAll,
+ onShowPaid,
+ onShowNotPaid,
+ onShowRefunded,
+ onShowNotWired,
+ onShowWired,
+ onSelectDate,
+ isPaidActive,
+ isRefundedActive,
+ isNotWiredActive,
+ onCreate,
+ isNotPaidActive,
+ isWiredActive,
+}: ListPageProps): VNode {
+ const { i18n } = useTranslationContext();
+ const dateTooltip = i18n.str`select date to show nearby orders`;
+ const [pickDate, setPickDate] = useState(false);
+ const [settings] = useSettings();
+
+ return (
+ <Fragment>
+ <div class="columns">
+ <div class="column is-two-thirds">
+ <div class="tabs" style={{ overflow: "inherit" }}>
+ <ul>
+ <li class={isNotPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowNotPaid}>
+ <i18n.Translate>New</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowPaid}>
+ <i18n.Translate>Paid</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isRefundedActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show orders with refunds`}
+ >
+ <a onClick={onShowRefunded}>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isNotWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowNotWired}>
+ <i18n.Translate>Not wired</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowWired}>
+ <i18n.Translate>Completed</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isAllActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="column ">
+ <div class="buttons is-right">
+ <div class="field has-addons">
+ {jumpToDate && (
+ <div class="control">
+ <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
+ <span
+ class="icon"
+ data-tooltip={i18n.str`clear date filter`}
+ >
+ <i class="mdi mdi-close" />
+ </span>
+ </a>
+ </div>
+ )}
+ <div class="control">
+ <span class="has-tooltip-top" data-tooltip={dateTooltip}>
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
+ placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
+ onClick={() => {
+ setPickDate(true);
+ }}
+ />
+ </span>
+ </div>
+ <div class="control">
+ <span class="has-tooltip-left" data-tooltip={dateTooltip}>
+ <a
+ class="button is-fullwidth"
+ onClick={() => {
+ setPickDate(true);
+ }}
+ >
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <DatePicker
+ opened={pickDate}
+ closeFunction={() => setPickDate(false)}
+ dateReceiver={onSelectDate}
+ />
+
+ <CardTable
+ orders={orders}
+ onCreate={onCreate}
+ onCopyURL={onCopyURL}
+ onSelect={onSelectOrder}
+ onRefund={onRefundOrder}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
new file mode 100644
index 000000000..b2806bb79
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
@@ -0,0 +1,417 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { mergeRefunds } from "../../../../utils/amount.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
+interface Props {
+ orders: Entity[];
+ onRefund: (value: Entity) => void;
+ onCopyURL: (id: string) => void;
+ onCreate: () => void;
+ onSelect: (order: Entity) => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ orders,
+ onCreate,
+ onRefund,
+ onCopyURL,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <i18n.Translate>Orders</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+
+ <div class="card-header-icon" aria-label="more options">
+ <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}>
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {orders.length > 0 ? (
+ <Table
+ instances={orders}
+ onSelect={onSelect}
+ onRefund={onRefund}
+ onCopyURL={(o) => onCopyURL(o.id)}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onRefund: (id: Entity) => void;
+ onCopyURL: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function Table({
+ instances,
+ onSelect,
+ onRefund,
+ onCopyURL,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer orders</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Date</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Amount</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 400 }}>
+ <i18n.Translate>Summary</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 50 }} />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(i.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.summary}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ {i.refundable && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onRefund(i)}
+ >
+ <i18n.Translate>Refund</i18n.Translate>
+ </button>
+ )}
+ {!i.paid && (
+ <button
+ class="button is-small is-info jb-modal"
+ type="button"
+ onClick={(): void => onCopyURL(i)}
+ >
+ <i18n.Translate>copy url</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older orders</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ No orders have been found matching your query!
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface RefundModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
+ order: MerchantBackend.Orders.MerchantOrderStatusResponse;
+}
+
+export function RefundModal({
+ order,
+ onCancel,
+ onConfirm,
+}: RefundModalProps): VNode {
+ type State = { mainReason?: string; description?: string; refund?: string };
+ const [form, setValue] = useState<State>({});
+ const [settings] = useSettings();
+ const { i18n } = useTranslationContext();
+ // const [errors, setErrors] = useState<FormErrors<State>>({});
+
+ const refunds = (
+ order.order_status === "paid" ? order.refund_details : []
+ ).reduce(mergeRefunds, []);
+
+ const config = useConfigContext();
+ const totalRefunded = refunds
+ .map((r) => r.amount)
+ .reduce(
+ (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount,
+ Amounts.zeroOfCurrency(config.currency),
+ );
+ const orderPrice =
+ order.order_status === "paid"
+ ? Amounts.parseOrThrow(order.contract_terms.amount)
+ : undefined;
+ const totalRefundable = !orderPrice
+ ? Amounts.zeroOfCurrency(totalRefunded.currency)
+ : refunds.length
+ ? Amounts.sub(orderPrice, totalRefunded).amount
+ : orderPrice;
+
+ const isRefundable = Amounts.isNonZero(totalRefundable);
+ const duplicatedText = i18n.str`duplicated`;
+
+ const errors: FormErrors<State> = {
+ mainReason: !form.mainReason ? i18n.str`required` : undefined,
+ description:
+ !form.description && form.mainReason !== duplicatedText
+ ? i18n.str`required`
+ : undefined,
+ refund: !form.refund
+ ? i18n.str`required`
+ : !Amounts.parse(form.refund)
+ ? i18n.str`invalid format`
+ : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
+ ? i18n.str`this value exceed the refundable amount`
+ : undefined,
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const validateAndConfirm = () => {
+ try {
+ if (!form.refund) return;
+ onConfirm({
+ refund: Amounts.stringify(
+ Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount,
+ ),
+ reason:
+ form.description === undefined
+ ? form.mainReason || ""
+ : `${form.mainReason}: ${form.description}`,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ //FIXME: parameters in the translation
+ return (
+ <ConfirmModal
+ description="refund"
+ danger
+ active
+ disabled={!isRefundable || hasErrors}
+ onCancel={onCancel}
+ onConfirm={validateAndConfirm}
+ >
+ {refunds.length > 0 && (
+ <div class="columns">
+ <div class="column is-12">
+ <InputGroup
+ name="asd"
+ label={`${Amounts.stringify(totalRefunded)} was already refunded`}
+ >
+ <table class="table is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>date</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>amount</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>reason</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {refunds.map((r) => {
+ return (
+ <tr key={r.timestamp.t_s}>
+ <td>
+ {r.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(r.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td>{r.amount}</td>
+ <td>{r.reason}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </InputGroup>
+ </div>
+ </div>
+ )}
+
+ {isRefundable && (
+ <FormProvider<State>
+ errors={errors}
+ object={form}
+ valueHandler={(d) => setValue(d as any)}
+ >
+ <InputCurrency<State>
+ name="refund"
+ label={i18n.str`Refund`}
+ tooltip={i18n.str`amount to be refunded`}
+ >
+ <i18n.Translate>Max refundable:</i18n.Translate>{" "}
+ {Amounts.stringify(totalRefundable)}
+ </InputCurrency>
+ <InputSelector
+ name="mainReason"
+ label={i18n.str`Reason`}
+ values={[
+ i18n.str`Choose one...`,
+ duplicatedText,
+ i18n.str`requested by the customer`,
+ i18n.str`other`,
+ ]}
+ tooltip={i18n.str`why this order is being refunded`}
+ />
+ {form.mainReason && form.mainReason !== duplicatedText ? (
+ <Input<State>
+ label={i18n.str`Description`}
+ name="description"
+ tooltip={i18n.str`more information to give context`}
+ />
+ ) : undefined}
+ </FormProvider>
+ )}
+ </ConfirmModal>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
new file mode 100644
index 000000000..92e714fb8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -0,0 +1,231 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ InstanceOrderFilter,
+ useInstanceOrders,
+ useOrderAPI,
+ useOrderDetails,
+} from "../../../../hooks/order.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { RefundModal } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+}
+
+export default function OrderList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" });
+ const [orderToBeRefunded, setOrderToBeRefunded] = useState<
+ MerchantBackend.Orders.OrderHistoryEntry | undefined
+ >(undefined);
+
+ const setNewDate = (date?: Date): void =>
+ setFilter((prev) => ({ ...prev, date }));
+
+ const result = useInstanceOrders(filter, setNewDate);
+ const { refundOrder, getPaymentURL } = useOrderAPI();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const isNotPaidActive = filter.paid === "no" ? "is-active" : "";
+ const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : "";
+ const isRefundedActive = filter.refunded === "yes" ? "is-active" : "";
+ const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : "";
+ const isWiredActive = filter.wired === "yes" ? "is-active" : "";
+ const isAllActive =
+ filter.paid === undefined &&
+ filter.refunded === undefined &&
+ filter.wired === undefined
+ ? "is-active"
+ : "";
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getPaymentURL}
+ onSelect={onSelect}
+ description={i18n.str`jump to order with the given product ID`}
+ placeholder={i18n.str`order id`}
+ />
+
+ <ListPage
+ orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))}
+ onLoadMoreBefore={result.loadMorePrev}
+ hasMoreBefore={!result.isReachingStart}
+ onLoadMoreAfter={result.loadMore}
+ hasMoreAfter={!result.isReachingEnd}
+ onSelectOrder={(order) => onSelect(order.id)}
+ onRefundOrder={(value) => setOrderToBeRefunded(value)}
+ isAllActive={isAllActive}
+ isNotWiredActive={isNotWiredActive}
+ isWiredActive={isWiredActive}
+ isPaidActive={isPaidActive}
+ isNotPaidActive={isNotPaidActive}
+ isRefundedActive={isRefundedActive}
+ jumpToDate={filter.date}
+ onCopyURL={(id) =>
+ getPaymentURL(id).then((resp) => copyToClipboard(resp.data))
+ }
+ onCreate={onCreate}
+ onSelectDate={setNewDate}
+ onShowAll={() => setFilter({})}
+ onShowNotPaid={() => setFilter({ paid: "no" })}
+ onShowPaid={() => setFilter({ paid: "yes" })}
+ onShowRefunded={() => setFilter({ refunded: "yes" })}
+ onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })}
+ onShowWired={() => setFilter({ wired: "yes" })}
+ />
+
+ {orderToBeRefunded && (
+ <RefundModalForTable
+ id={orderToBeRefunded.order_id}
+ onCancel={() => setOrderToBeRefunded(undefined)}
+ onConfirm={(value) =>
+ refundOrder(orderToBeRefunded.order_id, value)
+ .then(() =>
+ setNotif({
+ message: i18n.str`refund created successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not create the refund`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ .then(() => setOrderToBeRefunded(undefined))
+ }
+ onLoadError={(error) => {
+ setNotif({
+ message: i18n.str`could not create the refund`,
+ type: "ERROR",
+ description: error.message,
+ });
+ setOrderToBeRefunded(undefined);
+ return <div />;
+ }}
+ onUnauthorized={onUnauthorized}
+ onNotFound={() => {
+ setNotif({
+ message: i18n.str`could not get the order to refund`,
+ type: "ERROR",
+ // description: error.message
+ });
+ setOrderToBeRefunded(undefined);
+ return <div />;
+ }}
+ />
+ )}
+ </section>
+ );
+}
+
+interface RefundProps {
+ id: string;
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCancel: () => void;
+ onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void;
+}
+
+function RefundModalForTable({
+ id,
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onConfirm,
+ onCancel,
+}: RefundProps): VNode {
+ const result = useOrderDetails(id);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <RefundModal
+ order={result.data}
+ onCancel={onCancel}
+ onConfirm={onConfirm}
+ />
+ );
+}
+
+async function copyToClipboard(text: string): Promise<void> {
+ return navigator.clipboard.writeText(text);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
new file mode 100644
index 000000000..26f851cc8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
new file mode 100644
index 000000000..ffeaa064a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -0,0 +1,179 @@
+/*
+ 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 { isRfc3548Base32Charset, randomRfc3548Base32Key } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const algorithms = [0, 1, 2];
+const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const backend = useBackendContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+
+ const [showKey, setShowKey] = useState(false);
+
+ const errors: FormErrors<Entity> = {
+ otp_device_id: !state.otp_device_id
+ ? i18n.str`required`
+ : !/[a-zA-Z0-9]*/.test(state.otp_device_id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+ otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined,
+ otp_key: !state.otp_key
+ ? i18n.str`required`
+ : !isRfc3548Base32Charset(state.otp_key)
+ ? i18n.str`just letters and numbers from 2 to 7`
+ : state.otp_key.length !== 32
+ ? i18n.str`size of the key should be 32`
+ : undefined,
+ otp_device_description: !state.otp_device_description
+ ? i18n.str`required`
+ : !/[a-zA-Z0-9]*/.test(state.otp_device_description)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="otp_device_id"
+ label={i18n.str`ID`}
+ tooltip={i18n.str`Internal id on the system`}
+ />
+ <Input<Entity>
+ name="otp_device_description"
+ label={i18n.str`Descripiton`}
+ tooltip={i18n.str`Useful to identify the device physically`}
+ />
+ <InputSelector<Entity>
+ name="otp_algorithm"
+ label={i18n.str`Verification algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ fromStr={(v) => Number(v)}
+ />
+ {state.otp_algorithm && state.otp_algorithm > 0 ? (
+ <Fragment>
+ <InputWithAddon<Entity>
+ expand
+ name="otp_key"
+ label={i18n.str`Device key`}
+ inputType={showKey ? "text" : "password"}
+ help="Be sure to be very hard to guess or use the random generator"
+ tooltip={i18n.str`Your device need to have exactly the same value`}
+ fromStr={(v) => v.toUpperCase()}
+ addonAfterAction={() => {
+ setShowKey(!showKey);
+ }}
+ addonAfter={
+ <span class="icon">
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setState((s) => ({
+ ...s,
+ otp_key: randomRfc3548Base32Key(),
+ }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ }
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..db3842711
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
@@ -0,0 +1,104 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useBackendContext } from "../../../../context/backend.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+}
+
+function isNotUndefined<X>(x: X | undefined): x is X {
+ return !!x;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const { id: instanceId } = useInstanceContext();
+ const issuer = new URL(backendURL).hostname;
+ const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
+ const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
+
+ return (
+ <Template onConfirm={onConfirm} >
+ <p class="is-size-5">
+ <i18n.Translate>
+ You can scan the next QR code with your device or safe the key before continue.
+ </i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ readonly
+ class="input"
+ value={entity.otp_device_id}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label"><i18n.Translate>Description</i18n.Translate></label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.otp_device_description}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <QR
+ text={qrText}
+ />
+ <div
+ style={{
+ color: "grey",
+ fontSize: "small",
+ width: 200,
+ textAlign: "center",
+ margin: "auto",
+ wordBreak: "break-all",
+ }}
+ >
+ {qrTextSafe}
+ </div>
+ </Template>
+ );
+}
+
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
new file mode 100644
index 000000000..648846793
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+
+export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createOtpDevice } = useOtpDeviceAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null)
+
+ if (created) {
+ return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createOtpDevice(request)
+ .then((d) => {
+ setCreated(request)
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
new file mode 100644
index 000000000..b18049674
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/OtpDevices/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
new file mode 100644
index 000000000..4efee9781
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: MerchantBackend.OTP.OtpDeviceEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+ onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ devices={devices.map((o) => ({
+ ...o,
+ id: String(o.otp_device_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
new file mode 100644
index 000000000..0c28027fe
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -0,0 +1,211 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceEntry;
+
+interface Props {
+ devices: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>OTP Devices</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new devices`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {devices.length > 0 ? (
+ <Table
+ instances={devices}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer devices</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.otp_device_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected devices from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older devices</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no devices yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
new file mode 100644
index 000000000..2aae8738a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
@@ -0,0 +1,106 @@
+/*
+ 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 { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListOtpDevices({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteOtpDevice } = useOtpDeviceAPI();
+ const result = useInstanceOtpDevices({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.otp_devices}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.otp_device_id);
+ }}
+ onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) =>
+ deleteOtpDevice(e.otp_device_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`validator delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the validator`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
new file mode 100644
index 000000000..d6b1d65e0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
new file mode 100644
index 000000000..85bb272aa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
@@ -0,0 +1,186 @@
+/*
+ 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 { randomRfc3548Base32Key } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ device: Entity;
+}
+const algorithms = [0, 1, 2];
+const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
+export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(device);
+ const [showKey, setShowKey] = useState(false);
+
+ const errors: FormErrors<Entity> = {};
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Device: <b>{device.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="otp_device_description"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`Useful to identify the device physically`}
+ />
+ <InputSelector<Entity>
+ name="otp_algorithm"
+ label={i18n.str`Verification algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ fromStr={(v) => Number(v)}
+ />
+ {state.otp_algorithm && state.otp_algorithm > 0 ? (
+ <Fragment>
+ <InputWithAddon<Entity>
+ name="otp_key"
+ label={i18n.str`Device key`}
+ readonly={state.otp_key === undefined}
+ inputType={showKey ? "text" : "password"}
+ help={
+ state.otp_key === undefined
+ ? "Not modified"
+ : "Be sure to be very hard to guess or use the random generator"
+ }
+ tooltip={i18n.str`Your device need to have exactly the same value`}
+ fromStr={(v) => v.toUpperCase()}
+ addonAfterAction={() => {
+ setShowKey(!showKey);
+ }}
+ addonAfter={
+ <span
+ class="icon"
+ onClick={() => {
+ setShowKey(!showKey);
+ }}
+ >
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ state.otp_key === undefined ? (
+ <button
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: "" }));
+ }}
+ class="button"
+ >
+ change key
+ </button>
+ ) : (
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setState((s) => ({
+ ...s,
+ otp_key: randomRfc3548Base32Key(),
+ }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ )
+ }
+ />
+ </Fragment>
+ ) : undefined}{" "}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
new file mode 100644
index 000000000..52f6c6c29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
@@ -0,0 +1,102 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js";
+
+export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ vid: string;
+}
+export default function UpdateValidator({
+ vid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateOtpDevice } = useOtpDeviceAPI();
+ const result = useOtpDeviceDetails(vid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ device={{
+ id: vid,
+ otp_algorithm: result.data.otp_algorithm,
+ otp_device_description: result.data.device_description,
+ otp_key: undefined,
+ otp_ctr: result.data.otp_ctr
+ }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateOtpDevice(vid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
new file mode 100644
index 000000000..2fc0819bb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Product/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
new file mode 100644
index 000000000..becaf8f3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductAddDetail & {
+ product_id: string;
+};
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onCreate(result);
+ return Promise.reject();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm onSubscribe={addFormSubmitter} />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..6b02430cc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
@@ -0,0 +1,72 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+import emptyImage from "../../assets/empty.png";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Image</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <img src={entity.image} style={{ width: 200, height: 200 }} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Description</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <textarea class="input" readonly value={entity.description} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Price</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.price} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Template>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx
new file mode 100644
index 000000000..0c30ff14c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx
@@ -0,0 +1,47 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { AuditorBackend, MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Products.ProductDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
+ const { createProduct } = useProductAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
new file mode 100644
index 000000000..c2c4d548c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
+
+export default {
+ title: "Pages/Product/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onSelect: { action: "onSelect" },
+ onDelete: { action: "onDelete" },
+ onUpdate: { action: "onUpdate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ instances: [
+ {
+ id: "orderid",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: 15,
+ unit: "bar",
+ address: {},
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx
new file mode 100644
index 000000000..275f855cb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx
@@ -0,0 +1,496 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import emptyImage from "../../../../assets/empty.png";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & WithId;
+
+interface Props {
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ onSelect: (product: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onCreate: () => void;
+ selected?: boolean;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <i18n.Translate>Inventory</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add product to inventory`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ onUpdate={onUpdate}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string | undefined;
+ instances: Entity[];
+ onSelect: (id: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onDelete: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string | undefined>;
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Price per unit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Taxes</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sales</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Stock</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sold</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ const restStockInfo = !i.next_restock
+ ? ""
+ : i.next_restock.t_s === "never"
+ ? "never"
+ : `restock at ${format(
+ new Date(i.next_restock.t_s * 1000),
+ dateFormatForSettings(settings),
+ )}`;
+ let stockInfo: ComponentChildren = "";
+ if (i.total_stock < 0) {
+ stockInfo = "infinite";
+ } else {
+ const totalStock = i.total_stock - i.total_lost - i.total_sold;
+ stockInfo = (
+ <label title={restStockInfo}>
+ {totalStock} {i.unit}
+ </label>
+ );
+ }
+
+ const isFree = Amounts.isZero(Amounts.parseOrThrow(i.price));
+
+ return (
+ <Fragment key={i.id}>
+ <tr key="info">
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <img
+ src={i.image ? i.image : emptyImage}
+ style={{
+ border: "solid black 1px",
+ maxHeight: "2em",
+ width: "auto",
+ height: "auto",
+ }}
+ />
+ </td>
+ <td
+ class="has-tooltip-right"
+ data-tooltip={i.description}
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {sum(i.taxes)}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {difference(i.price, sum(i.taxes))}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {stockInfo}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <span style={{"whiteSpace":"nowrap"}}>
+
+ {i.total_sold} {i.unit}
+ </span>
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`go to product update page`}
+ >
+ <button
+ class="button is-small is-success "
+ type="button"
+ onClick={(): void => onSelect(i)}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </span>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`remove this product from the database`}
+ >
+ <button
+ class="button is-small is-danger"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </td>
+ </tr>
+ {rowSelection === i.id && (
+ <tr key="form">
+ <td colSpan={10}>
+ <FastProductUpdateForm
+ product={i}
+ onUpdate={(prod) =>
+ onUpdate(i.id, prod).then((r) =>
+ rowSelectionHandler(undefined),
+ )
+ }
+ onCancel={() => rowSelectionHandler(undefined)}
+ />
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+interface FastProductUpdateFormProps {
+ product: Entity;
+ onUpdate: (
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onCancel: () => void;
+}
+interface FastProductUpdate {
+ incoming: number;
+ lost: number;
+ price: string;
+}
+interface UpdatePrice {
+ price: string;
+}
+
+function FastProductWithInfiniteStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<UpdatePrice>({ price: product.price });
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`update the product with new price`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-expanded">
+
+ <div class="buttons mt-5">
+
+ <button class="button mt-5" onClick={onCancel}>
+ <i18n.Translate>Clone</i18n.Translate>
+ </button>
+ </div>
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`update product with new price`}
+ >
+ <button
+ class="button is-info"
+ onClick={() =>
+ onUpdate({
+ ...product,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm update</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductWithManagedStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<FastProductUpdate>({
+ incoming: 0,
+ lost: 0,
+ price: product.price,
+ });
+
+ const currentStock =
+ product.total_stock - product.total_sold - product.total_lost;
+
+ const errors: FormErrors<FastProductUpdate> = {
+ lost:
+ currentStock + value.incoming < value.lost
+ ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
+ })`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputNumber<FastProductUpdate>
+ name="incoming"
+ label={i18n.str`Incoming`}
+ tooltip={i18n.str`add more elements to the inventory`}
+ />
+ <InputNumber<FastProductUpdate>
+ name="lost"
+ label={i18n.str`Lost`}
+ tooltip={i18n.str`report elements lost in the inventory`}
+ />
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`new price for the product`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={
+ hasErrors
+ ? i18n.str`the are value with errors`
+ : i18n.str`update product with new stock and price`
+ }
+ >
+ <button
+ class="button is-info"
+ disabled={hasErrors}
+ onClick={() =>
+ onUpdate({
+ ...product,
+ total_stock: product.total_stock + value.incoming,
+ total_lost: product.total_lost + value.lost,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductUpdateForm(props: FastProductUpdateFormProps) {
+ return props.product.total_stock === -1 ? (
+ <FastProductWithInfiniteStockUpdateForm {...props} />
+ ) : (
+ <FastProductWithManagedStockUpdateForm {...props} />
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no products yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+function difference(price: string, tax: number) {
+ if (!tax) return price;
+ const ps = price.split(":");
+ const p = parseInt(ps[1], 10);
+ ps[1] = `${p - tax}`;
+ return ps.join(":");
+}
+function sum(taxes: MerchantBackend.Tax[]) {
+ return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx
new file mode 100644
index 000000000..34b21daa2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx
@@ -0,0 +1,150 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useInstanceProducts,
+ useProductAPI,
+} from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+export default function ProductList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const result = useInstanceProducts();
+ const { deleteProduct, updateProduct, getProduct } = useProductAPI();
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Products.ProductDetail & WithId | null>(null);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getProduct}
+ onSelect={onSelect}
+ description={i18n.str`jump to product with the given product ID`}
+ placeholder={i18n.str`product id`}
+ />
+
+ <CardTable
+ instances={result.data}
+ onCreate={onCreate}
+ onUpdate={(id, prod) =>
+ updateProduct(id, prod)
+ .then(() =>
+ setNotif({
+ message: i18n.str`product updated successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not update the product`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ onSelect={(product) => onSelect(product.id)}
+ onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) =>
+ setDeleting(prod)
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete product`}
+ description={`Delete the product "${deleting.description}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteProduct(deleting.id);
+ setNotif({
+ message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete product`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the product named <b>&quot;{deleting.description}&quot;</b> (ID:{" "}
+ <b>{deleting.id}</b>), the stock and related information will be lost
+ </p>
+ <p class="warning">
+ Deleting an product <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
new file mode 100644
index 000000000..a85b13b8b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Product/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithManagedStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: 15,
+ unit: "bar",
+ address: {},
+ },
+});
+
+export const WithInfiniteStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: -1,
+ unit: "bar",
+ address: {},
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
new file mode 100644
index 000000000..97715171e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ product: Entity;
+}
+
+export function UpdatePage({ product, onUpdate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onUpdate(result);
+ return Promise.resolve();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>Product id:</i18n.Translate>
+ <b>{product.product_id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm
+ initial={product}
+ onSubscribe={addFormSubmitter}
+ alreadyExist
+ />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx
new file mode 100644
index 000000000..8e0f7647f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx
@@ -0,0 +1,95 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Products.ProductAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ pid: string;
+}
+export default function UpdateProduct({
+ pid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateProduct } = useProductAPI();
+ const result = useProductDetails(pid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ product={{ ...result.data, product_id: pid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateProduct(pid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
new file mode 100644
index 000000000..5542c028a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Reserve/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
new file mode 100644
index 000000000..e46941b6d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
@@ -0,0 +1,277 @@
+/*
+ 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, RequestError, useApiContext, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ PAYTO_WIRE_METHOD_LOOKUP,
+ URL_REGEX,
+} from "../../../../utils/constants.js";
+import { useBackendBaseRequest } from "../../../../hooks/backend.js";
+import { parsePaytoUri } from "@gnu-taler/taler-util";
+
+type Entity = MerchantBackend.Rewards.ReserveCreateRequest;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+enum Steps {
+ EXCHANGE,
+ WIRE_METHOD,
+}
+
+interface ViewProps {
+ step: Steps;
+ setCurrentStep: (s: Steps) => void;
+ reserve: Partial<Entity>;
+ onBack?: () => void;
+ submitForm: () => Promise<void>;
+ setReserve: StateUpdater<Partial<Entity>>;
+}
+function ViewStep({
+ step,
+ setCurrentStep,
+ reserve,
+ onBack,
+ submitForm,
+ setReserve,
+}: ViewProps): VNode {
+ const { i18n } = useTranslationContext();
+ const {request} = useApiContext()
+ const [wireMethods, setWireMethods] = useState<Array<string>>([]);
+ const [exchangeQueryError, setExchangeQueryError] = useState<
+ string | undefined
+ >(undefined);
+
+ useEffect(() => {
+ setExchangeQueryError(undefined);
+ }, [reserve.exchange_url]);
+
+ switch (step) {
+ case Steps.EXCHANGE: {
+ const errors: FormErrors<Entity> = {
+ initial_balance: !reserve.initial_balance
+ ? "cannot be empty"
+ : !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0)
+ ? i18n.str`it should be greater than 0`
+ : undefined,
+ exchange_url: !reserve.exchange_url
+ ? i18n.str`cannot be empty`
+ : !URL_REGEX.test(reserve.exchange_url)
+ ? i18n.str`must be a valid URL`
+ : !exchangeQueryError
+ ? undefined
+ : exchangeQueryError,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ return (
+ <Fragment>
+ <FormProvider<Entity>
+ object={reserve}
+ errors={errors}
+ valueHandler={setReserve}
+ >
+ <InputCurrency<Entity>
+ name="initial_balance"
+ label={i18n.str`Initial balance`}
+ tooltip={i18n.str`balance prior to deposit`}
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`URL of exchange`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ class="has-tooltip-left"
+ onClick={() => {
+ if (!reserve.exchange_url) {
+ return Promise.resolve();
+ }
+
+ return request<any>(reserve.exchange_url, "keys")
+ .then((r) => {
+ console.log(r)
+ if (r.loading) return;
+ if (r.ok) {
+ const wireMethods = r.data.accounts.map((a: any) => {
+ const p = parsePaytoUri(a.payto_uri);
+ const r = p?.targetType
+ return r
+ }).filter((x:any) => !!x);
+ setWireMethods(Array.from(new Set(wireMethods)));
+ }
+ setCurrentStep(Steps.WIRE_METHOD);
+ return;
+ })
+ .catch((r: RequestError<{}>) => {
+ console.log(r.cause)
+ setExchangeQueryError(r.cause.message);
+ });
+ }}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </Fragment>
+ );
+ }
+
+ case Steps.WIRE_METHOD: {
+ const errors: FormErrors<Entity> = {
+ wire_method: !reserve.wire_method
+ ? i18n.str`cannot be empty`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ return (
+ <Fragment>
+ <FormProvider<Entity>
+ object={reserve}
+ errors={errors}
+ valueHandler={setReserve}
+ >
+ <InputCurrency<Entity>
+ name="initial_balance"
+ label={i18n.str`Initial balance`}
+ tooltip={i18n.str`balance prior to deposit`}
+ readonly
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`URL of exchange`}
+ readonly
+ />
+ <InputSelector<Entity>
+ name="wire_method"
+ label={i18n.str`Wire method`}
+ tooltip={i18n.str`method to use for wire transfer`}
+ values={wireMethods}
+ placeholder={i18n.str`Select one wire method`}
+ />
+ </FormProvider>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button
+ class="button"
+ onClick={() => setCurrentStep(Steps.EXCHANGE)}
+ >
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </Fragment>
+ );
+ }
+ }
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [reserve, setReserve] = useState<Partial<Entity>>({});
+
+ const submitForm = () => {
+ return onCreate(reserve as Entity);
+ };
+
+ const [currentStep, setCurrentStep] = useState(Steps.EXCHANGE);
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ <li class={currentStep === Steps.EXCHANGE ? "is-active" : ""}>
+ <a style={{ cursor: "initial" }}>
+ <span>Step 1: Specify exchange</span>
+ </a>
+ </li>
+ <li
+ class={currentStep === Steps.WIRE_METHOD ? "is-active" : ""}
+ >
+ <a style={{ cursor: "initial" }}>
+ <span>Step 2: Select wire method</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <ViewStep
+ step={currentStep}
+ reserve={reserve}
+ setCurrentStep={setCurrentStep}
+ setReserve={setReserve}
+ submitForm={submitForm}
+ onBack={onBack}
+ />
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
new file mode 100644
index 000000000..445ca3ef0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
@@ -0,0 +1,120 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatedSuccessfully as TestedComponent } from "./CreatedSuccessfully.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+export default {
+ title: "Pages/Reserve/CreatedSuccessfully",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const OneBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
+
+export const ThreeBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank/bank1.taler:8080/asd",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank/bank2.taler:8080/qwe",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
+
+export const NoBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payo://x-talr-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..1d512c843
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
@@ -0,0 +1,190 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { MerchantBackend, WireAccount } from "../../../../declaration.js";
+
+type Entity = {
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
+};
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+function isNotUndefined<X>(x: X | undefined): x is X {
+ return !!x;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ readonly
+ class="input"
+ value={entity.request.initial_balance}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Wire transfer subject</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.response.reserve_pub}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <ShowAccountsOfReserveAsQRWithLink
+ accounts={entity.response.accounts ?? []}
+ message={entity.response.reserve_pub}
+ amount={entity.request.initial_balance}
+ />
+ </Template>
+ );
+}
+
+export function ShowAccountsOfReserveAsQRWithLink({
+ accounts,
+ message,
+ amount,
+}: {
+ accounts: WireAccount[];
+ message: string;
+ amount: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const accountsInfo = !accounts
+ ? []
+ : accounts
+ .map((acc) => {
+ const p = parsePaytoUri(acc.payto_uri);
+ if (p) {
+ p.params["message"] = message;
+ p.params["amount"] = amount;
+ }
+ return p;
+ })
+ .filter(isNotUndefined);
+
+ const links = accountsInfo.map((a) => stringifyPaytoUri(a));
+
+ if (links.length === 0) {
+ return (
+ <Fragment>
+ <p class="is-size-5">
+ The reserve have invalid accounts. List of invalid payto URIs below:
+ </p>
+ <ul>
+ {accounts.map((a, idx) => {
+ return <li key={idx}>{a.payto_uri}</li>;
+ })}
+ </ul>
+ </Fragment>
+ );
+ }
+
+ if (links.length === 1) {
+ return (
+ <Fragment>
+ <p class="is-size-5">
+ <i18n.Translate>
+ To complete the setup of the reserve, you must now initiate a wire
+ transfer using the given wire transfer subject and crediting the
+ specified amount to the indicated account of the exchange.
+ </i18n.Translate>
+ </p>
+ <p style={{ margin: 10 }}>
+ <b>Exchange bank account</b>
+ </p>
+ <QR text={links[0]} />
+ <p class="is-size-5">
+ <i18n.Translate>
+ If your system supports RFC 8905, you can do this by opening this
+ URI:
+ </i18n.Translate>
+ </p>
+ <pre>
+ <a target="_blank" rel="noreferrer" href={links[0]}>
+ {links[0]}
+ </a>
+ </pre>
+ </Fragment>
+ );
+ }
+
+ return (
+ <div>
+ <p class="is-size-5">
+ <i18n.Translate>
+ To complete the setup of the reserve, you must now initiate a wire
+ transfer using the given wire transfer subject and crediting the
+ specified amount to one of the indicated account of the exchange.
+ </i18n.Translate>
+ </p>
+
+ <p style={{ margin: 10 }}>
+ <b>Exchange bank accounts</b>
+ </p>
+ <p class="is-size-5">
+ <i18n.Translate>
+ If your system supports RFC 8905, you can do this by clicking on the
+ URI below the QR code:
+ </i18n.Translate>
+ </p>
+ {links.map((link) => {
+ return (
+ <Fragment>
+ <QR text={link} />
+ <pre>
+ <a target="_blank" rel="noreferrer" href={link}>
+ {link}
+ </a>
+ </pre>
+ </Fragment>
+ );
+ })}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx
new file mode 100644
index 000000000..4bbaf1459
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useReservesAPI } from "../../../../hooks/reserves.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+import { CreatePage } from "./CreatePage.js";
+interface Props {
+ onBack: () => void;
+ onConfirm: () => void;
+}
+export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
+ const { createReserve } = useReservesAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ const [createdOk, setCreatedOk] = useState<
+ | {
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
+ }
+ | undefined
+ >(undefined);
+
+ if (createdOk) {
+ return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />;
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => {
+ return createReserve(request)
+ .then((r) => setCreatedOk({ request, response: r.data }))
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create reserve`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
new file mode 100644
index 000000000..d8840eeac
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
@@ -0,0 +1,266 @@
+/*
+ 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 {
+ Amounts,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { QR } from "../../../../components/exception/QR.js";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { TextField } from "../../../../components/form/TextField.js";
+import { SimpleModal } from "../../../../components/modal/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useRewardDetails } from "../../../../hooks/reserves.js";
+import { RewardInfo } from "./RewardInfo.js";
+import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.ReserveDetail;
+type CT = MerchantBackend.ContractTerms;
+
+interface Props {
+ onBack: () => void;
+ selected: Entity;
+ id: string;
+}
+
+export function DetailPage({ id, selected, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const didExchangeAckTransfer = Amounts.isNonZero(
+ Amounts.parseOrThrow(selected.exchange_initial_amount),
+ );
+
+ return (
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="section main-section">
+ <FormProvider object={{ ...selected, id }} valueHandler={null}>
+ <InputDate<Entity>
+ name="creation_time"
+ label={i18n.str`Created at`}
+ readonly
+ />
+ <InputDate<Entity>
+ name="expiration_time"
+ label={i18n.str`Valid until`}
+ readonly
+ />
+ <InputCurrency<Entity>
+ name="merchant_initial_amount"
+ label={i18n.str`Created balance`}
+ readonly
+ />
+ <TextField<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ readonly
+ >
+ <a target="_blank" rel="noreferrer" href={selected.exchange_url}>
+ {selected.exchange_url}
+ </a>
+ </TextField>
+
+ {didExchangeAckTransfer && (
+ <Fragment>
+ <InputCurrency<Entity>
+ name="exchange_initial_amount"
+ label={i18n.str`Exchange balance`}
+ readonly
+ />
+ <InputCurrency<Entity>
+ name="pickup_amount"
+ label={i18n.str`Picked up`}
+ readonly
+ />
+ <InputCurrency<Entity>
+ name="committed_amount"
+ label={i18n.str`Committed`}
+ readonly
+ />
+ </Fragment>
+ )}
+ <Input name="id" label={i18n.str`Subject`} readonly />
+ </FormProvider>
+
+ {didExchangeAckTransfer ? (
+ <Fragment>
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <i18n.Translate>Rewards</i18n.Translate>
+ </p>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {selected.rewards && selected.rewards.length > 0 ? (
+ <Table rewards={selected.rewards} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ ) : selected.accounts ? (
+ <ShowAccountsOfReserveAsQRWithLink
+ accounts={selected.accounts}
+ amount={selected.merchant_initial_amount}
+ message={id}
+ />
+ ) : undefined}
+
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ No reward has been authorized from this reserve
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface TableProps {
+ rewards: MerchantBackend.Rewards.RewardStatusEntry[];
+}
+
+function Table({ rewards }: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Authorized</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Picked up</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Reason</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expiration</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {rewards.map((t, i) => {
+ return <RewardRow id={t.reward_id} key={i} entry={t} />;
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function RewardRow({
+ id,
+ entry,
+}: {
+ id: string;
+ entry: MerchantBackend.Rewards.RewardStatusEntry;
+}) {
+ const [selected, setSelected] = useState(false);
+ const result = useRewardDetails(id);
+ const [settings] = useSettings();
+ if (result.loading) {
+ return (
+ <tr>
+ <td>...</td>
+ <td>...</td>
+ <td>...</td>
+ <td>...</td>
+ </tr>
+ );
+ }
+ if (!result.ok) {
+ return (
+ <tr>
+ <td>...</td> {/* authorized */}
+ <td>{entry.total_amount}</td>
+ <td>{entry.reason}</td>
+ <td>...</td> {/* expired */}
+ </tr>
+ );
+ }
+ const info = result.data;
+ function onSelect() {
+ setSelected(true);
+ }
+ return (
+ <Fragment>
+ {selected && (
+ <SimpleModal
+ description="reward"
+ active
+ onCancel={() => setSelected(false)}
+ >
+ <RewardInfo id={id} amount={info.total_authorized} entity={info} />
+ </SimpleModal>
+ )}
+ <tr>
+ <td onClick={onSelect}>{info.total_authorized}</td>
+ <td onClick={onSelect}>{info.total_picked_up}</td>
+ <td onClick={onSelect}>{info.reason}</td>
+ <td onClick={onSelect}>
+ {info.expiration.t_s === "never"
+ ? "never"
+ : format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))}
+ </td>
+ </tr>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
new file mode 100644
index 000000000..41c715f20
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
@@ -0,0 +1,126 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Reserve/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Funded = createExample(TestedComponent, {
+ id: "THISISTHERESERVEID",
+ selected: {
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
+ exchange_url: "http://exchange.taler/",
+ },
+});
+
+export const NotYetFunded = createExample(TestedComponent, {
+ id: "THISISTHERESERVEID",
+ selected: {
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:0",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
+ exchange_url: "http://exchange.taler/",
+ },
+});
+
+export const FundedWithEmptyRewards = createExample(TestedComponent, {
+ id: "THISISTHERESERVEID",
+ selected: {
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
+ exchange_url: "http://exchange.taler/",
+ rewards: [
+ {
+ reason: "asdasd",
+ reward_id: "123",
+ total_amount: "TESTKUDOS:1",
+ },
+ ],
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
new file mode 100644
index 000000000..491028695
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
@@ -0,0 +1,94 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ datetimeFormatForSettings,
+ useSettings,
+} from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.RewardDetails;
+
+interface Props {
+ id: string;
+ entity: Entity;
+ amount: string;
+}
+
+export function RewardInfo({
+ id: merchantRewardId,
+ amount,
+ entity,
+}: Props): VNode {
+ const { url: backendURL } = useBackendContext();
+ const [settings] = useSettings();
+ const rewardURL = "not-supported";
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={amount} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">URL</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field" style={{ overflowWrap: "anywhere" }}>
+ <p class="control">
+ <a target="_blank" rel="noreferrer" href={rewardURL}>
+ {rewardURL}
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Valid until</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={
+ !entity.expiration || entity.expiration.t_s === "never"
+ ? "never"
+ : format(
+ entity.expiration.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
+ }
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
new file mode 100644
index 000000000..8e2a74529
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useReserveDetails } from "../../../../hooks/reserves.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ rid: string;
+
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onDelete: () => void;
+ onBack: () => void;
+}
+export default function DetailReserve({
+ rid,
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onBack,
+ onDelete,
+}: Props): VNode {
+ const result = useReserveDetails(rid);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+ return (
+ <Fragment>
+ <DetailPage selected={result.data} onBack={onBack} id={rid} />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
new file mode 100644
index 000000000..e205ee621
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
@@ -0,0 +1,124 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import * as yup from "yup";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import {
+ ConfirmModal,
+ ContinueModal,
+} from "../../../../components/modal/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { AuthorizeRewardSchema } from "../../../../schemas/index.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+
+interface AuthorizeRewardModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void;
+ rewardAuthorized?: {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+ };
+}
+
+export function AuthorizeRewardModal({
+ onCancel,
+ onConfirm,
+ rewardAuthorized,
+}: AuthorizeRewardModalProps): VNode {
+ // const result = useOrderDetails(id)
+ type State = MerchantBackend.Rewards.RewardCreateRequest;
+ const [form, setValue] = useState<Partial<State>>({});
+ const { i18n } = useTranslationContext();
+
+ // const [errors, setErrors] = useState<FormErrors<State>>({})
+ let errors: FormErrors<State> = {};
+ try {
+ AuthorizeRewardSchema.validateSync(form, { abortEarly: false });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as any[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const validateAndConfirm = () => {
+ onConfirm(form as State);
+ };
+ if (rewardAuthorized) {
+ return (
+ <ContinueModal description="reward" active onConfirm={onCancel}>
+ <CreatedSuccessfully
+ entity={rewardAuthorized.response}
+ request={rewardAuthorized.request}
+ onConfirm={onCancel}
+ />
+ </ContinueModal>
+ );
+ }
+
+ return (
+ <ConfirmModal
+ description="New reward"
+ active
+ onCancel={onCancel}
+ disabled={hasErrors}
+ onConfirm={validateAndConfirm}
+ >
+ <FormProvider<State>
+ errors={errors}
+ object={form}
+ valueHandler={setValue}
+ >
+ <InputCurrency<State>
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`amount of reward`}
+ />
+ <Input<State>
+ name="justification"
+ label={i18n.str`Justification`}
+ inputType="multiline"
+ tooltip={i18n.str`reason for the reward`}
+ />
+ <Input<State>
+ name="next_url"
+ label={i18n.str`URL after reward`}
+ tooltip={i18n.str`URL to visit after reward payment`}
+ />
+ </FormProvider>
+ </ConfirmModal>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..b78236bc7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
@@ -0,0 +1,102 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.RewardCreateConfirmation;
+
+interface Props {
+ entity: Entity;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({
+ request,
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const [settings] = useSettings();
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={request.amount} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Justification</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={request.justification} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">URL</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={entity.reward_status_url} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Valid until</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={
+ !entity.reward_expiration ||
+ entity.reward_expiration.t_s === "never"
+ ? "never"
+ : format(
+ entity.reward_expiration.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
+ }
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
new file mode 100644
index 000000000..b070bbde3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
@@ -0,0 +1,96 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
+
+export default {
+ title: "Pages/Reserve/List",
+ component: TestedComponent,
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const AllFunded = createExample(TestedComponent, {
+ instances: [
+ {
+ id: "reseverId",
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ {
+ id: "reseverId2",
+ active: true,
+ committed_amount: "TESTKUDOS:13",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ ],
+});
+
+export const Empty = createExample(TestedComponent, {
+ instances: [],
+});
+
+export const OneNotYetFunded = createExample(TestedComponent, {
+ instances: [
+ {
+ id: "reseverId",
+ active: true,
+ committed_amount: "TESTKUDOS:0",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:0",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
new file mode 100644
index 000000000..795e7ec82
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
@@ -0,0 +1,320 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId;
+
+interface Props {
+ instances: Entity[];
+ onNewReward: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+ onDelete: (id: Entity) => void;
+ onCreate: () => void;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onNewReward,
+ onDelete,
+}: Props): VNode {
+ const [withoutFunds, withFunds] = instances.reduce((prev, current) => {
+ const amount = current.exchange_initial_amount;
+ if (amount.endsWith(":0")) {
+ prev[0] = prev[0].concat(current);
+ } else {
+ prev[1] = prev[1].concat(current);
+ }
+ return prev;
+ }, new Array<Array<Entity>>([], []));
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ {withoutFunds.length > 0 && (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash" />
+ </span>
+ <i18n.Translate>Reserves not yet funded</i18n.Translate>
+ </p>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ <TableWithoutFund
+ instances={withoutFunds}
+ onNewReward={onNewReward}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash" />
+ </span>
+ <i18n.Translate>Reserves ready</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options" />
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new reserve`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {withFunds.length > 0 ? (
+ <Table
+ instances={withFunds}
+ onNewReward={onNewReward}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+interface TableProps {
+ instances: Entity[];
+ onNewReward: (id: Entity) => void;
+ onDelete: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+}
+
+function Table({ instances, onNewReward, onSelect, onDelete }: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Created at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expires at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Initial</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Picked up</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Committed</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.creation_time.t_s === "never"
+ ? "never"
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.expiration_time.t_s === "never"
+ ? "never"
+ : format(
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.exchange_initial_amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.pickup_amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.committed_amount}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-small is-danger has-tooltip-left"
+ data-tooltip={i18n.str`delete selected reserve from the database`}
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ Delete
+ </button>
+ <button
+ class="button is-small is-info has-tooltip-left"
+ data-tooltip={i18n.str`authorize new reward from selected reserve`}
+ type="button"
+ onClick={(): void => onNewReward(i)}
+ >
+ New Reward
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no ready reserves yet, add more pressing the + sign or fund
+ them
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+function TableWithoutFund({
+ instances,
+ onSelect,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Created at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expires at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expected Balance</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.creation_time.t_s === "never"
+ ? "never"
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.expiration_time.t_s === "never"
+ ? "never"
+ : format(
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.merchant_initial_amount}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-small is-danger jb-modal has-tooltip-left"
+ type="button"
+ data-tooltip={i18n.str`delete selected reserve from the database`}
+ onClick={(): void => onDelete(i)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx
new file mode 100644
index 000000000..b26ff0000
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx
@@ -0,0 +1,171 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceReserves,
+ useReservesAPI,
+} from "../../../../hooks/reserves.js";
+import { Notification } from "../../../../utils/types.js";
+import { AuthorizeRewardModal } from "./AutorizeRewardModal.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onSelect: (id: string) => void;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+}
+
+interface RewardConfirmation {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+}
+
+export default function ListRewards({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onSelect,
+ onCreate,
+}: Props): VNode {
+ const result = useInstanceReserves();
+ const { deleteReserve, authorizeRewardReserve } = useReservesAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [reserveForReward, setReserveForReward] = useState<string | undefined>(
+ undefined,
+ );
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null);
+ const [rewardAuthorized, setRewardAuthorized] = useState<
+ RewardConfirmation | undefined
+ >(undefined);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ {reserveForReward && (
+ <AuthorizeRewardModal
+ onCancel={() => {
+ setReserveForReward(undefined);
+ setRewardAuthorized(undefined);
+ }}
+ rewardAuthorized={rewardAuthorized}
+ onConfirm={async (request) => {
+ try {
+ const response = await authorizeRewardReserve(
+ reserveForReward,
+ request,
+ );
+ setRewardAuthorized({
+ request,
+ response: response.data,
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`could not create the reward`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ setReserveForReward(undefined);
+ }
+ }}
+ />
+ )}
+
+ <CardTable
+ instances={result.data.reserves
+ .filter((r) => r.active)
+ .map((o) => ({ ...o, id: o.reserve_pub }))}
+ onCreate={onCreate}
+ onDelete={(reserve) => {
+ setDeleting(reserve)
+ }}
+ onSelect={(reserve) => onSelect(reserve.id)}
+ onNewReward={(reserve) => setReserveForReward(reserve.id)}
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete reserve`}
+ description={`Delete the reserve`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteReserve(deleting.reserve_pub);
+ setNotif({
+ message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete reserve`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the reserve for <b>&quot;{deleting.merchant_initial_amount}&quot;</b> you won't be able to create more rewards. <br />
+ Reserve ID: <b>{deleting.reserve_pub}</b>
+ </p>
+ <p class="warning">
+ Deleting an template <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
new file mode 100644
index 000000000..c9d17ea3b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Templates/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
new file mode 100644
index 000000000..947f3572c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -0,0 +1,259 @@
+/*
+ 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 {
+ Amounts,
+ MerchantTemplateContractDetails,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { InputTab } from "../../../../components/form/InputTab.js";
+
+enum Steps {
+ BOTH_FIXED,
+ FIXED_PRICE,
+ FIXED_SUMMARY,
+ NON_FIXED,
+}
+
+type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps };
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const devices = useInstanceOtpDevices()
+
+ const [state, setState] = useState<Partial<Entity>>({
+ template_contract: {
+ minimum_age: 0,
+ pay_duration: {
+ d_us: 1000 * 1000 * 60 * 30, //30 min
+ },
+ },
+ type: Steps.NON_FIXED,
+ });
+
+ const parsedPrice = !state.template_contract?.amount
+ ? undefined
+ : Amounts.parse(state.template_contract?.amount);
+
+ const errors: FormErrors<Entity> = {
+ template_id: !state.template_id
+ ? i18n.str`should not be empty`
+ : !/[a-zA-Z0-9]*/.test(state.template_id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+ template_description: !state.template_description
+ ? i18n.str`should not be empty`
+ : undefined,
+ template_contract: !state.template_contract
+ ? undefined
+ : undefinedIfEmpty({
+ amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.amount
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.summary
+ ? i18n.str`required`
+ : undefined,
+ minimum_age:
+ state.template_contract.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.template_contract.pay_duration
+ ? i18n.str`can't be empty`
+ : state.template_contract.pay_duration.d_us === "forever"
+ ? undefined
+ : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (state.template_contract) {
+ if (state.type === Steps.NON_FIXED) {
+ delete state.template_contract.amount;
+ delete state.template_contract.summary;
+ } else if (state.type === Steps.FIXED_SUMMARY) {
+ delete state.template_contract.amount;
+ } else if (state.type === Steps.FIXED_PRICE) {
+ delete state.template_contract.summary;
+ }
+ }
+ delete state.type
+ return onCreate(state as any);
+ };
+
+ const deviceList = !devices.ok ? [] : devices.data.otp_devices
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputWithAddon<Entity>
+ name="template_id"
+ help={`${backendURL}/templates/${state.template_id ?? ""}`}
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the template in URLs.`}
+ />
+ <Input<Entity>
+ name="template_description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <InputTab
+ name="type"
+ label={i18n.str`Type`}
+ help={(() => {
+ switch (state.type) {
+ case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
+ case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
+ case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
+ case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
+ }
+ })()}
+ tooltip={i18n.str`Define what the user be allowed to modify`}
+ values={[
+ Steps.NON_FIXED,
+ Steps.FIXED_PRICE,
+ Steps.FIXED_SUMMARY,
+ Steps.BOTH_FIXED,
+ ]}
+ toStr={(v: Steps): string => {
+ switch (v) {
+ case Steps.NON_FIXED: return i18n.str`Simple`
+ case Steps.FIXED_PRICE: return i18n.str`With price`
+ case Steps.FIXED_SUMMARY: return i18n.str`With summary`
+ case Steps.BOTH_FIXED: return i18n.str`With price and summary`
+ }
+ }}
+ />
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
+ <Input
+ name="template_contract.summary"
+ inputType="multiline"
+ label={i18n.str`Fixed summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ : undefined}
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
+ <InputCurrency
+ name="template_contract.amount"
+ label={i18n.str`Fixed price`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ : undefined}
+ <InputNumber
+ name="template_contract.minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration
+ name="template_contract.pay_duration"
+ label={i18n.str`Payment timeout`}
+ help=""
+ tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
+ />
+ <Input<Entity>
+ name="otp_id"
+ label={i18n.str`OTP device`}
+ readonly
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ <InputSearchOnList
+ label={i18n.str`Search device`}
+ onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))}
+ list={deviceList.map(e => ({
+ description: e.device_description,
+ id: e.otp_device_id
+ }))}
+ />
+
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx
new file mode 100644
index 000000000..a29ee53b6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTemplateAPI } from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { createTemplate } = useTemplateAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => {
+ return createTemplate(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
new file mode 100644
index 000000000..702e9ba4a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
new file mode 100644
index 000000000..bf6062c34
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ templates: MerchantBackend.Template.TemplateEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onSelect: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onQR: (e: MerchantBackend.Template.TemplateEntry) => void;
+}
+
+export function ListPage({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onNewOrder,
+ onQR,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <CardTable
+ templates={templates.map((o) => ({
+ ...o,
+ id: String(o.template_id),
+ }))}
+ onQR={onQR}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onNewOrder={onNewOrder}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx
new file mode 100644
index 000000000..9fdf4ead9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -0,0 +1,235 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.TemplateEntry;
+
+interface Props {
+ templates: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onNewOrder: (e: Entity) => void;
+ onQR: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onQR,
+ onNewOrder,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Templates</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new templates`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {templates.length > 0 ? (
+ <Table
+ instances={templates}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onNewOrder={onNewOrder}
+ onQR={onQR}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onNewOrder: (e: Entity) => void;
+ onQR: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onNewOrder,
+ onQR,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer templates</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.template_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.template_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.template_description}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected templates from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`use template to create new order`}
+ onClick={() => onNewOrder(i)}
+ >
+ Use template
+ </button>
+ <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`create qr code for the template`}
+ onClick={() => onQR(i)}
+ >
+ QR
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older templates</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no templates yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx
new file mode 100644
index 000000000..c7927b772
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -0,0 +1,152 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceTemplates,
+ useTemplateAPI,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onNewOrder: (id: string) => void;
+ onQR: (id: string) => void;
+}
+
+export default function ListTemplates({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onQR,
+ onSelect,
+ onNewOrder,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteTemplate, testTemplateExist } = useTemplateAPI();
+ const result = useInstanceTemplates({ position }, (id) => setPosition(id));
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Template.TemplateEntry | null>(null);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={testTemplateExist}
+ onSelect={onSelect}
+ description={i18n.str`jump to template with the given template ID`}
+ placeholder={i18n.str`template id`}
+ />
+
+ <ListPage
+ templates={result.data.templates}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.template_id);
+ }}
+ onNewOrder={(e) => {
+ onNewOrder(e.template_id);
+ }}
+ onQR={(e) => {
+ onQR(e.template_id);
+ }}
+ onDelete={(e: MerchantBackend.Template.TemplateEntry) => {
+ setDeleting(e)
+ }
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete template`}
+ description={`Delete the template "${deleting.template_description}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteTemplate(deleting.template_id);
+ setNotif({
+ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete template`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the template <b>&quot;{deleting.template_description}&quot;</b> (ID:{" "}
+ <b>{deleting.template_id}</b>) you may loose information
+ </p>
+ <p class="warning">
+ Deleting an template <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx
new file mode 100644
index 000000000..eb853c8ff
--- /dev/null
+++ b/packages/auditor-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/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
new file mode 100644
index 000000000..5140aae3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -0,0 +1,172 @@
+/*
+ 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 { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { QR } from "../../../../components/exception/QR.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.UsingTemplateDetails;
+
+interface Props {
+ contract: MerchantBackend.Template.TemplateContractDetails;
+ id: string;
+ onBack?: () => void;
+}
+
+export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const { id: instanceId } = useInstanceContext();
+ const config = useConfigContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ amount: contract.amount,
+ summary: contract.summary,
+ });
+
+ const errors: FormErrors<Entity> = {};
+
+ const fixedAmount = !!contract.amount;
+ const fixedSummary = !!contract.summary;
+
+ const templateParams: Record<string, string> = {}
+ if (!fixedAmount) {
+ if (state.amount) {
+ templateParams.amount = state.amount
+ } else {
+ templateParams.amount = config.currency
+ }
+ }
+
+ if (!fixedSummary) {
+ templateParams.summary = state.summary ?? ""
+ }
+
+ const merchantBaseUrl = new URL(backendURL).href;
+
+ const payTemplateUri = stringifyPayTemplateUri({
+ merchantBaseUrl,
+ templateId,
+ templateParams
+ })
+
+ const issuer = encodeURIComponent(
+ `${new URL(backendURL).host}/${instanceId}`,
+ );
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <p class="is-size-5 mt-5 mb-5">
+ <i18n.Translate>
+ Here you can specify a default value for fields that are not
+ fixed. Default values can be edited by the customer before the
+ payment.
+ </i18n.Translate>
+ </p>
+
+ <p></p>
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputCurrency<Entity>
+ name="amount"
+ label={
+ fixedAmount
+ ? i18n.str`Fixed amount`
+ : i18n.str`Default amount`
+ }
+ readonly={fixedAmount}
+ tooltip={i18n.str`Amount of the order`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ readonly={fixedSummary}
+ label={
+ fixedSummary
+ ? i18n.str`Fixed summary`
+ : i18n.str`Default summary`
+ }
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <button
+ class="button is-info"
+ onClick={() => saveAsPDF(templateId)}
+ >
+ <i18n.Translate>Print</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ <section id="printThis">
+ <QR text={payTemplateUri} />
+ <pre style={{ textAlign: "center" }}>
+ <a href={payTemplateUri}>{payTemplateUri}</a>
+ </pre>
+ </section>
+ </div>
+ );
+}
+
+function saveAsPDF(name: string): void {
+ const printWindow = window.open("", "", "height=400,width=800");
+ if (!printWindow) return;
+ const divContents = document.getElementById("printThis");
+ if (!divContents) return;
+ printWindow.document.write(
+ `<html><head><title>Order template for ${name}</title><style>`,
+ );
+ printWindow.document.write("</style></head><body>&nbsp;</body></html>");
+ printWindow.document.close();
+ printWindow.document.body.appendChild(divContents.cloneNode(true));
+ printWindow.addEventListener("load", () => {
+ printWindow.print();
+ printWindow.close();
+ });
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx
new file mode 100644
index 000000000..7db7478f7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { QrPage } from "./QrPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+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 result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
new file mode 100644
index 000000000..8d07cb31f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Templates/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
new file mode 100644
index 000000000..b578d4664
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -0,0 +1,254 @@
+/*
+ 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 {
+ Amounts,
+ MerchantTemplateContractDetails,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { InputTab } from "../../../../components/form/InputTab.js";
+
+enum Steps {
+ BOTH_FIXED,
+ FIXED_PRICE,
+ FIXED_SUMMARY,
+ NON_FIXED,
+}
+
+type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ template: Entity;
+}
+
+export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+
+ const intialStep =
+ template.template_contract?.amount === undefined && template.template_contract?.summary === undefined
+ ? Steps.NON_FIXED
+ : template.template_contract?.summary === undefined
+ ? Steps.FIXED_PRICE
+ : template.template_contract?.amount === undefined
+ ? Steps.FIXED_SUMMARY
+ : Steps.BOTH_FIXED;
+
+ const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ ...template, type: intialStep });
+
+ const parsedPrice = !state.template_contract?.amount
+ ? undefined
+ : Amounts.parse(state.template_contract?.amount);
+
+ const errors: FormErrors<Entity> = {
+ template_description: !state.template_description
+ ? i18n.str`should not be empty`
+ : undefined,
+ template_contract: !state.template_contract
+ ? undefined
+ : undefinedIfEmpty({
+ amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.amount
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.summary
+ ? i18n.str`required`
+ : undefined,
+ minimum_age:
+ state.template_contract.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.template_contract.pay_duration
+ ? i18n.str`can't be empty`
+ : state.template_contract.pay_duration.d_us === "forever"
+ ? undefined
+ : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (state.template_contract) {
+ if (state.type === Steps.NON_FIXED) {
+ delete state.template_contract.amount;
+ delete state.template_contract.summary;
+ } else if (state.type === Steps.FIXED_SUMMARY) {
+ delete state.template_contract.amount;
+ } else if (state.type === Steps.FIXED_PRICE) {
+ delete state.template_contract.summary;
+ }
+ }
+ delete state.type
+ return onUpdate(state as any);
+ };
+
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ {backendURL}/templates/{template.id}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputWithAddon<Entity>
+ name="id"
+ addonBefore={`templates/`}
+ readonly
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the template in URLs.`}
+ />
+
+ <Input<Entity>
+ name="template_description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <InputTab
+ name="type"
+ label={i18n.str`Type`}
+ help={(() => {
+ switch (state.type) {
+ case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
+ case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
+ case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
+ case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
+ }
+ })()}
+ tooltip={i18n.str`Define what the user be allowed to modify`}
+ values={[
+ Steps.NON_FIXED,
+ Steps.FIXED_PRICE,
+ Steps.FIXED_SUMMARY,
+ Steps.BOTH_FIXED,
+ ]}
+ toStr={(v: Steps): string => {
+ switch (v) {
+ case Steps.NON_FIXED: return i18n.str`Simple`
+ case Steps.FIXED_PRICE: return i18n.str`With price`
+ case Steps.FIXED_SUMMARY: return i18n.str`With summary`
+ case Steps.BOTH_FIXED: return i18n.str`With price and summary`
+ }
+ }}
+ />
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
+ <Input
+ name="template_contract.summary"
+ inputType="multiline"
+ label={i18n.str`Fixed summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ : undefined}
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
+ <InputCurrency
+ name="template_contract.amount"
+ label={i18n.str`Fixed price`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ : undefined}
+ <InputNumber
+ name="template_contract.minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration
+ name="template_contract.pay_duration"
+ label={i18n.str`Payment timeout`}
+ help=""
+ tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx
new file mode 100644
index 000000000..3adca45db
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx
@@ -0,0 +1,99 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+export default function UpdateTemplate({
+ tid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateTemplate } = useTemplateAPI();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ template={{ ...result.data, id: tid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateTemplate(tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
new file mode 100644
index 000000000..13576d94d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.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 { UsePage as TestedComponent } from "./UsePage.js";
+
+export default {
+ title: "Pages/Templates/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
new file mode 100644
index 000000000..983804d3e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
@@ -0,0 +1,143 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.UsingTemplateDetails;
+
+interface Props {
+ id: string;
+ template: MerchantBackend.Template.TemplateDetails;
+ onCreateOrder: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ amount: template.template_contract.amount,
+ summary: template.template_contract.summary,
+ });
+
+ const errors: FormErrors<Entity> = {
+ amount:
+ !template.template_contract.amount && !state.amount
+ ? i18n.str`Amount is required`
+ : undefined,
+ summary:
+ !template.template_contract.summary && !state.summary
+ ? i18n.str`Order summary is required`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (template.template_contract.amount) {
+ delete state.amount;
+ }
+ if (template.template_contract.summary) {
+ delete state.summary;
+ }
+ return onCreateOrder(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>New order for template</i18n.Translate>:{" "}
+ <b>{id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputCurrency<Entity>
+ name="amount"
+ label={i18n.str`Amount`}
+ readonly={!!template.template_contract.amount}
+ tooltip={i18n.str`Amount of the order`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Order summary`}
+ readonly={!!template.template_contract.summary}
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx
new file mode 100644
index 000000000..ed1242ef5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -0,0 +1,101 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { UsePage } from "./UsePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onOrderCreated: (id: string) => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+
+export default function TemplateUsePage({
+ tid,
+ onOrderCreated,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { createOrderFromTemplate } = useTemplateAPI();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <UsePage
+ template={result.data}
+ id={tid}
+ onBack={onBack}
+ onCreateOrder={(
+ request: MerchantBackend.Template.UsingTemplateDetails,
+ ) => {
+ return createOrderFromTemplate(tid, request)
+ .then((res) => onOrderCreated(res.data.order_id))
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create order from template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx
new file mode 100644
index 000000000..549e7581f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -0,0 +1,183 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import { FormProvider } from "../../../components/form/FormProvider.js";
+import { Input } from "../../../components/form/Input.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { AccessToken } from "../../../declaration.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+
+interface Props {
+ instanceId: string;
+ hasToken: boolean | undefined;
+ onClearToken: (c: AccessToken | undefined) => void;
+ onNewToken: (c: AccessToken | undefined, s: AccessToken) => void;
+ onBack?: () => void;
+}
+
+export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ old_token: "",
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const errors = {
+ old_token: hasToken && !form.old_token
+ ? i18n.str`you need your access token to perform the operation`
+ : undefined,
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const instance = useInstanceContext();
+
+ const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`;
+
+ async function submitForm() {
+ if (hasErrors) return;
+ const oldToken = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined;
+ const newToken = `secret-token:${form.new_token}` as AccessToken;
+ onNewToken(oldToken, newToken)
+ }
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ {text}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ {!hasToken &&
+ <NotificationCard
+ notification={{
+ message: i18n.str`This instance doesn't have authentication token.`,
+ description: i18n.str`You can leave it empty if there is another layer of security.`,
+ type: "WARN",
+ }}
+ />
+ }
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider errors={errors} object={form} valueHandler={setValue}>
+ <Fragment>
+ {hasToken && (
+ <Fragment>
+ <Input<State>
+ name="old_token"
+ label={i18n.str`Current access token`}
+ tooltip={i18n.str`access token currently in use`}
+ inputType="password"
+ />
+ <p>
+ <i18n.Translate>
+ Clearing the access token will mean public access to the instance.
+ </i18n.Translate>
+ </p>
+ <div class="buttons is-right mt-5">
+ <button
+ class="button"
+ onClick={() => {
+ if (hasToken) {
+ const oldToken = `secret-token:${form.old_token}` as AccessToken;
+ onClearToken(oldToken)
+ } else {
+ onClearToken(undefined)
+ }
+ }}
+ >
+ <i18n.Translate>Clear token</i18n.Translate>
+ </button>
+ </div>
+ </Fragment>
+ )}
+
+
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </Fragment>
+ </FormProvider>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm change</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx
new file mode 100644
index 000000000..22365c9e1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx
@@ -0,0 +1,106 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Loading } from "../../../components/exception/loading.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
+import { DetailPage } from "./DetailPage.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { Notification } from "../../../utils/types.js";
+import { useBackendContext } from "../../../context/backend.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onChange: () => void;
+ onNotFound: () => VNode;
+ onCancel: () => void;
+}
+
+export default function Token({
+ onLoadError,
+ onChange,
+ onUnauthorized,
+ onNotFound,
+ onCancel,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { clearAccessToken, setNewAccessToken } = useInstanceAPI();
+ const { id } = useInstanceContext();
+ const result = useInstanceDetails()
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const hasToken = result.data.auth.method === "token"
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <DetailPage
+ instanceId={id}
+ onBack={onCancel}
+ hasToken={hasToken}
+ onClearToken={async (currentToken): Promise<void> => {
+ try {
+ await clearAccessToken(currentToken);
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ onNewToken={async (currentToken, newToken): Promise<void> => {
+ try {
+ await setNewAccessToken(currentToken, newToken);
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx
new file mode 100644
index 000000000..5f0f56f2d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Token",
+ component: TestedComponent,
+};
+
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
new file mode 100644
index 000000000..64b67335c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
@@ -0,0 +1,45 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Transfer/Create",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ accounts: ["payto://x-taler-bank/account1", "payto://x-taler-bank/account2"],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
new file mode 100644
index 000000000..13f5f3c12
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
@@ -0,0 +1,146 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ CROCKFORD_BASE32_REGEX,
+ URL_REGEX,
+} from "../../../../utils/constants.js";
+
+type Entity = MerchantBackend.Transfers.TransferInformation;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ accounts: string[];
+}
+
+export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { currency } = useConfigContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ wtid: "",
+ // payto_uri: ,
+ // exchange_url: 'http://exchange.taler:8081/',
+ credit_amount: ``,
+ });
+
+ const errors: FormErrors<Entity> = {
+ wtid: !state.wtid
+ ? i18n.str`cannot be empty`
+ : !CROCKFORD_BASE32_REGEX.test(state.wtid)
+ ? i18n.str`check the id, does not look valid`
+ : state.wtid.length !== 52
+ ? i18n.str`should have 52 characters, current ${state.wtid.length}`
+ : undefined,
+ payto_uri: !state.payto_uri ? i18n.str`cannot be empty` : undefined,
+ credit_amount: !state.credit_amount ? i18n.str`cannot be empty` : undefined,
+ exchange_url: !state.exchange_url
+ ? i18n.str`cannot be empty`
+ : !URL_REGEX.test(state.exchange_url)
+ ? i18n.str`URL doesn't have the right format`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputSelector
+ name="payto_uri"
+ label={i18n.str`Credited bank account`}
+ values={accounts}
+ placeholder={i18n.str`Select one account`}
+ tooltip={i18n.str`Bank account of the merchant where the payment was received`}
+ />
+ <Input<Entity>
+ name="wtid"
+ label={i18n.str`Wire transfer ID`}
+ help=""
+ tooltip={i18n.str`unique identifier of the wire transfer used by the exchange, must be 52 characters long`}
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`Base URL of the exchange that made the transfer, should have been in the wire transfer subject`}
+ help="http://exchange.taler:8081/"
+ />
+ <InputCurrency<Entity>
+ name="credit_amount"
+ label={i18n.str`Amount credited`}
+ tooltip={i18n.str`Actual amount that was wired to the merchant's bank account`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
new file mode 100644
index 000000000..25551a031
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useTransferAPI } from "../../../../hooks/transfer.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { informTransfer } = useTransferAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const instance = useInstanceBankAccounts();
+ const accounts = !instance.ok
+ ? []
+ : instance.data.accounts.map((a) => a.payto_uri);
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ accounts={accounts}
+ onCreate={(request: MerchantBackend.Transfers.TransferInformation) => {
+ return informTransfer(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform transfer`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
new file mode 100644
index 000000000..92b3f9853
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
@@ -0,0 +1,93 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Transfer/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onDelete: { action: "onDelete" },
+ onLoadMoreBefore: { action: "onLoadMoreBefore" },
+ onLoadMoreAfter: { action: "onLoadMoreAfter" },
+ onShowAll: { action: "onShowAll" },
+ onShowVerified: { action: "onShowVerified" },
+ onShowUnverified: { action: "onShowUnverified" },
+ onChangePayTo: { action: "onChangePayTo" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ transfers: [
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ ],
+ accounts: ["payto://x-taler-bank/bank/some_account"],
+});
+export const Empty = createExample(TestedComponent, {
+ transfers: [],
+ accounts: [],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
new file mode 100644
index 000000000..02b12c4c2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
@@ -0,0 +1,134 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ transfers: MerchantBackend.Transfers.TransferDetails[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onShowAll: () => void;
+ onShowVerified: () => void;
+ onShowUnverified: () => void;
+ isVerifiedTransfers?: boolean;
+ isNonVerifiedTransfers?: boolean;
+ isAllTransfers?: boolean;
+ accounts: string[];
+ onChangePayTo: (p?: string) => void;
+ payTo?: string;
+ onCreate: () => void;
+ onDelete: () => void;
+}
+
+export function ListPage({
+ payTo,
+ onChangePayTo,
+ transfers,
+ onCreate,
+ onDelete,
+ accounts,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+ isAllTransfers,
+ isNonVerifiedTransfers,
+ isVerifiedTransfers,
+ onShowAll,
+ onShowUnverified,
+ onShowVerified,
+}: Props): VNode {
+ const form = { payto_uri: payTo };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <FormProvider
+ object={form}
+ valueHandler={(updater) => onChangePayTo(updater(form).payto_uri)}
+ >
+ <InputSelector
+ name="payto_uri"
+ label={i18n.str`Account URI`}
+ values={accounts}
+ placeholder={i18n.str`Select one account`}
+ tooltip={i18n.str`filter by account address`}
+ />
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ <div class="tabs">
+ <ul>
+ <li class={isAllTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isVerifiedTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`}
+ >
+ <a onClick={onShowVerified}>
+ <i18n.Translate>Verified</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isNonVerifiedTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show wire transfers claimed by the exchange`}
+ >
+ <a onClick={onShowUnverified}>
+ <i18n.Translate>Unverified</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <CardTable
+ transfers={transfers.map((o) => ({
+ ...o,
+ id: String(o.transfer_serial_id),
+ }))}
+ accounts={accounts}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
new file mode 100644
index 000000000..b6b1cf328
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
@@ -0,0 +1,229 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Transfers.TransferDetails & WithId;
+
+interface Props {
+ transfers: Entity[];
+ onDelete: (id: Entity) => void;
+ onCreate: () => void;
+ accounts: string[];
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ transfers,
+ onCreate,
+ onDelete,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-arrow-left-right" />
+ </span>
+ <i18n.Translate>Transfers</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new transfer`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {transfers.length > 0 ? (
+ <Table
+ instances={transfers}
+ onDelete={onDelete}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more transfers before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer transfers</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Credit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Exchange URL</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Confirmed</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Verified</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Executed at</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td>{i.id}</td>
+ <td>{i.credit_amount}</td>
+ <td>{i.payto_uri}</td>
+ <td>{i.exchange_url}</td>
+ <td>{i.confirmed ? i18n.str`yes` : i18n.str`no`}</td>
+ <td>{i.verified ? i18n.str`yes` : i18n.str`no`}</td>
+ <td>
+ {i.execution_time
+ ? i.execution_time.t_s == "never"
+ ? i18n.str`never`
+ : format(
+ i.execution_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
+ : i18n.str`unknown`}
+ </td>
+ <td>
+ {i.verified === undefined ? (
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected transfer from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ ) : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more transfer after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older transfers</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no transfer yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx
new file mode 100644
index 000000000..0fdbb9bc3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx
@@ -0,0 +1,118 @@
+/*
+ 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 { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useInstanceTransfers } from "../../../../hooks/transfer.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+}
+interface Form {
+ verified?: "yes" | "no";
+ payto_uri?: string;
+}
+
+export default function ListTransfer({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onNotFound,
+}: Props): VNode {
+ const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s });
+
+ const [position, setPosition] = useState<string | undefined>(undefined);
+
+ const instance = useInstanceBankAccounts();
+ const accounts = !instance.ok
+ ? []
+ : instance.data.accounts.map((a) => a.payto_uri);
+ const [form, setForm] = useState<Form>({ payto_uri: "" });
+
+ const shoulUseDefaultAccount = accounts.length === 1
+ useEffect(() => {
+ if (shoulUseDefaultAccount) {
+ setForm({...form, payto_uri: accounts[0]})
+ }
+ }, [shoulUseDefaultAccount])
+
+ const isVerifiedTransfers = form.verified === "yes";
+ const isNonVerifiedTransfers = form.verified === "no";
+ const isAllTransfers = form.verified === undefined;
+
+ const result = useInstanceTransfers(
+ {
+ position,
+ payto_uri: form.payto_uri === "" ? undefined : form.payto_uri,
+ verified: form.verified,
+ },
+ (id) => setPosition(id),
+ );
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <ListPage
+ accounts={accounts}
+ transfers={result.data.transfers}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onDelete={() => {
+ null;
+ }}
+ // position={position} setPosition={setPosition}
+ onShowAll={() => setFilter(undefined)}
+ onShowUnverified={() => setFilter("no")}
+ onShowVerified={() => setFilter("yes")}
+ isAllTransfers={isAllTransfers}
+ isVerifiedTransfers={isVerifiedTransfers}
+ isNonVerifiedTransfers={isNonVerifiedTransfers}
+ payTo={form.payto_uri}
+ onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
new file mode 100644
index 000000000..84cc95e72
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
@@ -0,0 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+
+export default function UpdateTransfer(): VNode {
+ return <div>order transfer page</div>;
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
new file mode 100644
index 000000000..817a7025c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Instance/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ selected: {
+ name: "name",
+ auth: { method: "external" },
+ address: {},
+ user_type: "business",
+ use_stefan: true,
+ jurisdiction: {},
+ default_pay_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ merchant_pub: "ASDWQEKASJDKSADJ",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
new file mode 100644
index 000000000..a27a0cb06
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -0,0 +1,176 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../components/form/FormProvider.js";
+import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { undefinedIfEmpty } from "../../../utils/table.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+ default_pay_delay: Duration,
+ default_wire_transfer_delay: Duration,
+};
+
+//MerchantBackend.Instances.InstanceAuthConfigurationMessage
+interface Props {
+ onUpdate: (d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void;
+ selected: MerchantBackend.Instances.QueryInstancesResponse;
+ isLoading: boolean;
+ onBack: () => void;
+}
+
+function convert(
+ from: MerchantBackend.Instances.QueryInstancesResponse,
+): Entity {
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = from;
+
+ const defaults = {
+ use_stefan: false,
+ default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay),
+ default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay),
+ };
+ return { ...defaults, ...rest };
+}
+
+export function UpdatePage({
+ onUpdate,
+ selected,
+ onBack,
+}: Props): VNode {
+ const { id } = useInstanceContext();
+
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ name: !value.name ? i18n.str`required` : undefined,
+ user_type: !value.user_type
+ ? i18n.str`required`
+ : value.user_type !== "business" && value.user_type !== "individual"
+ ? i18n.str`should be business or individual`
+ : undefined,
+ default_pay_delay: !value.default_pay_delay
+ ? i18n.str`required`
+ : !!value.default_wire_transfer_delay &&
+ value.default_wire_transfer_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ?
+ i18n.str`pay delay can't be greater than wire transfer delay` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n.str`required`
+ : undefined,
+ address: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ jurisdiction: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = async (): Promise<void> => {
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>;
+ const result: MerchantBackend.Instances.InstanceReconfigurationMessage = {
+ default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay),
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay),
+ ...rest,
+ }
+ await onUpdate(result);
+ };
+ // const [active, setActive] = useState(false);
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>Instance id</i18n.Translate>: <b>{id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields showId={false} />
+ </FormProvider>
+
+ <div class="buttons is-right mt-4">
+ <button
+ class="button"
+ onClick={onBack}
+ data-tooltip="cancel operation"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <AsyncButton
+ onClick={submit}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx
new file mode 100644
index 000000000..e44cf5c0f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx
@@ -0,0 +1,118 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ HttpResponse,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import {
+ useInstanceAPI,
+ useInstanceDetails,
+ useManagedInstanceDetails,
+ useManagementAPI,
+} from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export interface Props {
+ onBack: () => void;
+ onConfirm: () => void;
+
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onUpdateError: (e: HttpError<MerchantBackend.ErrorDetail>) => void;
+}
+
+export default function Update(props: Props): VNode {
+ const { updateInstance } = useInstanceAPI();
+ const result = useInstanceDetails();
+ return CommonUpdate(props, result, updateInstance, );
+}
+
+export function AdminUpdate(props: Props & { instanceId: string }): VNode {
+ const { updateInstance } = useManagementAPI(
+ props.instanceId,
+ );
+ const result = useManagedInstanceDetails(props.instanceId);
+ return CommonUpdate(props, result, updateInstance, );
+}
+
+function CommonUpdate(
+ {
+ onBack,
+ onConfirm,
+ onLoadError,
+ onNotFound,
+ onUpdateError,
+ onUnauthorized,
+ }: Props,
+ result: HttpResponse<
+ MerchantBackend.Instances.QueryInstancesResponse,
+ MerchantBackend.ErrorDetail
+ >,
+ updateInstance: any,
+): VNode {
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ onBack={onBack}
+ isLoading={false}
+ selected={result.data}
+ onUpdate={(
+ d: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ): Promise<void> => {
+ return updateInstance(d)
+ .then(onConfirm)
+ .catch((error: Error) =>
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ );
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
new file mode 100644
index 000000000..4857ede97
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Webhooks/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
new file mode 100644
index 000000000..bfa2a883e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
@@ -0,0 +1,183 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+
+ const errors: FormErrors<Entity> = {
+ webhook_id: !state.webhook_id ? i18n.str`required` : undefined,
+ event_type: !state.event_type ? i18n.str`required`
+ : state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"`
+ : undefined,
+ http_method: !state.http_method
+ ? i18n.str`required`
+ : !validMethod.includes(state.http_method)
+ ? i18n.str`should be one of '${validMethod.join(", ")}'`
+ : undefined,
+ url: !state.url ? i18n.str`required` : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="webhook_id"
+ label={i18n.str`ID`}
+ tooltip={i18n.str`Webhook ID to use`}
+ />
+ <InputSelector
+ name="event_type"
+ label={i18n.str`Event`}
+ values={[
+ i18n.str`Choose one...`,
+ i18n.str`pay`,
+ i18n.str`refund`,
+ ]}
+ tooltip={i18n.str`The event of the webhook: why the webhook is used`}
+ />
+ <InputSelector
+ name="http_method"
+ label={i18n.str`Method`}
+ values={[
+ i18n.str`Choose one...`,
+ i18n.str`GET`,
+ i18n.str`POST`,
+ i18n.str`PUT`,
+ i18n.str`PATCH`,
+ i18n.str`HEAD`,
+ ]}
+ tooltip={i18n.str`Method used by the webhook`}
+ />
+
+ <Input<Entity>
+ name="url"
+ label={i18n.str`URL`}
+ tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
+ />
+
+ <p>
+ The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string
+ between <pre style={{ display: "inline", padding: 0 }}>&#123;&#123;</pre> and <pre style={{ display: "inline", padding: 0 }}>&#125;&#125;</pre> will
+ be replaced with replaced with the value of the corresponding variable.
+ </p>
+ <p>
+ For example <pre style={{ display: "inline", padding: 0 }}>&#123;&#123;contract_terms.amount&#125;&#125;</pre> will be replaced
+ with the the order's price
+ </p>
+ <p>
+ The short list of variables are:
+ </p>
+ <div class="menu">
+
+ <ul class="menu-list" style={{ listStyleType: "disc", marginLeft: 20 }}>
+ <li><b>contract_terms.summary:</b> order's description </li>
+ <li><b>contract_terms.amount:</b> order's price </li>
+ <li><b>order_id:</b> order's unique identification </li>
+ {state.event_type === "refund" && <Fragment>
+ <li><b>refund_amout:</b> the amount that was being refunded</li>
+ <li><b>reason:</b> the reason entered by the merchant staff for granting the refund</li>
+ <li><b>timestamp:</b> time of the refund in nanoseconds since 1970</li>
+ </Fragment>}
+ </ul>
+ </div>
+ {/* <Input<Entity>
+ name="header_template"
+ label={i18n.str`Http header`}
+ inputType="multiline"
+ tooltip={i18n.str`Header template of the webhook`}
+ /> */}
+ <Input<Entity>
+ name="body_template"
+ inputType="multiline"
+ label={i18n.str`Http body`}
+ tooltip={i18n.str`Body template by the webhook`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
new file mode 100644
index 000000000..924e6d9b8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
+ const { createWebhook } = useWebhookAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => {
+ return createWebhook(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
new file mode 100644
index 000000000..702e9ba4a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
new file mode 100644
index 000000000..87e221e3c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ webhooks: MerchantBackend.Webhooks.WebhookEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
+ onSelect: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
+}
+
+export function ListPage({
+ webhooks,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ webhooks={webhooks.map((o) => ({
+ ...o,
+ id: String(o.webhook_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
new file mode 100644
index 000000000..42a179d2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -0,0 +1,218 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookEntry;
+
+interface Props {
+ webhooks: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ webhooks,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Webhooks</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new webhooks`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {webhooks.length > 0 ? (
+ <Table
+ instances={webhooks}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer webhooks</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Event type</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.webhook_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.webhook_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.event_type}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected webhook from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ {/* <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`test webhook`}
+ onClick={() => onNewOrder(i)}
+ >
+ Test
+ </button> */}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older webhooks</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no webhooks yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
new file mode 100644
index 000000000..a6f6f1511
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
@@ -0,0 +1,109 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceWebhooks,
+ useWebhookAPI,
+} from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListWebhooks({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteWebhook } = useWebhookAPI();
+ const result = useInstanceWebhooks({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ webhooks={result.data.webhooks}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.webhook_id);
+ }}
+ onDelete={(e: MerchantBackend.Webhooks.WebhookEntry) =>
+ deleteWebhook(e.webhook_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`webhook delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the webhook`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
new file mode 100644
index 000000000..8d07cb31f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Templates/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
new file mode 100644
index 000000000..76a23b6e5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
@@ -0,0 +1,146 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ webhook: Entity;
+}
+const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
+
+export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(webhook);
+
+ const errors: FormErrors<Entity> = {
+ event_type: !state.event_type ? i18n.str`required` : undefined,
+ http_method: !state.http_method
+ ? i18n.str`required`
+ : !validMethod.includes(state.http_method)
+ ? i18n.str`should be one of '${validMethod.join(", ")}'`
+ : undefined,
+ url: !state.url ? i18n.str`required` : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Webhook: <b>{webhook.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="event_type"
+ label={i18n.str`Event`}
+ tooltip={i18n.str`The event of the webhook: why the webhook is used`}
+ />
+ <Input<Entity>
+ name="http_method"
+ label={i18n.str`Method`}
+ tooltip={i18n.str`Method used by the webhook`}
+ />
+ <Input<Entity>
+ name="url"
+ label={i18n.str`URL`}
+ tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
+ />
+ <Input<Entity>
+ name="header_template"
+ label={i18n.str`Header`}
+ inputType="multiline"
+ tooltip={i18n.str`Header template of the webhook`}
+ />
+ <Input<Entity>
+ name="body_template"
+ inputType="multiline"
+ label={i18n.str`Body`}
+ tooltip={i18n.str`Body template by the webhook`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
new file mode 100644
index 000000000..3f723ed87
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
@@ -0,0 +1,99 @@
+/*
+ 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useWebhookAPI,
+ useWebhookDetails,
+} from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+export default function UpdateWebhook({
+ tid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateWebhook } = useWebhookAPI();
+ const result = useWebhookDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ webhook={{ ...result.data, id: tid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateWebhook(tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/login/index.tsx b/packages/auditor-backoffice-ui/src/paths/login/index.tsx
new file mode 100644
index 000000000..1c98b7c9b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/login/index.tsx
@@ -0,0 +1,202 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, h, VNode } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useBackendContext } from "../../context/backend.js";
+import { useInstanceContext } from "../../context/instance.js";
+import { AccessToken, LoginToken } from "../../declaration.js";
+import { useCredentialsChecker } from "../../hooks/backend.js";
+
+interface Props {
+ onConfirm: (token: LoginToken | undefined) => void;
+}
+
+function normalizeToken(r: string): AccessToken {
+ return `secret-token:${r}` as AccessToken;
+}
+
+export function LoginPage({ onConfirm }: Props): VNode {
+ const { url: backendURL } = useBackendContext();
+ const { admin, id } = useInstanceContext();
+ const { requestNewLoginToken } = useCredentialsChecker();
+ const [token, setToken] = useState("");
+
+ const { i18n } = useTranslationContext();
+
+
+ const doLogin = useCallback(async function doLoginImpl() {
+ const secretToken = normalizeToken(token);
+ const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}`
+ const result = await requestNewLoginToken(baseUrl, secretToken);
+ if (result.valid) {
+ const { token, expiration } = result
+ onConfirm({ token, expiration });
+ } else {
+ onConfirm(undefined);
+ }
+ }, [id, token])
+
+ if (admin && id !== "default") {
+ //admin trying to access another instance
+ return (<div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <p>
+ <i18n.Translate>Need the access token for the instance.</i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "flex-end",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>)
+ }
+
+ return (
+ <div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <i18n.Translate>Please enter your access token.</i18n.Translate>
+
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "space-between",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <div />
+ <AsyncButton
+ type="is-info"
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode {
+ const [running, setRunning] = useState(false)
+ return <button class={"button " + type} disabled={disabled || running} onClick={() => {
+ setRunning(true)
+ onClick().then(() => {
+ setRunning(false)
+ }).catch(() => {
+ setRunning(false)
+ })
+ }}>
+ {children}
+ </button>
+}
+
+
diff --git a/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
new file mode 100644
index 000000000..061a67025
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
@@ -0,0 +1,34 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { Link } from "preact-router";
+
+export default function NotFoundPage(): VNode {
+ return (
+ <div>
+ <p>That page doesn&apos;t exist.</p>
+ <Link href="/">
+ <h4>Back to Home</h4>
+ </Link>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
new file mode 100644
index 000000000..093c3d09d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
@@ -0,0 +1,112 @@
+import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { FormErrors, FormProvider } from "../../components/form/FormProvider.js";
+import { InputSelector } from "../../components/form/InputSelector.js";
+import { InputToggle } from "../../components/form/InputToggle.js";
+import { LangSelector } from "../../components/menu/LangSelector.js";
+import { Settings, useSettings } from "../../hooks/useSettings.js";
+
+function getBrowserLang(): string | undefined {
+ if (typeof window === "undefined") return undefined;
+ if (window.navigator.languages) return window.navigator.languages[0];
+ if (window.navigator.language) return window.navigator.language;
+ return undefined;
+}
+
+export function Settings({ onClose }: { onClose?: () => void }): VNode {
+ const { i18n } = useTranslationContext()
+ const borwserLang = getBrowserLang()
+ //const { update } = useLang()
+
+ const [value, updateValue] = useSettings()
+ const errors: FormErrors<Settings> = {
+ }
+
+ function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
+ const next = s(value)
+ const v: Settings = {
+ advanceOrderMode: next.advanceOrderMode ?? false,
+ dateFormat: next.dateFormat ?? "ymd"
+ }
+ updateValue(v)
+ }
+
+ return <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div>
+
+ <FormProvider<Settings>
+ name="settings"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Language</i18n.Translate>
+ <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}>
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <div class="field field-body has-addons is-flex-grow-3">
+ <LangSelector />
+ &nbsp;
+ {borwserLang !== undefined && <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-2"
+ onClick={(e) => {
+ //update(borwserLang.substring(0, 2))
+ }}
+ >
+ <i18n.Translate>Set default</i18n.Translate>
+ </button>}
+ </div>
+ </div>
+ <InputToggle<Settings>
+ label={i18n.str`Advance order creation`}
+ tooltip={i18n.str`Shows more options in the order creation form`}
+ name="advanceOrderMode"
+ />
+ <InputSelector<Settings>
+ name="dateFormat"
+ label={i18n.str`Date format`}
+ expand={true}
+ help={
+ value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : ""
+ }
+ toStr={(e) => {
+ if (e === "ymd") return "year month day"
+ if (e === "mdy") return "month day year"
+ if (e === "dmy") return "day month year"
+ return "choose one"
+ }}
+ values={[
+ "ymd",
+ "mdy",
+ "dmy",
+ ]}
+ tooltip={i18n.str`how the date is going to be displayed`}
+ />
+ </FormProvider>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section >
+ {onClose &&
+ <section class="section is-main-section">
+ <button
+ class="button"
+ onClick={onClose}
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </section>
+ }
+ </div >
+} \ No newline at end of file