aboutsummaryrefslogtreecommitdiff
path: root/packages/auditor-backoffice-ui/src/paths
diff options
context:
space:
mode:
authorNic Eigel <nic@eigel.ch>2024-04-26 13:10:29 +0200
committerNic Eigel <nic@eigel.ch>2024-04-26 13:10:29 +0200
commitdf64119729e9d55430c3b9133d660c431070f67b (patch)
tree4a6467a4920208e82892b3bb587a7c5664ebff76 /packages/auditor-backoffice-ui/src/paths
parent14090fa4f21b736ea7068f27d97a2c8acb57ee7e (diff)
downloadwallet-core-df64119729e9d55430c3b9133d660c431070f67b.tar.gz
wallet-core-df64119729e9d55430c3b9133d660c431070f67b.tar.bz2
wallet-core-df64119729e9d55430c3b9133d660c431070f67b.zip
incomplete changes to be more like merchant backend
Diffstat (limited to 'packages/auditor-backoffice-ui/src/paths')
-rw-r--r--packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx226
-rw-r--r--packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx417
-rw-r--r--packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx224
4 files changed, 974 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx
new file mode 100644
index 000000000..156c577f4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/List.stories.tsx
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Order/List",
+ component: TestedComponent,
+ argTypes: {
+ onShowAll: { action: "onShowAll" },
+ onShowPaid: { action: "onShowPaid" },
+ onShowRefunded: { action: "onShowRefunded" },
+ onShowNotWired: { action: "onShowNotWired" },
+ onCopyURL: { action: "onCopyURL" },
+ onSelectDate: { action: "onSelectDate" },
+ onLoadMoreBefore: { action: "onLoadMoreBefore" },
+ onLoadMoreAfter: { action: "onLoadMoreAfter" },
+ onSelectOrder: { action: "onSelectOrder" },
+ onRefundOrder: { action: "onRefundOrder" },
+ onSearchOrderById: { action: "onSearchOrderById" },
+ onCreate: { action: "onCreate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ orders: [
+ {
+ id: "123",
+ amount: "TESTKUDOS:10",
+ paid: false,
+ refundable: true,
+ row_id: 1,
+ summary: "summary",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "123",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: true,
+ refundable: true,
+ row_id: 2,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ {
+ id: "456",
+ amount: "TESTKUDOS:1",
+ paid: false,
+ refundable: false,
+ row_id: 3,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "456",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: false,
+ refundable: false,
+ row_id: 4,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx
new file mode 100644
index 000000000..9f80719a1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/ListPage.tsx
@@ -0,0 +1,226 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode, Fragment } from "preact";
+import { useState } from "preact/hooks";
+import { DatePicker } from "../../../../components/picker/DatePicker.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { CardTable } from "./Table.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+export interface ListPageProps {
+ onShowAll: () => void;
+ onShowNotPaid: () => void;
+ onShowPaid: () => void;
+ onShowRefunded: () => void;
+ onShowNotWired: () => void;
+ onShowWired: () => void;
+ onCopyURL: (id: string) => void;
+ isAllActive: string;
+ isPaidActive: string;
+ isNotPaidActive: string;
+ isRefundedActive: string;
+ isNotWiredActive: string;
+ isWiredActive: string;
+
+ jumpToDate?: Date;
+ onSelectDate: (date?: Date) => void;
+
+ orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[];
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+
+ onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onCreate: () => void;
+}
+
+export function ListPage({
+ hasMoreAfter,
+ hasMoreBefore,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ orders,
+ isAllActive,
+ onSelectOrder,
+ onRefundOrder,
+ jumpToDate,
+ onCopyURL,
+ onShowAll,
+ onShowPaid,
+ onShowNotPaid,
+ onShowRefunded,
+ onShowNotWired,
+ onShowWired,
+ onSelectDate,
+ isPaidActive,
+ isRefundedActive,
+ isNotWiredActive,
+ onCreate,
+ isNotPaidActive,
+ isWiredActive,
+}: ListPageProps): VNode {
+ const { i18n } = useTranslationContext();
+ const dateTooltip = i18n.str`select date to show nearby orders`;
+ const [pickDate, setPickDate] = useState(false);
+ const [settings] = useSettings();
+
+ return (
+ <Fragment>
+ <div class="columns">
+ <div class="column is-two-thirds">
+ <div class="tabs" style={{ overflow: "inherit" }}>
+ <ul>
+ <li class={isNotPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowNotPaid}>
+ <i18n.Translate>New</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowPaid}>
+ <i18n.Translate>Paid</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isRefundedActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show orders with refunds`}
+ >
+ <a onClick={onShowRefunded}>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isNotWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowNotWired}>
+ <i18n.Translate>Not wired</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowWired}>
+ <i18n.Translate>Completed</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isAllActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="column ">
+ <div class="buttons is-right">
+ <div class="field has-addons">
+ {jumpToDate && (
+ <div class="control">
+ <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
+ <span
+ class="icon"
+ data-tooltip={i18n.str`clear date filter`}
+ >
+ <i class="mdi mdi-close" />
+ </span>
+ </a>
+ </div>
+ )}
+ <div class="control">
+ <span class="has-tooltip-top" data-tooltip={dateTooltip}>
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
+ placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
+ onClick={() => {
+ setPickDate(true);
+ }}
+ />
+ </span>
+ </div>
+ <div class="control">
+ <span class="has-tooltip-left" data-tooltip={dateTooltip}>
+ <a
+ class="button is-fullwidth"
+ onClick={() => {
+ setPickDate(true);
+ }}
+ >
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <DatePicker
+ opened={pickDate}
+ closeFunction={() => setPickDate(false)}
+ dateReceiver={onSelectDate}
+ />
+
+ <CardTable
+ orders={orders}
+ onCreate={onCreate}
+ onCopyURL={onCopyURL}
+ onSelect={onSelectOrder}
+ onRefund={onRefundOrder}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx
new file mode 100644
index 000000000..b2806bb79
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/Table.tsx
@@ -0,0 +1,417 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { mergeRefunds } from "../../../../utils/amount.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
+interface Props {
+ orders: Entity[];
+ onRefund: (value: Entity) => void;
+ onCopyURL: (id: string) => void;
+ onCreate: () => void;
+ onSelect: (order: Entity) => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ orders,
+ onCreate,
+ onRefund,
+ onCopyURL,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <i18n.Translate>Orders</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+
+ <div class="card-header-icon" aria-label="more options">
+ <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}>
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {orders.length > 0 ? (
+ <Table
+ instances={orders}
+ onSelect={onSelect}
+ onRefund={onRefund}
+ onCopyURL={(o) => onCopyURL(o.id)}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onRefund: (id: Entity) => void;
+ onCopyURL: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function Table({
+ instances,
+ onSelect,
+ onRefund,
+ onCopyURL,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer orders</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Date</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Amount</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 400 }}>
+ <i18n.Translate>Summary</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 50 }} />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(i.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.summary}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ {i.refundable && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onRefund(i)}
+ >
+ <i18n.Translate>Refund</i18n.Translate>
+ </button>
+ )}
+ {!i.paid && (
+ <button
+ class="button is-small is-info jb-modal"
+ type="button"
+ onClick={(): void => onCopyURL(i)}
+ >
+ <i18n.Translate>copy url</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older orders</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ No orders have been found matching your query!
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface RefundModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
+ order: MerchantBackend.Orders.MerchantOrderStatusResponse;
+}
+
+export function RefundModal({
+ order,
+ onCancel,
+ onConfirm,
+}: RefundModalProps): VNode {
+ type State = { mainReason?: string; description?: string; refund?: string };
+ const [form, setValue] = useState<State>({});
+ const [settings] = useSettings();
+ const { i18n } = useTranslationContext();
+ // const [errors, setErrors] = useState<FormErrors<State>>({});
+
+ const refunds = (
+ order.order_status === "paid" ? order.refund_details : []
+ ).reduce(mergeRefunds, []);
+
+ const config = useConfigContext();
+ const totalRefunded = refunds
+ .map((r) => r.amount)
+ .reduce(
+ (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount,
+ Amounts.zeroOfCurrency(config.currency),
+ );
+ const orderPrice =
+ order.order_status === "paid"
+ ? Amounts.parseOrThrow(order.contract_terms.amount)
+ : undefined;
+ const totalRefundable = !orderPrice
+ ? Amounts.zeroOfCurrency(totalRefunded.currency)
+ : refunds.length
+ ? Amounts.sub(orderPrice, totalRefunded).amount
+ : orderPrice;
+
+ const isRefundable = Amounts.isNonZero(totalRefundable);
+ const duplicatedText = i18n.str`duplicated`;
+
+ const errors: FormErrors<State> = {
+ mainReason: !form.mainReason ? i18n.str`required` : undefined,
+ description:
+ !form.description && form.mainReason !== duplicatedText
+ ? i18n.str`required`
+ : undefined,
+ refund: !form.refund
+ ? i18n.str`required`
+ : !Amounts.parse(form.refund)
+ ? i18n.str`invalid format`
+ : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
+ ? i18n.str`this value exceed the refundable amount`
+ : undefined,
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const validateAndConfirm = () => {
+ try {
+ if (!form.refund) return;
+ onConfirm({
+ refund: Amounts.stringify(
+ Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount,
+ ),
+ reason:
+ form.description === undefined
+ ? form.mainReason || ""
+ : `${form.mainReason}: ${form.description}`,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ //FIXME: parameters in the translation
+ return (
+ <ConfirmModal
+ description="refund"
+ danger
+ active
+ disabled={!isRefundable || hasErrors}
+ onCancel={onCancel}
+ onConfirm={validateAndConfirm}
+ >
+ {refunds.length > 0 && (
+ <div class="columns">
+ <div class="column is-12">
+ <InputGroup
+ name="asd"
+ label={`${Amounts.stringify(totalRefunded)} was already refunded`}
+ >
+ <table class="table is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>date</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>amount</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>reason</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {refunds.map((r) => {
+ return (
+ <tr key={r.timestamp.t_s}>
+ <td>
+ {r.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(r.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td>{r.amount}</td>
+ <td>{r.reason}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </InputGroup>
+ </div>
+ </div>
+ )}
+
+ {isRefundable && (
+ <FormProvider<State>
+ errors={errors}
+ object={form}
+ valueHandler={(d) => setValue(d as any)}
+ >
+ <InputCurrency<State>
+ name="refund"
+ label={i18n.str`Refund`}
+ tooltip={i18n.str`amount to be refunded`}
+ >
+ <i18n.Translate>Max refundable:</i18n.Translate>{" "}
+ {Amounts.stringify(totalRefundable)}
+ </InputCurrency>
+ <InputSelector
+ name="mainReason"
+ label={i18n.str`Reason`}
+ values={[
+ i18n.str`Choose one...`,
+ duplicatedText,
+ i18n.str`requested by the customer`,
+ i18n.str`other`,
+ ]}
+ tooltip={i18n.str`why this order is being refunded`}
+ />
+ {form.mainReason && form.mainReason !== duplicatedText ? (
+ <Input<State>
+ label={i18n.str`Description`}
+ name="description"
+ tooltip={i18n.str`more information to give context`}
+ />
+ ) : undefined}
+ </FormProvider>
+ )}
+ </ConfirmModal>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx
new file mode 100644
index 000000000..1bec26e28
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/deposit_confirmations/list/index.tsx
@@ -0,0 +1,224 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { AuditorBackend } from "../../../declaration.js";
+import { Notification } from "../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { RefundModal } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<AuditorBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+}
+
+export default function DepositConfirmationList({
+ 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);
+}