commit ed9cefef1457f5fb9f5646bdc014c4efc1f23514
parent fd1ad66496a34b158ddb648934e8d7e1d0ab16b7
Author: Sebastian <sebasjm@taler-systems.com>
Date: Tue, 9 Dec 2025 14:59:30 -0300
fix #10658
Diffstat:
5 files changed, 157 insertions(+), 80 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -53,7 +53,9 @@ import UpdateCategory from "./paths/instance/categories/update/index.js";
import ListKYCPage from "./paths/instance/kyc/list/index.js";
import OrderCreatePage from "./paths/instance/orders/create/index.js";
import OrderDetailsPage from "./paths/instance/orders/details/index.js";
-import OrderListPage from "./paths/instance/orders/list/index.js";
+import OrderListPage, {
+ OrderListSection,
+} from "./paths/instance/orders/list/index.js";
import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js";
import ValidatorListPage from "./paths/instance/otp_devices/list/index.js";
import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js";
@@ -110,6 +112,7 @@ export enum InstancePaths {
inventory_new = "/inventory/new",
order_list = "/orders",
+ order_list_section = "/orders/:section",
order_new = "/order/new",
order_details = "/order/:oid/details",
@@ -509,12 +512,40 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.order_list}
component={OrderListPage}
+ section={OrderListSection.NEW}
onCreate={() => {
route(InstancePaths.order_new);
}}
onSelect={(id: string) => {
route(InstancePaths.order_details.replace(":oid", id));
}}
+ onChangeSection={(s?: OrderListSection) => {
+ if (!s) {
+ route(InstancePaths.order_list);
+ } else {
+ route(InstancePaths.order_list_section.replace(":section", s));
+ }
+ }}
+ />
+ {/**
+ * Order pages
+ */}
+ <Route
+ path={InstancePaths.order_list_section}
+ component={OrderListPage}
+ onCreate={() => {
+ route(InstancePaths.order_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ onChangeSection={(s?: OrderListSection) => {
+ if (!s) {
+ route(InstancePaths.order_list);
+ } else {
+ route(InstancePaths.order_list_section.replace(":section", s));
+ }
+ }}
/>
<Route
path={InstancePaths.order_details}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -37,6 +37,8 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: Update bank Account`;
case InstancePaths.order_list:
return `${id}: Orders`;
+ case InstancePaths.order_list_section:
+ return `${id}: Orders`;
case InstancePaths.order_new:
return `${id}: New order`;
case InstancePaths.inventory_list:
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
@@ -25,25 +25,19 @@ import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { DatePicker } from "../../../../components/picker/DatePicker.js";
-import { dateFormatForSettings, usePreference } from "../../../../hooks/preference.js";
+import {
+ dateFormatForSettings,
+ usePreference,
+} from "../../../../hooks/preference.js";
import { CardTable } from "./Table.js";
import { WithId } from "../../../../declaration.js";
+import { OrderListSection } from "./index.js";
const TALER_SCREEN_ID = 47;
export interface ListPageProps {
- onShowAll: () => void;
- onShowNotPaid: () => void;
- onShowPaid: () => void;
- onShowRefunded: () => void;
- onShowNotWired: () => void;
- onShowWired: () => void;
- isAllActive: string;
- isPaidActive: string;
- isNotPaidActive: string;
- isRefundedActive: string;
- isNotWiredActive: string;
- isWiredActive: string;
+ onChangeSection: (s: OrderListSection) => void;
+ section: OrderListSection;
jumpToDate?: AbsoluteTime;
onSelectDate: (date?: AbsoluteTime) => void;
@@ -61,23 +55,13 @@ export function ListPage({
onLoadMoreAfter,
onLoadMoreBefore,
orders,
- isAllActive,
onSelectOrder,
onRefundOrder,
jumpToDate,
- onShowAll,
- onShowPaid,
- onShowNotPaid,
- onShowRefunded,
- onShowNotWired,
- onShowWired,
+ onChangeSection,
onSelectDate,
- isPaidActive,
- isRefundedActive,
- isNotWiredActive,
+ section,
onCreate,
- isNotPaidActive,
- isWiredActive,
}: ListPageProps): VNode {
const { i18n } = useTranslationContext();
const dateTooltip = i18n.str`Select date to show nearby orders`;
@@ -90,62 +74,86 @@ export function ListPage({
<div class="column is-two-thirds">
<div class="tabs" style={{ overflow: "inherit" }}>
<ul>
- <li class={isNotPaidActive}>
+ <li
+ class={
+ section === OrderListSection.NEW ? "is-active" : undefined
+ }
+ >
<div
class="has-tooltip-right"
data-tooltip={i18n.str`Only show paid orders`}
>
- <a onClick={onShowNotPaid}>
+ <a onClick={() => onChangeSection(OrderListSection.NEW)}>
<i18n.Translate>New</i18n.Translate>
</a>
</div>
</li>
- <li class={isPaidActive}>
+ <li
+ class={
+ section === OrderListSection.PAID ? "is-active" : undefined
+ }
+ >
<div
class="has-tooltip-right"
data-tooltip={i18n.str`Only show paid orders`}
>
- <a onClick={onShowPaid}>
+ <a onClick={() => onChangeSection(OrderListSection.PAID)}>
<i18n.Translate>Paid</i18n.Translate>
</a>
</div>
</li>
- <li class={isRefundedActive}>
+ <li
+ class={
+ section === OrderListSection.REFUNDED
+ ? "is-active"
+ : undefined
+ }
+ >
<div
class="has-tooltip-right"
data-tooltip={i18n.str`Only show orders with refunds`}
>
- <a onClick={onShowRefunded}>
+ <a onClick={() => onChangeSection(OrderListSection.REFUNDED)}>
<i18n.Translate>Refunded</i18n.Translate>
</a>
</div>
</li>
- <li class={isNotWiredActive}>
+ <li
+ class={
+ section === OrderListSection.PENDING ? "is-active" : undefined
+ }
+ >
<div
class="has-tooltip-left"
data-tooltip={i18n.str`Show only paid orders for which the wire transfer is still pending.`}
>
- <a onClick={onShowNotWired}>
+ <a onClick={() => onChangeSection(OrderListSection.PENDING)}>
<i18n.Translate>Not wired</i18n.Translate>
</a>
</div>
</li>
- <li class={isWiredActive}>
+ <li
+ class={
+ section === OrderListSection.INCOMING
+ ? "is-active"
+ : undefined
+ }
+ >
<div
class="has-tooltip-left"
data-tooltip={i18n.str`Only display orders that have already been transferred by the payment service provider`}
>
- <a onClick={onShowWired}>
+ <a onClick={() => onChangeSection(OrderListSection.INCOMING)}>
<i18n.Translate>Completed</i18n.Translate>
</a>
</div>
</li>
- <li class={isAllActive}>
+ <li class={section === undefined ? "is-active" : undefined}>
<div
class="has-tooltip-right"
data-tooltip={i18n.str`Remove all filters`}
>
- <a onClick={onShowAll}>
+ <a onClick={() => onChangeSection(OrderListSection.ALL)}>
<i18n.Translate>All</i18n.Translate>
</a>
</div>
@@ -158,7 +166,10 @@ export function ListPage({
<div class="field has-addons">
{jumpToDate && (
<div class="control">
- <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
+ <a
+ class="button is-fullwidth"
+ onClick={() => onSelectDate(undefined)}
+ >
<span
class="icon"
data-tooltip={i18n.str`Clear date filter`}
@@ -174,8 +185,17 @@ export function ListPage({
class="input"
type="text"
readonly
- value={!jumpToDate || jumpToDate.t_ms === "never" ? "" : format(jumpToDate.t_ms, dateFormatForSettings(settings))}
- placeholder={i18n.str`Jump to date (${dateFormatForSettings(settings)})`}
+ value={
+ !jumpToDate || jumpToDate.t_ms === "never"
+ ? ""
+ : format(
+ jumpToDate.t_ms,
+ dateFormatForSettings(settings),
+ )
+ }
+ placeholder={i18n.str`Jump to date (${dateFormatForSettings(
+ settings,
+ )})`}
onClick={() => {
setPickDate(true);
}}
@@ -205,14 +225,14 @@ export function ListPage({
opened={pickDate}
closeFunction={() => setPickDate(false)}
dateReceiver={(d) => {
- onSelectDate(AbsoluteTime.fromMilliseconds(d.getTime()))
+ onSelectDate(AbsoluteTime.fromMilliseconds(d.getTime()));
}}
/>
<CardTable
orders={orders}
onCreate={onCreate}
- showRefund={!!isRefundedActive}
+ section={section}
onSelect={onSelectOrder}
onRefund={onRefundOrder}
onLoadMoreAfter={onLoadMoreAfter}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
@@ -56,6 +56,7 @@ import {
} from "../../../../hooks/preference.js";
import { mergeRefunds } from "../../../../utils/amount.js";
import { getOrderAmountAndWirefee } from "../details/DetailPage.js";
+import { OrderListSection } from "./index.js";
const TALER_SCREEN_ID = 48;
@@ -64,15 +65,15 @@ interface Props {
orders: Entity[];
onRefund: (value: Entity) => void;
onCreate: () => void;
+ section?: OrderListSection;
onSelect: (order: Entity) => void;
onLoadMoreBefore?: () => void;
onLoadMoreAfter?: () => void;
- showRefund?: boolean;
}
export function CardTable({
orders,
- showRefund = false,
+ section,
onCreate,
onRefund,
onSelect,
@@ -123,7 +124,7 @@ export function CardTable({
instances={orders}
onSelect={onSelect}
onRefund={onRefund}
- showRefund={showRefund}
+ section={section}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
@@ -146,12 +147,12 @@ interface TableProps {
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
onLoadMoreAfter?: () => void;
- showRefund: boolean;
+ section?: OrderListSection;
}
function Table({
instances,
- showRefund,
+ section,
onSelect,
onRefund,
onLoadMoreAfter,
@@ -196,7 +197,7 @@ function Table({
<th style={{ minWidth: 100 }}>
<i18n.Translate>Amount</i18n.Translate>
</th>
- {showRefund ? (
+ {section === OrderListSection.REFUNDED ? (
<th style={{ minWidth: 100 }}>
<i18n.Translate>Refunded</i18n.Translate>
</th>
@@ -229,7 +230,7 @@ function Table({
>
{i.amount}
</td>
- {showRefund ? (
+ {section === OrderListSection.REFUNDED ? (
<td
onClick={(): void => onSelect(i)}
style={{ cursor: "pointer" }}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -41,7 +41,7 @@ import { useSessionContext } from "../../../../context/session.js";
import {
InstanceOrderFilter,
useInstanceOrders,
- useOrderDetails
+ useOrderDetails,
} from "../../../../hooks/order.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
@@ -52,11 +52,57 @@ const TALER_SCREEN_ID = 46;
interface Props {
onSelect: (id: string) => void;
+ onChangeSection: (s: OrderListSection) => void;
onCreate: () => void;
+ section: OrderListSection;
}
-export default function OrderList({ onCreate, onSelect }: Props): VNode {
- const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: false });
+export enum OrderListSection {
+ NEW = "new", // paid = false
+ PAID = "paid", // paid = true
+ REFUNDED = "refunded", // refunded = true
+ PENDING = "pending", // paid = true, wired = false
+ INCOMING = "incoming", // wired = true,
+ SETTLED = "settled", // wired = true,
+ ALL = "all", //
+}
+
+function sectionToFilter(s?: OrderListSection): {
+ paid?: boolean;
+ wired?: boolean;
+ refunded?: boolean;
+} {
+ switch (s) {
+ case OrderListSection.NEW:
+ return { paid: false };
+ case OrderListSection.PAID:
+ return { paid: true };
+ case OrderListSection.REFUNDED:
+ return { refunded: true };
+ case OrderListSection.PENDING:
+ return { paid: true, wired: false };
+ case OrderListSection.INCOMING:
+ return { wired: true };
+ case OrderListSection.SETTLED:
+ case OrderListSection.ALL:
+ case undefined:
+ return {};
+ default:
+ assertUnreachable(s);
+ }
+}
+
+export default function OrderList({
+ onCreate,
+ onSelect,
+ onChangeSection,
+ section,
+}: Props): VNode {
+ const [filter, setFilter] = useState<{
+ date?: AbsoluteTime;
+ position?: string;
+ }>({});
+
const [orderToBeRefunded, setOrderToBeRefunded] = useState<
TalerMerchantApi.OrderHistoryEntry | undefined
>(undefined);
@@ -64,7 +110,8 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode {
const setNewDate = (date?: AbsoluteTime): void =>
setFilter((prev) => ({ ...prev, date }));
- const result = useInstanceOrders(filter, (d) =>
+
+ const result = useInstanceOrders({ ...sectionToFilter(section), ...filter}, (d) =>
setFilter({ ...filter, position: d }),
);
const [notification, safeFunctionHandler] = useLocalNotificationBetter();
@@ -90,20 +137,6 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode {
}
}
- const isNotPaidActive = filter.paid === false ? "is-active" : "";
- const isPaidActive =
- filter.paid === true && filter.wired === undefined ? "is-active" : "";
- const isRefundedActive = filter.refunded === true ? "is-active" : "";
- const isNotWiredActive =
- filter.wired === false && filter.paid === true ? "is-active" : "";
- const isWiredActive = filter.wired === true ? "is-active" : "";
- const isAllActive =
- filter.paid === undefined &&
- filter.refunded === undefined &&
- filter.wired === undefined
- ? "is-active"
- : "";
-
const data = {} as TalerMerchantApi.RefundRequest;
const refund = safeFunctionHandler(
lib.instance.addRefund.bind(lib.instance),
@@ -146,21 +179,11 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode {
onLoadMoreAfter={result.loadNext}
onSelectOrder={(order) => onSelect(order.id)}
onRefundOrder={(value) => setOrderToBeRefunded(value)}
- isAllActive={isAllActive}
- isNotWiredActive={isNotWiredActive}
- isWiredActive={isWiredActive}
- isPaidActive={isPaidActive}
- isNotPaidActive={isNotPaidActive}
- isRefundedActive={isRefundedActive}
+ section={section}
jumpToDate={filter.date}
onSelectDate={setNewDate}
onCreate={onCreate}
- onShowAll={() => setFilter({})}
- onShowNotPaid={() => setFilter({ paid: false })}
- onShowPaid={() => setFilter({ paid: true })}
- onShowRefunded={() => setFilter({ refunded: true })}
- onShowNotWired={() => setFilter({ wired: false, paid: true })}
- onShowWired={() => setFilter({ wired: true })}
+ onChangeSection={onChangeSection}
/>
{orderToBeRefunded && (