From 3e060b80428943c6562250a6ff77eff10a0259b7 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 24 Oct 2022 10:46:14 +0200 Subject: repo: integrate packages from former merchant-backoffice.git --- .../src/paths/admin/create/Create.stories.tsx | 46 ++ .../src/paths/admin/create/CreatePage.tsx | 234 +++++++ .../admin/create/InstanceCreatedSuccessfully.tsx | 65 ++ .../src/paths/admin/create/index.tsx | 74 ++ .../src/paths/admin/list/TableActive.tsx | 184 +++++ .../src/paths/admin/list/View.stories.tsx | 82 +++ .../src/paths/admin/list/View.tsx | 80 +++ .../src/paths/admin/list/index.tsx | 126 ++++ .../src/paths/instance/details/DetailPage.tsx | 87 +++ .../src/paths/instance/details/Details.stories.tsx | 61 ++ .../src/paths/instance/details/index.tsx | 65 ++ .../src/paths/instance/kyc/list/ListPage.tsx | 178 +++++ .../src/paths/instance/kyc/list/index.tsx | 51 ++ .../instance/orders/create/Create.stories.tsx | 70 ++ .../paths/instance/orders/create/CreatePage.tsx | 576 +++++++++++++++ .../orders/create/OrderCreatedSuccessfully.tsx | 89 +++ .../src/paths/instance/orders/create/index.tsx | 82 +++ .../instance/orders/details/Detail.stories.tsx | 137 ++++ .../paths/instance/orders/details/DetailPage.tsx | 776 +++++++++++++++++++++ .../src/paths/instance/orders/details/Timeline.tsx | 128 ++++ .../src/paths/instance/orders/details/index.tsx | 67 ++ .../paths/instance/orders/list/List.stories.tsx | 107 +++ .../src/paths/instance/orders/list/ListPage.tsx | 146 ++++ .../src/paths/instance/orders/list/Table.tsx | 412 +++++++++++ .../src/paths/instance/orders/list/index.tsx | 171 +++++ .../instance/products/create/Create.stories.tsx | 42 ++ .../paths/instance/products/create/CreatePage.tsx | 65 ++ .../products/create/CreatedSuccessfully.tsx | 67 ++ .../src/paths/instance/products/create/index.tsx | 55 ++ .../paths/instance/products/list/List.stories.tsx | 58 ++ .../src/paths/instance/products/list/Table.tsx | 479 +++++++++++++ .../src/paths/instance/products/list/index.tsx | 80 +++ .../instance/products/update/Update.stories.tsx | 71 ++ .../paths/instance/products/update/UpdatePage.tsx | 77 ++ .../src/paths/instance/products/update/index.tsx | 71 ++ .../instance/reserves/create/Create.stories.tsx | 42 ++ .../paths/instance/reserves/create/CreatePage.tsx | 168 +++++ .../create/CreatedSuccessfully.stories.tsx | 53 ++ .../reserves/create/CreatedSuccessfully.tsx | 79 +++ .../src/paths/instance/reserves/create/index.tsx | 71 ++ .../paths/instance/reserves/details/DetailPage.tsx | 278 ++++++++ .../instance/reserves/details/Details.stories.tsx | 105 +++ .../paths/instance/reserves/details/TipInfo.tsx | 87 +++ .../src/paths/instance/reserves/details/index.tsx | 56 ++ .../instance/reserves/list/AutorizeTipModal.tsx | 85 +++ .../instance/reserves/list/CreatedSuccessfully.tsx | 100 +++ .../paths/instance/reserves/list/List.stories.tsx | 102 +++ .../src/paths/instance/reserves/list/Table.tsx | 313 +++++++++ .../src/paths/instance/reserves/list/index.tsx | 117 ++++ .../instance/transfers/create/Create.stories.tsx | 43 ++ .../paths/instance/transfers/create/CreatePage.tsx | 104 +++ .../src/paths/instance/transfers/create/index.tsx | 60 ++ .../paths/instance/transfers/list/List.stories.tsx | 93 +++ .../src/paths/instance/transfers/list/ListPage.tsx | 89 +++ .../src/paths/instance/transfers/list/Table.tsx | 225 ++++++ .../src/paths/instance/transfers/list/index.tsx | 85 +++ .../src/paths/instance/transfers/update/index.tsx | 26 + .../src/paths/instance/update/Update.stories.tsx | 61 ++ .../src/paths/instance/update/UpdatePage.tsx | 259 +++++++ .../src/paths/instance/update/index.tsx | 113 +++ .../src/paths/login/index.tsx | 29 + .../src/paths/notfound/index.tsx | 36 + 62 files changed, 8038 insertions(+) create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/details/Details.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/login/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/notfound/index.tsx (limited to 'packages/merchant-backoffice-ui/src/paths') diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx new file mode 100644 index 000000000..c1287557d --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx @@ -0,0 +1,46 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode, FunctionalComponent } from 'preact'; +import { CreatePage as TestedComponent } from './CreatePage'; + + +export default { + title: 'Pages/Instance/Create', + component: TestedComponent, + argTypes: { + onCreate: { action: 'onCreate' }, + goBack: { action: 'goBack' }, + } +}; + +function createExample(Component: FunctionalComponent, props: Partial) { + const r = (args: any) => + r.args = props + return r +} + +export const Example = createExample(TestedComponent, { +}); +// export const Example = (a: any): VNode => ; +// Example.args = { +// isLoading: false +// } diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx new file mode 100644 index 000000000..1851e52f1 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -0,0 +1,234 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import * as yup from "yup"; +import { AsyncButton } from "../../../components/exception/AsyncButton"; +import { + FormErrors, + FormProvider, +} from "../../../components/form/FormProvider"; +import { SetTokenNewInstanceModal } from "../../../components/modal"; +import { MerchantBackend } from "../../../declaration"; +import { Translate, useTranslator } from "../../../i18n"; +import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields"; +import { INSTANCE_ID_REGEX, PAYTO_REGEX } from "../../../utils/constants"; +import { Amounts } from "@gnu-taler/taler-util"; + +export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & { + auth_token?: string; +}; + +interface Props { + onCreate: (d: Entity) => Promise; + onBack?: () => void; + forceId?: string; +} + +function with_defaults(id?: string): Partial { + return { + id, + payto_uris: [], + default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours + default_wire_fee_amortization: 1, + default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 * 1000 }, // two days + }; +} + +function undefinedIfEmpty(obj: T): T | undefined { + return Object.keys(obj).some((k) => (obj as any)[k] !== undefined) + ? obj + : undefined; +} + +export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { + const [value, valueHandler] = useState(with_defaults(forceId)); + const [isTokenSet, updateIsTokenSet] = useState(false); + const [isTokenDialogActive, updateIsTokenDialogActive] = + useState(false); + + const i18n = useTranslator(); + + const errors: FormErrors = { + id: !value.id + ? i18n`required` + : !INSTANCE_ID_REGEX.test(value.id) + ? i18n`is not valid` + : undefined, + name: !value.name ? i18n`required` : undefined, + payto_uris: + !value.payto_uris || !value.payto_uris.length + ? i18n`required` + : undefinedIfEmpty( + value.payto_uris.map((p) => { + return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined; + }) + ), + default_max_deposit_fee: !value.default_max_deposit_fee + ? i18n`required` + : !Amounts.parse(value.default_max_deposit_fee) + ? i18n`invalid format` + : undefined, + default_max_wire_fee: !value.default_max_wire_fee + ? i18n`required` + : !Amounts.parse(value.default_max_wire_fee) + ? i18n`invalid format` + : undefined, + default_wire_fee_amortization: + value.default_wire_fee_amortization === undefined + ? i18n`required` + : isNaN(value.default_wire_fee_amortization) + ? i18n`is not a number` + : value.default_wire_fee_amortization < 1 + ? i18n`must be 1 or greater` + : undefined, + default_pay_delay: !value.default_pay_delay ? i18n`required` : undefined, + default_wire_transfer_delay: !value.default_wire_transfer_delay + ? i18n`required` + : undefined, + address: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n`max 7 lines` + : undefined, + }), + jurisdiction: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n`max 7 lines` + : undefined, + }), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined + ); + + const submit = (): Promise => { + // 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 }) + return onCreate(value as Entity); + }; + + function updateToken(token: string | null) { + valueHandler((old) => ({ + ...old, + auth_token: token === null ? undefined : token, + })); + } + + return ( +
+
+
+
+ {isTokenDialogActive && ( + { + updateIsTokenDialogActive(false); + updateIsTokenSet(false); + }} + onClear={() => { + updateToken(null); + updateIsTokenDialogActive(false); + updateIsTokenSet(true); + }} + onConfirm={(newToken) => { + updateToken(newToken); + updateIsTokenDialogActive(false); + updateIsTokenSet(true); + }} + /> + )} +
+
+
+ +
+
+
+
+

+ +

+
+
+
+
+ +
+
+
+
+ + errors={errors} + object={value} + valueHandler={valueHandler} + > + + + +
+ {onBack && ( + + )} + + Confirm + +
+
+
+
+
+
+ ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx new file mode 100644 index 000000000..00b3f20fc --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { h, VNode } from "preact"; +import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully"; +import { Entity } from "./index"; + +export function InstanceCreatedSuccessfully({ entity, onConfirm }: { entity: Entity; onConfirm: () => void; }): VNode { + return +
+
+ +
+
+
+

+ +

+
+
+
+
+
+ +
+
+
+

+ +

+
+
+
+
+
+ +
+
+
+

+ {entity.auth.method === 'external' && 'external'} + {entity.auth.method === 'token' && + } +

+
+
+
+
; +} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx new file mode 100644 index 000000000..aaed6d666 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx @@ -0,0 +1,74 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../components/menu"; +import { MerchantBackend } from "../../../declaration"; +import { useAdminAPI } from "../../../hooks/instance"; +import { useTranslator } from "../../../i18n"; +import { Notification } from "../../../utils/types"; +import { CreatePage } from "./CreatePage"; +import { InstanceCreatedSuccessfully } from "./InstanceCreatedSuccessfully"; + +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(undefined); + const [createdOk, setCreatedOk] = useState(undefined); + const i18n = useTranslator(); + + if (createdOk) { + return ( + + ); + } + + return ( + + + + { + return createInstance(d) + .then(() => { + setCreatedOk(d); + }) + .catch((error) => { + setNotif({ + message: i18n`Failed to create instance`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx new file mode 100644 index 000000000..928658910 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx @@ -0,0 +1,184 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode } from "preact"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration"; +import { Translate, useTranslator } from "../../../i18n"; + +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([]); + const [rowSelection, rowSelectionHandler] = useState([]) + + 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 = useTranslator() + + return
+
+

Instances

+ +
+ + +
+
+ + + +
+ +
+
+
+
+ {instances.length > 0 ? + : + + } + + + + +} +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; + setInstanceName: (s:string) => void; +} + +function toggleSelected(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 { + return ( +
+
+ + + + + + + + + {instances.map(i => { + return + + + + + + })} + + +
+ + IDName +
+ + { + setInstanceName(i.id); + }}>{i.id}{i.name} +
+ + {!i.deleted && + + } + {i.deleted && + + } +
+
+
+ ) +} + +function EmptyTable(): VNode { + return
+

+ +

+

There is no instances yet, add more pressing the + sign

+
+} + + +interface Actions { + element: MerchantBackend.Instances.Instance; + type: 'DELETE' | 'UPDATE'; +} + +function notEmpty(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +function buildActions(intances: MerchantBackend.Instances.Instance[], selected: string[], action: 'DELETE'): Actions[] { + return selected.map(id => intances.find(i => i.id === id)) + .filter(notEmpty) + .map(id => ({ element: id, type: action })) +} + + diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx new file mode 100644 index 000000000..3da8c2e50 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx @@ -0,0 +1,82 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h } from 'preact'; +import { View } from './View'; + + +export default { + title: 'Pages/Instance/List', + component: View, + argTypes: { + onSelect: { action: 'onSelect' }, + }, +}; + +export const Empty = (a: any) => ; +Empty.args = { + instances: [] +} + +export const WithDefaultInstance = (a: any) => ; +WithDefaultInstance.args = { + instances: [{ + id: 'default', + name: 'the default instance', + merchant_pub: 'abcdef', + payment_targets: [] + }] +} + +export const WithFiveInstance = (a: any) => ; +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/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx new file mode 100644 index 000000000..a77a5a1bf --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../declaration"; +import { CardTable as CardTableActive } from './TableActive'; +import { useState } from 'preact/hooks'; +import { Translate, useTranslator } from "../../../i18n"; + +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 = useTranslator() + + const showingInstances = showIsDeleted ? + instances.filter(i => i.deleted) : (showIsActive ? + instances.filter(i => !i.deleted) : + instances) + + return
+ +
+ + +
+ +
+} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx new file mode 100644 index 000000000..c5609fd10 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -0,0 +1,126 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading"; +import { NotificationCard } from "../../../components/menu"; +import { DeleteModal, PurgeModal } from "../../../components/modal"; +import { MerchantBackend } from "../../../declaration"; +import { HttpError } from "../../../hooks/backend"; +import { useAdminAPI, useBackendInstances } from "../../../hooks/instance"; +import { useTranslator } from "../../../i18n"; +import { Notification } from "../../../utils/types"; +import { View } from "./View"; + +interface Props { + onCreate: () => void; + onUpdate: (id: string) => void; + instances: MerchantBackend.Instances.Instance[]; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (error: HttpError) => VNode; + setInstanceName: (s: string) => void; +} + +export default function Instances({ + onUnauthorized, + onLoadError, + onNotFound, + onCreate, + onUpdate, + setInstanceName, +}: Props): VNode { + const result = useBackendInstances(); + const [deleting, setDeleting] = + useState(null); + const [purging, setPurging] = + useState(null); + const { deleteInstance, purgeInstance } = useAdminAPI(); + const [notif, setNotif] = useState(undefined); + const i18n = useTranslator(); + + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return ; + if (!result.ok) return onLoadError(result); + + return ( + + + + {deleting && ( + setDeleting(null)} + onConfirm={async (): Promise => { + try { + await deleteInstance(deleting.id); + // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) + setNotif({ + message: i18n`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n`Failed to delete instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + // pushNotification({ message: 'delete_error', type: 'ERROR' }) + } + setDeleting(null); + }} + /> + )} + {purging && ( + setPurging(null)} + onConfirm={async (): Promise => { + try { + await purgeInstance(purging.id); + setNotif({ + message: i18n`Instance "${purging.name}" (ID: ${purging.id}) has been disabled`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n`Failed to purge instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setPurging(null); + }} + /> + )} + + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx new file mode 100644 index 000000000..2561f5842 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx @@ -0,0 +1,87 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "../../../components/form/FormProvider"; +import { Input } from "../../../components/form/Input"; +import { MerchantBackend } from "../../../declaration"; +import { useTranslator } from "../../../i18n"; + +type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage; +interface Props { + onUpdate: () => void; + onDelete: () => void; + selected: MerchantBackend.Instances.QueryInstancesResponse; +} + +function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity { + const { accounts, ...rest } = from + const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri) + const defaults = { + default_wire_fee_amortization: 1, + default_pay_delay: { d_us: 1000 * 60 * 60 }, //one hour + default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 }, //two hours + } + return { ...defaults, ...rest, payto_uris }; +} + +export function DetailPage({ selected }: Props): VNode { + const [value, valueHandler] = useState>(convert(selected)) + + const i18n = useTranslator() + + return
+
+
+
+
+
+

+ Here goes the instance description +

+
+
+ +
+
+ +
+
+
+
+ object={value} valueHandler={valueHandler} > + + name="name" readonly label={i18n`Name`} /> + name="payto_uris" readonly label={i18n`Account address`} /> + + +
+
+
+
+ +
+ +} \ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/Details.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/Details.stories.tsx new file mode 100644 index 000000000..fb7c9144c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/Details.stories.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { DetailPage as TestedComponent } from "./DetailPage"; + +export default { + title: "Pages/Instance/Detail", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample( + Component: FunctionalComponent, + props: Partial +) { + const r = (args: any) => ; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + selected: { + accounts: [], + name: "name", + auth: { method: "external" }, + address: {}, + jurisdiction: {}, + default_max_deposit_fee: "TESTKUDOS:2", + default_max_wire_fee: "TESTKUDOS:1", + default_pay_delay: { + d_us: 1000000, + }, + default_wire_fee_amortization: 1, + default_wire_transfer_delay: { + d_us: 100000, + }, + merchant_pub: "ASDWQEKASJDKSADJ", + }, +}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx new file mode 100644 index 000000000..15675891e --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading"; +import { DeleteModal } from "../../../components/modal"; +import { useInstanceContext } from "../../../context/instance"; +import { HttpError } from "../../../hooks/backend"; +import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance"; +import { DetailPage } from "./DetailPage"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => 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(false) + + const { deleteInstance } = useInstanceAPI() + + if (result.clientError && result.isUnauthorized) return onUnauthorized() + if (result.clientError && result.isNotfound) return onNotFound() + if (result.loading) return + if (!result.ok) return onLoadError(result) + + return + setDeleting(true)} + /> + {deleting && setDeleting(false)} + onConfirm={async (): Promise => { + try { + await deleteInstance() + onDelete() + } catch (error) { + } + setDeleting(false) + }} + />} + + +} \ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx new file mode 100644 index 000000000..52363d3cd --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -0,0 +1,178 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; + +export interface Props { + status: MerchantBackend.Instances.AccountKycRedirects; +} + +export function ListPage({ status }: Props): VNode { + const i18n = useTranslator(); + + return ( +
+
+
+

+ + + + Pending KYC verification +

+ +
+
+
+
+
+ {status.pending_kycs.length > 0 ? ( + + ) : ( + + )} +
+
+
+
+ + {status.timeout_kycs.length > 0 ? ( +
+
+

+ + + + Timed out +

+ +
+
+
+
+
+ {status.timeout_kycs.length > 0 ? ( + + ) : ( + + )} +
+
+
+
+ ) : undefined} +
+ ); +} +interface PendingTableProps { + entries: MerchantBackend.Instances.MerchantAccountKycRedirect[]; +} + +interface TimedOutTableProps { + entries: MerchantBackend.Instances.ExchangeKycTimeout[]; +} + +function PendingTable({ entries }: PendingTableProps): VNode { + return ( +
+ + + + + + + + + + {entries.map((e, i) => { + return ( + + + + + + ); + })} + +
+ Exchange + + Target account + + KYC URL +
{e.exchange_url}{e.payto_uri} + + {e.kyc_url} + +
+
+ ); +} + +function TimedOutTable({ entries }: TimedOutTableProps): VNode { + return ( +
+ + + + + + + + + + {entries.map((e, i) => { + return ( + + + + + + ); + })} + +
+ Exchange + + Code + + Http Status +
{e.exchange_url}{e.exchange_code}{e.exchange_http_status}
+
+ ); +} + +function EmptyTable(): VNode { + return ( +
+

+ + + +

+

+ No pending kyc verification! +

+
+ ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx new file mode 100644 index 000000000..5dff01994 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -0,0 +1,51 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { Loading } from "../../../../components/exception/loading"; +import { HttpError } from "../../../../hooks/backend"; +import { useInstanceKYCDetails } from "../../../../hooks/instance"; +import { ListPage } from "./ListPage"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => VNode; + onNotFound: () => VNode; +} + +export default function ListKYC({ + onUnauthorized, + onLoadError, + onNotFound, +}: Props): VNode { + const result = useInstanceKYCDetails(); + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return ; + if (!result.ok) return onLoadError(result); + + const status = result.data.type === "ok" ? undefined : result.data.status; + + if (!status) { + return
no kyc required
; + } + return ; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx new file mode 100644 index 000000000..43df8484a --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage"; + +export default { + title: "Pages/Order/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, +}; + +function createExample( + Component: FunctionalComponent, + props: Partial +) { + const r = (args: any) => ; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + instanceConfig: { + default_max_deposit_fee: "", + default_max_wire_fee: "", + default_pay_delay: { + d_us: 1000 * 60 * 60, + }, + default_wire_fee_amortization: 1, + }, + 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/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx new file mode 100644 index 000000000..c08d8ee1d --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -0,0 +1,576 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { add, isAfter, isBefore, isFuture } from "date-fns"; +import { Amounts } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { + FormProvider, + FormErrors, +} from "../../../../components/form/FormProvider"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { InputDate } from "../../../../components/form/InputDate"; +import { InputGroup } from "../../../../components/form/InputGroup"; +import { InputLocation } from "../../../../components/form/InputLocation"; +import { ProductList } from "../../../../components/product/ProductList"; +import { useConfigContext } from "../../../../context/config"; +import { Duration, MerchantBackend, WithId } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; +import { OrderCreateSchema as schema } from "../../../../schemas/index"; +import { rate } from "../../../../utils/amount"; +import { InventoryProductForm } from "../../../../components/product/InventoryProductForm"; +import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm"; +import { InputNumber } from "../../../../components/form/InputNumber"; +import { InputBoolean } from "../../../../components/form/InputBoolean"; + +interface Props { + onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; + onBack?: () => void; + instanceConfig: InstanceConfig; + instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[]; +} +interface InstanceConfig { + default_max_wire_fee: string; + default_max_deposit_fee: string; + default_wire_fee_amortization: number; + default_pay_delay: Duration; +} + +function with_defaults(config: InstanceConfig): Partial { + const defaultPayDeadline = + !config.default_pay_delay || config.default_pay_delay.d_us === "forever" + ? undefined + : add(new Date(), { seconds: config.default_pay_delay.d_us / 1000 }); + + return { + inventoryProducts: {}, + products: [], + pricing: {}, + payments: { + max_wire_fee: config.default_max_wire_fee, + max_fee: config.default_max_deposit_fee, + wire_fee_amortization: config.default_wire_fee_amortization, + pay_deadline: defaultPayDeadline, + refund_deadline: defaultPayDeadline, + createToken: true, + }, + 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?: Date; + pay_deadline?: Date; + wire_transfer_deadline?: Date; + auto_refund_deadline?: Date; + max_fee?: string; + max_wire_fee?: string; + wire_fee_amortization?: number; + createToken: boolean; + minimum_age?: number; +} +interface Entity { + inventoryProducts: ProductMap; + products: MerchantBackend.Product[]; + pricing: Partial; + payments: Partial; + shipping: Partial; + extra: string; +} + +const stringIsValidJSON = (value: string) => { + try { + JSON.parse(value.trim()); + return true; + } catch { + return false; + } +}; + +function undefinedIfEmpty(obj: T): T | undefined { + return Object.keys(obj).some((k) => (obj as any)[k] !== undefined) + ? obj + : undefined; +} + +export function CreatePage({ + onCreate, + onBack, + instanceConfig, + instanceInventory, +}: Props): VNode { + const [value, valueHandler] = useState(with_defaults(instanceConfig)); + const config = useConfigContext(); + const zero = Amounts.getZero(config.currency); + + const inventoryList = Object.values(value.inventoryProducts || {}); + const productList = Object.values(value.products || {}); + + const i18n = useTranslator(); + + const errors: FormErrors = { + pricing: undefinedIfEmpty({ + summary: !value.pricing?.summary ? i18n`required` : undefined, + order_price: !value.pricing?.order_price + ? i18n`required` + : Amounts.isZero(value.pricing.order_price) + ? i18n`must be greater than 0` + : undefined, + }), + extra: + value.extra && !stringIsValidJSON(value.extra) + ? i18n`not a valid json` + : undefined, + payments: undefinedIfEmpty({ + refund_deadline: !value.payments?.refund_deadline + ? undefined + : !isFuture(value.payments.refund_deadline) + ? i18n`should be in the future` + : value.payments.pay_deadline && + isBefore(value.payments.refund_deadline, value.payments.pay_deadline) + ? i18n`refund deadline cannot be before pay deadline` + : value.payments.wire_transfer_deadline && + isBefore( + value.payments.wire_transfer_deadline, + value.payments.refund_deadline + ) + ? i18n`wire transfer deadline cannot be before refund deadline` + : undefined, + pay_deadline: !value.payments?.pay_deadline + ? undefined + : !isFuture(value.payments.pay_deadline) + ? i18n`should be in the future` + : value.payments.wire_transfer_deadline && + isBefore( + value.payments.wire_transfer_deadline, + value.payments.pay_deadline + ) + ? i18n`wire transfer deadline cannot be before pay deadline` + : undefined, + auto_refund_deadline: !value.payments?.auto_refund_deadline + ? undefined + : !isFuture(value.payments.auto_refund_deadline) + ? i18n`should be in the future` + : !value.payments?.refund_deadline + ? i18n`should have a refund deadline` + : !isAfter( + value.payments.refund_deadline, + value.payments.auto_refund_deadline + ) + ? i18n`auto refund cannot be after refund deadline` + : undefined, + }), + shipping: undefinedIfEmpty({ + delivery_date: !value.shipping?.delivery_date + ? undefined + : !isFuture(value.shipping.delivery_date) + ? i18n`should be in the future` + : undefined, + }), + }; + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined + ); + + const submit = (): void => { + const order = 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: value.extra, + pay_deadline: value.payments.pay_deadline + ? { + t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), + } + : undefined, + wire_transfer_deadline: value.payments.wire_transfer_deadline + ? { + t_s: Math.floor( + value.payments.wire_transfer_deadline.getTime() / 1000 + ), + } + : undefined, + refund_deadline: value.payments.refund_deadline + ? { + t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000), + } + : undefined, + auto_refund: value.payments.auto_refund_deadline + ? { + d_us: Math.floor( + value.payments.auto_refund_deadline.getTime() * 1000 + ), + } + : undefined, + wire_fee_amortization: value.payments.wire_fee_amortization as number, + max_fee: value.payments.max_fee as string, + max_wire_fee: value.payments.max_wire_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)); + + useEffect(() => { + valueHandler((v) => { + return { + ...v, + pricing: { + ...v.pricing, + products_price: hasProducts ? totalAsString : undefined, + order_price: hasProducts ? totalAsString : undefined, + }, + }; + }); + }, [hasProducts, totalAsString]); + + const discountOrRise = rate( + value.pricing?.order_price || `${config.currency}:0`, + totalAsString + ); + + const minAgeByProducts = allProducts.reduce( + (cur, prev) => + !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age, + 0 + ); + return ( +
+
+
+
+
+ {/* // FIXME: translating plural singular */} + 0 && ( +

+ {allProducts.length} products with a total price of{" "} + {totalAsString}. +

+ ) + } + tooltip={i18n`Manage list of products in the order.`} + > + + + { + setEditingProduct(undefined); + return addNewProduct(p); + }} + /> + + {allProducts.length > 0 && ( + { + if (e.product_id) { + removeProductFromTheInventoryList(e.product_id); + } else { + removeFromNewProduct(index); + setEditingProduct(e); + } + }, + }, + ]} + /> + )} +
+ + + errors={errors} + object={value} + valueHandler={valueHandler as any} + > + {hasProducts ? ( + + + 0 && + (discountOrRise < 1 + ? `discount of %${Math.round( + (1 - discountOrRise) * 100 + )}` + : `rise of %${Math.round((discountOrRise - 1) * 100)}`) + } + tooltip={i18n`Amount to be paid by the customer`} + /> + + ) : ( + + )} + + + + + + {value.shipping?.delivery_date && ( + + + + )} + + + + + + + + + + + + + + 0 + ? i18n`Min age defined by the producs is ${minAgeByProducts}` + : undefined + } + /> + + + + + + + +
+ {onBack && ( + + )} + +
+
+
+
+
+
+ ); +} + +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, + }; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx new file mode 100644 index 000000000..14c5d68c3 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx @@ -0,0 +1,89 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully"; +import { useOrderAPI } from "../../../../hooks/order"; +import { Translate } from "../../../../i18n"; +import { Entity } from "./index"; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function OrderCreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Props): VNode { + const { getPaymentURL } = useOrderAPI() + const [url, setURL] = useState(undefined) + + useEffect(() => { + getPaymentURL(entity.response.order_id).then(response => { + setURL(response.data) + }) + }, [getPaymentURL, entity.response.order_id]) + + return +
+
+ +
+
+
+

+ +

+
+
+
+
+
+ +
+
+
+

+ +

+
+
+
+
+
+ +
+
+
+

+ +

+
+
+
+
+
+ +
+
+
+

+ +

+
+
+
+
; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx new file mode 100644 index 000000000..c447c4b04 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -0,0 +1,82 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { Fragment, h, VNode } from 'preact'; +import { useState } from 'preact/hooks'; +import { Loading } from '../../../../components/exception/loading'; +import { NotificationCard } from '../../../../components/menu'; +import { MerchantBackend } from '../../../../declaration'; +import { HttpError } from '../../../../hooks/backend'; +import { useInstanceDetails } from '../../../../hooks/instance'; +import { useOrderAPI } from '../../../../hooks/order'; +import { useInstanceProducts } from '../../../../hooks/product'; +import { Notification } from '../../../../utils/types'; +import { CreatePage } from './CreatePage'; +import { OrderCreatedSuccessfully } from './OrderCreatedSuccessfully'; + +export type Entity = { + request: MerchantBackend.Orders.PostOrderRequest, + response: MerchantBackend.Orders.PostOrderResponse +} +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (error: HttpError) => VNode; +} +export default function OrderCreate({ onConfirm, onBack, onLoadError, onNotFound, onUnauthorized }: Props): VNode { + const { createOrder } = useOrderAPI() + const [notif, setNotif] = useState(undefined) + + const detailsResult = useInstanceDetails() + const inventoryResult = useInstanceProducts() + + if (detailsResult.clientError && detailsResult.isUnauthorized) return onUnauthorized() + if (detailsResult.clientError && detailsResult.isNotfound) return onNotFound() + if (detailsResult.loading) return + if (!detailsResult.ok) return onLoadError(detailsResult) + + if (inventoryResult.clientError && inventoryResult.isUnauthorized) return onUnauthorized() + if (inventoryResult.clientError && inventoryResult.isNotfound) return onNotFound() + if (inventoryResult.loading) return + if (!inventoryResult.ok) return onLoadError(inventoryResult) + + return + + + + { + createOrder(request).then(onConfirm).catch((error) => { + setNotif({ + message: 'could not create order', + type: "ERROR", + description: error.message + }) + }) + }} + instanceConfig={detailsResult.data} + instanceInventory={inventoryResult.data} + /> + +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx new file mode 100644 index 000000000..812b11a76 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx @@ -0,0 +1,137 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { addDays } from "date-fns"; +import { h, VNode, FunctionalComponent } from "preact"; +import { MerchantBackend } from "../../../../declaration"; +import { DetailPage as TestedComponent } from "./DetailPage"; + +export default { + title: "Pages/Order/Detail", + component: TestedComponent, + argTypes: { + onRefund: { action: "onRefund" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample( + Component: FunctionalComponent, + props: Partial +) { + const r = (args: any) => ; + r.args = props; + return r; +} + +const defaultContractTerm = { + amount: "TESTKUDOS:10", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + auditors: [], + exchanges: [], + max_fee: "TESTKUDOS:1", + max_wire_fee: "TESTKUDOS:1", + merchant: {} as any, + merchant_base_url: "http://merchant.url/", + order_id: "2021.165-03GDFC26Y1NNG", + products: [], + summary: "text summary", + wire_fee_amortization: 1, + 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/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx new file mode 100644 index 000000000..4bb7051d6 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -0,0 +1,776 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "../../../../components/form/FormProvider"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { InputDate } from "../../../../components/form/InputDate"; +import { InputDuration } from "../../../../components/form/InputDuration"; +import { InputGroup } from "../../../../components/form/InputGroup"; +import { InputLocation } from "../../../../components/form/InputLocation"; +import { TextField } from "../../../../components/form/TextField"; +import { ProductList } from "../../../../components/product/ProductList"; +import { useBackendContext } from "../../../../context/backend"; +import { MerchantBackend } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; +import { mergeRefunds } from "../../../../utils/amount"; +import { RefundModal } from "../list/Table"; +import { Event, Timeline } from "./Timeline"; + +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 = useTranslator(); + + return ( + + object={value} valueHandler={null}> + + readonly + name="summary" + label={i18n`Summary`} + tooltip={i18n`human-readable description of the whole purchase`} + /> + + readonly + name="amount" + label={i18n`Amount`} + tooltip={i18n`total price for the transaction`} + /> + {value.fulfillment_url && ( + + readonly + name="fulfillment_url" + label={i18n`Fulfillment URL`} + tooltip={i18n`URL for this purchase`} + /> + )} + + readonly + name="max_fee" + label={i18n`Max fee`} + tooltip={i18n`maximum total deposit fee accepted by the merchant for this contract`} + /> + + readonly + name="max_wire_fee" + label={i18n`Max wire fee`} + tooltip={i18n`maximum wire fee accepted by the merchant`} + /> + + readonly + name="wire_fee_amortization" + label={i18n`Wire fee amortization`} + tooltip={i18n`over how many customer transactions does the merchant expect to amortize wire fees on average`} + /> + + readonly + name="timestamp" + label={i18n`Created at`} + tooltip={i18n`time when this contract was generated`} + /> + + readonly + name="refund_deadline" + label={i18n`Refund deadline`} + tooltip={i18n`after this deadline has passed no refunds will be accepted`} + /> + + readonly + name="pay_deadline" + label={i18n`Payment deadline`} + tooltip={i18n`after this deadline, the merchant won't accept payments for the contract`} + /> + + readonly + name="wire_transfer_deadline" + label={i18n`Wire transfer deadline`} + tooltip={i18n`transfer deadline for the exchange`} + /> + + readonly + name="delivery_date" + label={i18n`Delivery date`} + tooltip={i18n`time indicating when the order should be delivered`} + /> + {value.delivery_date && ( + + + + )} + + readonly + name="auto_refund" + label={i18n`Auto-refund delay`} + tooltip={i18n`how long the wallet should try to get an automatic refund for the purchase`} + /> + + readonly + name="extra" + label={i18n`Extra info`} + tooltip={i18n`extra data that is only interpreted by the merchant frontend`} + /> + + + ); +} + +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>(order); + const i18n = useTranslator(); + + return ( +
+
+
+
+
+
+
+
+
+
+ Order #{id} +
+ claimed +
+
+
+
+
+
+
+

{order.contract_terms.amount}

+
+
+
+ +
+
+
+
+

+ + claimed at: + {" "} + {format( + new Date(order.contract_terms.timestamp.t_s * 1000), + "yyyy-MM-dd HH:mm:ss" + )} +

+
+
+
+
+
+
+ +
+
+
+
+ Timeline +
+ +
+
+
+ Payment details +
+ + object={value} + valueHandler={valueHandler} + > + + + + name="order_status" + readonly + label={i18n`Order status`} + /> + +
+
+
+ + {order.contract_terms.products.length ? ( + +
+ Product list +
+ +
+ ) : undefined} + + {value.contract_terms && ( + + )} +
+
+
+
+
+ ); +} +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 [value, valueHandler] = useState>(order); + const { url } = useBackendContext(); + const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part + const proto = url.startsWith("http://") ? "taler+http" : "taler"; + const refundurl = `${proto}://refund/${refundHost}/${order.contract_terms.order_id}/`; + const refundable = + new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000; + const i18n = useTranslator(); + + 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.getZero(amount.currency)); + value.refund_taken = Amounts.stringify(refund_taken); + + return ( +
+
+
+
+
+
+
+
+
+
+ Order #{id} +
+ paid +
+ {order.wired ? ( +
+ wired +
+ ) : null} + {order.refunded ? ( +
+ refunded +
+ ) : null} +
+
+
+
+
+
+

{order.contract_terms.amount}

+
+
+
+
+

+
+ + + +
+

+
+
+
+ +
+
+
+
+

+ + {order.contract_terms.fulfillment_url} + +

+

+ {format( + new Date(order.contract_terms.timestamp.t_s * 1000), + "yyyy/MM/dd HH:mm:ss" + )} +

+
+
+
+
+
+
+ +
+
+
+
+ Timeline +
+ +
+
+
+ Payment details +
+ + object={value} + valueHandler={valueHandler} + > + {/* name="deposit_total" readonly label={i18n`Deposit total`} /> */} + {order.refunded && ( + + name="refund_amount" + readonly + label={i18n`Refunded amount`} + /> + )} + {order.refunded && ( + + name="refund_taken" + readonly + label={i18n`Refund taken`} + /> + )} + + name="order_status" + readonly + label={i18n`Order status`} + /> + + name="order_status_url" + label={i18n`Status URL`} + > + + {order.order_status_url} + + + {order.refunded && ( + + name="order_status_url" + label={i18n`Refund URI`} + > + + {refundurl} + + + )} + +
+
+
+ + {order.contract_terms.products.length ? ( + +
+ Product list +
+ +
+ ) : undefined} + + {value.contract_terms && ( + + )} +
+
+
+
+
+ ); +} + +function UnpaidPage({ + id, + order, +}: { + id: string; + order: MerchantBackend.Orders.CheckPaymentUnpaidResponse; +}) { + const [value, valueHandler] = useState>(order); + const i18n = useTranslator(); + return ( +
+
+
+
+
+
+

+ Order #{id} +

+
+
+ unpaid +
+
+
+ +
+
+
+
+

+ + pay at: + {" "} + + {order.order_status_url} + +

+

+ + created at: + {" "} + {order.creation_time.t_s === "never" + ? "never" + : format( + new Date(order.creation_time.t_s * 1000), + "yyyy-MM-dd HH:mm:ss" + )} +

+
+
+
+
+
+
+ +
+
+
+
+ object={value} valueHandler={valueHandler}> + + readonly + name="summary" + label={i18n`Summary`} + tooltip={i18n`human-readable description of the whole purchase`} + /> + + readonly + name="total_amount" + label={i18n`Amount`} + tooltip={i18n`total price for the transaction`} + /> + + name="order_status" + readonly + label={i18n`Order status`} + /> + + name="order_status_url" + readonly + label={i18n`Order status URL`} + /> + name="taler_pay_uri" label={i18n`Payment URI`}> + + {value.taler_pay_uri} + + + +
+
+
+
+
+ ); +} + +export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { + const [showRefund, setShowRefund] = useState(undefined); + + const DetailByStatus = function () { + switch (selected.order_status) { + case "claimed": + return ; + case "paid": + return ; + case "unpaid": + return ; + default: + return ( +
+ + Unknown order status. This is an error, please contact the + administrator. + +
+ ); + } + }; + + return ( + + {DetailByStatus()} + {showRefund && ( + setShowRefund(undefined)} + onConfirm={(value) => { + onRefund(showRefund, value); + setShowRefund(undefined); + }} + /> + )} +
+
+
+
+ +
+
+
+
+ + ); +} + +async function copyToClipboard(text: string) { + return navigator.clipboard.writeText(text); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx new file mode 100644 index 000000000..bea65607a --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx @@ -0,0 +1,128 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { format } from "date-fns"; +import { h } from "preact"; +import { useEffect, useState } from "preact/hooks"; + +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 [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 ( +
+ {events.map((e, i) => { + return ( +
+ {(() => { + switch (e.type) { + case "deadline": + return ( +
+ +
+ ); + case "delivery": + return ( +
+ +
+ ); + case "start": + return ( +
+ +
+ ); + case "wired": + return ( +
+ +
+ ); + case "wired-range": + return ( +
+ +
+ ); + case "refund": + return ( +
+ +
+ ); + case "refund-taken": + return ( +
+ +
+ ); + case "now": + return ( +
+ +
+ ); + } + })()} +
+

{format(e.when, "yyyy/MM/dd HH:mm:ss")}

+

{e.description}

+
+
+ ); + })} +
+ ); +} +export interface Event { + when: Date; + description: string; + type: + | "start" + | "refund" + | "refund-taken" + | "wired" + | "wired-range" + | "deadline" + | "delivery" + | "now"; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx new file mode 100644 index 000000000..cd4e163eb --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading"; +import { NotificationCard } from "../../../../components/menu"; +import { HttpError } from "../../../../hooks/backend"; +import { useOrderDetails, useOrderAPI } from "../../../../hooks/order"; +import { useTranslator } from "../../../../i18n"; +import { Notification } from "../../../../utils/types"; +import { DetailPage } from "./DetailPage"; + +export interface Props { + oid: string; + + onBack: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (error: HttpError) => VNode; +} + +export default function Update({ oid, onBack, onLoadError, onNotFound, onUnauthorized }: Props): VNode { + const { refundOrder } = useOrderAPI(); + const result = useOrderDetails(oid) + const [notif, setNotif] = useState(undefined) + + const i18n = useTranslator() + + if (result.clientError && result.isUnauthorized) return onUnauthorized() + if (result.clientError && result.isNotfound) return onNotFound() + if (result.loading) return + if (!result.ok) return onLoadError(result) + + return + + + + refundOrder(id, value) + .then(() => setNotif({ + message: i18n`refund created successfully`, + type: "SUCCESS" + })).catch((error) => setNotif({ + message: i18n`could not create the refund`, + type: "ERROR", + description: error.message + })) + } + selected={result.data} + /> + +} \ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx new file mode 100644 index 000000000..1dbb3f20d --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx @@ -0,0 +1,107 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ListPage as TestedComponent } from "./ListPage"; + +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( + Component: FunctionalComponent, + props: Partial +) { + const r = (args: any) => ; + 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/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx new file mode 100644 index 000000000..032801bde --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -0,0 +1,146 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { format } from 'date-fns'; +import { h, VNode } from 'preact'; +import { useState } from 'preact/hooks'; +import { DatePicker } from '../../../../components/picker/DatePicker'; +import { MerchantBackend, WithId } from '../../../../declaration'; +import { Translate, useTranslator } from '../../../../i18n'; +import { CardTable } from './Table'; + +export interface ListPageProps { + errorOrderId: string | undefined, + + onShowAll: () => void, + onShowPaid: () => void, + onShowRefunded: () => void, + onShowNotWired: () => void, + onCopyURL: (id: string) => void; + isAllActive: string, + isPaidActive: string, + isRefundedActive: string, + isNotWiredActive: 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; + onSearchOrderById: (id: string) => void; + onCreate: () => void; +} + +export function ListPage({ orders, errorOrderId, isAllActive, onSelectOrder, onRefundOrder, onSearchOrderById, jumpToDate, onCopyURL, onShowAll, onShowPaid, onShowRefunded, onShowNotWired, onSelectDate, isPaidActive, isRefundedActive, isNotWiredActive, onCreate }: ListPageProps): VNode { + const i18n = useTranslator(); + const dateTooltip = i18n`select date to show nearby orders`; + const [pickDate, setPickDate] = useState(false); + const [orderId, setOrderId] = useState(''); + + return
+ +
+
+
+
+
+ setOrderId(e.currentTarget.value)} placeholder={i18n`order id`} /> + {errorOrderId &&

{errorOrderId}

} +
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ {jumpToDate && } +
+ + { setPickDate(true); }} /> + +
+ +
+
+
+
+ + setPickDate(false)} + dateReceiver={onSelectDate} /> + + +
; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx new file mode 100644 index 000000000..60d5fae59 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -0,0 +1,412 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { InputGroup } from "../../../../components/form/InputGroup"; +import { InputSelector } from "../../../../components/form/InputSelector"; +import { ConfirmModal } from "../../../../components/modal"; +import { useConfigContext } from "../../../../context/config"; +import { MerchantBackend, WithId } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; +import { mergeRefunds } from "../../../../utils/amount"; + +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([]); + + const i18n = useTranslator(); + + return ( +
+
+

+ + + + Orders +

+ +
+ +
+ + + +
+
+
+
+
+ {orders.length > 0 ? ( + onCopyURL(o.id)} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + + )} + + + + + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onRefund: (id: Entity) => void; + onCopyURL: (id: Entity) => void; + onSelect: (id: Entity) => void; + rowSelectionHandler: StateUpdater; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function Table({ + instances, + onSelect, + onRefund, + onCopyURL, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + return ( +
+ {onLoadMoreBefore && ( + + )} +
+ + + + + + + + + {instances.map((i) => { + return ( + + + + + + + ); + })} + +
+ Date + + Amount + + Summary + +
onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.timestamp.t_s === "never" + ? "never" + : format( + new Date(i.timestamp.t_s * 1000), + "yyyy/MM/dd HH:mm:ss" + )} + onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.amount} + onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.summary} + +
+ {i.refundable && ( + + )} + {!i.paid && ( + + )} +
+
+ {onLoadMoreAfter && ( + + )} +
+ ); +} + +function EmptyTable(): VNode { + return ( +
+

+ + + +

+

+ No orders have been found matching your query! +

+
+ ); +} + +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({}); + const i18n = useTranslator(); + // const [errors, setErrors] = useState>({}); + + 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.getZero(config.currency) + ); + const orderPrice = + order.order_status === "paid" + ? Amounts.parseOrThrow(order.contract_terms.amount) + : undefined; + const totalRefundable = !orderPrice + ? Amounts.getZero(totalRefunded.currency) + : refunds.length + ? Amounts.sub(orderPrice, totalRefunded).amount + : orderPrice; + + const isRefundable = Amounts.isNonZero(totalRefundable); + const duplicatedText = i18n`duplicated`; + + const errors: FormErrors = { + mainReason: !form.mainReason ? i18n`required` : undefined, + description: + !form.description && form.mainReason !== duplicatedText + ? i18n`required` + : undefined, + refund: !form.refund + ? i18n`required` + : !Amounts.parse(form.refund) + ? i18n`invalid format` + : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1 + ? i18n`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 ( + + {refunds.length > 0 && ( +
+
+ + + + + + + + + + + {refunds.map((r) => { + return ( + + + + + + ); + })} + +
+ date + + amount + + reason +
+ {r.timestamp.t_s === "never" + ? "never" + : format( + new Date(r.timestamp.t_s * 1000), + "yyyy-MM-dd HH:mm:ss" + )} + {r.amount}{r.reason}
+
+
+
+ )} + + {isRefundable && ( + + errors={errors} + object={form} + valueHandler={(d) => setValue(d as any)} + > + + name="refund" + label={i18n`Refund`} + tooltip={i18n`amount to be refunded`} + > + Max refundable:{" "} + {Amounts.stringify(totalRefundable)} + + + {form.mainReason && form.mainReason !== duplicatedText ? ( + + label={i18n`Description`} + name="description" + tooltip={i18n`more information to give context`} + /> + ) : undefined} + + )} +
+ ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx new file mode 100644 index 000000000..47e143fb7 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -0,0 +1,171 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode, Fragment } from 'preact'; +import { useState } from 'preact/hooks'; +import { Loading } from '../../../../components/exception/loading'; +import { NotificationCard } from '../../../../components/menu'; +import { MerchantBackend, WithId } from '../../../../declaration'; +import { HttpError } from '../../../../hooks/backend'; +import { InstanceOrderFilter, useInstanceOrders, useOrderAPI, useOrderDetails } from '../../../../hooks/order'; +import { useTranslator } from '../../../../i18n'; +import { Notification } from '../../../../utils/types'; +import { RefundModal } from './Table'; +import { ListPage } from './ListPage'; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => VNode; + onNotFound: () => VNode; + onSelect: (id: string) => void; + onCreate: () => void; +} + +export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNotFound }: Props): VNode { + const [filter, setFilter] = useState({}) + const [orderToBeRefunded, setOrderToBeRefunded] = useState(undefined) + + const setNewDate = (date?: Date) => setFilter(prev => ({ ...prev, date })) + + const result = useInstanceOrders(filter, setNewDate) + const { refundOrder, getPaymentURL } = useOrderAPI() + + const [notif, setNotif] = useState(undefined) + + if (result.clientError && result.isUnauthorized) return onUnauthorized() + if (result.clientError && result.isNotfound) return onNotFound() + if (result.loading) return + if (!result.ok) return onLoadError(result) + + const isPaidActive = filter.paid === 'yes' ? "is-active" : '' + const isRefundedActive = filter.refunded === 'yes' ? "is-active" : '' + const isNotWiredActive = filter.wired === 'no' ? "is-active" : '' + const isAllActive = filter.paid === undefined && filter.refunded === undefined && filter.wired === undefined ? 'is-active' : '' + + const i18n = useTranslator() + const [errorOrderId, setErrorOrderId] = useState(undefined) + + async function testIfOrderExistAndSelect(orderId: string) { + if (!orderId) { + setErrorOrderId(i18n`Enter an order id`) + return; + } + try { + await getPaymentURL(orderId) + onSelect(orderId) + setErrorOrderId(undefined) + } catch { + setErrorOrderId(i18n`order not found`) + } + } + + return + + + ({ ...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)} + + errorOrderId={errorOrderId} + isAllActive={isAllActive} + isNotWiredActive={isNotWiredActive} + isPaidActive={isPaidActive} + isRefundedActive={isRefundedActive} + jumpToDate={filter.date} + onCopyURL={(id) => getPaymentURL(id).then((resp) => copyToClipboard(resp.data))} + + onCreate={onCreate} + onSearchOrderById={testIfOrderExistAndSelect} + onSelectDate={setNewDate} + onShowAll={() => setFilter({})} + onShowPaid={() => setFilter({ paid: 'yes' })} + onShowRefunded={() => setFilter({ refunded: 'yes' })} + onShowNotWired={() => setFilter({ wired: 'no' })} + + /> + + {orderToBeRefunded && setOrderToBeRefunded(undefined)} + onConfirm={(value) => refundOrder(orderToBeRefunded.order_id, value) + .then(() => setNotif({ + message: i18n`refund created successfully`, + type: "SUCCESS" + })) + .catch((error) => setNotif({ + message: i18n`could not create the refund`, + type: "ERROR", + description: error.message + })) + .then(() => setOrderToBeRefunded(undefined))} + onLoadError={(error) => { + setNotif({ + message: i18n`could not create the refund`, + type: "ERROR", + description: error.message + }); + setOrderToBeRefunded(undefined); + return
; + }} + onUnauthorized={onUnauthorized} + onNotFound={() => { + setNotif({ + message: i18n`could not get the order to refund`, + type: "ERROR", + // description: error.message + }); + setOrderToBeRefunded(undefined); + return
; + }} />} + +} + +interface RefundProps { + id: string; + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => VNode; + onNotFound: () => VNode; + onCancel: () => void; + onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void; +} + +function RefundModalForTable({ id, onUnauthorized, onLoadError, onNotFound, onConfirm, onCancel }: RefundProps) { + const result = useOrderDetails(id); + + if (result.clientError && result.isUnauthorized) return onUnauthorized() + if (result.clientError && result.isNotfound) return onNotFound() + if (result.loading) return + if (!result.ok) return onLoadError(result) + + return +} + +async function copyToClipboard(text: string) { + return navigator.clipboard.writeText(text) +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx new file mode 100644 index 000000000..1d9ea53f6 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx @@ -0,0 +1,42 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode, FunctionalComponent } from 'preact'; +import { CreatePage as TestedComponent } from './CreatePage'; + + +export default { + title: 'Pages/Product/Create', + component: TestedComponent, + argTypes: { + onCreate: { action: 'onCreate' }, + onBack: { action: 'onBack' }, + }, +}; + +function createExample(Component: FunctionalComponent, props: Partial) { + const r = (args: any) => + r.args = props + return r +} + +export const Example = createExample(TestedComponent, { +}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx new file mode 100644 index 000000000..ed669f67f --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode } from "preact"; +import { AsyncButton } from "../../../../components/exception/AsyncButton"; +import { ProductForm } from "../../../../components/product/ProductForm"; +import { MerchantBackend } from "../../../../declaration"; +import { useListener } from "../../../../hooks/listener"; +import { Translate, useTranslator } from "../../../../i18n"; + +type Entity = MerchantBackend.Products.ProductAddDetail & { product_id: string} + +interface Props { + onCreate: (d: Entity) => Promise; + onBack?: () => void; +} + + +export function CreatePage({ onCreate, onBack }: Props): VNode { + + const [submitForm, addFormSubmitter] = useListener((result) => { + if (result) return onCreate(result) + return Promise.reject() + }) + + const i18n = useTranslator() + + return
+
+
+
+
+ + +
+ {onBack && } + Confirm +
+ +
+
+
+
+
+} \ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx new file mode 100644 index 000000000..b56750405 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { h, VNode } from "preact"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully"; +import { Entity } from "./index"; +import emptyImage from "../../assets/empty.png"; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Props): VNode { + + return