diff options
Diffstat (limited to 'packages/auditor-backoffice-ui/src/paths/instance/orders')
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); -} |