diff options
author | Nic Eigel <nic@eigel.ch> | 2024-04-26 13:10:29 +0200 |
---|---|---|
committer | Nic Eigel <nic@eigel.ch> | 2024-04-26 13:10:29 +0200 |
commit | df64119729e9d55430c3b9133d660c431070f67b (patch) | |
tree | 4a6467a4920208e82892b3bb587a7c5664ebff76 /packages/auditor-backoffice-ui/src/paths | |
parent | 14090fa4f21b736ea7068f27d97a2c8acb57ee7e (diff) | |
download | wallet-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')
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); +} |