diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx')
-rw-r--r-- | packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 719 |
1 files changed, 471 insertions, 248 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx index 56bb65b90..7be3d23f6 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021 Taler Systems S.A. + (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 @@ -19,69 +19,81 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { add, isAfter, isBefore, isFuture } from "date-fns"; -import { Amounts } from "@gnu-taler/taler-util"; -import { Fragment, h, VNode } from "preact"; +import { + AbsoluteTime, + AmountString, + Amounts, + Duration, + TalerMerchantApi, + TalerProtocolDuration, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { format, isFuture } from "date-fns"; +import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { - FormProvider, 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 { ProductList } from "../../../../components/product/ProductList.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { Duration, MerchantBackend, WithId } from "../../../../declaration.js"; -import { Translate, useTranslator } from "../../../../i18n/index.js"; -import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; -import { rate } from "../../../../utils/amount.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 { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputBoolean } from "../../../../components/form/InputBoolean.js"; +import { ProductList } from "../../../../components/product/ProductList.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { usePreference } from "../../../../hooks/preference.js"; +import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; interface Props { - onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; + onCreate: (d: TalerMerchantApi.PostOrderRequest) => void; onBack?: () => void; instanceConfig: InstanceConfig; - instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[]; + instanceInventory: (TalerMerchantApi.ProductDetail & WithId)[]; } interface InstanceConfig { - default_max_wire_fee: string; - default_max_deposit_fee: string; - default_wire_fee_amortization: number; - default_pay_delay: Duration; + use_stefan: boolean; + default_pay_delay: TalerProtocolDuration; + default_wire_transfer_delay: TalerProtocolDuration; } -function with_defaults(config: InstanceConfig): Partial<Entity> { - const defaultPayDeadline = - !config.default_pay_delay || config.default_pay_delay.d_us === "forever" - ? undefined - : add(new Date(), { seconds: config.default_pay_delay.d_us / 1000 }); +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_wire_fee: config.default_max_wire_fee, - max_fee: config.default_max_deposit_fee, - wire_fee_amortization: config.default_wire_fee_amortization, + max_fee: undefined, + createToken: true, pay_deadline: defaultPayDeadline, refund_deadline: defaultPayDeadline, - createToken: true, + wire_transfer_deadline: defaultWireDeadline, }, shipping: {}, - extra: "", + extra: {}, }; } interface ProductAndQuantity { - product: MerchantBackend.Products.ProductDetail & WithId; + product: TalerMerchantApi.ProductDetail & WithId; quantity: number; } export interface ProductMap { @@ -95,156 +107,144 @@ interface Pricing { } interface Shipping { delivery_date?: Date; - delivery_location?: MerchantBackend.Location; + delivery_location?: TalerMerchantApi.Location; fullfilment_url?: string; } interface Payments { - refund_deadline?: Date; - pay_deadline?: Date; - wire_transfer_deadline?: Date; - auto_refund_deadline?: Date; + refund_deadline: Duration; + pay_deadline: Duration; + wire_transfer_deadline: Duration; + auto_refund_deadline: Duration; max_fee?: string; - max_wire_fee?: string; - wire_fee_amortization?: number; createToken: boolean; minimum_age?: number; } interface Entity { inventoryProducts: ProductMap; - products: MerchantBackend.Product[]; + products: TalerMerchantApi.Product[]; pricing: Partial<Pricing>; payments: Partial<Payments>; shipping: Partial<Shipping>; - extra: string; + 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 [value, valueHandler] = useState(with_defaults(instanceConfig)); - const config = useConfigContext(); + const { config } = useSessionContext(); + const instance_default = with_defaults(instanceConfig, config.currency); + const [value, valueHandler] = useState(instance_default); const zero = Amounts.zeroOfCurrency(config.currency); - + const [settings, updateSettings] = usePreference(); const inventoryList = Object.values(value.inventoryProducts || {}); const productList = Object.values(value.products || {}); - const i18n = useTranslator(); + 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`required` : undefined, + summary: !value.pricing?.summary ? i18n.str`required` : undefined, order_price: !value.pricing?.order_price - ? i18n`required` - : Amounts.isZero(value.pricing.order_price) - ? i18n`must be greater than 0` - : undefined, + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, }), - extra: - value.extra && !stringIsValidJSON(value.extra) - ? i18n`not a valid json` - : undefined, payments: undefinedIfEmpty({ refund_deadline: !value.payments?.refund_deadline ? undefined - : !isFuture(value.payments.refund_deadline) - ? i18n`should be in the future` : value.payments.pay_deadline && - isBefore(value.payments.refund_deadline, value.payments.pay_deadline) - ? i18n`refund deadline cannot be before pay deadline` - : value.payments.wire_transfer_deadline && - isBefore( - value.payments.wire_transfer_deadline, - value.payments.refund_deadline - ) - ? i18n`wire transfer deadline cannot be before refund deadline` - : undefined, + 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 - ? undefined - : !isFuture(value.payments.pay_deadline) - ? i18n`should be in the future` + ? i18n.str`required` : value.payments.wire_transfer_deadline && - isBefore( - value.payments.wire_transfer_deadline, - value.payments.pay_deadline - ) - ? i18n`wire transfer deadline cannot be before pay 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 - : !isFuture(value.payments.auto_refund_deadline) - ? i18n`should be in the future` : !value.payments?.refund_deadline - ? i18n`should have a refund deadline` - : !isAfter( - value.payments.refund_deadline, - value.payments.auto_refund_deadline - ) - ? i18n`auto refund cannot be after refund deadline` - : undefined, + ? 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`should be in the future` - : undefined, + ? i18n.str`should be in the future` + : undefined, }), }; const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined + (k) => (errors as any)[k] !== undefined, ); const submit = (): void => { - const order = schema.cast(value); + const order = value as any; //schema.cast(value); if (!value.payments) return; if (!value.shipping) return; - const request: MerchantBackend.Orders.PostOrderRequest = { + const request: TalerMerchantApi.PostOrderRequest = { order: { amount: order.pricing.order_price, summary: order.pricing.summary, products: productList, - extra: value.extra, - pay_deadline: value.payments.pay_deadline - ? { - t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), - } - : undefined, - wire_transfer_deadline: value.payments.wire_transfer_deadline - ? { - t_s: Math.floor( - value.payments.wire_transfer_deadline.getTime() / 1000 - ), - } - : undefined, - refund_deadline: value.payments.refund_deadline - ? { - t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000), - } - : undefined, + extra: undefinedIfEmpty(value.extra), + pay_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.pay_deadline!, + ), + ), + wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.wire_transfer_deadline!, + ), + ), + refund_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.refund_deadline!, + ), + ), auto_refund: value.payments.auto_refund_deadline - ? { - d_us: Math.floor( - value.payments.auto_refund_deadline.getTime() * 1000 - ), - } + ? Duration.toTalerProtocolDuration( + value.payments.auto_refund_deadline, + ) : undefined, - wire_fee_amortization: value.payments.wire_fee_amortization as number, - max_fee: value.payments.max_fee as string, - max_wire_fee: value.payments.max_wire_fee as string, - + max_fee: value.payments.max_fee as AmountString, delivery_date: value.shipping.delivery_date ? { t_s: value.shipping.delivery_date.getTime() / 1000 } : undefined, @@ -263,8 +263,8 @@ export function CreatePage({ }; const addProductToTheInventoryList = ( - product: MerchantBackend.Products.ProductDetail & WithId, - quantity: number + product: TalerMerchantApi.ProductDetail & WithId, + quantity: number, ) => { valueHandler((v) => { const inventoryProducts = { ...v.inventoryProducts }; @@ -281,7 +281,7 @@ export function CreatePage({ }); }; - const addNewProduct = async (product: MerchantBackend.Product) => { + const addNewProduct = async (product: TalerMerchantApi.Product) => { return valueHandler((v) => { const products = v.products ? [...v.products, product] : []; return { ...v, products }; @@ -297,7 +297,7 @@ export function CreatePage({ }; const [editingProduct, setEditingProduct] = useState< - MerchantBackend.Product | undefined + TalerMerchantApi.Product | undefined >(undefined); const totalPriceInventory = inventoryList.reduce((prev, cur) => { @@ -308,7 +308,7 @@ export function CreatePage({ 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; + return Amounts.add(prev, Amounts.mult(p, cur.quantity ?? 0).amount).amount; }, zero); const hasProducts = inventoryList.length > 0 || productList.length > 0; @@ -317,6 +317,8 @@ export function CreatePage({ const totalAsString = Amounts.stringify(totalPrice.amount); const allProducts = productList.concat(inventoryList.map(asProduct)); + const [newField, setNewField] = useState(""); + useEffect(() => { valueHandler((v) => { return { @@ -331,25 +333,65 @@ export function CreatePage({ }, [hasProducts, totalAsString]); const discountOrRise = rate( - value.pricing?.order_price || `${config.currency}:0`, - totalAsString + parsedPrice ?? Amounts.zeroOfCurrency(config.currency), + totalPrice.amount, ); - const minAgeByProducts = allProducts.reduce( + const minAgeByProducts = inventoryList.reduce( (cur, prev) => - !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age, - 0 + !prev.product.minimum_age || cur > prev.product.minimum_age ? cur : prev.product.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("advanceOrderMode", false); + }} + > + <a> + <span> + <i18n.Translate>Simple</i18n.Translate> + </span> + </a> + </li> + <li + class={settings.advanceOrderMode ? "is-active" : ""} + onClick={() => { + updateSettings("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`Manage products in order`} + label={i18n.str`Manage products in order`} alternative={ allProducts.length > 0 && ( <p> @@ -358,7 +400,7 @@ export function CreatePage({ </p> ) } - tooltip={i18n`Manage list of products in the order.`} + tooltip={i18n.str`Manage list of products in the order.`} > <InventoryProductForm currentProducts={value.inventoryProducts || {}} @@ -366,21 +408,23 @@ export function CreatePage({ inventory={instanceInventory} /> - <NonInventoryProductFrom - productToEdit={editingProduct} - onAddProduct={(p) => { - setEditingProduct(undefined); - return addNewProduct(p); - }} - /> + {settings.advanceOrderMode && ( + <NonInventoryProductFrom + productToEdit={editingProduct} + onAddProduct={(p) => { + setEditingProduct(undefined); + return addNewProduct(p); + }} + /> + )} {allProducts.length > 0 && ( <ProductList list={allProducts} actions={[ { - name: i18n`Remove`, - tooltip: i18n`Remove this product from the order.`, + name: i18n.str`Remove`, + tooltip: i18n.str`Remove this product from the order.`, handler: (e, index) => { if (e.product_id) { removeProductFromTheInventoryList(e.product_id); @@ -404,141 +448,299 @@ export function CreatePage({ <Fragment> <InputCurrency name="pricing.products_price" - label={i18n`Total price`} + label={i18n.str`Total price`} readonly - tooltip={i18n`total product price added up`} + tooltip={i18n.str`total product price added up`} /> <InputCurrency name="pricing.order_price" - label={i18n`Total price`} + label={i18n.str`Total price`} addonAfter={ discountOrRise > 0 && (discountOrRise < 1 ? `discount of %${Math.round( - (1 - discountOrRise) * 100 + (1 - discountOrRise) * 100, )}` : `rise of %${Math.round((discountOrRise - 1) * 100)}`) } - tooltip={i18n`Amount to be paid by the customer`} + tooltip={i18n.str`Amount to be paid by the customer`} /> </Fragment> ) : ( <InputCurrency name="pricing.order_price" - label={i18n`Order price`} - tooltip={i18n`final order price`} + label={i18n.str`Order price`} + tooltip={i18n.str`final order price`} /> )} <Input name="pricing.summary" inputType="multiline" - label={i18n`Summary`} - tooltip={i18n`Title of the order to be shown to the customer`} + label={i18n.str`Summary`} + tooltip={i18n.str`Title of the order to be shown to the customer`} /> - <InputGroup - name="shipping" - label={i18n`Shipping and Fulfillment`} - initialActive - > - <InputDate - name="shipping.delivery_date" - label={i18n`Delivery date`} - tooltip={i18n`Deadline for physical delivery assured by the merchant.`} - /> - {value.shipping?.delivery_date && ( - <InputGroup - name="shipping.delivery_location" - label={i18n`Location`} - tooltip={i18n`address where the products will be delivered`} - > - <InputLocation name="shipping.delivery_location" /> - </InputGroup> - )} - <Input - name="shipping.fullfilment_url" - label={i18n`Fulfillment URL`} - tooltip={i18n`URL to which the user will be redirected after successful payment.`} - /> - </InputGroup> - - <InputGroup - name="payments" - label={i18n`Taler payment options`} - tooltip={i18n`Override default Taler payment settings for this order`} - > - <InputDate - name="payments.pay_deadline" - label={i18n`Payment deadline`} - tooltip={i18n`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`} - /> - <InputDate - name="payments.refund_deadline" - label={i18n`Refund deadline`} - tooltip={i18n`Time until which the order can be refunded by the merchant.`} - /> - <InputDate - name="payments.wire_transfer_deadline" - label={i18n`Wire transfer deadline`} - tooltip={i18n`Deadline for the exchange to make the wire transfer.`} - /> - <InputDate - name="payments.auto_refund_deadline" - label={i18n`Auto-refund deadline`} - tooltip={i18n`Time until which the wallet will automatically check for refunds without user interaction.`} - /> + {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> + )} - <InputCurrency - name="payments.max_fee" - label={i18n`Maximum deposit fee`} - tooltip={i18n`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} - /> - <InputCurrency - name="payments.max_wire_fee" - label={i18n`Maximum wire fee`} - tooltip={i18n`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`} - /> - <InputNumber - name="payments.wire_fee_amortization" - label={i18n`Wire fee amortization`} - tooltip={i18n`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`} - /> - <InputBoolean - name="payments.createToken" - label={i18n`Create token`} - tooltip={i18n`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`} - /> - <InputNumber - name="payments.minimum_age" - label={i18n`Minimum age required`} - tooltip={i18n`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`Min age defined by the producs is ${minAgeByProducts}` - : undefined - } - /> - </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> + )} - <InputGroup - name="extra" - label={i18n`Additional information`} - tooltip={i18n`Custom information to be included in the contract for this order.`} - > - <Input + {settings.advanceOrderMode && ( + <InputGroup name="extra" - inputType="multiline" - label={`Value`} - tooltip={i18n`You must enter a value in JavaScript Object Notation (JSON).`} - /> - </InputGroup> + 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, idx) => { + return ( + <Input + name={`extra.${key}`} + key={String(idx)} + 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 + ) { + delete value.extra[key]; + } + valueHandler({ + ...value, + }); + e.preventDefault(); + }} + > + 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]: "", + }, + }); + e.preventDefault(); + }} + > + add + </button> + </div> + </InputGroup> + )} </FormProvider> <div class="buttons is-right mt-5"> {onBack && ( <button class="button" onClick={onBack}> - <Translate>Cancel</Translate> + <i18n.Translate>Cancel</i18n.Translate> </button> )} <button @@ -546,7 +748,7 @@ export function CreatePage({ onClick={submit} disabled={hasErrors} > - <Translate>Confirm</Translate> + <i18n.Translate>Confirm</i18n.Translate> </button> </div> </div> @@ -557,7 +759,7 @@ export function CreatePage({ ); } -function asProduct(p: ProductAndQuantity): MerchantBackend.Product { +function asProduct(p: ProductAndQuantity): TalerMerchantApi.Product { return { product_id: p.product.id, image: p.product.image, @@ -566,6 +768,27 @@ function asProduct(p: ProductAndQuantity): MerchantBackend.Product { 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> + ); +} |