aboutsummaryrefslogtreecommitdiff
path: root/packages/auditor-backoffice-ui/src/paths/instance/orders
diff options
context:
space:
mode:
authorNic Eigel <nic@eigel.ch>2024-06-24 09:16:33 +0200
committerNic Eigel <nic@eigel.ch>2024-06-24 09:16:33 +0200
commit75ff04672c011cf4c47b8f07d327adbf59323396 (patch)
tree24577e0429f641e3c19005b5a9180af8186232d0 /packages/auditor-backoffice-ui/src/paths/instance/orders
parent0b90a34e7c7c5d9bcca9a2ebe74df9fdfafc6577 (diff)
downloadwallet-core-75ff04672c011cf4c47b8f07d327adbf59323396.tar.xz
fixing merge-error
Diffstat (limited to 'packages/auditor-backoffice-ui/src/paths/instance/orders')
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx705
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx135
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx770
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx129
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx226
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx417
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx231
12 files changed, 0 insertions, 3114 deletions
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
deleted file mode 100644
index fc814b68f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h, VNode, FunctionalComponent } from "preact";
-import { CreatePage as TestedComponent } from "./CreatePage.js";
-
-export default {
- title: "Pages/Order/Create",
- component: TestedComponent,
- argTypes: {
- onCreate: { action: "onCreate" },
- goBack: { action: "goBack" },
- },
-};
-
-function createExample<Props>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props>,
-) {
- const r = (args: any) => <Component {...args} />;
- r.args = props;
- return r;
-}
-
-export const Example = createExample(TestedComponent, {
- instanceConfig: {
- default_pay_delay: {
- d_us: 1000 * 1000 * 60 * 60, //one hour
- },
- default_wire_transfer_delay: {
- d_us: 1000 * 1000 * 60 * 60, //one hour
- },
- use_stefan: true,
- },
- instanceInventory: [
- {
- id: "t-shirt-1",
- description: "a m size t-shirt",
- price: "TESTKUDOS:1",
- total_stock: -1,
- },
- {
- id: "t-shirt-2",
- price: "TESTKUDOS:1",
- description: "a xl size t-shirt",
- } as any,
- {
- id: "t-shirt-3",
- price: "TESTKUDOS:1",
- description: "a s size t-shirt",
- } as any,
- ],
-});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
deleted file mode 100644
index 92271f52f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
+++ /dev/null
@@ -1,705 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { format, isFuture } from "date-fns";
-import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import {
- FormErrors,
- FormProvider,
-} from "../../../../components/form/FormProvider.js";
-import { Input } from "../../../../components/form/Input.js";
-import { InputCurrency } from "../../../../components/form/InputCurrency.js";
-import { InputDate } from "../../../../components/form/InputDate.js";
-import { InputDuration } from "../../../../components/form/InputDuration.js";
-import { InputGroup } from "../../../../components/form/InputGroup.js";
-import { InputLocation } from "../../../../components/form/InputLocation.js";
-import { InputNumber } from "../../../../components/form/InputNumber.js";
-import { InputToggle } from "../../../../components/form/InputToggle.js";
-import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js";
-import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js";
-import { ProductList } from "../../../../components/product/ProductList.js";
-import { useConfigContext } from "../../../../context/config.js";
-import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { useSettings } from "../../../../hooks/useSettings.js";
-import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
-import { rate } from "../../../../utils/amount.js";
-import { undefinedIfEmpty } from "../../../../utils/table.js";
-
-interface Props {
- onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
- onBack?: () => void;
- instanceConfig: InstanceConfig;
- instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
-}
-interface InstanceConfig {
- use_stefan: boolean;
- default_pay_delay: TalerProtocolDuration;
- default_wire_transfer_delay: TalerProtocolDuration;
-}
-
-function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> {
- const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay);
- const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay);
-
- return {
- inventoryProducts: {},
- products: [],
- pricing: {},
- payments: {
- max_fee: undefined,
- createToken: true,
- pay_deadline: (defaultPayDeadline),
- refund_deadline: (defaultPayDeadline),
- wire_transfer_deadline: (defaultWireDeadline),
- },
- shipping: {},
- extra: {},
- };
-}
-
-interface ProductAndQuantity {
- product: MerchantBackend.Products.ProductDetail & WithId;
- quantity: number;
-}
-export interface ProductMap {
- [id: string]: ProductAndQuantity;
-}
-
-interface Pricing {
- products_price: string;
- order_price: string;
- summary: string;
-}
-interface Shipping {
- delivery_date?: Date;
- delivery_location?: MerchantBackend.Location;
- fullfilment_url?: string;
-}
-interface Payments {
- refund_deadline: Duration;
- pay_deadline: Duration;
- wire_transfer_deadline: Duration;
- auto_refund_deadline: Duration;
- max_fee?: string;
- createToken: boolean;
- minimum_age?: number;
-}
-interface Entity {
- inventoryProducts: ProductMap;
- products: MerchantBackend.Product[];
- pricing: Partial<Pricing>;
- payments: Partial<Payments>;
- shipping: Partial<Shipping>;
- extra: Record<string, string>;
-}
-
-const stringIsValidJSON = (value: string) => {
- try {
- JSON.parse(value.trim());
- return true;
- } catch {
- return false;
- }
-};
-
-export function CreatePage({
- onCreate,
- onBack,
- instanceConfig,
- instanceInventory,
-}: Props): VNode {
- const config = useConfigContext();
- const instance_default = with_defaults(instanceConfig, config.currency)
- const [value, valueHandler] = useState(instance_default);
- const zero = Amounts.zeroOfCurrency(config.currency);
- const [settings, updateSettings] = useSettings()
- const inventoryList = Object.values(value.inventoryProducts || {});
- const productList = Object.values(value.products || {});
-
- const { i18n } = useTranslationContext();
-
- const parsedPrice = !value.pricing?.order_price
- ? undefined
- : Amounts.parse(value.pricing.order_price);
-
- const errors: FormErrors<Entity> = {
- pricing: undefinedIfEmpty({
- summary: !value.pricing?.summary ? i18n.str`required` : undefined,
- order_price: !value.pricing?.order_price
- ? i18n.str`required`
- : !parsedPrice
- ? i18n.str`not valid`
- : Amounts.isZero(parsedPrice)
- ? i18n.str`must be greater than 0`
- : undefined,
- }),
- payments: undefinedIfEmpty({
- refund_deadline: !value.payments?.refund_deadline
- ? undefined
- : value.payments.pay_deadline &&
- Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1
- ? i18n.str`refund deadline cannot be before pay deadline`
- : value.payments.wire_transfer_deadline &&
- Duration.cmp(
- value.payments.wire_transfer_deadline,
- value.payments.refund_deadline,
- ) === -1
- ? i18n.str`wire transfer deadline cannot be before refund deadline`
- : undefined,
- pay_deadline: !value.payments?.pay_deadline
- ? i18n.str`required`
- : value.payments.wire_transfer_deadline &&
- Duration.cmp(
- value.payments.wire_transfer_deadline,
- value.payments.pay_deadline,
- ) === -1
- ? i18n.str`wire transfer deadline cannot be before pay deadline`
- : undefined,
- wire_transfer_deadline: !value.payments?.wire_transfer_deadline
- ? i18n.str`required`
- : undefined,
- auto_refund_deadline: !value.payments?.auto_refund_deadline
- ? undefined
- : !value.payments?.refund_deadline
- ? i18n.str`should have a refund deadline`
- : Duration.cmp(
- value.payments.refund_deadline,
- value.payments.auto_refund_deadline,
- ) == -1
- ? i18n.str`auto refund cannot be after refund deadline`
- : undefined,
-
- }),
- shipping: undefinedIfEmpty({
- delivery_date: !value.shipping?.delivery_date
- ? undefined
- : !isFuture(value.shipping.delivery_date)
- ? i18n.str`should be in the future`
- : undefined,
- }),
- };
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
-
- const submit = (): void => {
- const order = value as any; //schema.cast(value);
- if (!value.payments) return;
- if (!value.shipping) return;
-
- const request: MerchantBackend.Orders.PostOrderRequest = {
- order: {
- amount: order.pricing.order_price,
- summary: order.pricing.summary,
- products: productList,
- extra: undefinedIfEmpty(value.extra),
- pay_deadline: !value.payments.pay_deadline ?
- i18n.str`required` :
- AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline))
- ,// : undefined,
- wire_transfer_deadline: value.payments.wire_transfer_deadline
- ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline))
- : undefined,
- refund_deadline: value.payments.refund_deadline
- ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline))
- : undefined,
- auto_refund: value.payments.auto_refund_deadline
- ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline)
- : undefined,
- max_fee: value.payments.max_fee as string,
-
- delivery_date: value.shipping.delivery_date
- ? { t_s: value.shipping.delivery_date.getTime() / 1000 }
- : undefined,
- delivery_location: value.shipping.delivery_location,
- fulfillment_url: value.shipping.fullfilment_url,
- minimum_age: value.payments.minimum_age,
- },
- inventory_products: inventoryList.map((p) => ({
- product_id: p.product.id,
- quantity: p.quantity,
- })),
- create_token: value.payments.createToken,
- };
-
- onCreate(request);
- };
-
- const addProductToTheInventoryList = (
- product: MerchantBackend.Products.ProductDetail & WithId,
- quantity: number,
- ) => {
- valueHandler((v) => {
- const inventoryProducts = { ...v.inventoryProducts };
- inventoryProducts[product.id] = { product, quantity };
- return { ...v, inventoryProducts };
- });
- };
-
- const removeProductFromTheInventoryList = (id: string) => {
- valueHandler((v) => {
- const inventoryProducts = { ...v.inventoryProducts };
- delete inventoryProducts[id];
- return { ...v, inventoryProducts };
- });
- };
-
- const addNewProduct = async (product: MerchantBackend.Product) => {
- return valueHandler((v) => {
- const products = v.products ? [...v.products, product] : [];
- return { ...v, products };
- });
- };
-
- const removeFromNewProduct = (index: number) => {
- valueHandler((v) => {
- const products = v.products ? [...v.products] : [];
- products.splice(index, 1);
- return { ...v, products };
- });
- };
-
- const [editingProduct, setEditingProduct] = useState<
- MerchantBackend.Product | undefined
- >(undefined);
-
- const totalPriceInventory = inventoryList.reduce((prev, cur) => {
- const p = Amounts.parseOrThrow(cur.product.price);
- return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
- }, zero);
-
- const totalPriceProducts = productList.reduce((prev, cur) => {
- if (!cur.price) return zero;
- const p = Amounts.parseOrThrow(cur.price);
- return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
- }, zero);
-
- const hasProducts = inventoryList.length > 0 || productList.length > 0;
- const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts);
-
- const totalAsString = Amounts.stringify(totalPrice.amount);
- const allProducts = productList.concat(inventoryList.map(asProduct));
-
- const [newField, setNewField] = useState("")
-
- useEffect(() => {
- valueHandler((v) => {
- return {
- ...v,
- pricing: {
- ...v.pricing,
- products_price: hasProducts ? totalAsString : undefined,
- order_price: hasProducts ? totalAsString : undefined,
- },
- };
- });
- }, [hasProducts, totalAsString]);
-
- const discountOrRise = rate(
- parsedPrice ?? Amounts.zeroOfCurrency(config.currency),
- totalPrice.amount,
- );
-
- const minAgeByProducts = allProducts.reduce(
- (cur, prev) =>
- !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age,
- 0,
- );
-
- // if there is no default pay deadline
- const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline
- // and there is no default wire deadline
- const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline
- // user required to set the taler options
- const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline
-
-
- return (
- <div>
-
- <section class="section is-main-section">
- <div class="tabs is-toggle is-fullwidth is-small">
- <ul>
- <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
- updateSettings({
- ...settings,
- advanceOrderMode: false
- })
- }}>
- <a >
- <span><i18n.Translate>Simple</i18n.Translate></span>
- </a>
- </li>
- <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
- updateSettings({
- ...settings,
- advanceOrderMode: true
- })
- }}>
- <a >
- <span><i18n.Translate>Advanced</i18n.Translate></span>
- </a>
- </li>
- </ul>
- </div>
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- {/* // FIXME: translating plural singular */}
- <InputGroup
- name="inventory_products"
- label={i18n.str`Manage products in order`}
- alternative={
- allProducts.length > 0 && (
- <p>
- {allProducts.length} products with a total price of{" "}
- {totalAsString}.
- </p>
- )
- }
- tooltip={i18n.str`Manage list of products in the order.`}
- >
- <InventoryProductForm
- currentProducts={value.inventoryProducts || {}}
- onAddProduct={addProductToTheInventoryList}
- inventory={instanceInventory}
- />
-
- {settings.advanceOrderMode &&
- <NonInventoryProductFrom
- productToEdit={editingProduct}
- onAddProduct={(p) => {
- setEditingProduct(undefined);
- return addNewProduct(p);
- }}
- />
- }
-
- {allProducts.length > 0 && (
- <ProductList
- list={allProducts}
- actions={[
- {
- name: i18n.str`Remove`,
- tooltip: i18n.str`Remove this product from the order.`,
- handler: (e, index) => {
- if (e.product_id) {
- removeProductFromTheInventoryList(e.product_id);
- } else {
- removeFromNewProduct(index);
- setEditingProduct(e);
- }
- },
- },
- ]}
- />
- )}
- </InputGroup>
-
- <FormProvider<Entity>
- errors={errors}
- object={value}
- valueHandler={valueHandler as any}
- >
- {hasProducts ? (
- <Fragment>
- <InputCurrency
- name="pricing.products_price"
- label={i18n.str`Total price`}
- readonly
- tooltip={i18n.str`total product price added up`}
- />
- <InputCurrency
- name="pricing.order_price"
- label={i18n.str`Total price`}
- addonAfter={
- discountOrRise > 0 &&
- (discountOrRise < 1
- ? `discount of %${Math.round(
- (1 - discountOrRise) * 100,
- )}`
- : `rise of %${Math.round((discountOrRise - 1) * 100)}`)
- }
- tooltip={i18n.str`Amount to be paid by the customer`}
- />
- </Fragment>
- ) : (
- <InputCurrency
- name="pricing.order_price"
- label={i18n.str`Order price`}
- tooltip={i18n.str`final order price`}
- />
- )}
-
- <Input
- name="pricing.summary"
- inputType="multiline"
- label={i18n.str`Summary`}
- tooltip={i18n.str`Title of the order to be shown to the customer`}
- />
-
- {settings.advanceOrderMode &&
- <InputGroup
- name="shipping"
- label={i18n.str`Shipping and Fulfillment`}
- initialActive
- >
- <InputDate
- name="shipping.delivery_date"
- label={i18n.str`Delivery date`}
- tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
- />
- {value.shipping?.delivery_date && (
- <InputGroup
- name="shipping.delivery_location"
- label={i18n.str`Location`}
- tooltip={i18n.str`address where the products will be delivered`}
- >
- <InputLocation name="shipping.delivery_location" />
- </InputGroup>
- )}
- <Input
- name="shipping.fullfilment_url"
- label={i18n.str`Fulfillment URL`}
- tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
- />
- </InputGroup>
- }
-
- {(settings.advanceOrderMode || requiresSomeTalerOptions) &&
- <InputGroup
- name="payments"
- label={i18n.str`Taler payment options`}
- tooltip={i18n.str`Override default Taler payment settings for this order`}
- >
- {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDuration
- name="payments.pay_deadline"
- label={i18n.str`Payment time`}
- help={<DeadlineHelp duration={value.payments?.pay_deadline} />}
- withForever
- withoutClear
- tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`}
- side={
- <span>
- <button class="button" onClick={() => {
- const c = {
- ...value,
- payments: {
- ...(value.payments ?? {}),
- pay_deadline: instance_default.payments?.pay_deadline
- }
- }
- valueHandler(c)
- }}>
- <i18n.Translate>default</i18n.Translate>
- </button>
- </span>
- }
- />}
- {settings.advanceOrderMode && <InputDuration
- name="payments.refund_deadline"
- label={i18n.str`Refund time`}
- help={<DeadlineHelp duration={value.payments?.refund_deadline} />}
- withForever
- withoutClear
- tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`}
- side={
- <span>
- <button class="button" onClick={() => {
- valueHandler({
- ...value,
- payments: {
- ...(value.payments ?? {}),
- refund_deadline: instance_default.payments?.refund_deadline
- }
- })
- }}>
- <i18n.Translate>default</i18n.Translate>
- </button>
- </span>
- }
- />}
- {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDuration
- name="payments.wire_transfer_deadline"
- label={i18n.str`Wire transfer time`}
- help={<DeadlineHelp duration={value.payments?.wire_transfer_deadline} />}
- withoutClear
- withForever
- tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`}
- side={
- <span>
- <button class="button" onClick={() => {
- valueHandler({
- ...value,
- payments: {
- ...(value.payments ?? {}),
- wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline
- }
- })
- }}>
- <i18n.Translate>default</i18n.Translate>
- </button>
- </span>
- }
- />}
- {settings.advanceOrderMode && <InputDuration
- name="payments.auto_refund_deadline"
- label={i18n.str`Auto-refund time`}
- help={<DeadlineHelp duration={value.payments?.auto_refund_deadline} />}
- tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
- withForever
- />}
-
- {settings.advanceOrderMode && <InputCurrency
- name="payments.max_fee"
- label={i18n.str`Maximum fee`}
- tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
- />}
- {settings.advanceOrderMode && <InputToggle
- name="payments.createToken"
- label={i18n.str`Create token`}
- tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`}
- />}
- {settings.advanceOrderMode && <InputNumber
- name="payments.minimum_age"
- label={i18n.str`Minimum age required`}
- tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
- help={
- minAgeByProducts > 0
- ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
- : i18n.str`No product with age restriction in this order`
- }
- />}
- </InputGroup>
- }
-
- {settings.advanceOrderMode &&
- <InputGroup
- name="extra"
- label={i18n.str`Additional information`}
- tooltip={i18n.str`Custom information to be included in the contract for this order.`}
- >
- {Object.keys(value.extra ?? {}).map((key) => {
-
- return <Input
- name={`extra.${key}`}
- inputType="multiline"
- label={key}
- tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
- side={
- <button class="button" onClick={(e) => {
- if (value.extra && value.extra[key] !== undefined) {
- console.log(value.extra)
- delete value.extra[key]
- }
- valueHandler({
- ...value,
- })
- }}>remove</button>
- }
- />
- })}
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Custom field name</i18n.Translate>
- <span class="icon has-tooltip-right" data-tooltip={"new extra field"}>
- <i class="mdi mdi-information" />
- </span>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} />
- </p>
- </div>
- </div>
- <button class="button" onClick={(e) => {
- setNewField("")
- valueHandler({
- ...value,
- extra: {
- ...(value.extra ?? {}),
- [newField]: ""
- }
- })
- }}>add</button>
- </div>
- </InputGroup>
- }
- </FormProvider>
-
- <div class="buttons is-right mt-5">
- {onBack && (
- <button class="button" onClick={onBack}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- )}
- <button
- class="button is-success"
- onClick={submit}
- disabled={hasErrors}
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </button>
- </div>
- </div>
- <div class="column" />
- </div>
- </section>
- </div>
- );
-}
-
-function asProduct(p: ProductAndQuantity): MerchantBackend.Product {
- return {
- product_id: p.product.id,
- image: p.product.image,
- price: p.product.price,
- unit: p.product.unit,
- quantity: p.quantity,
- description: p.product.description,
- taxes: p.product.taxes,
- minimum_age: p.product.minimum_age,
- };
-}
-
-
-function DeadlineHelp({ duration }: { duration?: Duration }): VNode {
- const { i18n } = useTranslationContext();
- const [now, setNow] = useState(AbsoluteTime.now())
- useEffect(() => {
- const iid = setInterval(() => {
- setNow(AbsoluteTime.now())
- }, 60 * 1000)
- return () => {
- clearInterval(iid)
- }
- })
- if (!duration) return <i18n.Translate>Disabled</i18n.Translate>
- const when = AbsoluteTime.addDuration(now, duration)
- if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate>
- return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate>
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
deleted file mode 100644
index 3f7b20f52..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js";
-import { useOrderAPI } from "../../../../hooks/order.js";
-import { Entity } from "./index.js";
-
-interface Props {
- entity: Entity;
- onConfirm: () => void;
- onCreateAnother?: () => void;
-}
-
-export function OrderCreatedSuccessfully({
- entity,
- onConfirm,
- onCreateAnother,
-}: Props): VNode {
- const { getPaymentURL } = useOrderAPI();
- const [url, setURL] = useState<string | undefined>(undefined);
- const { i18n } = useTranslationContext();
- useEffect(() => {
- getPaymentURL(entity.response.order_id).then((response) => {
- setURL(response.data);
- });
- }, [getPaymentURL, entity.response.order_id]);
-
- return (
- <CreatedSuccessfully
- onConfirm={onConfirm}
- onCreateAnother={onCreateAnother}
- >
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Amount</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input
- class="input"
- readonly
- value={entity.request.order.amount}
- />
- </p>
- </div>
- </div>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Summary</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input
- class="input"
- readonly
- value={entity.request.order.summary}
- />
- </p>
- </div>
- </div>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Order ID</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input class="input" readonly value={entity.response.order_id} />
- </p>
- </div>
- </div>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Payment URL</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input class="input" readonly value={url} />
- </p>
- </div>
- </div>
- </div>
- </CreatedSuccessfully>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
deleted file mode 100644
index d2a8619ce..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Loading } from "../../../../components/exception/loading.js";
-import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useInstanceDetails } from "../../../../hooks/instance.js";
-import { useOrderAPI } from "../../../../hooks/order.js";
-import { useInstanceProducts } from "../../../../hooks/product.js";
-import { Notification } from "../../../../utils/types.js";
-import { CreatePage } from "./CreatePage.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-
-export type Entity = {
- request: MerchantBackend.Orders.PostOrderRequest;
- response: MerchantBackend.Orders.PostOrderResponse;
-};
-interface Props {
- onBack?: () => void;
- onConfirm: (id: string) => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
-}
-export default function OrderCreate({
- onConfirm,
- onBack,
- onLoadError,
- onNotFound,
- onUnauthorized,
-}: Props): VNode {
- const { createOrder } = useOrderAPI();
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
-
- const detailsResult = useInstanceDetails();
- const inventoryResult = useInstanceProducts();
-
- if (detailsResult.loading) return <Loading />;
- if (inventoryResult.loading) return <Loading />;
-
- if (!detailsResult.ok) {
- if (
- detailsResult.type === ErrorType.CLIENT &&
- detailsResult.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- detailsResult.type === ErrorType.CLIENT &&
- detailsResult.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(detailsResult);
- }
-
- if (!inventoryResult.ok) {
- if (
- inventoryResult.type === ErrorType.CLIENT &&
- inventoryResult.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- inventoryResult.type === ErrorType.CLIENT &&
- inventoryResult.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(inventoryResult);
- }
-
- return (
- <Fragment>
- <NotificationCard notification={notif} />
-
- <CreatePage
- onBack={onBack}
- onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
- createOrder(request)
- .then((r) => {
- return onConfirm(r.data.order_id)
- })
- .catch((error) => {
- setNotif({
- message: "could not create order",
- type: "ERROR",
- description: error.message,
- });
- });
- }}
- instanceConfig={detailsResult.data}
- instanceInventory={inventoryResult.data}
- />
- </Fragment>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
deleted file mode 100644
index 22c6944b3..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { addDays } from "date-fns";
-import { h, VNode, FunctionalComponent } from "preact";
-import { MerchantBackend } from "../../../../declaration.js";
-import { DetailPage as TestedComponent } from "./DetailPage.js";
-
-export default {
- title: "Pages/Order/Detail",
- component: TestedComponent,
- argTypes: {
- onRefund: { action: "onRefund" },
- onBack: { action: "onBack" },
- },
-};
-
-function createExample<Props>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props>,
-) {
- const r = (args: any) => <Component {...args} />;
- r.args = props;
- return r;
-}
-
-const defaultContractTerm = {
- amount: "TESTKUDOS:10",
- timestamp: {
- t_s: new Date().getTime() / 1000,
- },
- auditors: [],
- exchanges: [],
- max_fee: "TESTKUDOS:1",
- merchant: {} as any,
- merchant_base_url: "http://merchant.url/",
- order_id: "2021.165-03GDFC26Y1NNG",
- products: [],
- summary: "text summary",
- wire_transfer_deadline: {
- t_s: "never",
- },
- refund_deadline: { t_s: "never" },
- merchant_pub: "ASDASDASDSd",
- nonce: "QWEQWEQWE",
- pay_deadline: {
- t_s: "never",
- },
- wire_method: "x-taler-bank",
- h_wire: "asd",
-} as MerchantBackend.ContractTerms;
-
-// contract_terms: defaultContracTerm,
-export const Claimed = createExample(TestedComponent, {
- id: "2021.165-03GDFC26Y1NNG",
- selected: {
- order_status: "claimed",
- contract_terms: defaultContractTerm,
- },
-});
-
-export const PaidNotRefundable = createExample(TestedComponent, {
- id: "2021.165-03GDFC26Y1NNG",
- selected: {
- order_status: "paid",
- contract_terms: defaultContractTerm,
- refunded: false,
- deposit_total: "TESTKUDOS:10",
- exchange_ec: 0,
- order_status_url: "http://merchant.backend/status",
- exchange_hc: 0,
- refund_amount: "TESTKUDOS:0",
- refund_details: [],
- refund_pending: false,
- wire_details: [],
- wire_reports: [],
- wired: false,
- },
-});
-
-export const PaidRefundable = createExample(TestedComponent, {
- id: "2021.165-03GDFC26Y1NNG",
- selected: {
- order_status: "paid",
- contract_terms: {
- ...defaultContractTerm,
- refund_deadline: {
- t_s: addDays(new Date(), 2).getTime() / 1000,
- },
- },
- refunded: false,
- deposit_total: "TESTKUDOS:10",
- exchange_ec: 0,
- order_status_url: "http://merchant.backend/status",
- exchange_hc: 0,
- refund_amount: "TESTKUDOS:0",
- refund_details: [],
- refund_pending: false,
- wire_details: [],
- wire_reports: [],
- wired: false,
- },
-});
-
-export const Unpaid = createExample(TestedComponent, {
- id: "2021.165-03GDFC26Y1NNG",
- selected: {
- order_status: "unpaid",
- order_status_url: "http://merchant.backend/status",
- creation_time: {
- t_s: new Date().getTime() / 1000,
- },
- summary: "text summary",
- taler_pay_uri: "pay uri",
- total_amount: "TESTKUDOS:10",
- },
-});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
deleted file mode 100644
index abcddb38a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
+++ /dev/null
@@ -1,770 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { format, formatDistance } from "date-fns";
-import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { FormProvider } from "../../../../components/form/FormProvider.js";
-import { Input } from "../../../../components/form/Input.js";
-import { InputCurrency } from "../../../../components/form/InputCurrency.js";
-import { InputDate } from "../../../../components/form/InputDate.js";
-import { InputDuration } from "../../../../components/form/InputDuration.js";
-import { InputGroup } from "../../../../components/form/InputGroup.js";
-import { InputLocation } from "../../../../components/form/InputLocation.js";
-import { TextField } from "../../../../components/form/TextField.js";
-import { ProductList } from "../../../../components/product/ProductList.js";
-import { useBackendContext } from "../../../../context/backend.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
-import { mergeRefunds } from "../../../../utils/amount.js";
-import { RefundModal } from "../list/Table.js";
-import { Event, Timeline } from "./Timeline.js";
-
-type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
-type CT = MerchantBackend.ContractTerms;
-
-interface Props {
- onBack: () => void;
- selected: Entity;
- id: string;
- onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
-}
-
-type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & {
- refund_taken: string;
-};
-type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse;
-type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse;
-
-function ContractTerms({ value }: { value: CT }) {
- const { i18n } = useTranslationContext();
-
- return (
- <InputGroup name="contract_terms" label={i18n.str`Contract Terms`}>
- <FormProvider<CT> object={value} valueHandler={null}>
- <Input<CT>
- readonly
- name="summary"
- label={i18n.str`Summary`}
- tooltip={i18n.str`human-readable description of the whole purchase`}
- />
- <InputCurrency<CT>
- readonly
- name="amount"
- label={i18n.str`Amount`}
- tooltip={i18n.str`total price for the transaction`}
- />
- {value.fulfillment_url && (
- <Input<CT>
- readonly
- name="fulfillment_url"
- label={i18n.str`Fulfillment URL`}
- tooltip={i18n.str`URL for this purchase`}
- />
- )}
- <Input<CT>
- readonly
- name="max_fee"
- label={i18n.str`Max fee`}
- tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
- />
- <InputDate<CT>
- readonly
- name="timestamp"
- label={i18n.str`Created at`}
- tooltip={i18n.str`time when this contract was generated`}
- />
- <InputDate<CT>
- readonly
- name="refund_deadline"
- label={i18n.str`Refund deadline`}
- tooltip={i18n.str`after this deadline has passed no refunds will be accepted`}
- />
- <InputDate<CT>
- readonly
- name="pay_deadline"
- label={i18n.str`Payment deadline`}
- tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`}
- />
- <InputDate<CT>
- readonly
- name="wire_transfer_deadline"
- label={i18n.str`Wire transfer deadline`}
- tooltip={i18n.str`transfer deadline for the exchange`}
- />
- <InputDate<CT>
- readonly
- name="delivery_date"
- label={i18n.str`Delivery date`}
- tooltip={i18n.str`time indicating when the order should be delivered`}
- />
- {value.delivery_date && (
- <InputGroup
- name="delivery_location"
- label={i18n.str`Location`}
- tooltip={i18n.str`where the order will be delivered`}
- >
- <InputLocation name="payments.delivery_location" />
- </InputGroup>
- )}
- <InputDuration<CT>
- readonly
- name="auto_refund"
- label={i18n.str`Auto-refund delay`}
- tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`}
- />
- <Input<CT>
- readonly
- name="extra"
- label={i18n.str`Extra info`}
- tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`}
- />
- </FormProvider>
- </InputGroup>
- );
-}
-
-function ClaimedPage({
- id,
- order,
-}: {
- id: string;
- order: MerchantBackend.Orders.CheckPaymentClaimedResponse;
-}) {
- const events: Event[] = [];
- if (order.contract_terms.timestamp.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.timestamp.t_s * 1000),
- description: "order created",
- type: "start",
- });
- }
- if (order.contract_terms.pay_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
- description: "pay deadline",
- type: "deadline",
- });
- }
- if (order.contract_terms.refund_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
- description: "refund deadline",
- type: "deadline",
- });
- }
- if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
- description: "wire deadline",
- type: "deadline",
- });
- }
- if (
- order.contract_terms.delivery_date &&
- order.contract_terms.delivery_date.t_s !== "never"
- ) {
- events.push({
- when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
- description: "delivery",
- type: "delivery",
- });
- }
-
- const [value, valueHandler] = useState<Partial<Claimed>>(order);
- const { i18n } = useTranslationContext();
- const [settings] = useSettings()
-
- return (
- <div>
- <section class="section">
- <div class="columns">
- <div class="column" />
- <div class="column is-10">
- <section class="hero is-hero-bar">
- <div class="hero-body">
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <i18n.Translate>Order</i18n.Translate> #{id}
- <div class="tag is-info ml-4">
- <i18n.Translate>claimed</i18n.Translate>
- </div>
- </div>
- </div>
- </div>
-
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <h1 class="title">{order.contract_terms.amount}</h1>
- </div>
- </div>
- </div>
-
- <div class="level">
- <div class="level-left" style={{ maxWidth: "100%" }}>
- <div class="level-item" style={{ maxWidth: "100%" }}>
- <div
- class="content"
- style={{
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
- }}
- >
- <p>
- <b>
- <i18n.Translate>claimed at</i18n.Translate>:
- </b>{" "}
- {format(
- new Date(order.contract_terms.timestamp.t_s * 1000),
- datetimeFormatForSettings(settings)
- )}
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
-
- <section class="section">
- <div class="columns">
- <div class="column is-4">
- <div class="title">
- <i18n.Translate>Timeline</i18n.Translate>
- </div>
- <Timeline events={events} />
- </div>
- <div class="column is-8">
- <div class="title">
- <i18n.Translate>Payment details</i18n.Translate>
- </div>
- <FormProvider<Claimed>
- object={value}
- valueHandler={valueHandler}
- >
- <Input
- name="contract_terms.summary"
- readonly
- inputType="multiline"
- label={i18n.str`Summary`}
- />
- <InputCurrency
- name="contract_terms.amount"
- readonly
- label={i18n.str`Amount`}
- />
- <Input<Claimed>
- name="order_status"
- readonly
- label={i18n.str`Order status`}
- />
- </FormProvider>
- </div>
- </div>
- </section>
-
- {order.contract_terms.products.length ? (
- <Fragment>
- <div class="title">
- <i18n.Translate>Product list</i18n.Translate>
- </div>
- <ProductList list={order.contract_terms.products} />
- </Fragment>
- ) : undefined}
-
- {value.contract_terms && (
- <ContractTerms value={value.contract_terms} />
- )}
- </div>
- <div class="column" />
- </div>
- </section>
- </div>
- );
-}
-function PaidPage({
- id,
- order,
- onRefund,
-}: {
- id: string;
- order: MerchantBackend.Orders.CheckPaymentPaidResponse;
- onRefund: (id: string) => void;
-}) {
- const events: Event[] = [];
- if (order.contract_terms.timestamp.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.timestamp.t_s * 1000),
- description: "order created",
- type: "start",
- });
- }
- if (order.contract_terms.pay_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
- description: "pay deadline",
- type: "deadline",
- });
- }
- if (order.contract_terms.refund_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
- description: "refund deadline",
- type: "deadline",
- });
- }
- if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
- description: "wire deadline",
- type: "deadline",
- });
- }
- if (
- order.contract_terms.delivery_date &&
- order.contract_terms.delivery_date.t_s !== "never"
- ) {
- if (order.contract_terms.delivery_date)
- events.push({
- when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
- description: "delivery",
- type: "delivery",
- });
- }
- order.refund_details.reduce(mergeRefunds, []).forEach((e) => {
- if (e.timestamp.t_s !== "never") {
- events.push({
- when: new Date(e.timestamp.t_s * 1000),
- description: `refund: ${e.amount}: ${e.reason}`,
- type: e.pending ? "refund" : "refund-taken",
- });
- }
- });
- if (order.wire_details && order.wire_details.length) {
- if (order.wire_details.length > 1) {
- let last: MerchantBackend.Orders.TransactionWireTransfer | null = null;
- let first: MerchantBackend.Orders.TransactionWireTransfer | null = null;
- let total: AmountJson | null = null;
-
- order.wire_details.forEach((w) => {
- if (last === null || last.execution_time.t_s < w.execution_time.t_s) {
- last = w;
- }
- if (first === null || first.execution_time.t_s > w.execution_time.t_s) {
- first = w;
- }
- total =
- total === null
- ? Amounts.parseOrThrow(w.amount)
- : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount;
- });
- const last_time = last!.execution_time.t_s;
- if (last_time !== "never") {
- events.push({
- when: new Date(last_time * 1000),
- description: `wired ${Amounts.stringify(total!)}`,
- type: "wired-range",
- });
- }
- const first_time = first!.execution_time.t_s;
- if (first_time !== "never") {
- events.push({
- when: new Date(first_time * 1000),
- description: `wire transfer started...`,
- type: "wired-range",
- });
- }
- } else {
- order.wire_details.forEach((e) => {
- if (e.execution_time.t_s !== "never") {
- events.push({
- when: new Date(e.execution_time.t_s * 1000),
- description: `wired ${e.amount}`,
- type: "wired",
- });
- }
- });
- }
- }
-
- const now = new Date()
- const nextEvent = events.find((e) => {
- return e.when.getTime() > now.getTime()
- })
-
- const [value, valueHandler] = useState<Partial<Paid>>(order);
- const { url: backendURL } = useBackendContext()
- const refundurl = stringifyRefundUri({
- merchantBaseUrl: backendURL,
- orderId: order.contract_terms.order_id
- })
- const refundable =
- new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
- const { i18n } = useTranslationContext();
-
- const amount = Amounts.parseOrThrow(order.contract_terms.amount);
- const refund_taken = order.refund_details.reduce((prev, cur) => {
- if (cur.pending) return prev;
- return Amounts.add(prev, Amounts.parseOrThrow(cur.amount)).amount;
- }, Amounts.zeroOfCurrency(amount.currency));
- value.refund_taken = Amounts.stringify(refund_taken);
-
- return (
- <div>
- <section class="section">
- <div class="columns">
- <div class="column" />
- <div class="column is-10">
- <section class="hero is-hero-bar">
- <div class="hero-body">
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <i18n.Translate>Order</i18n.Translate> #{id}
- <div class="tag is-success ml-4">
- <i18n.Translate>paid</i18n.Translate>
- </div>
- {order.wired ? (
- <div class="tag is-success ml-4">
- <i18n.Translate>wired</i18n.Translate>
- </div>
- ) : null}
- {order.refunded ? (
- <div class="tag is-danger ml-4">
- <i18n.Translate>refunded</i18n.Translate>
- </div>
- ) : null}
- </div>
- </div>
- </div>
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <h1 class="title">{order.contract_terms.amount}</h1>
- </div>
- </div>
- <div class="level-right">
- <div class="level-item">
- <h1 class="title">
- <div class="buttons">
- <span
- class="has-tooltip-left"
- data-tooltip={
- refundable
- ? i18n.str`refund order`
- : i18n.str`not refundable`
- }
- >
- <button
- class="button is-danger"
- disabled={!refundable}
- onClick={() => onRefund(id)}
- >
- <i18n.Translate>refund</i18n.Translate>
- </button>
- </span>
- </div>
- </h1>
- </div>
- </div>
- </div>
-
- <div class="level">
- <div class="level-left" style={{ maxWidth: "100%" }}>
- <div class="level-item" style={{ maxWidth: "100%" }}>
- <div
- class="content"
- style={{
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
- }}
- >
- {nextEvent &&
- <p>
- <i18n.Translate>Next event in </i18n.Translate> {formatDistance(
- nextEvent.when,
- new Date(),
- // "yyyy/MM/dd HH:mm:ss",
- )}
- </p>
- }
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
-
- <section class="section">
- <div class="columns">
- <div class="column is-4">
- <div class="title">
- <i18n.Translate>Timeline</i18n.Translate>
- </div>
- <Timeline events={events} />
- </div>
- <div class="column is-8">
- <div class="title">
- <i18n.Translate>Payment details</i18n.Translate>
- </div>
- <FormProvider<Paid>
- object={value}
- valueHandler={valueHandler}
- >
- {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n.str`Deposit total`} /> */}
- {order.refunded && (
- <InputCurrency<Paid>
- name="refund_amount"
- readonly
- label={i18n.str`Refunded amount`}
- />
- )}
- {order.refunded && (
- <InputCurrency<Paid>
- name="refund_taken"
- readonly
- label={i18n.str`Refund taken`}
- />
- )}
- <Input<Paid>
- name="order_status"
- readonly
- label={i18n.str`Order status`}
- />
- <TextField<Paid>
- name="order_status_url"
- label={i18n.str`Status URL`}
- >
- <a
- target="_blank"
- rel="noreferrer"
- href={order.order_status_url}
- >
- {order.order_status_url}
- </a>
- </TextField>
- {order.refunded && (
- <TextField<Paid>
- name="order_status_url"
- label={i18n.str`Refund URI`}
- >
- <a target="_blank" rel="noreferrer" href={refundurl}>
- {refundurl}
- </a>
- </TextField>
- )}
- </FormProvider>
- </div>
- </div>
- </section>
-
- {order.contract_terms.products.length ? (
- <Fragment>
- <div class="title">
- <i18n.Translate>Product list</i18n.Translate>
- </div>
- <ProductList list={order.contract_terms.products} />
- </Fragment>
- ) : undefined}
-
- {value.contract_terms && (
- <ContractTerms value={value.contract_terms} />
- )}
- </div>
- <div class="column" />
- </div>
- </section>
- </div>
- );
-}
-
-function UnpaidPage({
- id,
- order,
-}: {
- id: string;
- order: MerchantBackend.Orders.CheckPaymentUnpaidResponse;
-}) {
- const [value, valueHandler] = useState<Partial<Unpaid>>(order);
- const { i18n } = useTranslationContext();
- const [settings] = useSettings()
- return (
- <div>
- <section class="hero is-hero-bar">
- <div class="hero-body">
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <h1 class="title">
- <i18n.Translate>Order</i18n.Translate> #{id}
- </h1>
- </div>
- <div class="tag is-dark">
- <i18n.Translate>unpaid</i18n.Translate>
- </div>
- </div>
- </div>
-
- <div class="level">
- <div class="level-left" style={{ maxWidth: "100%" }}>
- <div class="level-item" style={{ maxWidth: "100%" }}>
- <div
- class="content"
- style={{
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
- }}
- >
- <p>
- <b>
- <i18n.Translate>pay at</i18n.Translate>:
- </b>{" "}
- <a
- href={order.order_status_url}
- rel="nofollow"
- target="new"
- >
- {order.order_status_url}
- </a>
- </p>
- <p>
- <b>
- <i18n.Translate>created at</i18n.Translate>:
- </b>{" "}
- {order.creation_time.t_s === "never"
- ? "never"
- : format(
- new Date(order.creation_time.t_s * 1000),
- datetimeFormatForSettings(settings)
- )}
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
-
- <section class="section is-main-section">
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- <FormProvider<Unpaid> object={value} valueHandler={valueHandler}>
- <Input<Unpaid>
- readonly
- name="summary"
- label={i18n.str`Summary`}
- tooltip={i18n.str`human-readable description of the whole purchase`}
- />
- <InputCurrency<Unpaid>
- readonly
- name="total_amount"
- label={i18n.str`Amount`}
- tooltip={i18n.str`total price for the transaction`}
- />
- <Input<Unpaid>
- name="order_status"
- readonly
- label={i18n.str`Order status`}
- />
- <Input<Unpaid>
- name="order_status_url"
- readonly
- label={i18n.str`Order status URL`}
- />
- <TextField<Unpaid>
- name="taler_pay_uri"
- label={i18n.str`Payment URI`}
- >
- <a target="_blank" rel="noreferrer" href={value.taler_pay_uri}>
- {value.taler_pay_uri}
- </a>
- </TextField>
- </FormProvider>
- </div>
- <div class="column" />
- </div>
- </section>
- </div>
- );
-}
-
-export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
- const [showRefund, setShowRefund] = useState<string | undefined>(undefined);
- const { i18n } = useTranslationContext();
- const DetailByStatus = function () {
- switch (selected.order_status) {
- case "claimed":
- return <ClaimedPage id={id} order={selected} />;
- case "paid":
- return <PaidPage id={id} order={selected} onRefund={setShowRefund} />;
- case "unpaid":
- return <UnpaidPage id={id} order={selected} />;
- default:
- return (
- <div>
- <i18n.Translate>
- Unknown order status. This is an error, please contact the
- administrator.
- </i18n.Translate>
- </div>
- );
- }
- };
-
- return (
- <Fragment>
- {DetailByStatus()}
- {showRefund && (
- <RefundModal
- order={selected}
- onCancel={() => setShowRefund(undefined)}
- onConfirm={(value) => {
- onRefund(showRefund, value);
- setShowRefund(undefined);
- }}
- />
- )}
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- <div class="buttons is-right mt-5">
- <button class="button" onClick={onBack}>
- <i18n.Translate>Back</i18n.Translate>
- </button>
- </div>
- </div>
- <div class="column" />
- </div>
- </Fragment>
- );
-}
-
-async function copyToClipboard(text: string) {
- return navigator.clipboard.writeText(text);
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
deleted file mode 100644
index 1aae4da21..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import { format } from "date-fns";
-import { h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
-
-interface Props {
- events: Event[];
-}
-
-export function Timeline({ events: e }: Props) {
- const events = [...e];
- events.push({
- when: new Date(),
- description: "now",
- type: "now",
- });
-
- events.sort((a, b) => a.when.getTime() - b.when.getTime());
- const [settings] = useSettings();
- const [state, setState] = useState(events);
- useEffect(() => {
- const handle = setTimeout(() => {
- const eventsWithoutNow = state.filter((e) => e.type !== "now");
- eventsWithoutNow.push({
- when: new Date(),
- description: "now",
- type: "now",
- });
- setState(eventsWithoutNow);
- }, 1000);
- return () => {
- clearTimeout(handle);
- };
- });
- return (
- <div class="timeline">
- {events.map((e, i) => {
- return (
- <div key={i} class="timeline-item">
- {(() => {
- switch (e.type) {
- case "deadline":
- return (
- <div class="timeline-marker is-icon ">
- <i class="mdi mdi-flag" />
- </div>
- );
- case "delivery":
- return (
- <div class="timeline-marker is-icon ">
- <i class="mdi mdi-delivery" />
- </div>
- );
- case "start":
- return (
- <div class="timeline-marker is-icon">
- <i class="mdi mdi-flag " />
- </div>
- );
- case "wired":
- return (
- <div class="timeline-marker is-icon is-success">
- <i class="mdi mdi-cash" />
- </div>
- );
- case "wired-range":
- return (
- <div class="timeline-marker is-icon is-success">
- <i class="mdi mdi-cash" />
- </div>
- );
- case "refund":
- return (
- <div class="timeline-marker is-icon is-danger">
- <i class="mdi mdi-cash" />
- </div>
- );
- case "refund-taken":
- return (
- <div class="timeline-marker is-icon is-success">
- <i class="mdi mdi-cash" />
- </div>
- );
- case "now":
- return (
- <div class="timeline-marker is-icon is-info">
- <i class="mdi mdi-clock" />
- </div>
- );
- }
- })()}
- <div class="timeline-content">
- {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>}
- <p>{e.description}</p>
- </div>
- </div>
- );
- })}
- </div>
- );
-}
-export interface Event {
- when: Date;
- description: string;
- type:
- | "start"
- | "refund"
- | "refund-taken"
- | "wired"
- | "wired-range"
- | "deadline"
- | "delivery"
- | "now";
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
deleted file mode 100644
index dfeaa4447..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import {
- useTranslationContext,
- HttpError,
- ErrorType,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Loading } from "../../../../components/exception/loading.js";
-import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
-import { Notification } from "../../../../utils/types.js";
-import { DetailPage } from "./DetailPage.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-
-export interface Props {
- oid: string;
-
- onBack: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
-}
-
-export default function Update({
- oid,
- onBack,
- onLoadError,
- onNotFound,
- onUnauthorized,
-}: Props): VNode {
- const { refundOrder } = useOrderAPI();
- const result = useOrderDetails(oid);
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
-
- const { i18n } = useTranslationContext();
-
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
- }
-
- return (
- <Fragment>
- <NotificationCard notification={notif} />
-
- <DetailPage
- onBack={onBack}
- id={oid}
- onRefund={(id, value) =>
- refundOrder(id, value)
- .then(() =>
- setNotif({
- message: i18n.str`refund created successfully`,
- type: "SUCCESS",
- }),
- )
- .catch((error) =>
- setNotif({
- message: i18n.str`could not create the refund`,
- type: "ERROR",
- description: error.message,
- }),
- )
- }
- selected={result.data}
- />
- </Fragment>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
deleted file mode 100644
index 9df006083..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h, VNode, FunctionalComponent } from "preact";
-import { ListPage as TestedComponent } from "./ListPage.js";
-
-export default {
- title: "Pages/Order/List",
- component: TestedComponent,
- argTypes: {
- onShowAll: { action: "onShowAll" },
- onShowPaid: { action: "onShowPaid" },
- onShowRefunded: { action: "onShowRefunded" },
- onShowNotWired: { action: "onShowNotWired" },
- onCopyURL: { action: "onCopyURL" },
- onSelectDate: { action: "onSelectDate" },
- onLoadMoreBefore: { action: "onLoadMoreBefore" },
- onLoadMoreAfter: { action: "onLoadMoreAfter" },
- onSelectOrder: { action: "onSelectOrder" },
- onRefundOrder: { action: "onRefundOrder" },
- onSearchOrderById: { action: "onSearchOrderById" },
- onCreate: { action: "onCreate" },
- },
-};
-
-function createExample<Props>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props>,
-) {
- const r = (args: any) => <Component {...args} />;
- r.args = props;
- return r;
-}
-
-export const Example = createExample(TestedComponent, {
- orders: [
- {
- id: "123",
- amount: "TESTKUDOS:10",
- paid: false,
- refundable: true,
- row_id: 1,
- summary: "summary",
- timestamp: {
- t_s: new Date().getTime() / 1000,
- },
- order_id: "123",
- },
- {
- id: "234",
- amount: "TESTKUDOS:12",
- paid: true,
- refundable: true,
- row_id: 2,
- summary:
- "summary with long text, very very long text that someone want to add as a description of the order",
- timestamp: {
- t_s: new Date().getTime() / 1000,
- },
- order_id: "234",
- },
- {
- id: "456",
- amount: "TESTKUDOS:1",
- paid: false,
- refundable: false,
- row_id: 3,
- summary:
- "summary with long text, very very long text that someone want to add as a description of the order",
- timestamp: {
- t_s: new Date().getTime() / 1000,
- },
- order_id: "456",
- },
- {
- id: "234",
- amount: "TESTKUDOS:12",
- paid: false,
- refundable: false,
- row_id: 4,
- summary:
- "summary with long text, very very long text that someone want to add as a description of the order",
- timestamp: {
- t_s: new Date().getTime() / 1000,
- },
- order_id: "234",
- },
- ],
-});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
deleted file mode 100644
index 187a1d0b4..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
-import { h, VNode, Fragment } from "preact";
-import { useState } from "preact/hooks";
-import { DatePicker } from "../../../../components/picker/DatePicker.js";
-import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { CardTable } from "./Table.js";
-import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
-
-export interface ListPageProps {
- onShowAll: () => void;
- onShowNotPaid: () => void;
- onShowPaid: () => void;
- onShowRefunded: () => void;
- onShowNotWired: () => void;
- onShowWired: () => void;
- onCopyURL: (id: string) => void;
- isAllActive: string;
- isPaidActive: string;
- isNotPaidActive: string;
- isRefundedActive: string;
- isNotWiredActive: string;
- isWiredActive: string;
-
- jumpToDate?: Date;
- onSelectDate: (date?: Date) => void;
-
- orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[];
- onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
- onLoadMoreAfter?: () => void;
-
- onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
- onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
- onCreate: () => void;
-}
-
-export function ListPage({
- hasMoreAfter,
- hasMoreBefore,
- onLoadMoreAfter,
- onLoadMoreBefore,
- orders,
- isAllActive,
- onSelectOrder,
- onRefundOrder,
- jumpToDate,
- onCopyURL,
- onShowAll,
- onShowPaid,
- onShowNotPaid,
- onShowRefunded,
- onShowNotWired,
- onShowWired,
- onSelectDate,
- isPaidActive,
- isRefundedActive,
- isNotWiredActive,
- onCreate,
- isNotPaidActive,
- isWiredActive,
-}: ListPageProps): VNode {
- const { i18n } = useTranslationContext();
- const dateTooltip = i18n.str`select date to show nearby orders`;
- const [pickDate, setPickDate] = useState(false);
- const [settings] = useSettings();
-
- return (
- <Fragment>
- <div class="columns">
- <div class="column is-two-thirds">
- <div class="tabs" style={{ overflow: "inherit" }}>
- <ul>
- <li class={isNotPaidActive}>
- <div
- class="has-tooltip-right"
- data-tooltip={i18n.str`only show paid orders`}
- >
- <a onClick={onShowNotPaid}>
- <i18n.Translate>New</i18n.Translate>
- </a>
- </div>
- </li>
- <li class={isPaidActive}>
- <div
- class="has-tooltip-right"
- data-tooltip={i18n.str`only show paid orders`}
- >
- <a onClick={onShowPaid}>
- <i18n.Translate>Paid</i18n.Translate>
- </a>
- </div>
- </li>
- <li class={isRefundedActive}>
- <div
- class="has-tooltip-right"
- data-tooltip={i18n.str`only show orders with refunds`}
- >
- <a onClick={onShowRefunded}>
- <i18n.Translate>Refunded</i18n.Translate>
- </a>
- </div>
- </li>
- <li class={isNotWiredActive}>
- <div
- class="has-tooltip-left"
- data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
- >
- <a onClick={onShowNotWired}>
- <i18n.Translate>Not wired</i18n.Translate>
- </a>
- </div>
- </li>
- <li class={isWiredActive}>
- <div
- class="has-tooltip-left"
- data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
- >
- <a onClick={onShowWired}>
- <i18n.Translate>Completed</i18n.Translate>
- </a>
- </div>
- </li>
- <li class={isAllActive}>
- <div
- class="has-tooltip-right"
- data-tooltip={i18n.str`remove all filters`}
- >
- <a onClick={onShowAll}>
- <i18n.Translate>All</i18n.Translate>
- </a>
- </div>
- </li>
- </ul>
- </div>
- </div>
- <div class="column ">
- <div class="buttons is-right">
- <div class="field has-addons">
- {jumpToDate && (
- <div class="control">
- <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
- <span
- class="icon"
- data-tooltip={i18n.str`clear date filter`}
- >
- <i class="mdi mdi-close" />
- </span>
- </a>
- </div>
- )}
- <div class="control">
- <span class="has-tooltip-top" data-tooltip={dateTooltip}>
- <input
- class="input"
- type="text"
- readonly
- value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
- placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
- onClick={() => {
- setPickDate(true);
- }}
- />
- </span>
- </div>
- <div class="control">
- <span class="has-tooltip-left" data-tooltip={dateTooltip}>
- <a
- class="button is-fullwidth"
- onClick={() => {
- setPickDate(true);
- }}
- >
- <span class="icon">
- <i class="mdi mdi-calendar" />
- </span>
- </a>
- </span>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <DatePicker
- opened={pickDate}
- closeFunction={() => setPickDate(false)}
- dateReceiver={onSelectDate}
- />
-
- <CardTable
- orders={orders}
- onCreate={onCreate}
- onCopyURL={onCopyURL}
- onSelect={onSelectOrder}
- onRefund={onRefundOrder}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
- onLoadMoreAfter={onLoadMoreAfter}
- onLoadMoreBefore={onLoadMoreBefore}
- />
- </Fragment>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
deleted file mode 100644
index cbe8331ca..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
+++ /dev/null
@@ -1,417 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { Amounts } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
-import { h, VNode } from "preact";
-import { StateUpdater, useState } from "preact/hooks";
-import {
- FormErrors,
- FormProvider,
-} from "../../../../components/form/FormProvider.js";
-import { Input } from "../../../../components/form/Input.js";
-import { InputCurrency } from "../../../../components/form/InputCurrency.js";
-import { InputGroup } from "../../../../components/form/InputGroup.js";
-import { InputSelector } from "../../../../components/form/InputSelector.js";
-import { ConfirmModal } from "../../../../components/modal/index.js";
-import { useConfigContext } from "../../../../context/config.js";
-import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { mergeRefunds } from "../../../../utils/amount.js";
-import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
-
-type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
-interface Props {
- orders: Entity[];
- onRefund: (value: Entity) => void;
- onCopyURL: (id: string) => void;
- onCreate: () => void;
- onSelect: (order: Entity) => void;
- onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
- onLoadMoreAfter?: () => void;
-}
-
-export function CardTable({
- orders,
- onCreate,
- onRefund,
- onCopyURL,
- onSelect,
- onLoadMoreAfter,
- onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
-}: Props): VNode {
- const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
-
- const { i18n } = useTranslationContext();
-
- return (
- <div class="card has-table">
- <header class="card-header">
- <p class="card-header-title">
- <span class="icon">
- <i class="mdi mdi-cash-register" />
- </span>
- <i18n.Translate>Orders</i18n.Translate>
- </p>
-
- <div class="card-header-icon" aria-label="more options" />
-
- <div class="card-header-icon" aria-label="more options">
- <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}>
- <button class="button is-info" type="button" onClick={onCreate}>
- <span class="icon is-small">
- <i class="mdi mdi-plus mdi-36px" />
- </span>
- </button>
- </span>
- </div>
- </header>
- <div class="card-content">
- <div class="b-table has-pagination">
- <div class="table-wrapper has-mobile-cards">
- {orders.length > 0 ? (
- <Table
- instances={orders}
- onSelect={onSelect}
- onRefund={onRefund}
- onCopyURL={(o) => onCopyURL(o.id)}
- rowSelection={rowSelection}
- rowSelectionHandler={rowSelectionHandler}
- onLoadMoreAfter={onLoadMoreAfter}
- onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
- />
- ) : (
- <EmptyTable />
- )}
- </div>
- </div>
- </div>
- </div>
- );
-}
-interface TableProps {
- rowSelection: string[];
- instances: Entity[];
- onRefund: (id: Entity) => void;
- onCopyURL: (id: Entity) => void;
- onSelect: (id: Entity) => void;
- rowSelectionHandler: StateUpdater<string[]>;
- onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
- onLoadMoreAfter?: () => void;
-}
-
-function Table({
- instances,
- onSelect,
- onRefund,
- onCopyURL,
- onLoadMoreAfter,
- onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
-}: TableProps): VNode {
- const { i18n } = useTranslationContext();
- const [settings] = useSettings();
- return (
- <div class="table-container">
- {hasMoreBefore && (
- <button
- class="button is-fullwidth"
- onClick={onLoadMoreBefore}
- >
- <i18n.Translate>load newer orders</i18n.Translate>
- </button>
- )}
- <table class="table is-striped is-hoverable is-fullwidth">
- <thead>
- <tr>
- <th style={{ minWidth: 100 }}>
- <i18n.Translate>Date</i18n.Translate>
- </th>
- <th style={{ minWidth: 100 }}>
- <i18n.Translate>Amount</i18n.Translate>
- </th>
- <th style={{ minWidth: 400 }}>
- <i18n.Translate>Summary</i18n.Translate>
- </th>
- <th style={{ minWidth: 50 }} />
- </tr>
- </thead>
- <tbody>
- {instances.map((i) => {
- return (
- <tr key={i.id}>
- <td
- onClick={(): void => onSelect(i)}
- style={{ cursor: "pointer" }}
- >
- {i.timestamp.t_s === "never"
- ? "never"
- : format(
- new Date(i.timestamp.t_s * 1000),
- datetimeFormatForSettings(settings),
- )}
- </td>
- <td
- onClick={(): void => onSelect(i)}
- style={{ cursor: "pointer" }}
- >
- {i.amount}
- </td>
- <td
- onClick={(): void => onSelect(i)}
- style={{ cursor: "pointer" }}
- >
- {i.summary}
- </td>
- <td class="is-actions-cell right-sticky">
- <div class="buttons is-right">
- {i.refundable && (
- <button
- class="button is-small is-danger jb-modal"
- type="button"
- onClick={(): void => onRefund(i)}
- >
- <i18n.Translate>Refund</i18n.Translate>
- </button>
- )}
- {!i.paid && (
- <button
- class="button is-small is-info jb-modal"
- type="button"
- onClick={(): void => onCopyURL(i)}
- >
- <i18n.Translate>copy url</i18n.Translate>
- </button>
- )}
- </div>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- {hasMoreAfter && (
- <button
- class="button is-fullwidth"
- onClick={onLoadMoreAfter}
- >
- <i18n.Translate>load older orders</i18n.Translate>
- </button>
- )}
- </div>
- );
-}
-
-function EmptyTable(): VNode {
- const { i18n } = useTranslationContext();
- return (
- <div class="content has-text-grey has-text-centered">
- <p>
- <span class="icon is-large">
- <i class="mdi mdi-emoticon-sad mdi-48px" />
- </span>
- </p>
- <p>
- <i18n.Translate>
- No orders have been found matching your query!
- </i18n.Translate>
- </p>
- </div>
- );
-}
-
-interface RefundModalProps {
- onCancel: () => void;
- onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
- order: MerchantBackend.Orders.MerchantOrderStatusResponse;
-}
-
-export function RefundModal({
- order,
- onCancel,
- onConfirm,
-}: RefundModalProps): VNode {
- type State = { mainReason?: string; description?: string; refund?: string };
- const [form, setValue] = useState<State>({});
- const [settings] = useSettings();
- const { i18n } = useTranslationContext();
- // const [errors, setErrors] = useState<FormErrors<State>>({});
-
- const refunds = (
- order.order_status === "paid" ? order.refund_details : []
- ).reduce(mergeRefunds, []);
-
- const config = useConfigContext();
- const totalRefunded = refunds
- .map((r) => r.amount)
- .reduce(
- (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount,
- Amounts.zeroOfCurrency(config.currency),
- );
- const orderPrice =
- order.order_status === "paid"
- ? Amounts.parseOrThrow(order.contract_terms.amount)
- : undefined;
- const totalRefundable = !orderPrice
- ? Amounts.zeroOfCurrency(totalRefunded.currency)
- : refunds.length
- ? Amounts.sub(orderPrice, totalRefunded).amount
- : orderPrice;
-
- const isRefundable = Amounts.isNonZero(totalRefundable);
- const duplicatedText = i18n.str`duplicated`;
-
- const errors: FormErrors<State> = {
- mainReason: !form.mainReason ? i18n.str`required` : undefined,
- description:
- !form.description && form.mainReason !== duplicatedText
- ? i18n.str`required`
- : undefined,
- refund: !form.refund
- ? i18n.str`required`
- : !Amounts.parse(form.refund)
- ? i18n.str`invalid format`
- : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
- ? i18n.str`this value exceed the refundable amount`
- : undefined,
- };
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
-
- const validateAndConfirm = () => {
- try {
- if (!form.refund) return;
- onConfirm({
- refund: Amounts.stringify(
- Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount,
- ),
- reason:
- form.description === undefined
- ? form.mainReason || ""
- : `${form.mainReason}: ${form.description}`,
- });
- } catch (err) {
- console.log(err);
- }
- };
-
- //FIXME: parameters in the translation
- return (
- <ConfirmModal
- description="refund"
- danger
- active
- disabled={!isRefundable || hasErrors}
- onCancel={onCancel}
- onConfirm={validateAndConfirm}
- >
- {refunds.length > 0 && (
- <div class="columns">
- <div class="column is-12">
- <InputGroup
- name="asd"
- label={`${Amounts.stringify(totalRefunded)} was already refunded`}
- >
- <table class="table is-fullwidth">
- <thead>
- <tr>
- <th>
- <i18n.Translate>date</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>amount</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>reason</i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody>
- {refunds.map((r) => {
- return (
- <tr key={r.timestamp.t_s}>
- <td>
- {r.timestamp.t_s === "never"
- ? "never"
- : format(
- new Date(r.timestamp.t_s * 1000),
- datetimeFormatForSettings(settings),
- )}
- </td>
- <td>{r.amount}</td>
- <td>{r.reason}</td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </InputGroup>
- </div>
- </div>
- )}
-
- {isRefundable && (
- <FormProvider<State>
- errors={errors}
- object={form}
- valueHandler={(d) => setValue(d as any)}
- >
- <InputCurrency<State>
- name="refund"
- label={i18n.str`Refund`}
- tooltip={i18n.str`amount to be refunded`}
- >
- <i18n.Translate>Max refundable:</i18n.Translate>{" "}
- {Amounts.stringify(totalRefundable)}
- </InputCurrency>
- <InputSelector
- name="mainReason"
- label={i18n.str`Reason`}
- values={[
- i18n.str`Choose one...`,
- duplicatedText,
- i18n.str`requested by the customer`,
- i18n.str`other`,
- ]}
- tooltip={i18n.str`why this order is being refunded`}
- />
- {form.mainReason && form.mainReason !== duplicatedText ? (
- <Input<State>
- label={i18n.str`Description`}
- name="description"
- tooltip={i18n.str`more information to give context`}
- />
- ) : undefined}
- </FormProvider>
- )}
- </ConfirmModal>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
deleted file mode 100644
index 369ef8c8b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import {
- ErrorType,
- HttpError,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Loading } from "../../../../components/exception/loading.js";
-import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import {
- InstanceOrderFilter,
- useInstanceOrders,
- useOrderAPI,
- useOrderDetails,
-} from "../../../../hooks/order.js";
-import { Notification } from "../../../../utils/types.js";
-import { ListPage } from "./ListPage.js";
-import { RefundModal } from "./Table.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
-
-interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- onNotFound: () => VNode;
- onSelect: (id: string) => void;
- onCreate: () => void;
-}
-
-export default function OrderList({
- onUnauthorized,
- onLoadError,
- onCreate,
- onSelect,
- onNotFound,
-}: Props): VNode {
- const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" });
- const [orderToBeRefunded, setOrderToBeRefunded] = useState<
- MerchantBackend.Orders.OrderHistoryEntry | undefined
- >(undefined);
-
- const setNewDate = (date?: Date): void =>
- setFilter((prev) => ({ ...prev, date }));
-
- const result = useInstanceOrders(filter, setNewDate);
- const { refundOrder, getPaymentURL } = useOrderAPI();
-
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
-
- const { i18n } = useTranslationContext();
-
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
- }
-
- const isNotPaidActive = filter.paid === "no" ? "is-active" : "";
- const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : "";
- const isRefundedActive = filter.refunded === "yes" ? "is-active" : "";
- const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : "";
- const isWiredActive = filter.wired === "yes" ? "is-active" : "";
- const isAllActive =
- filter.paid === undefined &&
- filter.refunded === undefined &&
- filter.wired === undefined
- ? "is-active"
- : "";
-
- return (
- <section class="section is-main-section">
- <NotificationCard notification={notif} />
-
- <JumpToElementById
- testIfExist={getPaymentURL}
- onSelect={onSelect}
- description={i18n.str`jump to order with the given product ID`}
- placeholder={i18n.str`order id`}
- />
-
- <ListPage
- orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))}
- onLoadMoreBefore={result.loadMorePrev}
- hasMoreBefore={!result.isReachingStart}
- onLoadMoreAfter={result.loadMore}
- hasMoreAfter={!result.isReachingEnd}
- onSelectOrder={(order) => onSelect(order.id)}
- onRefundOrder={(value) => setOrderToBeRefunded(value)}
- isAllActive={isAllActive}
- isNotWiredActive={isNotWiredActive}
- isWiredActive={isWiredActive}
- isPaidActive={isPaidActive}
- isNotPaidActive={isNotPaidActive}
- isRefundedActive={isRefundedActive}
- jumpToDate={filter.date}
- onCopyURL={(id) =>
- getPaymentURL(id).then((resp) => copyToClipboard(resp.data))
- }
- onCreate={onCreate}
- onSelectDate={setNewDate}
- onShowAll={() => setFilter({})}
- onShowNotPaid={() => setFilter({ paid: "no" })}
- onShowPaid={() => setFilter({ paid: "yes" })}
- onShowRefunded={() => setFilter({ refunded: "yes" })}
- onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })}
- onShowWired={() => setFilter({ wired: "yes" })}
- />
-
- {orderToBeRefunded && (
- <RefundModalForTable
- id={orderToBeRefunded.order_id}
- onCancel={() => setOrderToBeRefunded(undefined)}
- onConfirm={(value) =>
- refundOrder(orderToBeRefunded.order_id, value)
- .then(() =>
- setNotif({
- message: i18n.str`refund created successfully`,
- type: "SUCCESS",
- }),
- )
- .catch((error) =>
- setNotif({
- message: i18n.str`could not create the refund`,
- type: "ERROR",
- description: error.message,
- }),
- )
- .then(() => setOrderToBeRefunded(undefined))
- }
- onLoadError={(error) => {
- setNotif({
- message: i18n.str`could not create the refund`,
- type: "ERROR",
- description: error.message,
- });
- setOrderToBeRefunded(undefined);
- return <div />;
- }}
- onUnauthorized={onUnauthorized}
- onNotFound={() => {
- setNotif({
- message: i18n.str`could not get the order to refund`,
- type: "ERROR",
- // description: error.message
- });
- setOrderToBeRefunded(undefined);
- return <div />;
- }}
- />
- )}
- </section>
- );
-}
-
-interface RefundProps {
- id: string;
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- onNotFound: () => VNode;
- onCancel: () => void;
- onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void;
-}
-
-function RefundModalForTable({
- id,
- onUnauthorized,
- onLoadError,
- onNotFound,
- onConfirm,
- onCancel,
-}: RefundProps): VNode {
- const result = useOrderDetails(id);
-
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
- }
-
- return (
- <RefundModal
- order={result.data}
- onCancel={onCancel}
- onConfirm={onConfirm}
- />
- );
-}
-
-async function copyToClipboard(text: string): Promise<void> {
- return navigator.clipboard.writeText(text);
-}