commit cf65a507088b90577a32bf0a957becc6096caff4 parent 6aa49378d152c4b099263a272bd574b85508ce5d Author: Sebastian <sebasjm@gmail.com> Date: Wed, 30 Mar 2022 17:08:49 -0300 upgrade timestamp and duration Diffstat:
35 files changed, 3068 insertions(+), 1839 deletions(-)
diff --git a/packages/merchant-backend/render-examples.ts b/packages/merchant-backend/render-examples.ts @@ -58,23 +58,23 @@ files.forEach(file => { //enhance the example with more information example.contract_terms_json = () => JSON.stringify(example.contract_terms); - example.contract_terms.timestamp_str = () => example.contract_terms.timestamp && format(example.contract_terms.timestamp.t_ms, 'dd MMM yyyy HH:mm:ss'); + example.contract_terms.timestamp_str = () => example.contract_terms.timestamp && format(example.contract_terms.timestamp.t_s, 'dd MMM yyyy HH:mm:ss'); example.contract_terms.hasProducts = () => example.contract_terms.products?.length > 0; example.contract_terms.hasAuditors = () => example.contract_terms.auditors?.length > 0; example.contract_terms.hasExchanges = () => example.contract_terms.exchanges?.length > 0; example.contract_terms.products.forEach(p => { - p.delivery_date_str = () => p.delivery_date && format(p.delivery_date.t_ms, 'dd MM yyyy HH:mm:ss') + p.delivery_date_str = () => p.delivery_date && format(p.delivery_date.t_s, 'dd MM yyyy HH:mm:ss') p.hasTaxes = () => p.taxes?.length > 0 }) example.contract_terms.has_delivery_info = () => example.contract_terms.delivery_date || example.contract_terms.delivery_location - example.contract_terms.delivery_date_str = () => example.contract_terms.delivery_date && format(example.contract_terms.delivery_date.t_ms, 'dd MM yyyy HH:mm:ss') - example.contract_terms.pay_deadline_str = () => example.contract_terms.pay_deadline && format(example.contract_terms.pay_deadline.t_ms, 'dd MM yyyy HH:mm:ss') - example.contract_terms.wire_transfer_deadline_str = () => example.contract_terms.wire_transfer_deadline && format(example.contract_terms.wire_transfer_deadline.t_ms, 'dd MM yyyy HH:mm:ss') - example.contract_terms.refund_deadline_str = () => example.contract_terms.refund_deadline && format(example.contract_terms.refund_deadline.t_ms, 'dd MM yyyy HH:mm:ss') - example.contract_terms.auto_refund_str = () => example.contract_terms.auto_refund && formatDuration(intervalToDuration({ start: 0, end: example.contract_terms.auto_refund.d_ms })) + example.contract_terms.delivery_date_str = () => example.contract_terms.delivery_date && format(example.contract_terms.delivery_date.t_s, 'dd MM yyyy HH:mm:ss') + example.contract_terms.pay_deadline_str = () => example.contract_terms.pay_deadline && format(example.contract_terms.pay_deadline.t_s, 'dd MM yyyy HH:mm:ss') + example.contract_terms.wire_transfer_deadline_str = () => example.contract_terms.wire_transfer_deadline && format(example.contract_terms.wire_transfer_deadline.t_s, 'dd MM yyyy HH:mm:ss') + example.contract_terms.refund_deadline_str = () => example.contract_terms.refund_deadline && format(example.contract_terms.refund_deadline.t_s, 'dd MM yyyy HH:mm:ss') + example.contract_terms.auto_refund_str = () => example.contract_terms.auto_refund && formatDuration(intervalToDuration({ start: 0, end: example.contract_terms.auto_refund.d_us })) const output = mustache.render(html, example); diff --git a/packages/merchant-backend/src/declaration.d.ts b/packages/merchant-backend/src/declaration.d.ts @@ -35,12 +35,12 @@ interface Timestamp { // Milliseconds since epoch, or the special // value "forever" to represent an event that will // never happen. - t_ms: number | "never"; + t_s: number | "never"; } interface Duration { // Duration in milliseconds or "forever" // to represent an infinite duration. - d_ms: number | "forever"; + d_us: number | "forever"; } interface WithId { @@ -83,12 +83,12 @@ export namespace ExchangeBackend { // What date (inclusive) does this fee go into effect? // The different fees must cover the full time period in which // any of the denomination keys are valid without overlap. - start_date: Timestamp; + start_date: TalerProtocolTimestamp; // What date (exclusive) does this fee stop going into effect? // The different fees must cover the full time period in which // any of the denomination keys are valid without overlap. - end_date: Timestamp; + end_date: TalerProtocolTimestamp; // Signature of TALER_MasterWireFeePS with // purpose TALER_SIGNATURE_MASTER_WIRE_FEES. @@ -1040,7 +1040,7 @@ export namespace MerchantBackend { reason: string; // Timestamp indicating when the tip is set to expire (may be in the past). - expiration: Timestamp; + expiration: TalerProtocolTimestamp; // Reserve public key from which the tip is funded. reserve_pub: EddsaPublicKey; @@ -1307,17 +1307,17 @@ export namespace MerchantBackend { products: Product[]; // Time when this contract was generated - timestamp: Timestamp; + timestamp: TalerProtocolTimestamp; // After this deadline has passed, no refunds will be accepted. - refund_deadline: Timestamp; + refund_deadline: TalerProtocolTimestamp; // After this deadline, the merchant won't accept payments for the contact - pay_deadline: Timestamp; + pay_deadline: TalerProtocolTimestamp; // Transfer deadline for the exchange. Must be in the // deposit permissions of coins used to pay for this order. - wire_transfer_deadline: Timestamp; + wire_transfer_deadline: TalerProtocolTimestamp; // Merchant's public key used to sign this proposal; this information // is typically added by the backend Note that this can be an ephemeral key. @@ -1350,7 +1350,7 @@ export namespace MerchantBackend { // Time indicating when the order should be delivered. // May be overwritten by individual products. - delivery_date?: Timestamp; + delivery_date?: TalerProtocolTimestamp; // Nonce generated by the wallet and echoed by the merchant // in this field when the proposal is generated. diff --git a/packages/merchant-backend/src/hooks/order.ts b/packages/merchant-backend/src/hooks/order.ts @@ -118,7 +118,7 @@ export function useOrderDetails(oderId: string): HttpResponse<MerchantBackend.Or }; const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, HttpError>([`/private/orders/${oderId}`, token, url], fetcher, { - refreshInterval:0, + refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, revalidateOnReconnect: false, @@ -181,8 +181,8 @@ export function useInstanceOrders(args?: InstanceOrderFilter, updateFilter?: (d: if (beforeError) return beforeError if (afterError) return afterError - - + + const pagination = { isReachingEnd: afterData && afterData.data.orders.length < totalAfter, isReachingStart: (!args?.date) || (beforeData && beforeData.data.orders.length < totalBefore), @@ -191,7 +191,7 @@ export function useInstanceOrders(args?: InstanceOrderFilter, updateFilter?: (d: if (afterData.data.orders.length < MAX_RESULT_SIZE) { setPageAfter(pageAfter + 1) } else { - const from = afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_ms + const from = afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s if (from && updateFilter) updateFilter(new Date(from)) } }, @@ -200,12 +200,12 @@ export function useInstanceOrders(args?: InstanceOrderFilter, updateFilter?: (d: if (beforeData.data.orders.length < MAX_RESULT_SIZE) { setPageBefore(pageBefore + 1) } else if (beforeData) { - const from = beforeData.data.orders[beforeData.data.orders.length - 1].timestamp.t_ms + const from = beforeData.data.orders[beforeData.data.orders.length - 1].timestamp.t_s if (from && updateFilter) updateFilter(new Date(from)) } }, } - + const orders = !beforeData || !afterData ? [] : (beforeData || lastBefore).data.orders.slice().reverse().concat((afterData || lastAfter).data.orders) if (loadingAfter || loadingBefore) return { loading: true, data: { orders } } if (beforeData && afterData) { diff --git a/packages/merchant-backend/src/pages/RequestPayment.tsx b/packages/merchant-backend/src/pages/RequestPayment.tsx @@ -15,64 +15,73 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ -import { Fragment, h, render, VNode } from 'preact'; -import { render as renderToString } from 'preact-render-to-string'; -import { useEffect } from 'preact/hooks'; -import { Footer } from '../components/Footer'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { Fragment, h, render, VNode } from "preact"; +import { render as renderToString } from "preact-render-to-string"; +import { useEffect } from "preact/hooks"; +import { Footer } from "../components/Footer"; import "../css/pure-min.css"; import "../css/style.css"; -import { QR } from '../components/QR'; -import { Page, QRPlaceholder, WalletLink } from '../styled'; - +import { QR } from "../components/QR"; +import { Page, QRPlaceholder, WalletLink } from "../styled"; /** * This page creates a payment request QR code - * + * * It will build into a mustache html template for server side rendering - * + * * server side rendering params: * - order_status_url * - taler_pay_qrcode_svg * - taler_pay_uri * - order_summary - * + * * request params: * - pay_uri * - order_summary * - order_status_url */ - interface Props { - payURI?: string, - order_status_url?: string, - qr_code?: string, + payURI?: string; + order_status_url?: string; + qr_code?: string; } function Head({ order_summary }: { order_summary?: string }): VNode { - return <Fragment> - <meta charSet="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <noscript> - <meta http-equiv="refresh" content="1" /> - </noscript> - <title>Payment requested for {order_summary ? order_summary : `{{ order_summary }}`}</title> - </Fragment> + return ( + <Fragment> + <meta charSet="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <noscript> + <meta http-equiv="refresh" content="1" /> + </noscript> + <title> + Payment requested for{" "} + {order_summary ? order_summary : `{{ order_summary }}`} + </title> + </Fragment> + ); } -export function RequestPayment({ payURI, qr_code, order_status_url }: Props): VNode { +export function RequestPayment({ + payURI, + qr_code, + order_status_url, +}: Props): VNode { useEffect(() => { const longpollDelayMs = 60 * 1000; let checkUrl: URL; try { - checkUrl = new URL(order_status_url ? order_status_url : "{{& order_status_url }}"); + checkUrl = new URL( + order_status_url ? order_status_url : "{{& order_status_url }}" + ); } catch (e) { return; } - checkUrl.searchParams.set("timeout_ms", longpollDelayMs.toString()); + checkUrl.searchParams.set("timeout_s", longpollDelayMs.toString()); function check() { let retried = false; function retryOnce() { @@ -119,53 +128,58 @@ export function RequestPayment({ payURI, qr_code, order_status_url }: Props): VN }; req.onerror = function () { setTimeout(retryOnce, 500); - } + }; req.ontimeout = function () { setTimeout(retryOnce, 500); - } + }; req.timeout = longpollDelayMs; req.open("GET", checkUrl.href); req.send(); } setTimeout(check, 500); - }) - return <Page> - <section> - <h1 >Pay with Taler</h1> - <p> - Scan this QR code with your mobile wallet: - </p> - <QRPlaceholder dangerouslySetInnerHTML={{ __html: qr_code ? qr_code : `{{{ taler_pay_qrcode_svg }}}` }} /> - <p> - <WalletLink href={payURI ? payURI : `{{ taler_pay_uri }}`}> - Or open your Taller wallet - </WalletLink> - </p> - <p> - <a href="https://wallet.taler.net/">Don't have a Taler wallet yet? Install it!</a> - </p> - </section> - <Footer /> - </Page> - + }); + return ( + <Page> + <section> + <h1>Pay with Taler</h1> + <p>Scan this QR code with your mobile wallet:</p> + <QRPlaceholder + dangerouslySetInnerHTML={{ + __html: qr_code ? qr_code : `{{{ taler_pay_qrcode_svg }}}`, + }} + /> + <p> + <WalletLink href={payURI ? payURI : `{{ taler_pay_uri }}`}> + Or open your Taller wallet + </WalletLink> + </p> + <p> + <a href="https://wallet.taler.net/"> + Don't have a Taler wallet yet? Install it! + </a> + </p> + </section> + <Footer /> + </Page> + ); } export function mount(): void { try { - const fromLocation = new URL(window.location.href).searchParams - const os = fromLocation.get('order_summary') || undefined; + const fromLocation = new URL(window.location.href).searchParams; + const os = fromLocation.get("order_summary") || undefined; if (os) { render(<Head order_summary={os} />, document.head); } - const uri = fromLocation.get('pay_uri') || undefined; - const osu = fromLocation.get('order_status_url') || undefined; + const uri = fromLocation.get("pay_uri") || undefined; + const osu = fromLocation.get("order_status_url") || undefined; const qr_code = uri ? renderToString(<QR text={uri} />) : undefined; - render(<RequestPayment - payURI={uri} order_status_url={osu} - qr_code={qr_code} - />, document.body); + render( + <RequestPayment payURI={uri} order_status_url={osu} qr_code={qr_code} />, + document.body + ); } catch (e) { console.error("got error", e); if (e instanceof Error) { @@ -174,10 +188,9 @@ export function mount(): void { } } -export function buildTimeRendering(): { head: string, body: string } { +export function buildTimeRendering(): { head: string; body: string } { return { head: renderToString(<Head />), - body: renderToString(<RequestPayment />) - } + body: renderToString(<RequestPayment />), + }; } - diff --git a/packages/merchant-backend/src/pages/ShowOrderDetails.examples.ts b/packages/merchant-backend/src/pages/ShowOrderDetails.examples.ts @@ -28,7 +28,7 @@ const defaultContractTerms: MerchantBackend.ContractTerms = { amount: 'USD:10', summary: 'this is a short summary', pay_deadline: { - t_ms: new Date().getTime() + 6 * 24 * 60 * 60 * 1000 + t_s: new Date().getTime() + 6 * 24 * 60 * 60 * 1000 }, merchant: { name: 'the merchant (inc)', @@ -48,7 +48,7 @@ const defaultContractTerms: MerchantBackend.ContractTerms = { wire_fee_amortization: 1, products: [], timestamp: { - t_ms: new Date().getTime() + t_s: new Date().getTime() }, auditors: [], exchanges: [], @@ -57,11 +57,11 @@ const defaultContractTerms: MerchantBackend.ContractTerms = { merchant_pub: 'QWEASDQWEASD', nonce: 'NONCE', refund_deadline: { - t_ms: new Date().getTime() + 6 * 24 * 60 * 60 * 1000 + t_s: new Date().getTime() + 6 * 24 * 60 * 60 * 1000 }, wire_method: 'x-taler-bank', wire_transfer_deadline: { - t_ms: new Date().getTime() + 3 * 24 * 60 * 60 * 1000 + t_s: new Date().getTime() + 3 * 24 * 60 * 60 * 1000 }, }; @@ -85,7 +85,7 @@ export const exampleData: { [name: string]: Props } = { contract_terms: { ...defaultContractTerms, delivery_date: { - t_ms: inSixDays + t_s: inSixDays }, }, }, @@ -124,7 +124,7 @@ export const exampleData: { [name: string]: Props } = { town_location: 'town loc', }, delivery_date: { - t_ms: inSixDays + t_s: inSixDays }, }, }, @@ -136,7 +136,7 @@ export const exampleData: { [name: string]: Props } = { description: 'description of the first product', price: '5:USD', quantity: 1, - delivery_date: { t_ms: in10Minutes }, + delivery_date: { t_s: in10Minutes }, product_id: '12333', }, { description: 'another description', @@ -159,7 +159,7 @@ export const exampleData: { [name: string]: Props } = { price: '5:USD', quantity: 1, unit: 'beer', - delivery_date: { t_ms: in10Minutes }, + delivery_date: { t_s: in10Minutes }, product_id: '456', taxes: [{ name: 'VAT', tax: 'USD:1' @@ -212,7 +212,7 @@ export const exampleData: { [name: string]: Props } = { contract_terms: { ...defaultContractTerms, auto_refund: { - d_ms: 1000 * 60 * 60 * 26 + 1000 * 60 * 30 + d_us: 1000 * 60 * 60 * 26 + 1000 * 60 * 30 } }, }, diff --git a/packages/merchant-backend/src/pages/ShowOrderDetails.tsx b/packages/merchant-backend/src/pages/ShowOrderDetails.tsx @@ -15,32 +15,32 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ -import { format, formatDuration } from 'date-fns'; -import { intervalToDuration } from 'date-fns/esm'; -import { Fragment, h, render, VNode } from 'preact'; -import { render as renderToString } from 'preact-render-to-string'; -import { Footer } from '../components/Footer'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { format, formatDuration } from "date-fns"; +import { intervalToDuration } from "date-fns/esm"; +import { Fragment, h, render, VNode } from "preact"; +import { render as renderToString } from "preact-render-to-string"; +import { Footer } from "../components/Footer"; import "../css/pure-min.css"; import "../css/style.css"; -import { MerchantBackend } from '../declaration'; -import { Page, InfoBox, TableExpanded, TableSimple } from '../styled'; +import { MerchantBackend } from "../declaration"; +import { Page, InfoBox, TableExpanded, TableSimple } from "../styled"; /** * This page creates a payment request QR code - * + * * It will build into a mustache html template for server side rendering - * + * * server side rendering params: * - order_summary * - contract_terms * - refund_amount - * + * * request params: * - refund_amount - * - contract_terms + * - contract_terms * - order_summary */ @@ -52,332 +52,489 @@ export interface Props { } function Head({ order_summary }: { order_summary?: string }): VNode { - return <Fragment> - <meta charSet="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <noscript> - <meta http-equiv="refresh" content="1" /> - </noscript> - <title>Status of your order for {order_summary ? order_summary : `{{ order_summary }}`}</title> - <script>{` + return ( + <Fragment> + <meta charSet="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <noscript> + <meta http-equiv="refresh" content="1" /> + </noscript> + <title> + Status of your order for{" "} + {order_summary ? order_summary : `{{ order_summary }}`} + </title> + <script>{` var contractTermsStr = '{{{contract_terms_json}}}'; `}</script> - </Fragment> + </Fragment> + ); } -function Location({ templateName, location, btr }: { templateName: string, location: MerchantBackend.Location | undefined, btr?: boolean }) { - //FIXME: mustache strings show be constructed in a way that ends in the final output of the html but is not present in the +function Location({ + templateName, + location, + btr, +}: { + templateName: string; + location: MerchantBackend.Location | undefined; + btr?: boolean; +}) { + //FIXME: mustache strings show be constructed in a way that ends in the final output of the html but is not present in the // javascript code, otherwise when mustache render engine run over the html it will also replace string in the javascript code // that is made to run when the browser has javascript enable leading into undefined behavior. // that's why in the next fields we are using concatenations to build the mustache placeholder. - return <Fragment> - {btr && `{{`+`#${templateName}.building_name}}`} - <dd>{location?.building_name || (btr && `{{ ${templateName}.building_name }}`)} {location?.building_number || (btr && `{{ ${templateName}.building_number }}`)}</dd> - {btr && `{{`+`/${templateName}.building_name}}`} - - {btr && `{{`+`#${templateName}.country}}`} - <dd>{location?.country || (btr && `{{ ${templateName}.country }}`)} {location?.country_subdivision || (btr && `{{ ${templateName}.country_subdivision }}`)}</dd> - {btr && `{{`+`/${templateName}.country}}`} - - {btr && `{{`+`#${templateName}.district}}`} - <dd>{location?.district || (btr && `{{ ${templateName}.district }}`)}</dd> - {btr && `{{`+`/${templateName}.district}}`} - - {btr && `{{`+`#${templateName}.post_code}}`} - <dd>{location?.post_code || (btr && `{{ ${templateName}.post_code }}`)}</dd> - {btr && `{{`+`/${templateName}.post_code}}`} - - {btr && `{{`+`#${templateName}.street}}`} - <dd>{location?.street || (btr && `{{ ${templateName}.street }}`)}</dd> - {btr && `{{`+`/${templateName}.street}}`} - - {btr && `{{`+`#${templateName}.town}}`} - <dd>{location?.town || (btr && `{{ ${templateName}.town }}`)}</dd> - {btr && `{{`+`/${templateName}.town}}`} - - {btr && `{{`+`#${templateName}.town_location}}`} - <dd>{location?.town_location || (btr && `{{ ${templateName}.town_location }}`)}</dd> - {btr && `{{`+`/${templateName}.town_location}}`} - </Fragment> + return ( + <Fragment> + {btr && `{{` + `#${templateName}.building_name}}`} + <dd> + {location?.building_name || + (btr && `{{ ${templateName}.building_name }}`)}{" "} + {location?.building_number || + (btr && `{{ ${templateName}.building_number }}`)} + </dd> + {btr && `{{` + `/${templateName}.building_name}}`} + + {btr && `{{` + `#${templateName}.country}}`} + <dd> + {location?.country || (btr && `{{ ${templateName}.country }}`)}{" "} + {location?.country_subdivision || + (btr && `{{ ${templateName}.country_subdivision }}`)} + </dd> + {btr && `{{` + `/${templateName}.country}}`} + + {btr && `{{` + `#${templateName}.district}}`} + <dd>{location?.district || (btr && `{{ ${templateName}.district }}`)}</dd> + {btr && `{{` + `/${templateName}.district}}`} + + {btr && `{{` + `#${templateName}.post_code}}`} + <dd> + {location?.post_code || (btr && `{{ ${templateName}.post_code }}`)} + </dd> + {btr && `{{` + `/${templateName}.post_code}}`} + + {btr && `{{` + `#${templateName}.street}}`} + <dd>{location?.street || (btr && `{{ ${templateName}.street }}`)}</dd> + {btr && `{{` + `/${templateName}.street}}`} + + {btr && `{{` + `#${templateName}.town}}`} + <dd>{location?.town || (btr && `{{ ${templateName}.town }}`)}</dd> + {btr && `{{` + `/${templateName}.town}}`} + + {btr && `{{` + `#${templateName}.town_location}}`} + <dd> + {location?.town_location || + (btr && `{{ ${templateName}.town_location }}`)} + </dd> + {btr && `{{` + `/${templateName}.town_location}}`} + </Fragment> + ); } -export function ShowOrderDetails({ order_summary, refund_amount, contract_terms, btr }: Props): VNode { - const productList = (btr ? [{} as MerchantBackend.Product] : (contract_terms?.products || [])) - const auditorsList = (btr ? [{} as MerchantBackend.Auditor] : (contract_terms?.auditors || [])) - const exchangesList = (btr ? [{} as MerchantBackend.Exchange] : (contract_terms?.exchanges || [])) - const hasDeliveryInfo = btr || !!contract_terms?.delivery_date || !!contract_terms?.delivery_location - - return <Page> - <header> - <h1>Details of order {contract_terms?.order_id || `{{ contract_terms.order_id }}`}</h1> - </header> - - <section> - {btr && `{{#refund_amount}}`} - {(btr || refund_amount) && <section> - <InfoBox> - <b>Refunded:</b> The merchant refunded you <b>{refund_amount || `{{ refund_amount }}`}</b>. - </InfoBox> - </section>} - {btr && `{{/refund_amount}}`} - - <section> - <TableExpanded> - <dt>Order summary:</dt> - <dd>{contract_terms?.summary || `{{ contract_terms.summary }}`}</dd> - <dt>Amount paid:</dt> - <dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd> - <dt>Order date:</dt> - <dd>{contract_terms?.timestamp ? - (contract_terms?.timestamp.t_ms != 'never' ? - format(contract_terms?.timestamp.t_ms, 'dd MMM yyyy HH:mm:ss') : - 'never') - : `{{ contract_terms.timestamp_str }}`} </dd> - <dt>Merchant name:</dt> - <dd>{contract_terms?.merchant.name || `{{ contract_terms.merchant.name }}`}</dd> - </TableExpanded> - </section> - - {btr && `{{#contract_terms.hasProducts}}`} - {!productList.length ? null : <section> - <h2>Products purchased</h2> - <TableSimple> - {btr && '{{' + '#contract_terms.products' + '}}'} - {productList.map((p, i) => { - const taxList = (btr ? [{} as MerchantBackend.Tax] : (p.taxes || [])) - - return <Fragment key={i}> - <p>{p.description || `{{description}}`}</p> - <dl> - <dt>Quantity:</dt> - <dd>{p.quantity || `{{quantity}}`}</dd> - - <dt>Price:</dt> - <dd>{p.price || `{{price}}`}</dd> - - {btr && `{{#hasTaxes}}`} - {!taxList.length ? null : <Fragment> - {btr && '{{' + '#taxes' + '}}'} - {taxList.map((t, i) => { - return <Fragment key={i}> - <dt>{t.name || `{{name}}`}</dt> - <dd>{t.tax || `{{tax}}`}</dd> - </Fragment> - })} - {btr && '{{' + '/taxes' + '}}'} - </Fragment>} - {btr && `{{/hasTaxes}}`} - - {btr && `{{#delivery_date}}`} - {(btr || p.delivery_date) && <Fragment> - <dt>Delivered on:</dt> - <dd>{p.delivery_date ? - (p.delivery_date.t_ms != 'never' ? - format(p.delivery_date.t_ms, 'dd MMM yyyy HH:mm:ss') : - 'never') - : `{{ delivery_date_str }}`} </dd> - </Fragment>} - {btr && `{{/delivery_date}}`} - - {btr && `{{#unit}}`} - {(btr || p.unit) && <Fragment> - <dt>Product unit:</dt> - <dd>{p.unit || `{{.}}`}</dd> - </Fragment>} - {btr && `{{/unit}}`} - - {btr && `{{#product_id}}`} - {(btr || p.product_id) && <Fragment> - <dt>Product ID:</dt> - <dd>{p.product_id || `{{.}}`}</dd> - </Fragment>} - {btr && `{{/product_id}}`} - - </dl> - </Fragment> - })} - {btr && '{{' + '/contract_terms.products' + '}}'} - </TableSimple> - - </section>} - {btr && `{{/contract_terms.hasProducts}}`} - - - {btr && `{{#contract_terms.has_delivery_info}}`} - {!hasDeliveryInfo ? null : <section> - <h2>Delivery information</h2> - <TableExpanded> - {btr && `{{#contract_terms.delivery_date}}`} - {(btr || contract_terms?.delivery_date) && <Fragment> - <dt>Delivery date:</dt> - <dd>{contract_terms?.delivery_date ? - (contract_terms?.delivery_date.t_ms != 'never' ? - format(contract_terms?.delivery_date.t_ms, 'dd MMM yyyy HH:mm:ss') : - 'never') - : `{{ contract_terms.delivery_date_str }}`} </dd> - - </Fragment>} - {btr && `{{/contract_terms.delivery_date}}`} - - {btr && `{{#contract_terms.delivery_location}}`} - {(btr || contract_terms?.delivery_location) && <Fragment> - <dt>Delivery address:</dt> - <Location btr={btr} location={contract_terms?.delivery_location} templateName="contract_terms.delivery_location" /> - </Fragment>} - {btr && `{{/contract_terms.delivery_location}}`} - </TableExpanded> - </section>} - {btr && `{{/contract_terms.has_delivery_info}}`} +export function ShowOrderDetails({ + order_summary, + refund_amount, + contract_terms, + btr, +}: Props): VNode { + const productList = btr + ? [{} as MerchantBackend.Product] + : contract_terms?.products || []; + const auditorsList = btr + ? [{} as MerchantBackend.Auditor] + : contract_terms?.auditors || []; + const exchangesList = btr + ? [{} as MerchantBackend.Exchange] + : contract_terms?.exchanges || []; + const hasDeliveryInfo = + btr || + !!contract_terms?.delivery_date || + !!contract_terms?.delivery_location; + + return ( + <Page> + <header> + <h1> + Details of order{" "} + {contract_terms?.order_id || `{{ contract_terms.order_id }}`} + </h1> + </header> <section> - <h2>Full payment information</h2> - <TableExpanded> - <dt>Amount paid:</dt> - <dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd> - <dt>Wire transfer method:</dt> - <dd>{contract_terms?.wire_method || `{{ contract_terms.wire_method }}`}</dd> - <dt>Payment deadline:</dt> - <dd>{contract_terms?.pay_deadline ? - (contract_terms?.pay_deadline.t_ms != 'never' ? - format(contract_terms?.pay_deadline.t_ms, 'dd MMM yyyy HH:mm:ss') : - 'never') - : `{{ contract_terms.pay_deadline_str }}`} </dd> - <dt>Exchange transfer deadline:</dt> - <dd>{contract_terms?.wire_transfer_deadline ? - (contract_terms?.wire_transfer_deadline.t_ms != 'never' ? - format(contract_terms?.wire_transfer_deadline.t_ms, 'dd MMM yyyy HH:mm:ss') : - 'never') - : `{{ contract_terms.wire_transfer_deadline_str }}`} </dd> - <dt>Maximum deposit fee:</dt> - <dd>{contract_terms?.max_fee || `{{ contract_terms.max_fee }}`}</dd> - <dt>Maximum wire fee:</dt> - <dd>{contract_terms?.max_wire_fee || `{{ contract_terms.max_wire_fee }}`}</dd> - <dt>Wire fee amortization:</dt> - <dd>{contract_terms?.wire_fee_amortization || `{{ contract_terms.wire_fee_amortization }}`} transactions</dd> - </TableExpanded> - </section> - - <section> - <h2>Refund information</h2> - <TableExpanded> - <dt>Refund deadline:</dt> - <dd>{contract_terms?.refund_deadline ? - (contract_terms?.refund_deadline.t_ms != 'never' ? - format(contract_terms?.refund_deadline.t_ms, 'dd MMM yyyy HH:mm:ss') : - 'never') - : `{{ contract_terms.refund_deadline_str }}`} </dd> - - {btr && `{{#contract_terms.auto_refund}}`} - {(btr || contract_terms?.auto_refund) && <Fragment> - <dt>Attempt autorefund for:</dt> - <dd>{contract_terms?.auto_refund ? - (contract_terms?.auto_refund.d_ms != 'forever' ? - formatDuration(intervalToDuration({ start: 0, end: contract_terms?.auto_refund.d_ms })) : - 'forever') - : `{{ contract_terms.auto_refund_str }}`} </dd> - </Fragment>} - {btr && `{{/contract_terms.auto_refund}}`} - </TableExpanded> - </section> - - <section> - <h2>Additional order details</h2> - <TableExpanded> - <dt>Public reorder URL:</dt> - <dd> -- not defined yet -- </dd> - {btr && `{{#contract_terms.fulfillment_url}}`} - {(btr || contract_terms?.fulfillment_url) && <Fragment> - <dt>Fulfillment URL:</dt> - <dd>{contract_terms?.fulfillment_url || (btr && `{{ contract_terms.fulfillment_url }}`)}</dd> - </Fragment>} - {btr && `{{/contract_terms.fulfillment_url}}`} - {/* <dt>Fulfillment message:</dt> + {btr && `{{#refund_amount}}`} + {(btr || refund_amount) && ( + <section> + <InfoBox> + <b>Refunded:</b> The merchant refunded you{" "} + <b>{refund_amount || `{{ refund_amount }}`}</b>. + </InfoBox> + </section> + )} + {btr && `{{/refund_amount}}`} + + <section> + <TableExpanded> + <dt>Order summary:</dt> + <dd>{contract_terms?.summary || `{{ contract_terms.summary }}`}</dd> + <dt>Amount paid:</dt> + <dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd> + <dt>Order date:</dt> + <dd> + {contract_terms?.timestamp + ? contract_terms?.timestamp.t_s != "never" + ? format( + contract_terms?.timestamp.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.timestamp_str }}`}{" "} + </dd> + <dt>Merchant name:</dt> + <dd> + {contract_terms?.merchant.name || + `{{ contract_terms.merchant.name }}`} + </dd> + </TableExpanded> + </section> + + {btr && `{{#contract_terms.hasProducts}}`} + {!productList.length ? null : ( + <section> + <h2>Products purchased</h2> + <TableSimple> + {btr && "{{" + "#contract_terms.products" + "}}"} + {productList.map((p, i) => { + const taxList = btr + ? [{} as MerchantBackend.Tax] + : p.taxes || []; + + return ( + <Fragment key={i}> + <p>{p.description || `{{description}}`}</p> + <dl> + <dt>Quantity:</dt> + <dd>{p.quantity || `{{quantity}}`}</dd> + + <dt>Price:</dt> + <dd>{p.price || `{{price}}`}</dd> + + {btr && `{{#hasTaxes}}`} + {!taxList.length ? null : ( + <Fragment> + {btr && "{{" + "#taxes" + "}}"} + {taxList.map((t, i) => { + return ( + <Fragment key={i}> + <dt>{t.name || `{{name}}`}</dt> + <dd>{t.tax || `{{tax}}`}</dd> + </Fragment> + ); + })} + {btr && "{{" + "/taxes" + "}}"} + </Fragment> + )} + {btr && `{{/hasTaxes}}`} + + {btr && `{{#delivery_date}}`} + {(btr || p.delivery_date) && ( + <Fragment> + <dt>Delivered on:</dt> + <dd> + {p.delivery_date + ? p.delivery_date.t_s != "never" + ? format( + p.delivery_date.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ delivery_date_str }}`}{" "} + </dd> + </Fragment> + )} + {btr && `{{/delivery_date}}`} + + {btr && `{{#unit}}`} + {(btr || p.unit) && ( + <Fragment> + <dt>Product unit:</dt> + <dd>{p.unit || `{{.}}`}</dd> + </Fragment> + )} + {btr && `{{/unit}}`} + + {btr && `{{#product_id}}`} + {(btr || p.product_id) && ( + <Fragment> + <dt>Product ID:</dt> + <dd>{p.product_id || `{{.}}`}</dd> + </Fragment> + )} + {btr && `{{/product_id}}`} + </dl> + </Fragment> + ); + })} + {btr && "{{" + "/contract_terms.products" + "}}"} + </TableSimple> + </section> + )} + {btr && `{{/contract_terms.hasProducts}}`} + + {btr && `{{#contract_terms.has_delivery_info}}`} + {!hasDeliveryInfo ? null : ( + <section> + <h2>Delivery information</h2> + <TableExpanded> + {btr && `{{#contract_terms.delivery_date}}`} + {(btr || contract_terms?.delivery_date) && ( + <Fragment> + <dt>Delivery date:</dt> + <dd> + {contract_terms?.delivery_date + ? contract_terms?.delivery_date.t_s != "never" + ? format( + contract_terms?.delivery_date.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.delivery_date_str }}`}{" "} + </dd> + </Fragment> + )} + {btr && `{{/contract_terms.delivery_date}}`} + + {btr && `{{#contract_terms.delivery_location}}`} + {(btr || contract_terms?.delivery_location) && ( + <Fragment> + <dt>Delivery address:</dt> + <Location + btr={btr} + location={contract_terms?.delivery_location} + templateName="contract_terms.delivery_location" + /> + </Fragment> + )} + {btr && `{{/contract_terms.delivery_location}}`} + </TableExpanded> + </section> + )} + {btr && `{{/contract_terms.has_delivery_info}}`} + + <section> + <h2>Full payment information</h2> + <TableExpanded> + <dt>Amount paid:</dt> + <dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd> + <dt>Wire transfer method:</dt> + <dd> + {contract_terms?.wire_method || + `{{ contract_terms.wire_method }}`} + </dd> + <dt>Payment deadline:</dt> + <dd> + {contract_terms?.pay_deadline + ? contract_terms?.pay_deadline.t_s != "never" + ? format( + contract_terms?.pay_deadline.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.pay_deadline_str }}`}{" "} + </dd> + <dt>Exchange transfer deadline:</dt> + <dd> + {contract_terms?.wire_transfer_deadline + ? contract_terms?.wire_transfer_deadline.t_s != "never" + ? format( + contract_terms?.wire_transfer_deadline.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.wire_transfer_deadline_str }}`}{" "} + </dd> + <dt>Maximum deposit fee:</dt> + <dd>{contract_terms?.max_fee || `{{ contract_terms.max_fee }}`}</dd> + <dt>Maximum wire fee:</dt> + <dd> + {contract_terms?.max_wire_fee || + `{{ contract_terms.max_wire_fee }}`} + </dd> + <dt>Wire fee amortization:</dt> + <dd> + {contract_terms?.wire_fee_amortization || + `{{ contract_terms.wire_fee_amortization }}`}{" "} + transactions + </dd> + </TableExpanded> + </section> + + <section> + <h2>Refund information</h2> + <TableExpanded> + <dt>Refund deadline:</dt> + <dd> + {contract_terms?.refund_deadline + ? contract_terms?.refund_deadline.t_s != "never" + ? format( + contract_terms?.refund_deadline.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.refund_deadline_str }}`}{" "} + </dd> + + {btr && `{{#contract_terms.auto_refund}}`} + {(btr || contract_terms?.auto_refund) && ( + <Fragment> + <dt>Attempt autorefund for:</dt> + <dd> + {contract_terms?.auto_refund + ? contract_terms?.auto_refund.d_us != "forever" + ? formatDuration( + intervalToDuration({ + start: 0, + end: contract_terms?.auto_refund.d_us, + }) + ) + : "forever" + : `{{ contract_terms.auto_refund_str }}`}{" "} + </dd> + </Fragment> + )} + {btr && `{{/contract_terms.auto_refund}}`} + </TableExpanded> + </section> + + <section> + <h2>Additional order details</h2> + <TableExpanded> + <dt>Public reorder URL:</dt> + <dd> -- not defined yet -- </dd> + {btr && `{{#contract_terms.fulfillment_url}}`} + {(btr || contract_terms?.fulfillment_url) && ( + <Fragment> + <dt>Fulfillment URL:</dt> + <dd> + {contract_terms?.fulfillment_url || + (btr && `{{ contract_terms.fulfillment_url }}`)} + </dd> + </Fragment> + )} + {btr && `{{/contract_terms.fulfillment_url}}`} + {/* <dt>Fulfillment message:</dt> <dd> -- not defined yet -- </dd> */} - </TableExpanded> - </section> - - <section> - <h2>Full merchant information</h2> - <TableExpanded> - <dt>Merchant name:</dt> - <dd>{contract_terms?.merchant.name || `{{ contract_terms.merchant.name }}`}</dd> - <dt>Merchant address:</dt> - <Location btr={btr} location={contract_terms?.merchant.address} templateName="contract_terms.merchant.address" /> - <dt>Merchant's jurisdiction:</dt> - <Location btr={btr} location={contract_terms?.merchant.jurisdiction} templateName="contract_terms.merchant.jurisdiction" /> - <dt>Merchant URI:</dt> - <dd>{contract_terms?.merchant_base_url || `{{ contract_terms.merchant_base_url }}`}</dd> - <dt>Merchant's public key:</dt> - <dd>{contract_terms?.merchant_pub || `{{ contract_terms.merchant_pub }}`}</dd> - {/* <dt>Merchant's hash:</dt> + </TableExpanded> + </section> + + <section> + <h2>Full merchant information</h2> + <TableExpanded> + <dt>Merchant name:</dt> + <dd> + {contract_terms?.merchant.name || + `{{ contract_terms.merchant.name }}`} + </dd> + <dt>Merchant address:</dt> + <Location + btr={btr} + location={contract_terms?.merchant.address} + templateName="contract_terms.merchant.address" + /> + <dt>Merchant's jurisdiction:</dt> + <Location + btr={btr} + location={contract_terms?.merchant.jurisdiction} + templateName="contract_terms.merchant.jurisdiction" + /> + <dt>Merchant URI:</dt> + <dd> + {contract_terms?.merchant_base_url || + `{{ contract_terms.merchant_base_url }}`} + </dd> + <dt>Merchant's public key:</dt> + <dd> + {contract_terms?.merchant_pub || + `{{ contract_terms.merchant_pub }}`} + </dd> + {/* <dt>Merchant's hash:</dt> <dd> -- not defined yet -- </dd> */} - </TableExpanded> + </TableExpanded> + </section> + + {btr && `{{#contract_terms.hasAuditors}}`} + {!auditorsList.length ? null : ( + <section> + <h2>Auditors accepted by the merchant</h2> + <TableExpanded> + {btr && "{{" + "#contract_terms.auditors" + "}}"} + {auditorsList.map((p, i) => { + return ( + <Fragment key={i}> + <p>{p.name || `{{name}}`}</p> + <dt>Auditor's public key:</dt> + <dd>{p.auditor_pub || `{{auditor_pub}}`}</dd> + <dt>Auditor's URL:</dt> + <dd>{p.url || `{{url}}`}</dd> + </Fragment> + ); + })} + {btr && "{{" + "/contract_terms.auditors" + "}}"} + </TableExpanded> + </section> + )} + {btr && `{{/contract_terms.hasAuditors}}`} + + {btr && `{{#contract_terms.hasExchanges}}`} + {!exchangesList.length ? null : ( + <section> + <h2>Exchanges accepted by the merchant</h2> + <TableExpanded> + {btr && "{{" + "#contract_terms.exchanges" + "}}"} + {exchangesList.map((p, i) => { + return ( + <Fragment key={i}> + <dt>Exchange's URL:</dt> + <dd>{p.url || `{{url}}`}</dd> + <dt>Public key:</dt> + <dd>{p.master_pub || `{{master_pub}}`}</dd> + </Fragment> + ); + })} + {btr && "{{" + "/contract_terms.exchanges" + "}}"} + </TableExpanded> + </section> + )} + {btr && `{{/contract_terms.hasExchanges}}`} </section> - {btr && `{{#contract_terms.hasAuditors}}`} - {!auditorsList.length ? null : <section> - <h2>Auditors accepted by the merchant</h2> - <TableExpanded> - {btr && '{{' + '#contract_terms.auditors' + '}}'} - {auditorsList.map((p, i) => { - return <Fragment key={i}> - <p>{p.name || `{{name}}`}</p> - <dt>Auditor's public key:</dt> - <dd>{p.auditor_pub || `{{auditor_pub}}`}</dd> - <dt>Auditor's URL:</dt> - <dd>{p.url || `{{url}}`}</dd> - </Fragment> - })} - {btr && '{{' + '/contract_terms.auditors' + '}}'} - </TableExpanded> - </section>} - {btr && `{{/contract_terms.hasAuditors}}`} - - {btr && `{{#contract_terms.hasExchanges}}`} - {!exchangesList.length ? null : <section> - <h2>Exchanges accepted by the merchant</h2> - <TableExpanded> - {btr && '{{' + '#contract_terms.exchanges' + '}}'} - {exchangesList.map((p, i) => { - return <Fragment key={i}> - <dt>Exchange's URL:</dt> - <dd>{p.url || `{{url}}`}</dd> - <dt>Public key:</dt> - <dd>{p.master_pub || `{{master_pub}}`}</dd> - </Fragment> - })} - {btr && '{{' + '/contract_terms.exchanges' + '}}'} - </TableExpanded> - </section>} - {btr && `{{/contract_terms.hasExchanges}}`} - </section> - - <Footer /> - </Page> - + <Footer /> + </Page> + ); } export function mount(): void { try { - const fromLocation = new URL(window.location.href).searchParams - const os = fromLocation.get('order_summary') || undefined; + const fromLocation = new URL(window.location.href).searchParams; + const os = fromLocation.get("order_summary") || undefined; if (os) { render(<Head order_summary={os} />, document.head); } - const ra = fromLocation.get('refund_amount') || undefined; - const ct = fromLocation.get('contract_terms') || undefined; + const ra = fromLocation.get("refund_amount") || undefined; + const ct = fromLocation.get("contract_terms") || undefined; let contractTerms: MerchantBackend.ContractTerms | undefined; try { - contractTerms = JSON.parse((window as any).contractTermsStr) - } catch { } - - render(<ShowOrderDetails - contract_terms={contractTerms} - order_summary={os} refund_amount={ra} - />, document.body); - + contractTerms = JSON.parse((window as any).contractTermsStr); + } catch {} + + render( + <ShowOrderDetails + contract_terms={contractTerms} + order_summary={os} + refund_amount={ra} + />, + document.body + ); } catch (e) { console.error("got error", e); if (e instanceof Error) { @@ -386,9 +543,9 @@ export function mount(): void { } } -export function buildTimeRendering(): { head: string, body: string } { +export function buildTimeRendering(): { head: string; body: string } { return { head: renderToString(<Head />), - body: renderToString(<ShowOrderDetails btr />) - } + body: renderToString(<ShowOrderDetails btr />), + }; } diff --git a/packages/merchant-backend/src/utils/amount.ts b/packages/merchant-backend/src/utils/amount.ts @@ -38,10 +38,10 @@ export function mergeRefunds(prev: MerchantBackend.Orders.RefundDetails[], cur: let tail; if (prev.length === 0 || //empty list - cur.timestamp.t_ms === 'never' || //current doesnt have timestamp - (tail = prev[prev.length - 1]).timestamp.t_ms === 'never' || // last doesnt have timestamp + cur.timestamp.t_s === 'never' || //current doesnt have timestamp + (tail = prev[prev.length - 1]).timestamp.t_s === 'never' || // last doesnt have timestamp cur.reason !== tail.reason || //different reason - Math.abs(cur.timestamp.t_ms - tail.timestamp.t_ms) > 1000 * 60) {//more than 1 minute difference + Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60) {//more than 1 minute difference prev.push(cur) return prev diff --git a/packages/merchant-backoffice/src/components/form/InputDate.tsx b/packages/merchant-backoffice/src/components/form/InputDate.tsx @@ -52,13 +52,13 @@ export function InputDate<T>({ strValue = withTimestampSupport ? "unknown" : ""; } else if (value instanceof Date) { strValue = format(value, "yyyy/MM/dd"); - } else if (value.t_ms) { + } else if (value.t_s) { strValue = - value.t_ms === "never" + value.t_s === "never" ? withTimestampSupport ? "never" : "" - : format(new Date(value.t_ms), "yyyy/MM/dd"); + : format(new Date(value.t_s), "yyyy/MM/dd"); } return ( @@ -136,7 +136,7 @@ export function InputDate<T>({ <span data-tooltip={i18n`change value to never`}> <button class="button is-info" - onClick={() => onChange({ t_ms: "never" } as any)} + onClick={() => onChange({ t_s: "never" } as any)} > <Translate>never</Translate> </button> @@ -148,7 +148,7 @@ export function InputDate<T>({ closeFunction={() => setOpened(false)} dateReceiver={(d) => { if (withTimestampSupport) { - onChange({ t_ms: d.getTime() } as any); + onChange({ t_s: d.getTime() } as any); } else { onChange(d as any); } diff --git a/packages/merchant-backoffice/src/components/form/InputDuration.tsx b/packages/merchant-backoffice/src/components/form/InputDuration.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { intervalToDuration, formatDuration } from "date-fns"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -32,85 +32,141 @@ export interface Props<T> extends InputProps<T> { withForever?: boolean; } -export function InputDuration<T>({ name, expand, placeholder, tooltip, label, help, readonly, withForever }: Props<keyof T>): VNode { - const [opened, setOpened] = useState(false) - const i18n = useTranslator() +export function InputDuration<T>({ + name, + expand, + placeholder, + tooltip, + label, + help, + readonly, + withForever, +}: Props<keyof T>): VNode { + const [opened, setOpened] = useState(false); + const i18n = useTranslator(); const { error, required, value, onChange } = useField<T>(name); - let strValue = '' + let strValue = ""; if (!value) { - strValue = '' - } else if (value.d_ms === 'forever') { - strValue = i18n`forever` + strValue = ""; + } else if (value.d_us === "forever") { + strValue = i18n`forever`; } else { - strValue = formatDuration(intervalToDuration({ start: 0, end: value.d_ms }), { - locale: { - formatDistance: (name, value) => { - switch(name) { - case 'xMonths': return i18n`${value}M`; - case 'xYears': return i18n`${value}Y`; - case 'xDays': return i18n`${value}d`; - case 'xHours': return i18n`${value}h`; - case 'xMinutes': return i18n`${value}min`; - case 'xSeconds': return i18n`${value}sec`; - } + strValue = formatDuration( + intervalToDuration({ start: 0, end: value.d_us }), + { + locale: { + formatDistance: (name, value) => { + switch (name) { + case "xMonths": + return i18n`${value}M`; + case "xYears": + return i18n`${value}Y`; + case "xDays": + return i18n`${value}d`; + case "xHours": + return i18n`${value}h`; + case "xMinutes": + return i18n`${value}min`; + case "xSeconds": + return i18n`${value}sec`; + } + }, + localize: { + day: () => "s", + month: () => "m", + ordinalNumber: () => "th", + dayPeriod: () => "p", + quarter: () => "w", + era: () => "e", + }, }, - localize: { - day: () => 's', - month: () => 'm', - ordinalNumber: () => 'th', - dayPeriod: () => 'p', - quarter: () => 'w', - era: () => 'e' - } - }, - }) + } + ); } - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <div class="field has-addons"> - <p class={expand ? "control is-expanded " : "control "}> - <input class="input" type="text" - readonly value={strValue} - placeholder={placeholder} - onClick={() => { if (!readonly) setOpened(true) }} - /> - {required && <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span>} - {help} - </p> - <div class="control" onClick={() => { if (!readonly) setOpened(true) }}> - <a class="button is-static" > - <span class="icon"><i class="mdi mdi-clock" /></span> - </a> + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <div class="field has-addons"> + <p class={expand ? "control is-expanded " : "control "}> + <input + class="input" + type="text" + readonly + value={strValue} + placeholder={placeholder} + onClick={() => { + if (!readonly) setOpened(true); + }} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + {help} + </p> + <div + class="control" + onClick={() => { + if (!readonly) setOpened(true); + }} + > + <a class="button is-static"> + <span class="icon"> + <i class="mdi mdi-clock" /> + </span> + </a> + </div> </div> + {error && <p class="help is-danger">{error}</p>} </div> - {error && <p class="help is-danger">{error}</p>} + {withForever && ( + <span data-tooltip={i18n`change value to never`}> + <button + class="button is-info mr-3" + onClick={() => onChange({ d_us: "forever" } as any)} + > + <Translate>forever</Translate> + </button> + </span> + )} + {!readonly && ( + <span data-tooltip={i18n`change value to empty`}> + <button + class="button is-info " + onClick={() => onChange(undefined as any)} + > + <Translate>clear</Translate> + </button> + </span> + )} </div> - {withForever && <span data-tooltip={i18n`change value to never`}> - <button class="button is-info mr-3" onClick={() => onChange({ d_ms: 'forever' } as any)}><Translate>forever</Translate></button> - </span>} - {!readonly && <span data-tooltip={i18n`change value to empty`}> - <button class="button is-info " onClick={() => onChange(undefined as any)} ><Translate>clear</Translate></button> - </span>} + {opened && ( + <SimpleModal onCancel={() => setOpened(false)}> + <DurationPicker + days + hours + minutes + value={!value || value.d_us === "forever" ? 0 : value.d_us} + onChange={(v) => { + onChange({ d_us: v } as any); + }} + /> + </SimpleModal> + )} </div> - {opened && <SimpleModal onCancel={() => setOpened(false)}> - <DurationPicker days hours minutes - value={!value || value.d_ms === 'forever' ? 0 : value.d_ms} - onChange={(v) => { onChange({ d_ms: v } as any) }} - /> - </SimpleModal>} - </div> + ); } diff --git a/packages/merchant-backoffice/src/components/form/InputStock.stories.tsx b/packages/merchant-backoffice/src/components/form/InputStock.stories.tsx @@ -15,30 +15,39 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { addDays } from 'date-fns'; -import { h, VNode } from 'preact'; -import { useState } from 'preact/hooks'; +import { addDays } from "date-fns"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { FormProvider } from "./FormProvider"; -import { InputStock, Stock } from './InputStock' +import { InputStock, Stock } from "./InputStock"; export default { - title: 'Components/Form/InputStock', + title: "Components/Form/InputStock", component: InputStock, }; -type T = { stock?: Stock } +type T = { stock?: Stock }; export const CreateStockEmpty = () => { - const [state, setState] = useState<Partial<T>>({}) - return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> - <InputStock<T> name="stock" label="Stock" /> - <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div> - </FormProvider> -} + const [state, setState] = useState<Partial<T>>({}); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; export const CreateStockUnknownRestock = () => { const [state, setState] = useState<Partial<T>>({ @@ -46,13 +55,22 @@ export const CreateStockUnknownRestock = () => { current: 10, lost: 0, sold: 0, - } - }) - return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> - <InputStock<T> name="stock" label="Stock" /> - <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div> - </FormProvider> -} + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; export const CreateStockNoRestock = () => { const [state, setState] = useState<Partial<T>>({ @@ -60,14 +78,23 @@ export const CreateStockNoRestock = () => { current: 10, lost: 0, sold: 0, - nextRestock: { t_ms: 'never' } - } - }) - return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> - <InputStock<T> name="stock" label="Stock" /> - <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div> - </FormProvider> -} + nextRestock: { t_s: "never" }, + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; export const CreateStockWithRestock = () => { const [state, setState] = useState<Partial<T>>({ @@ -75,14 +102,23 @@ export const CreateStockWithRestock = () => { current: 15, lost: 0, sold: 0, - nextRestock: { t_ms: addDays(new Date(), 1).getTime() } - } - }) - return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> - <InputStock<T> name="stock" label="Stock" /> - <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div> - </FormProvider> -} + nextRestock: { t_s: addDays(new Date(), 1).getTime() }, + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; export const UpdatingProductWithManagedStock = () => { const [state, setState] = useState<Partial<T>>({ @@ -90,21 +126,37 @@ export const UpdatingProductWithManagedStock = () => { current: 100, lost: 0, sold: 0, - nextRestock: { t_ms: addDays(new Date(), 1).getTime() } - } - }) - return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> - <InputStock<T> name="stock" label="Stock" alreadyExist /> - <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div> - </FormProvider> -} + nextRestock: { t_s: addDays(new Date(), 1).getTime() }, + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" alreadyExist /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; export const UpdatingProductWithInfiniteStock = () => { - const [state, setState] = useState<Partial<T>>({}) - return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> - <InputStock<T> name="stock" label="Stock" alreadyExist /> - <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div> - </FormProvider> -} - - + const [state, setState] = useState<Partial<T>>({}); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" alreadyExist /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; diff --git a/packages/merchant-backoffice/src/components/product/ProductForm.tsx b/packages/merchant-backoffice/src/components/product/ProductForm.tsx @@ -15,19 +15,20 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; -import * as yup from 'yup'; +import * as yup from "yup"; import { useBackendContext } from "../../context/backend"; import { MerchantBackend } from "../../declaration"; import { useTranslator } from "../../i18n"; import { - ProductCreateSchema as createSchema, ProductUpdateSchema as updateSchema -} from '../../schemas'; + ProductCreateSchema as createSchema, + ProductUpdateSchema as updateSchema, +} from "../../schemas"; import { FormProvider, FormErrors } from "../form/FormProvider"; import { Input } from "../form/Input"; import { InputCurrency } from "../form/InputCurrency"; @@ -36,7 +37,7 @@ import { InputStock, Stock } from "../form/InputStock"; import { InputTaxes } from "../form/InputTaxes"; import { InputWithAddon } from "../form/InputWithAddon"; -type Entity = MerchantBackend.Products.ProductDetail & { product_id: string } +type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; interface Props { onSubscribe: (c?: () => Entity | undefined) => void; @@ -44,67 +45,122 @@ interface Props { alreadyExist?: boolean; } -export function ProductForm({ onSubscribe, initial, alreadyExist, }: Props) { +export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({ address: {}, description_i18n: {}, taxes: [], - next_restock: { t_ms: 'never' }, + next_restock: { t_s: "never" }, ...initial, - price: ':0', - stock: !initial || initial.total_stock === -1 ? undefined : { - current: initial.total_stock || 0, - lost: initial.total_lost || 0, - sold: initial.total_sold || 0, - address: initial.address, - nextRestock: initial.next_restock, - } - }) - let errors : FormErrors<Entity>= {} + price: ":0", + stock: + !initial || initial.total_stock === -1 + ? undefined + : { + current: initial.total_stock || 0, + lost: initial.total_lost || 0, + sold: initial.total_sold || 0, + address: initial.address, + nextRestock: initial.next_restock, + }, + }); + let errors: FormErrors<Entity> = {}; try { - (alreadyExist ? updateSchema : createSchema).validateSync(value, { abortEarly: false }) + (alreadyExist ? updateSchema : createSchema).validateSync(value, { + abortEarly: false, + }); } catch (err) { if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as yup.ValidationError[] - errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {} + ); } } - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined + ); const submit = useCallback((): Entity | undefined => { const stock: Stock = (value as any).stock; if (!stock) { - value.total_stock = -1 + value.total_stock = -1; } else { value.total_stock = stock.current; value.total_lost = stock.lost; - value.next_restock = stock.nextRestock instanceof Date ? { t_ms: stock.nextRestock.getTime() } : stock.nextRestock; + value.next_restock = + stock.nextRestock instanceof Date + ? { t_s: stock.nextRestock.getTime() } + : stock.nextRestock; value.address = stock.address; } delete (value as any).stock; - return value as MerchantBackend.Products.ProductDetail & { product_id: string } - }, [value]) + return value as MerchantBackend.Products.ProductDetail & { + product_id: string; + }; + }, [value]); useEffect(() => { - onSubscribe(hasErrors ? undefined : submit) - }, [submit, hasErrors]) + onSubscribe(hasErrors ? undefined : submit); + }, [submit, hasErrors]); const backend = useBackendContext(); - const i18n = useTranslator() - - return <div> - <FormProvider<Entity> name="product" errors={errors} object={value} valueHandler={valueHandler} > - - {alreadyExist ? undefined : <InputWithAddon<Entity> name="product_id" addonBefore={`${backend.url}/product/`} label={i18n`ID`} tooltip={i18n`product identification to use in URLs (for internal use only)`} />} - <InputImage<Entity> name="image" label={i18n`Image`} tooltip={i18n`illustration of the product for customers`} /> - <Input<Entity> name="description" inputType="multiline" label={i18n`Description`} tooltip={i18n`product description for customers`} /> - <Input<Entity> name="unit" label={i18n`Unit`} tooltip={i18n`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} /> - <InputCurrency<Entity> name="price" label={i18n`Price`} tooltip={i18n`sale price for customers, including taxes, for above units of the product`} /> - <InputStock name="stock" label={i18n`Stock`} alreadyExist={alreadyExist} tooltip={i18n`product inventory for products with finite supply (for internal use only)`} /> - <InputTaxes<Entity> name="taxes" label={i18n`Taxes`} tooltip={i18n`taxes included in the product price, exposed to customers`} /> - </FormProvider> - </div> + const i18n = useTranslator(); + + return ( + <div> + <FormProvider<Entity> + name="product" + errors={errors} + object={value} + valueHandler={valueHandler} + > + {alreadyExist ? undefined : ( + <InputWithAddon<Entity> + name="product_id" + addonBefore={`${backend.url}/product/`} + label={i18n`ID`} + tooltip={i18n`product identification to use in URLs (for internal use only)`} + /> + )} + <InputImage<Entity> + name="image" + label={i18n`Image`} + tooltip={i18n`illustration of the product for customers`} + /> + <Input<Entity> + name="description" + inputType="multiline" + label={i18n`Description`} + tooltip={i18n`product description for customers`} + /> + <Input<Entity> + name="unit" + label={i18n`Unit`} + tooltip={i18n`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} + /> + <InputCurrency<Entity> + name="price" + label={i18n`Price`} + tooltip={i18n`sale price for customers, including taxes, for above units of the product`} + /> + <InputStock + name="stock" + label={i18n`Stock`} + alreadyExist={alreadyExist} + tooltip={i18n`product inventory for products with finite supply (for internal use only)`} + /> + <InputTaxes<Entity> + name="taxes" + label={i18n`Taxes`} + tooltip={i18n`taxes included in the product price, exposed to customers`} + /> + </FormProvider> + </div> + ); } diff --git a/packages/merchant-backoffice/src/declaration.d.ts b/packages/merchant-backoffice/src/declaration.d.ts @@ -35,12 +35,10 @@ interface Timestamp { // Milliseconds since epoch, or the special // value "forever" to represent an event that will // never happen. - t_ms: number | "never"; + t_s: number | "never"; } interface Duration { - // Duration in milliseconds or "forever" - // to represent an infinite duration. - d_ms: number | "forever"; + d_us: number | "forever"; } interface WithId { @@ -190,7 +188,7 @@ export namespace MerchantBackend { taxes: Tax[]; // time indicating when this product should be delivered - delivery_date?: Timestamp; + delivery_date?: TalerProtocolTimestamp; } interface Merchant { // label for a location with the business address of the merchant @@ -1347,17 +1345,17 @@ export namespace MerchantBackend { products: Product[]; // Time when this contract was generated - timestamp: Timestamp; + timestamp: TalerProtocolTimestamp; // After this deadline has passed, no refunds will be accepted. - refund_deadline: Timestamp; + refund_deadline: TalerProtocolTimestamp; // After this deadline, the merchant won't accept payments for the contact - pay_deadline: Timestamp; + pay_deadline: TalerProtocolTimestamp; // Transfer deadline for the exchange. Must be in the // deposit permissions of coins used to pay for this order. - wire_transfer_deadline: Timestamp; + wire_transfer_deadline: TalerProtocolTimestamp; // Merchant's public key used to sign this proposal; this information // is typically added by the backend Note that this can be an ephemeral key. @@ -1390,7 +1388,7 @@ export namespace MerchantBackend { // Time indicating when the order should be delivered. // May be overwritten by individual products. - delivery_date?: Timestamp; + delivery_date?: TalerProtocolTimestamp; // Nonce generated by the wallet and echoed by the merchant // in this field when the proposal is generated. diff --git a/packages/merchant-backoffice/src/hooks/order.ts b/packages/merchant-backoffice/src/hooks/order.ts @@ -291,7 +291,7 @@ export function useInstanceOrders( } else { const from = afterData.data.orders[afterData.data.orders.length - 1].timestamp - .t_ms; + .t_s; if (from && updateFilter) updateFilter(new Date(from)); } }, @@ -302,7 +302,7 @@ export function useInstanceOrders( } else if (beforeData) { const from = beforeData.data.orders[beforeData.data.orders.length - 1].timestamp - .t_ms; + .t_s; if (from && updateFilter) updateFilter(new Date(from)); } }, diff --git a/packages/merchant-backoffice/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice/src/paths/admin/create/CreatePage.tsx @@ -48,9 +48,9 @@ function with_defaults(id?: string): Partial<Entity> { return { id, payto_uris: [], - default_pay_delay: { d_ms: 1000 * 60 * 60 }, // one hour + default_pay_delay: { d_us: 1000 * 60 * 60 }, // one hour default_wire_fee_amortization: 1, - default_wire_transfer_delay: { d_ms: 1000 * 2 * 60 * 60 * 24 }, // one day + default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 }, // one day }; } diff --git a/packages/merchant-backoffice/src/paths/instance/details/DetailPage.tsx b/packages/merchant-backoffice/src/paths/instance/details/DetailPage.tsx @@ -38,8 +38,8 @@ function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri) const defaults = { default_wire_fee_amortization: 1, - default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour - default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours + default_pay_delay: { d_us: 1000 * 60 * 60 }, //one hour + default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 }, //two hours } return { ...defaults, ...rest, payto_uris }; } diff --git a/packages/merchant-backoffice/src/paths/instance/details/Details.stories.tsx b/packages/merchant-backoffice/src/paths/instance/details/Details.stories.tsx @@ -15,46 +15,47 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h, VNode, FunctionalComponent } from 'preact'; -import { DetailPage as TestedComponent } from './DetailPage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode, FunctionalComponent } from "preact"; +import { DetailPage as TestedComponent } from "./DetailPage"; export default { - title: 'Pages/Instance/Detail', + title: "Pages/Instance/Detail", component: TestedComponent, argTypes: { - onUpdate: { action: 'onUpdate' }, - onBack: { action: 'onBack' }, + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r +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, { selected: { accounts: [], - name: 'name', - auth: {method:'external'}, + name: "name", + auth: { method: "external" }, address: {}, jurisdiction: {}, - default_max_deposit_fee: 'TESTKUDOS:2', - default_max_wire_fee: 'TESTKUDOS:1', + default_max_deposit_fee: "TESTKUDOS:2", + default_max_wire_fee: "TESTKUDOS:1", default_pay_delay: { - d_ms: 1000000, + d_us: 1000000, }, default_wire_fee_amortization: 1, default_wire_transfer_delay: { - d_ms: 100000, + d_us: 100000, }, - merchant_pub: 'ASDWQEKASJDKSADJ' - } + merchant_pub: "ASDWQEKASJDKSADJ", + }, }); - diff --git a/packages/merchant-backoffice/src/paths/instance/orders/create/Create.stories.tsx b/packages/merchant-backoffice/src/paths/instance/orders/create/Create.stories.tsx @@ -15,50 +15,56 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h, VNode, FunctionalComponent } from 'preact'; -import { CreatePage as TestedComponent } from './CreatePage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage"; export default { - title: 'Pages/Order/Create', + title: "Pages/Order/Create", component: TestedComponent, argTypes: { - onCreate: { action: 'onCreate' }, - goBack: { action: 'goBack' }, + 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 +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_max_deposit_fee: '', - default_max_wire_fee: '', + default_max_deposit_fee: "", + default_max_wire_fee: "", default_pay_delay: { - d_ms: 1000*60*60 + d_us: 1000 * 60 * 60, }, - default_wire_fee_amortization: 1 + default_wire_fee_amortization: 1, }, - 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] + 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/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx @@ -58,9 +58,9 @@ interface InstanceConfig { function with_defaults(config: InstanceConfig): Partial<Entity> { const defaultPayDeadline = - !config.default_pay_delay || config.default_pay_delay.d_ms === "forever" + !config.default_pay_delay || config.default_pay_delay.d_us === "forever" ? undefined - : add(new Date(), { seconds: config.default_pay_delay.d_ms / 1000 }); + : add(new Date(), { seconds: config.default_pay_delay.d_us / 1000 }); return { inventoryProducts: {}, @@ -223,13 +223,13 @@ export function CreatePage({ extra: value.extra, pay_deadline: value.payments.pay_deadline ? { - t_ms: + t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000) * 1000, } : undefined, wire_transfer_deadline: value.payments.wire_transfer_deadline ? { - t_ms: + t_s: Math.floor( value.payments.wire_transfer_deadline.getTime() / 1000 ) * 1000, @@ -237,7 +237,7 @@ export function CreatePage({ : undefined, refund_deadline: value.payments.refund_deadline ? { - t_ms: + t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000) * 1000, } @@ -247,7 +247,7 @@ export function CreatePage({ max_wire_fee: value.payments.max_wire_fee, delivery_date: value.shipping.delivery_date - ? { t_ms: value.shipping.delivery_date.getTime() } + ? { t_s: value.shipping.delivery_date.getTime() } : undefined, delivery_location: value.shipping.delivery_location, fulfillment_url: value.shipping.fullfilment_url, diff --git a/packages/merchant-backoffice/src/paths/instance/orders/details/Detail.stories.tsx b/packages/merchant-backoffice/src/paths/instance/orders/details/Detail.stories.tsx @@ -15,123 +15,123 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { addDays } from 'date-fns'; -import { h, VNode, FunctionalComponent } from 'preact'; -import { MerchantBackend } from '../../../../declaration'; -import { DetailPage as TestedComponent } from './DetailPage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { addDays } from "date-fns"; +import { h, VNode, FunctionalComponent } from "preact"; +import { MerchantBackend } from "../../../../declaration"; +import { DetailPage as TestedComponent } from "./DetailPage"; export default { - title: 'Pages/Order/Detail', + title: "Pages/Order/Detail", component: TestedComponent, argTypes: { - onRefund: { action: 'onRefund' }, - onBack: { action: 'onBack' }, + 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 +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', + amount: "TESTKUDOS:10", timestamp: { - t_ms: new Date().getTime(), + t_s: new Date().getTime(), }, auditors: [], exchanges: [], - max_fee: 'TESTKUDOS:1', - max_wire_fee: 'TESTKUDOS:1', - merchant: { - - } as any, - merchant_base_url: 'http://merchant.url/', - order_id: '2021.165-03GDFC26Y1NNG', + max_fee: "TESTKUDOS:1", + max_wire_fee: "TESTKUDOS:1", + merchant: {} as any, + merchant_base_url: "http://merchant.url/", + order_id: "2021.165-03GDFC26Y1NNG", products: [], - summary: 'text summary', + summary: "text summary", wire_fee_amortization: 1, wire_transfer_deadline: { - t_ms: 'never', + t_s: "never", }, - refund_deadline: { t_ms: 'never' }, - merchant_pub: 'ASDASDASDSd', - nonce: 'QWEQWEQWE', + refund_deadline: { t_s: "never" }, + merchant_pub: "ASDASDASDSd", + nonce: "QWEQWEQWE", pay_deadline: { - t_ms: 'never', + t_s: "never", }, - wire_method: 'x-taler-bank', - h_wire: 'asd', -} as MerchantBackend.ContractTerms + wire_method: "x-taler-bank", + h_wire: "asd", +} as MerchantBackend.ContractTerms; // contract_terms: defaultContracTerm, export const Claimed = createExample(TestedComponent, { - id: '2021.165-03GDFC26Y1NNG', + id: "2021.165-03GDFC26Y1NNG", selected: { - order_status: 'claimed', - contract_terms: defaultContractTerm + order_status: "claimed", + contract_terms: defaultContractTerm, }, }); export const PaidNotRefundable = createExample(TestedComponent, { - id: '2021.165-03GDFC26Y1NNG', + id: "2021.165-03GDFC26Y1NNG", selected: { - order_status: 'paid', + order_status: "paid", contract_terms: defaultContractTerm, refunded: false, - deposit_total: 'TESTKUDOS:10', + deposit_total: "TESTKUDOS:10", exchange_ec: 0, - order_status_url: 'http://merchant.backend/status', + order_status_url: "http://merchant.backend/status", exchange_hc: 0, - refund_amount: 'TESTKUDOS: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', + id: "2021.165-03GDFC26Y1NNG", selected: { - order_status: 'paid', + order_status: "paid", contract_terms: { ...defaultContractTerm, refund_deadline: { - t_ms: addDays(new Date(), 2).getTime() - } + t_s: addDays(new Date(), 2).getTime(), + }, }, refunded: false, - deposit_total: 'TESTKUDOS:10', + deposit_total: "TESTKUDOS:10", exchange_ec: 0, - order_status_url: 'http://merchant.backend/status', + order_status_url: "http://merchant.backend/status", exchange_hc: 0, - refund_amount: 'TESTKUDOS: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', + id: "2021.165-03GDFC26Y1NNG", selected: { - order_status: 'unpaid', - order_status_url: 'http://merchant.backend/status', + order_status: "unpaid", + order_status_url: "http://merchant.backend/status", creation_time: { - t_ms: new Date().getTime() + t_s: new Date().getTime(), }, - summary: 'text summary', - taler_pay_uri: 'pay uri', - total_amount: 'TESTKUDOS:10', - } + summary: "text summary", + taler_pay_uri: "pay uri", + total_amount: "TESTKUDOS:10", + }, }); diff --git a/packages/merchant-backoffice/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice/src/paths/instance/orders/details/DetailPage.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { format } from "date-fns"; @@ -39,7 +39,7 @@ import { RefundModal } from "../list/Table"; import { Event, Timeline } from "./Timeline"; type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; -type CT = MerchantBackend.ContractTerms +type CT = MerchantBackend.ContractTerms; interface Props { onBack: () => void; @@ -48,430 +48,687 @@ interface Props { onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; } -type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse -type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse -type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse - +type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse; +type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse; +type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse; function ContractTerms({ value }: { value: CT }) { - const i18n = useTranslator() - - return <InputGroup name="contract_terms" label={i18n`Contract Terms`}> - <FormProvider<CT> object={value} valueHandler={null} > - <Input<CT> readonly name="summary" label={i18n`Summary`} tooltip={i18n`human-readable description of the whole purchase`} /> - <InputCurrency<CT> readonly name="amount" label={i18n`Amount`} tooltip={i18n`total price for the transaction`} /> - {value.fulfillment_url && - <Input<CT> readonly name="fulfillment_url" label={i18n`Fulfillment URL`} tooltip={i18n`URL for this purchase`} /> - } - <Input<CT> readonly name="max_fee" label={i18n`Max fee`} tooltip={i18n`maximum total deposit fee accepted by the merchant for this contract`} /> - <Input<CT> readonly name="max_wire_fee" label={i18n`Max wire fee`} tooltip={i18n`maximum wire fee accepted by the merchant`} /> - <Input<CT> readonly name="wire_fee_amortization" label={i18n`Wire fee amortization`} tooltip={i18n`over how many customer transactions does the merchant expect to amortize wire fees on average`} /> - <InputDate<CT> readonly name="timestamp" label={i18n`Created at`} tooltip={i18n`time when this contract was generated`} /> - <InputDate<CT> readonly name="refund_deadline" label={i18n`Refund deadline`} tooltip={i18n`after this deadline has passed no refunds will be accepted`} /> - <InputDate<CT> readonly name="pay_deadline" label={i18n`Payment deadline`} tooltip={i18n`after this deadline, the merchant won't accept payments for the contract`} /> - <InputDate<CT> readonly name="wire_transfer_deadline" label={i18n`Wire transfer deadline`} tooltip={i18n`transfer deadline for the exchange`} /> - <InputDate<CT> readonly name="delivery_date" label={i18n`Delivery date`} tooltip={i18n`time indicating when the order should be delivered`} /> - {value.delivery_date && - <InputGroup name="delivery_location" label={i18n`Location`} tooltip={i18n`where the order will be delivered`} > - <InputLocation name="payments.delivery_location" /> - </InputGroup> - } - <InputDuration<CT> readonly name="auto_refund" label={i18n`Auto-refund delay`} tooltip={i18n`how long the wallet should try to get an automatic refund for the purchase`} /> - <Input<CT> readonly name="extra" label={i18n`Extra info`} tooltip={i18n`extra data that is only interpreted by the merchant frontend`} /> - </FormProvider> - </InputGroup> + const i18n = useTranslator(); + + return ( + <InputGroup name="contract_terms" label={i18n`Contract Terms`}> + <FormProvider<CT> object={value} valueHandler={null}> + <Input<CT> + readonly + name="summary" + label={i18n`Summary`} + tooltip={i18n`human-readable description of the whole purchase`} + /> + <InputCurrency<CT> + readonly + name="amount" + label={i18n`Amount`} + tooltip={i18n`total price for the transaction`} + /> + {value.fulfillment_url && ( + <Input<CT> + readonly + name="fulfillment_url" + label={i18n`Fulfillment URL`} + tooltip={i18n`URL for this purchase`} + /> + )} + <Input<CT> + readonly + name="max_fee" + label={i18n`Max fee`} + tooltip={i18n`maximum total deposit fee accepted by the merchant for this contract`} + /> + <Input<CT> + readonly + name="max_wire_fee" + label={i18n`Max wire fee`} + tooltip={i18n`maximum wire fee accepted by the merchant`} + /> + <Input<CT> + readonly + name="wire_fee_amortization" + label={i18n`Wire fee amortization`} + tooltip={i18n`over how many customer transactions does the merchant expect to amortize wire fees on average`} + /> + <InputDate<CT> + readonly + name="timestamp" + label={i18n`Created at`} + tooltip={i18n`time when this contract was generated`} + /> + <InputDate<CT> + readonly + name="refund_deadline" + label={i18n`Refund deadline`} + tooltip={i18n`after this deadline has passed no refunds will be accepted`} + /> + <InputDate<CT> + readonly + name="pay_deadline" + label={i18n`Payment deadline`} + tooltip={i18n`after this deadline, the merchant won't accept payments for the contract`} + /> + <InputDate<CT> + readonly + name="wire_transfer_deadline" + label={i18n`Wire transfer deadline`} + tooltip={i18n`transfer deadline for the exchange`} + /> + <InputDate<CT> + readonly + name="delivery_date" + label={i18n`Delivery date`} + tooltip={i18n`time indicating when the order should be delivered`} + /> + {value.delivery_date && ( + <InputGroup + name="delivery_location" + label={i18n`Location`} + tooltip={i18n`where the order will be delivered`} + > + <InputLocation name="payments.delivery_location" /> + </InputGroup> + )} + <InputDuration<CT> + readonly + name="auto_refund" + label={i18n`Auto-refund delay`} + tooltip={i18n`how long the wallet should try to get an automatic refund for the purchase`} + /> + <Input<CT> + readonly + name="extra" + label={i18n`Extra info`} + tooltip={i18n`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_ms !== 'never') { +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_ms), - description: 'order created', - type: 'start' - }) + when: new Date(order.contract_terms.timestamp.t_s), + description: "order created", + type: "start", + }); } - if (order.contract_terms.pay_deadline.t_ms !== 'never') { + if (order.contract_terms.pay_deadline.t_s !== "never") { events.push({ - when: new Date(order.contract_terms.pay_deadline.t_ms), - description: 'pay deadline', - type: 'deadline' - }) + when: new Date(order.contract_terms.pay_deadline.t_s), + description: "pay deadline", + type: "deadline", + }); } - if (order.contract_terms.refund_deadline.t_ms !== 'never') { + if (order.contract_terms.refund_deadline.t_s !== "never") { events.push({ - when: new Date(order.contract_terms.refund_deadline.t_ms), - description: 'refund deadline', - type: 'deadline' - }) + when: new Date(order.contract_terms.refund_deadline.t_s), + description: "refund deadline", + type: "deadline", + }); } - if (order.contract_terms.wire_transfer_deadline.t_ms !== 'never') { + if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { events.push({ - when: new Date(order.contract_terms.wire_transfer_deadline.t_ms), - description: 'wire deadline', - type: 'deadline' - }) + when: new Date(order.contract_terms.wire_transfer_deadline.t_s), + description: "wire deadline", + type: "deadline", + }); } - if (order.contract_terms.delivery_date && order.contract_terms.delivery_date.t_ms !== 'never') { + 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_ms), - description: 'delivery', - type: 'delivery' - }) + when: new Date(order.contract_terms.delivery_date?.t_s), + description: "delivery", + type: "delivery", + }); } - const [value, valueHandler] = useState<Partial<Claimed>>(order) - const i18n = useTranslator() - - 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"> - <Translate>Order</Translate> #{id} - <div class="tag is-info ml-4"><Translate>claimed</Translate></div> + const [value, valueHandler] = useState<Partial<Claimed>>(order); + const i18n = useTranslator(); + + 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"> + <Translate>Order</Translate> #{id} + <div class="tag is-info ml-4"> + <Translate>claimed</Translate> + </div> + </div> </div> </div> - </div> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title"> - {order.contract_terms.amount} - </h1> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title">{order.contract_terms.amount}</h1> + </div> </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><Translate>claimed at</Translate>:</b> {format(new Date(order.contract_terms.timestamp.t_ms), 'yyyy-MM-dd HH:mm:ss')}</p> + <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> + <Translate>claimed at</Translate>: + </b>{" "} + {format( + new Date(order.contract_terms.timestamp.t_s), + "yyyy-MM-dd HH:mm:ss" + )} + </p> + </div> </div> </div> </div> </div> - </div> - </section> + </section> - <section class="section"> - <div class="columns"> - <div class="column is-4"> - <div class="title"><Translate>Timeline</Translate></div> - <Timeline events={events} /> - </div> - <div class="column is-8" > - <div class="title"><Translate>Payment details</Translate></div> - <FormProvider<Claimed> object={value} valueHandler={valueHandler} > - <Input name="contract_terms.summary" readonly inputType="multiline" label={i18n`Summary`} /> - <InputCurrency name="contract_terms.amount" readonly label={i18n`Amount`} /> - <Input<Claimed> name="order_status" readonly label={i18n`Order status`} /> - </FormProvider> + <section class="section"> + <div class="columns"> + <div class="column is-4"> + <div class="title"> + <Translate>Timeline</Translate> + </div> + <Timeline events={events} /> + </div> + <div class="column is-8"> + <div class="title"> + <Translate>Payment details</Translate> + </div> + <FormProvider<Claimed> + object={value} + valueHandler={valueHandler} + > + <Input + name="contract_terms.summary" + readonly + inputType="multiline" + label={i18n`Summary`} + /> + <InputCurrency + name="contract_terms.amount" + readonly + label={i18n`Amount`} + /> + <Input<Claimed> + name="order_status" + readonly + label={i18n`Order status`} + /> + </FormProvider> + </div> </div> - </div> - </section> + </section> - {order.contract_terms.products.length ? <Fragment> - <div class="title"><Translate>Product list</Translate></div> - <ProductList list={order.contract_terms.products} /> - </Fragment> : undefined} + {order.contract_terms.products.length ? ( + <Fragment> + <div class="title"> + <Translate>Product list</Translate> + </div> + <ProductList list={order.contract_terms.products} /> + </Fragment> + ) : undefined} - {value.contract_terms && <ContractTerms value={value.contract_terms} />} + {value.contract_terms && ( + <ContractTerms value={value.contract_terms} /> + )} + </div> + <div class="column" /> </div> - <div class="column" /> - </div> - </section> - </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_ms !== 'never') { +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_ms), - description: 'order created', - type: 'start' - }) + when: new Date(order.contract_terms.timestamp.t_s), + description: "order created", + type: "start", + }); } - if (order.contract_terms.pay_deadline.t_ms !== 'never') { + if (order.contract_terms.pay_deadline.t_s !== "never") { events.push({ - when: new Date(order.contract_terms.pay_deadline.t_ms), - description: 'pay deadline', - type: 'deadline' - }) - + when: new Date(order.contract_terms.pay_deadline.t_s), + description: "pay deadline", + type: "deadline", + }); } - if (order.contract_terms.refund_deadline.t_ms !== 'never') { + if (order.contract_terms.refund_deadline.t_s !== "never") { events.push({ - when: new Date(order.contract_terms.refund_deadline.t_ms), - description: 'refund deadline', - type: 'deadline' - }) + when: new Date(order.contract_terms.refund_deadline.t_s), + description: "refund deadline", + type: "deadline", + }); } - if (order.contract_terms.wire_transfer_deadline.t_ms !== 'never') { + if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { events.push({ - when: new Date(order.contract_terms.wire_transfer_deadline.t_ms), - description: 'wire deadline', - type: 'deadline' - }) + when: new Date(order.contract_terms.wire_transfer_deadline.t_s), + description: "wire deadline", + type: "deadline", + }); } - if (order.contract_terms.delivery_date && order.contract_terms.delivery_date.t_ms !== 'never') { - if (order.contract_terms.delivery_date) events.push({ - when: new Date(order.contract_terms.delivery_date?.t_ms), - description: 'delivery', - type: 'delivery' - }) + 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), + description: "delivery", + type: "delivery", + }); } - order.refund_details.reduce(mergeRefunds, []).forEach(e => { + order.refund_details.reduce(mergeRefunds, []).forEach((e) => { events.push({ - when: new Date(e.timestamp.t_ms), + when: new Date(e.timestamp.t_s), description: `refund: ${e.amount}: ${e.reason}`, - type: 'refund', - }) - }) + type: "refund", + }); + }); 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 + 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_ms < w.execution_time.t_ms) { - last = w + 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_ms > w.execution_time.t_ms) { - first = 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 - }) + total = + total === null + ? Amounts.parseOrThrow(w.amount) + : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount; + }); events.push({ - when: new Date(last!.execution_time.t_ms), + when: new Date(last!.execution_time.t_s), description: `wired ${Amounts.stringify(total!)}`, - type: 'wired-range', - }) + type: "wired-range", + }); events.push({ - when: new Date(first!.execution_time.t_ms), + when: new Date(first!.execution_time.t_s), description: `wire transfer started...`, - type: 'wired-range', - }) + type: "wired-range", + }); } else { - order.wire_details.forEach(e => { + order.wire_details.forEach((e) => { events.push({ - when: new Date(e.execution_time.t_ms), + when: new Date(e.execution_time.t_s), description: `wired ${e.amount}`, - type: 'wired', - }) - }) - + type: "wired", + }); + }); } - } - const [value, valueHandler] = useState<Partial<Paid>>(order) - - const refundable = new Date().getTime() < order.contract_terms.refund_deadline.t_ms - const i18n = useTranslator() - - 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"> - <Translate>Order</Translate> #{id} - <div class="tag is-success ml-4"><Translate>paid</Translate></div> - {order.wired ? - <div class="tag is-success ml-4"><Translate>wired</Translate></div> : null - } - {order.refunded ? - <div class="tag is-danger ml-4"><Translate>refunded</Translate></div> : null - } + const [value, valueHandler] = useState<Partial<Paid>>(order); + + const refundable = + new Date().getTime() < order.contract_terms.refund_deadline.t_s; + const i18n = useTranslator(); + + 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"> + <Translate>Order</Translate> #{id} + <div class="tag is-success ml-4"> + <Translate>paid</Translate> + </div> + {order.wired ? ( + <div class="tag is-success ml-4"> + <Translate>wired</Translate> + </div> + ) : null} + {order.refunded ? ( + <div class="tag is-danger ml-4"> + <Translate>refunded</Translate> + </div> + ) : null} + </div> </div> </div> - </div> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title"> - {order.contract_terms.amount} - </h1> + <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-right"> - <div class="level-item"> - <h1 class="title"> - <div class="buttons"> - <span class="has-tooltip-left" data-tooltip={refundable ? i18n`refund order` : i18n`not refundable`}> - <button class="button is-danger" disabled={!refundable} onClick={() => onRefund(id)}><Translate>refund</Translate></button> - </span> - </div> - </h1> + <div class="level-right"> + <div class="level-item"> + <h1 class="title"> + <div class="buttons"> + <span + class="has-tooltip-left" + data-tooltip={ + refundable + ? i18n`refund order` + : i18n`not refundable` + } + > + <button + class="button is-danger" + disabled={!refundable} + onClick={() => onRefund(id)} + > + <Translate>refund</Translate> + </button> + </span> + </div> + </h1> + </div> </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', - // maxWidth: '100%', - }}> - <p><a href={order.contract_terms.fulfillment_url} rel="nofollow" target="new">{order.contract_terms.fulfillment_url}</a></p> - <p>{format(new Date(order.contract_terms.timestamp.t_ms), 'yyyy/MM/dd HH:mm:ss')}</p> + <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", + // maxWidth: '100%', + }} + > + <p> + <a + href={order.contract_terms.fulfillment_url} + rel="nofollow" + target="new" + > + {order.contract_terms.fulfillment_url} + </a> + </p> + <p> + {format( + new Date(order.contract_terms.timestamp.t_s), + "yyyy/MM/dd HH:mm:ss" + )} + </p> + </div> </div> </div> </div> </div> - </div> - </section> + </section> - <section class="section"> - <div class="columns"> - <div class="column is-4"> - <div class="title"><Translate>Timeline</Translate></div> - <Timeline events={events} /> - </div> - <div class="column is-8" > - <div class="title"><Translate>Payment details</Translate></div> - <FormProvider<Paid> object={value} valueHandler={valueHandler} > - {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n`Deposit total`} /> */} - {order.refunded && <InputCurrency<Paid> name="refund_amount" readonly label={i18n`Refunded amount`} />} - <Input<Paid> name="order_status" readonly label={i18n`Order status`} /> - <TextField<Paid> name="order_status_url" label={i18n`Status URL`} > - <a target="_blank" rel="noreferrer" href={order.order_status_url}> - {order.order_status_url} - </a> - </TextField> - </FormProvider> + <section class="section"> + <div class="columns"> + <div class="column is-4"> + <div class="title"> + <Translate>Timeline</Translate> + </div> + <Timeline events={events} /> + </div> + <div class="column is-8"> + <div class="title"> + <Translate>Payment details</Translate> + </div> + <FormProvider<Paid> + object={value} + valueHandler={valueHandler} + > + {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n`Deposit total`} /> */} + {order.refunded && ( + <InputCurrency<Paid> + name="refund_amount" + readonly + label={i18n`Refunded amount`} + /> + )} + <Input<Paid> + name="order_status" + readonly + label={i18n`Order status`} + /> + <TextField<Paid> + name="order_status_url" + label={i18n`Status URL`} + > + <a + target="_blank" + rel="noreferrer" + href={order.order_status_url} + > + {order.order_status_url} + </a> + </TextField> + </FormProvider> + </div> </div> - </div> - </section> + </section> + {order.contract_terms.products.length ? ( + <Fragment> + <div class="title"> + <Translate>Product list</Translate> + </div> + <ProductList list={order.contract_terms.products} /> + </Fragment> + ) : undefined} - {order.contract_terms.products.length ? <Fragment> - <div class="title"><Translate>Product list</Translate></div> - <ProductList list={order.contract_terms.products} /> - </Fragment> : undefined} - - {value.contract_terms && <ContractTerms value={value.contract_terms} />} + {value.contract_terms && ( + <ContractTerms value={value.contract_terms} /> + )} + </div> + <div class="column" /> </div> - <div class="column" /> - </div> - </section> - </div> + </section> + </div> + ); } -function UnpaidPage({ id, order }: { id: string; order: MerchantBackend.Orders.CheckPaymentUnpaidResponse }) { - const [value, valueHandler] = useState<Partial<Unpaid>>(order) - const i18n = useTranslator() - 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"> - <Translate>Order</Translate> #{id} - </h1> +function UnpaidPage({ + id, + order, +}: { + id: string; + order: MerchantBackend.Orders.CheckPaymentUnpaidResponse; +}) { + const [value, valueHandler] = useState<Partial<Unpaid>>(order); + const i18n = useTranslator(); + 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"> + <Translate>Order</Translate> #{id} + </h1> + </div> + <div class="tag is-dark"> + <Translate>unpaid</Translate> + </div> </div> - <div class="tag is-dark"><Translate>unpaid</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><Translate>pay at</Translate>:</b> <a href={order.order_status_url} rel="nofollow" target="new">{order.order_status_url}</a></p> - <p><b><Translate>created at</Translate>:</b> {format(new Date(order.creation_time.t_ms), 'yyyy-MM-dd HH:mm:ss')}</p> + <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> + <Translate>pay at</Translate>: + </b>{" "} + <a + href={order.order_status_url} + rel="nofollow" + target="new" + > + {order.order_status_url} + </a> + </p> + <p> + <b> + <Translate>created at</Translate>: + </b>{" "} + {format( + new Date(order.creation_time.t_s), + "yyyy-MM-dd HH:mm:ss" + )} + </p> + </div> </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`Summary`} tooltip={i18n`human-readable description of the whole purchase`} /> - <InputCurrency<Unpaid> readonly name="total_amount" label={i18n`Amount`} tooltip={i18n`total price for the transaction`} /> - <Input<Unpaid> name="order_status" readonly label={i18n`Order status`} /> - <Input<Unpaid> name="order_status_url" readonly label={i18n`Order status URL`} /> - <Input<Unpaid> name="taler_pay_uri" readonly label={i18n`Payment URI`} /> - </FormProvider> + </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`Summary`} + tooltip={i18n`human-readable description of the whole purchase`} + /> + <InputCurrency<Unpaid> + readonly + name="total_amount" + label={i18n`Amount`} + tooltip={i18n`total price for the transaction`} + /> + <Input<Unpaid> + name="order_status" + readonly + label={i18n`Order status`} + /> + <Input<Unpaid> + name="order_status_url" + readonly + label={i18n`Order status URL`} + /> + <Input<Unpaid> + name="taler_pay_uri" + readonly + label={i18n`Payment URI`} + /> + </FormProvider> + </div> + <div class="column" /> </div> - <div class="column" /> - </div> - </section> - - </div> + </section> + </div> + ); } export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { - const [showRefund, setShowRefund] = useState<string | undefined>(undefined) + const [showRefund, setShowRefund] = useState<string | undefined>(undefined); 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><Translate>Unknown order status. This is an error, please contact the administrator.</Translate></div> + 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> + <Translate> + Unknown order status. This is an error, please contact the + administrator. + </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}><Translate>Back</Translate></button> + }; + + 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}> + <Translate>Back</Translate> + </button> + </div> </div> + <div class="column" /> </div> - <div class="column" /> - </div> - - </Fragment> + </Fragment> + ); } async function copyToClipboard(text: string) { - return navigator.clipboard.writeText(text) + return navigator.clipboard.writeText(text); } - - diff --git a/packages/merchant-backoffice/src/paths/instance/orders/list/List.stories.tsx b/packages/merchant-backoffice/src/paths/instance/orders/list/List.stories.tsx @@ -15,83 +15,93 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h, VNode, FunctionalComponent } from 'preact'; -import { ListPage as TestedComponent } from './ListPage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode, FunctionalComponent } from "preact"; +import { ListPage as TestedComponent } from "./ListPage"; export default { - title: 'Pages/Order/List', + 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' }, + 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 +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_ms: new Date().getTime() + orders: [ + { + id: "123", + amount: "TESTKUDOS:10", + paid: false, + refundable: true, + row_id: 1, + summary: "summary", + timestamp: { + t_s: new Date().getTime(), + }, + order_id: "123", }, - 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_ms: new Date().getTime() + { + 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(), + }, + order_id: "234", }, - 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_ms: new Date().getTime() + { + 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(), + }, + order_id: "456", }, - 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_ms: new Date().getTime() + { + 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(), + }, + order_id: "234", }, - order_id: '234' - }] + ], }); diff --git a/packages/merchant-backoffice/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice/src/paths/instance/orders/list/Table.tsx @@ -15,14 +15,17 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { format } from "date-fns"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; -import { FormProvider, FormErrors } from "../../../../components/form/FormProvider"; +import { + FormProvider, + FormErrors, +} from "../../../../components/form/FormProvider"; import { Input } from "../../../../components/form/Input"; import { InputCurrency } from "../../../../components/form/InputCurrency"; import { InputGroup } from "../../../../components/form/InputGroup"; @@ -34,9 +37,9 @@ import { RefundSchema } from "../../../../schemas"; import { mergeRefunds } from "../../../../utils/amount"; import { Amounts } from "@gnu-taler/taler-util"; import { useConfigContext } from "../../../../context/config"; -import * as yup from 'yup'; +import * as yup from "yup"; -type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId +type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId; interface Props { orders: Entity[]; onRefund: (value: Entity) => void; @@ -49,43 +52,67 @@ interface Props { onLoadMoreAfter?: () => void; } +export function CardTable({ + orders, + onCreate, + onRefund, + onCopyURL, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); -export function CardTable({ orders, onCreate, onRefund, onCopyURL, onSelect, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string[]>([]) - - const i18n = useTranslator() + const i18n = useTranslator(); - 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><Translate>Orders</Translate></p> + 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> + <Translate>Orders</Translate> + </p> - <div class="card-header-icon" aria-label="more options" /> + <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`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 class="card-header-icon" aria-label="more options"> + <span class="has-tooltip-left" data-tooltip={i18n`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> - </div> + ); } interface TableProps { rowSelection: string[]; @@ -100,130 +127,267 @@ interface TableProps { onLoadMoreAfter?: () => void; } -function Table({ instances, onSelect, onRefund, onCopyURL, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode { - return <div class="table-container"> - {onLoadMoreBefore && <button class="button is-fullwidth" disabled={!hasMoreBefore} onClick={onLoadMoreBefore}><Translate>load newer orders</Translate></button>} - <table class="table is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th style={{ minWidth: 100 }}><Translate>Date</Translate></th> - <th style={{ minWidth: 100 }}><Translate>Amount</Translate></th> - <th style={{ minWidth: 400 }}><Translate>Summary</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' }} >{format(new Date(i.timestamp.t_ms), 'yyyy/MM/dd HH:mm:ss')}</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)}> - <Translate>Refund</Translate> - </button> - } - {(!i.paid) && - <button class="button is-small is-info jb-modal" type="button" onClick={(): void => onCopyURL(i)}> - <Translate>copy url</Translate> - </button> - } - </div> - </td> +function Table({ + instances, + onSelect, + onRefund, + onCopyURL, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + return ( + <div class="table-container"> + {onLoadMoreBefore && ( + <button + class="button is-fullwidth" + disabled={!hasMoreBefore} + onClick={onLoadMoreBefore} + > + <Translate>load newer orders</Translate> + </button> + )} + <table class="table is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th style={{ minWidth: 100 }}> + <Translate>Date</Translate> + </th> + <th style={{ minWidth: 100 }}> + <Translate>Amount</Translate> + </th> + <th style={{ minWidth: 400 }}> + <Translate>Summary</Translate> + </th> + <th style={{ minWidth: 50 }} /> </tr> - })} - </tbody> - </table> - {onLoadMoreAfter && <button class="button is-fullwidth" disabled={!hasMoreAfter} onClick={onLoadMoreAfter}><Translate>load older orders</Translate></button>} - </div> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {format(new Date(i.timestamp.t_s), "yyyy/MM/dd HH:mm:ss")} + </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)} + > + <Translate>Refund</Translate> + </button> + )} + {!i.paid && ( + <button + class="button is-small is-info jb-modal" + type="button" + onClick={(): void => onCopyURL(i)} + > + <Translate>copy url</Translate> + </button> + )} + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {onLoadMoreAfter && ( + <button + class="button is-fullwidth" + disabled={!hasMoreAfter} + onClick={onLoadMoreAfter} + > + <Translate>load older orders</Translate> + </button> + )} + </div> + ); } function EmptyTable(): VNode { - 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><Translate>No orders have been found matching your query!</Translate></p> - </div> + 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> + <Translate>No orders have been found matching your query!</Translate> + </p> + </div> + ); } interface RefundModalProps { onCancel: () => void; onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void; - order: MerchantBackend.Orders.MerchantOrderStatusResponse + 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>({}) +export function RefundModal({ + order, + onCancel, + onConfirm, +}: RefundModalProps): VNode { + type State = { mainReason?: string; description?: string; refund?: string }; + const [form, setValue] = useState<State>({}); const i18n = useTranslator(); - const [errors, setErrors] = useState<FormErrors<State>>({}) + const [errors, setErrors] = useState<FormErrors<State>>({}); const validateAndConfirm = () => { try { - RefundSchema.validateSync(form, { abortEarly: false }) + RefundSchema.validateSync(form, { abortEarly: false }); if (!form.refund) return; onConfirm({ refund: form.refund, - reason: `${form.mainReason}: ${form.description}` - }) + reason: `${form.mainReason}: ${form.description}`, + }); } catch (err) { if (err instanceof yup.ValidationError) { - const errors = err.inner as any[] - const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) - setErrors(pathMessages) + const errors = err.inner as any[]; + const pathMessages = errors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {} + ); + setErrors(pathMessages); } else { - console.log(err) + console.log(err); } } - } + }; - const refunds = (order.order_status === 'paid' ? order.refund_details : []).reduce(mergeRefunds, []) + 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.getZero(config.currency) ) - const orderPrice = (order.order_status === 'paid' ? Amounts.parseOrThrow(order.contract_terms.amount) : undefined) - const totalRefundable = !orderPrice ? Amounts.getZero(totalRefunded.currency) : (refunds.length ? Amounts.sub(orderPrice, totalRefunded).amount : orderPrice) + const config = useConfigContext(); + const totalRefunded = refunds + .map((r) => r.amount) + .reduce( + (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount, + Amounts.getZero(config.currency) + ); + const orderPrice = + order.order_status === "paid" + ? Amounts.parseOrThrow(order.contract_terms.amount) + : undefined; + const totalRefundable = !orderPrice + ? Amounts.getZero(totalRefunded.currency) + : refunds.length + ? Amounts.sub(orderPrice, totalRefunded).amount + : orderPrice; - const isRefundable = Amounts.isNonZero(totalRefundable) + const isRefundable = Amounts.isNonZero(totalRefundable); //FIXME: parameters in the translation - return <ConfirmModal description="refund" danger active onCancel={onCancel} onConfirm={validateAndConfirm}> - {refunds.length > 0 && <div class="columns"> - <div class="column is-2" /> - <div class="column is-8"> - <InputGroup name="asd" label={`${totalRefunded} was already refunded`}> - <table class="table is-fullwidth"> - <thead> - <tr> - <th><Translate>date</Translate></th> - <th><Translate>amount</Translate></th> - <th><Translate>reason</Translate></th> - </tr> - </thead> - <tbody> - {refunds.map(r => { - return <tr key={r.timestamp.t_ms}> - <td>{format(new Date(r.timestamp.t_ms), 'yyyy-MM-dd HH:mm:ss')}</td> - <td>{r.amount}</td> - <td>{r.reason}</td> - </tr> - })} - </tbody> - </table> - </InputGroup> - </div> - <div class="column is-2" /> - </div>} - - {isRefundable && <FormProvider<State> errors={errors} object={form} valueHandler={(d) => setValue(d as any)}> - <InputCurrency<State> name="refund" label={i18n`Refund`} tooltip={i18n`amount to be refunded`}> - <Translate>Max refundable:</Translate> {Amounts.stringify(totalRefundable)} - </InputCurrency> - <InputSelector name="mainReason" label={i18n`Reason`} values={[i18n`duplicated`, i18n`requested by the customer`, i18n`other`]} tooltip={i18n`why this order is being refunded`} /> - {form.mainReason && <Input<State> label={i18n`Description`} name="description" tooltip={i18n`more information to give context`} />} - </FormProvider>} - - </ConfirmModal> + return ( + <ConfirmModal + description="refund" + danger + active + onCancel={onCancel} + onConfirm={validateAndConfirm} + > + {refunds.length > 0 && ( + <div class="columns"> + <div class="column is-2" /> + <div class="column is-8"> + <InputGroup + name="asd" + label={`${totalRefunded} was already refunded`} + > + <table class="table is-fullwidth"> + <thead> + <tr> + <th> + <Translate>date</Translate> + </th> + <th> + <Translate>amount</Translate> + </th> + <th> + <Translate>reason</Translate> + </th> + </tr> + </thead> + <tbody> + {refunds.map((r) => { + return ( + <tr key={r.timestamp.t_s}> + <td> + {format( + new Date(r.timestamp.t_s), + "yyyy-MM-dd HH:mm:ss" + )} + </td> + <td>{r.amount}</td> + <td>{r.reason}</td> + </tr> + ); + })} + </tbody> + </table> + </InputGroup> + </div> + <div class="column is-2" /> + </div> + )} + + {isRefundable && ( + <FormProvider<State> + errors={errors} + object={form} + valueHandler={(d) => setValue(d as any)} + > + <InputCurrency<State> + name="refund" + label={i18n`Refund`} + tooltip={i18n`amount to be refunded`} + > + <Translate>Max refundable:</Translate>{" "} + {Amounts.stringify(totalRefundable)} + </InputCurrency> + <InputSelector + name="mainReason" + label={i18n`Reason`} + values={[ + i18n`duplicated`, + i18n`requested by the customer`, + i18n`other`, + ]} + tooltip={i18n`why this order is being refunded`} + /> + {form.mainReason && ( + <Input<State> + label={i18n`Description`} + name="description" + tooltip={i18n`more information to give context`} + /> + )} + </FormProvider> + )} + </ConfirmModal> + ); } diff --git a/packages/merchant-backoffice/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice/src/paths/instance/products/list/Table.tsx @@ -15,146 +15,290 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { format } from "date-fns" -import { ComponentChildren, Fragment, h, VNode } from "preact" -import { StateUpdater, useState } from "preact/hooks" -import { FormProvider, FormErrors } from "../../../../components/form/FormProvider" -import { InputCurrency } from "../../../../components/form/InputCurrency" -import { InputNumber } from "../../../../components/form/InputNumber" -import { MerchantBackend, WithId } from "../../../../declaration" + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { format } from "date-fns"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { + FormProvider, + FormErrors, +} from "../../../../components/form/FormProvider"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { InputNumber } from "../../../../components/form/InputNumber"; +import { MerchantBackend, WithId } from "../../../../declaration"; import emptyImage from "../../../../assets/empty.png"; -import { Translate, useTranslator } from "../../../../i18n" -import { Amounts } from "@gnu-taler/taler-util" +import { Translate, useTranslator } from "../../../../i18n"; +import { Amounts } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.Products.ProductDetail & WithId +type Entity = MerchantBackend.Products.ProductDetail & WithId; interface Props { instances: Entity[]; onDelete: (id: Entity) => void; onSelect: (product: Entity) => void; - onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => Promise<void>; + onUpdate: ( + id: string, + data: MerchantBackend.Products.ProductPatchDetail + ) => Promise<void>; onCreate: () => void; selected?: boolean; } -export function CardTable({ instances, onCreate, onSelect, onUpdate, onDelete }: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string | undefined>(undefined) - const i18n = useTranslator() - return <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-shopping" /></span><Translate>Products</Translate></p> - <div class="card-header-icon" aria-label="more options"> - <span class="has-tooltip-left" data-tooltip={i18n`add product to inventory`}> - <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"> - {instances.length > 0 ? - <Table instances={instances} onSelect={onSelect} onDelete={onDelete} onUpdate={onUpdate} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : - <EmptyTable /> - } +export function CardTable({ + instances, + onCreate, + onSelect, + onUpdate, + onDelete, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string | undefined>( + undefined + ); + const i18n = useTranslator(); + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-shopping" /> + </span> + <Translate>Products</Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n`add product to inventory`} + > + <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"> + {instances.length > 0 ? ( + <Table + instances={instances} + onSelect={onSelect} + onDelete={onDelete} + onUpdate={onUpdate} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + /> + ) : ( + <EmptyTable /> + )} + </div> </div> </div> </div> - </div> + ); } interface TableProps { rowSelection: string | undefined; instances: Entity[]; onSelect: (id: Entity) => void; - onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => Promise<void>; + onUpdate: ( + id: string, + data: MerchantBackend.Products.ProductPatchDetail + ) => Promise<void>; onDelete: (id: Entity) => void; rowSelectionHandler: StateUpdater<string | undefined>; } -function Table({ rowSelection, rowSelectionHandler, instances, onSelect, onUpdate, onDelete }: TableProps): VNode { - const i18n = useTranslator() +function Table({ + rowSelection, + rowSelectionHandler, + instances, + onSelect, + onUpdate, + onDelete, +}: TableProps): VNode { + const i18n = useTranslator(); return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> - <th><Translate>Image</Translate></th> - <th><Translate>Description</Translate></th> - <th><Translate>Sell</Translate></th> - <th><Translate>Taxes</Translate></th> - <th><Translate>Profit</Translate></th> - <th><Translate>Stock</Translate></th> - <th><Translate>Sold</Translate></th> + <th> + <Translate>Image</Translate> + </th> + <th> + <Translate>Description</Translate> + </th> + <th> + <Translate>Sell</Translate> + </th> + <th> + <Translate>Taxes</Translate> + </th> + <th> + <Translate>Profit</Translate> + </th> + <th> + <Translate>Stock</Translate> + </th> + <th> + <Translate>Sold</Translate> + </th> <th /> </tr> </thead> <tbody> - {instances.map(i => { - - const restStockInfo = !i.next_restock ? '' : ( - i.next_restock.t_ms === 'never' ? - 'never' : - `restock at ${format(new Date(i.next_restock.t_ms), 'yyyy/MM/dd')}` - ) - let stockInfo: ComponentChildren = ''; + {instances.map((i) => { + const restStockInfo = !i.next_restock + ? "" + : i.next_restock.t_s === "never" + ? "never" + : `restock at ${format( + new Date(i.next_restock.t_s), + "yyyy/MM/dd" + )}`; + let stockInfo: ComponentChildren = ""; if (i.total_stock < 0) { - stockInfo = 'infinite' + stockInfo = "infinite"; } else { - const totalStock = i.total_stock - i.total_lost - i.total_sold - stockInfo = <label title={restStockInfo}>{totalStock} {i.unit}</label> + const totalStock = i.total_stock - i.total_lost - i.total_sold; + stockInfo = ( + <label title={restStockInfo}> + {totalStock} {i.unit} + </label> + ); } - const isFree = Amounts.parseOrThrow(i.price).value === 0 - - return <Fragment key={i.id}><tr key="info"> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} > - <img src={i.image ? i.image : emptyImage} style={{ border: 'solid black 1px', width: 100, height: 100 }} /> - </td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.description}</td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} > - {isFree ? i18n`free` : `${i.price} / ${i.unit}`} - </td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{sum(i.taxes)}</td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{difference(i.price, sum(i.taxes))}</td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{stockInfo}</td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_sold} {i.unit}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <span class="has-tooltip-bottom" data-tooltip={i18n`go to product update page`}> - <button class="button is-small is-success " type="button" onClick={(): void => onSelect(i)}> - <Translate>Update</Translate> - </button> - </span> - <span class="has-tooltip-left" data-tooltip={i18n`remove this product from the database`}> - <button class="button is-small is-danger" type="button" onClick={(): void => onDelete(i)}> - <Translate>Delete</Translate> - </button> - </span> - </div> - </td> - </tr> - {rowSelection === i.id && <tr key="form"> - <td colSpan={10} > - <FastProductUpdateForm product={i} onUpdate={(prod) => onUpdate(i.id, prod).then(r => rowSelectionHandler(undefined))} onCancel={() => rowSelectionHandler(undefined)} /> - </td> - </tr>} - </Fragment> - })} + const isFree = Amounts.parseOrThrow(i.price).value === 0; + return ( + <Fragment key={i.id}> + <tr key="info"> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + <img + src={i.image ? i.image : emptyImage} + style={{ + border: "solid black 1px", + width: 100, + height: 100, + }} + /> + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {i.description} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {isFree ? i18n`free` : `${i.price} / ${i.unit}`} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {sum(i.taxes)} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {difference(i.price, sum(i.taxes))} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {stockInfo} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {i.total_sold} {i.unit} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <span + class="has-tooltip-bottom" + data-tooltip={i18n`go to product update page`} + > + <button + class="button is-small is-success " + type="button" + onClick={(): void => onSelect(i)} + > + <Translate>Update</Translate> + </button> + </span> + <span + class="has-tooltip-left" + data-tooltip={i18n`remove this product from the database`} + > + <button + class="button is-small is-danger" + type="button" + onClick={(): void => onDelete(i)} + > + <Translate>Delete</Translate> + </button> + </span> + </div> + </td> + </tr> + {rowSelection === i.id && ( + <tr key="form"> + <td colSpan={10}> + <FastProductUpdateForm + product={i} + onUpdate={(prod) => + onUpdate(i.id, prod).then((r) => + rowSelectionHandler(undefined) + ) + } + onCancel={() => rowSelectionHandler(undefined)} + /> + </td> + </tr> + )} + </Fragment> + ); + })} </tbody> </table> - </div>) + </div> + ); } interface FastProductUpdateFormProps { product: Entity; - onUpdate: (data: MerchantBackend.Products.ProductPatchDetail) => Promise<void>; + onUpdate: ( + data: MerchantBackend.Products.ProductPatchDetail + ) => Promise<void>; onCancel: () => void; } interface FastProductUpdate { @@ -166,90 +310,170 @@ interface UpdatePrice { price: string; } -function FastProductWithInfiniteStockUpdateForm({ product, onUpdate, onCancel }: FastProductUpdateFormProps) { - const [value, valueHandler] = useState<UpdatePrice>({ price: product.price }) - const i18n = useTranslator() - - return <Fragment> - <FormProvider<FastProductUpdate> name="added" object={value} valueHandler={valueHandler as any} > - <InputCurrency<FastProductUpdate> name="price" label={i18n`Price`} tooltip={i18n`update the product with new price`} /> - </FormProvider> - - <div class="buttons is-right mt-5"> - <button class="button" onClick={onCancel} ><Translate>Cancel</Translate></button> - <span class="has-tooltip-left" data-tooltip={i18n`update product with new price`}> - <button class="button is-info" onClick={() => onUpdate({ - ...product, - price: value.price, - })}><Translate>Confirm</Translate></button> - </span> - </div> +function FastProductWithInfiniteStockUpdateForm({ + product, + onUpdate, + onCancel, +}: FastProductUpdateFormProps) { + const [value, valueHandler] = useState<UpdatePrice>({ price: product.price }); + const i18n = useTranslator(); + + return ( + <Fragment> + <FormProvider<FastProductUpdate> + name="added" + object={value} + valueHandler={valueHandler as any} + > + <InputCurrency<FastProductUpdate> + name="price" + label={i18n`Price`} + tooltip={i18n`update the product with new price`} + /> + </FormProvider> - </Fragment> + <div class="buttons is-right mt-5"> + <button class="button" onClick={onCancel}> + <Translate>Cancel</Translate> + </button> + <span + class="has-tooltip-left" + data-tooltip={i18n`update product with new price`} + > + <button + class="button is-info" + onClick={() => + onUpdate({ + ...product, + price: value.price, + }) + } + > + <Translate>Confirm</Translate> + </button> + </span> + </div> + </Fragment> + ); } -function FastProductWithManagedStockUpdateForm({ product, onUpdate, onCancel }: FastProductUpdateFormProps) { +function FastProductWithManagedStockUpdateForm({ + product, + onUpdate, + onCancel, +}: FastProductUpdateFormProps) { const [value, valueHandler] = useState<FastProductUpdate>({ - incoming: 0, lost: 0, price: product.price - }) + incoming: 0, + lost: 0, + price: product.price, + }); - const currentStock = product.total_stock - product.total_sold - product.total_lost + const currentStock = + product.total_stock - product.total_sold - product.total_lost; const errors: FormErrors<FastProductUpdate> = { - lost: currentStock + value.incoming < value.lost ? - `lost cannot be greater that current + incoming (max ${currentStock + value.incoming})` - : undefined - } - - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) - const i18n = useTranslator() - - return <Fragment> - <FormProvider<FastProductUpdate> name="added" errors={errors} object={value} valueHandler={valueHandler as any} > - <InputNumber<FastProductUpdate> name="incoming" label={i18n`Incoming`} tooltip={i18n`add more elements to the inventory`} /> - <InputNumber<FastProductUpdate> name="lost" label={i18n`Lost`} tooltip={i18n`report elements lost in the inventory`} /> - <InputCurrency<FastProductUpdate> name="price" label={i18n`Price`} tooltip={i18n`new price for the product`} /> - </FormProvider> - - <div class="buttons is-right mt-5"> - <button class="button" onClick={onCancel} ><Translate>Cancel</Translate></button> - <span class="has-tooltip-left" data-tooltip={hasErrors ? i18n`the are value with errors` : i18n`update product with new stock and price`}> - <button class="button is-info" disabled={hasErrors} onClick={() => onUpdate({ - ...product, - total_stock: product.total_stock + value.incoming, - total_lost: product.total_lost + value.lost, - price: value.price, - }) - }><Translate>Confirm</Translate></button> - </span> - </div> + lost: + currentStock + value.incoming < value.lost + ? `lost cannot be greater that current + incoming (max ${ + currentStock + value.incoming + })` + : undefined, + }; - </Fragment> + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined + ); + const i18n = useTranslator(); + + return ( + <Fragment> + <FormProvider<FastProductUpdate> + name="added" + errors={errors} + object={value} + valueHandler={valueHandler as any} + > + <InputNumber<FastProductUpdate> + name="incoming" + label={i18n`Incoming`} + tooltip={i18n`add more elements to the inventory`} + /> + <InputNumber<FastProductUpdate> + name="lost" + label={i18n`Lost`} + tooltip={i18n`report elements lost in the inventory`} + /> + <InputCurrency<FastProductUpdate> + name="price" + label={i18n`Price`} + tooltip={i18n`new price for the product`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + <button class="button" onClick={onCancel}> + <Translate>Cancel</Translate> + </button> + <span + class="has-tooltip-left" + data-tooltip={ + hasErrors + ? i18n`the are value with errors` + : i18n`update product with new stock and price` + } + > + <button + class="button is-info" + disabled={hasErrors} + onClick={() => + onUpdate({ + ...product, + total_stock: product.total_stock + value.incoming, + total_lost: product.total_lost + value.lost, + price: value.price, + }) + } + > + <Translate>Confirm</Translate> + </button> + </span> + </div> + </Fragment> + ); } function FastProductUpdateForm(props: FastProductUpdateFormProps) { - return props.product.total_stock === -1 ? - <FastProductWithInfiniteStockUpdateForm {...props} /> : + return props.product.total_stock === -1 ? ( + <FastProductWithInfiniteStockUpdateForm {...props} /> + ) : ( <FastProductWithManagedStockUpdateForm {...props} /> + ); } function EmptyTable(): VNode { - 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><Translate>There is no products yet, add more pressing the + sign</Translate></p> - </div> + 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> + <Translate> + There is no products yet, add more pressing the + sign + </Translate> + </p> + </div> + ); } - function difference(price: string, tax: number) { if (!tax) return price; - const ps = price.split(':') - const p = parseInt(ps[1], 10) - ps[1] = `${p - tax}` - return ps.join(':') + const ps = price.split(":"); + const p = parseInt(ps[1], 10); + ps[1] = `${p - tax}`; + return ps.join(":"); } function sum(taxes: MerchantBackend.Tax[]) { - return taxes.reduce((p, c) => p + parseInt(c.tax.split(':')[1], 10), 0) + return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0); } diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/details/DetailPage.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/details/DetailPage.tsx @@ -251,9 +251,9 @@ function TipRow({ <td>{info.total_picked_up}</td> <td>{info.reason}</td> <td> - {info.expiration.t_ms === "never" + {info.expiration.t_s === "never" ? "never" - : format(info.expiration.t_ms, "yyyy/MM/dd HH:mm:ss")} + : format(info.expiration.t_s, "yyyy/MM/dd HH:mm:ss")} </td> </tr> ); diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/details/Details.stories.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/details/Details.stories.tsx @@ -15,88 +15,91 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h, VNode, FunctionalComponent } from 'preact'; -import { DetailPage as TestedComponent } from './DetailPage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode, FunctionalComponent } from "preact"; +import { DetailPage as TestedComponent } from "./DetailPage"; export default { - title: 'Pages/Reserve/Detail', + title: "Pages/Reserve/Detail", component: TestedComponent, argTypes: { - onUpdate: { action: 'onUpdate' }, - onBack: { action: 'onBack' }, + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props> +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; } export const Funded = createExample(TestedComponent, { - id:'THISISTHERESERVEID', + id: "THISISTHERESERVEID", selected: { active: true, - committed_amount: 'TESTKUDOS:10', + committed_amount: "TESTKUDOS:10", creation_time: { - t_ms: new Date().getTime(), + t_s: new Date().getTime(), }, - exchange_initial_amount: 'TESTKUDOS:10', + exchange_initial_amount: "TESTKUDOS:10", expiration_time: { - t_ms: new Date().getTime() + t_s: new Date().getTime(), }, - merchant_initial_amount: 'TESTKUDOS:10', - pickup_amount: 'TESTKUDOS:10', - payto_uri: 'payto://x-taler-bank/bank.taler:8080/account', - exchange_url: 'http://exchange.taler/', - } + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + payto_uri: "payto://x-taler-bank/bank.taler:8080/account", + exchange_url: "http://exchange.taler/", + }, }); export const NotYetFunded = createExample(TestedComponent, { - id:'THISISTHERESERVEID', + id: "THISISTHERESERVEID", selected: { active: true, - committed_amount: 'TESTKUDOS:10', + committed_amount: "TESTKUDOS:10", creation_time: { - t_ms: new Date().getTime(), + t_s: new Date().getTime(), }, - exchange_initial_amount: 'TESTKUDOS:0', + exchange_initial_amount: "TESTKUDOS:0", expiration_time: { - t_ms: new Date().getTime() + t_s: new Date().getTime(), }, - merchant_initial_amount: 'TESTKUDOS:10', - pickup_amount: 'TESTKUDOS:10', - payto_uri: 'payto://x-taler-bank/bank.taler:8080/account', - exchange_url: 'http://exchange.taler/', - } + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + payto_uri: "payto://x-taler-bank/bank.taler:8080/account", + exchange_url: "http://exchange.taler/", + }, }); export const FundedWithEmptyTips = createExample(TestedComponent, { - id:'THISISTHERESERVEID', + id: "THISISTHERESERVEID", selected: { active: true, - committed_amount: 'TESTKUDOS:10', + committed_amount: "TESTKUDOS:10", creation_time: { - t_ms: new Date().getTime(), + t_s: new Date().getTime(), }, - exchange_initial_amount: 'TESTKUDOS:10', + exchange_initial_amount: "TESTKUDOS:10", expiration_time: { - t_ms: new Date().getTime() + t_s: new Date().getTime(), }, - merchant_initial_amount: 'TESTKUDOS:10', - pickup_amount: 'TESTKUDOS:10', - payto_uri: 'payto://x-taler-bank/bank.taler:8080/account', - exchange_url: 'http://exchange.taler/', - tips:[{ - reason: 'asdasd', - tip_id: '123', - total_amount: 'TESTKUDOS:1' - }] - } + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + payto_uri: "payto://x-taler-bank/bank.taler:8080/account", + exchange_url: "http://exchange.taler/", + tips: [ + { + reason: "asdasd", + tip_id: "123", + total_amount: "TESTKUDOS:1", + }, + ], + }, }); - diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/list/CreatedSuccessfully.tsx @@ -22,60 +22,76 @@ type Entity = MerchantBackend.Tips.TipCreateConfirmation; interface Props { entity: Entity; - request: MerchantBackend.Tips.TipCreateRequest, + request: MerchantBackend.Tips.TipCreateRequest; onConfirm: () => void; onCreateAnother?: () => void; } -export function CreatedSuccessfully({ request, entity, onConfirm, onCreateAnother }: Props): VNode { - return <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Amount</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={request.amount} /> - </p> +export function CreatedSuccessfully({ + request, + entity, + onConfirm, + onCreateAnother, +}: Props): VNode { + return ( + <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Amount</label> </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Justification</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={request.justification} /> - </p> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={request.amount} /> + </p> + </div> </div> </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">URL</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={entity.tip_status_url} /> - </p> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Justification</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={request.justification} /> + </p> + </div> </div> </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Valid until</label> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">URL</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={entity.tip_status_url} /> + </p> + </div> + </div> </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={!entity.tip_expiration || entity.tip_expiration.t_ms === "never" ? "never" : format(entity.tip_expiration.t_ms, 'yyyy/MM/dd HH:mm:ss')} /> - </p> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Valid until</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={ + !entity.tip_expiration || + entity.tip_expiration.t_s === "never" + ? "never" + : format(entity.tip_expiration.t_s, "yyyy/MM/dd HH:mm:ss") + } + /> + </p> + </div> </div> </div> - </div> - </Fragment>; + </Fragment> + ); } diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/list/List.stories.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/list/List.stories.tsx @@ -15,83 +15,88 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h, VNode, FunctionalComponent } from 'preact'; -import { CardTable as TestedComponent } from './Table'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode, FunctionalComponent } from "preact"; +import { CardTable as TestedComponent } from "./Table"; export default { - title: 'Pages/Reserve/List', + title: "Pages/Reserve/List", component: TestedComponent, argTypes: { - onCreate: { action: 'onCreate' }, - onDelete: { action: 'onDelete' }, - onNewTip: { action: 'onNewTip' }, - onSelect: { action: 'onSelect' }, + onCreate: { action: "onCreate" }, + onDelete: { action: "onDelete" }, + onNewTip: { action: "onNewTip" }, + onSelect: { action: "onSelect" }, }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props> +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; } export const AllFunded = createExample(TestedComponent, { - instances: [{ - id: 'reseverId', - active: true, - committed_amount: 'TESTKUDOS:10', - creation_time: { - t_ms: new Date().getTime(), - }, - exchange_initial_amount: 'TESTKUDOS:10', - expiration_time: { - t_ms: new Date().getTime() - }, - merchant_initial_amount: 'TESTKUDOS:10', - pickup_amount: 'TESTKUDOS:10', - reserve_pub: 'WEQWDASDQWEASDADASDQWEQWEASDAS' - },{ - id: 'reseverId2', - active: true, - committed_amount: 'TESTKUDOS:13', - creation_time: { - t_ms: new Date().getTime(), + instances: [ + { + id: "reseverId", + active: true, + committed_amount: "TESTKUDOS:10", + creation_time: { + t_s: new Date().getTime(), + }, + exchange_initial_amount: "TESTKUDOS:10", + expiration_time: { + t_s: new Date().getTime(), + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", }, - exchange_initial_amount: 'TESTKUDOS:10', - expiration_time: { - t_ms: new Date().getTime() + { + id: "reseverId2", + active: true, + committed_amount: "TESTKUDOS:13", + creation_time: { + t_s: new Date().getTime(), + }, + exchange_initial_amount: "TESTKUDOS:10", + expiration_time: { + t_s: new Date().getTime(), + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", }, - merchant_initial_amount: 'TESTKUDOS:10', - pickup_amount: 'TESTKUDOS:10', - reserve_pub: 'WEQWDASDQWEASDADASDQWEQWEASDAS' - }] + ], }); export const Empty = createExample(TestedComponent, { - instances: [] + instances: [], }); - - export const OneNotYetFunded = createExample(TestedComponent, { - instances: [{ - id: 'reseverId', - active: true, - committed_amount: 'TESTKUDOS:0', - creation_time: { - t_ms: new Date().getTime(), - }, - exchange_initial_amount: 'TESTKUDOS:0', - expiration_time: { - t_ms: new Date().getTime() + instances: [ + { + id: "reseverId", + active: true, + committed_amount: "TESTKUDOS:0", + creation_time: { + t_s: new Date().getTime(), + }, + exchange_initial_amount: "TESTKUDOS:0", + expiration_time: { + t_s: new Date().getTime(), + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", }, - merchant_initial_amount: 'TESTKUDOS:10', - pickup_amount: 'TESTKUDOS:10', - reserve_pub: 'WEQWDASDQWEASDADASDQWEQWEASDAS' - }] + ], }); diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/list/Table.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/list/Table.tsx @@ -15,16 +15,16 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { format } from "date-fns" -import { Fragment, h, VNode } from "preact" -import { MerchantBackend, WithId } from "../../../../declaration" -import { Translate, useTranslator } from "../../../../i18n" +import { format } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { MerchantBackend, WithId } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; -type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId +type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId; interface Props { instances: Entity[]; @@ -34,59 +34,90 @@ interface Props { onCreate: () => void; } -export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete }: Props): VNode { - +export function CardTable({ + instances, + onCreate, + onSelect, + onNewTip, + onDelete, +}: Props): VNode { const [withoutFunds, withFunds] = instances.reduce((prev, current) => { - const amount = current.exchange_initial_amount - if (amount.endsWith(':0')) { - prev[0] = prev[0].concat(current) + const amount = current.exchange_initial_amount; + if (amount.endsWith(":0")) { + prev[0] = prev[0].concat(current); } else { - prev[1] = prev[1].concat(current) + prev[1] = prev[1].concat(current); } - return prev - }, new Array<Array<Entity>>([], [])) + return prev; + }, new Array<Array<Entity>>([], [])); - const i18n = useTranslator() + const i18n = useTranslator(); - return <Fragment> - {withoutFunds.length > 0 && <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves not yet funded</Translate></p> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - <TableWithoutFund instances={withoutFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} /> + return ( + <Fragment> + {withoutFunds.length > 0 && ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-cash" /> + </span> + <Translate>Reserves not yet funded</Translate> + </p> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + <TableWithoutFund + instances={withoutFunds} + onNewTip={onNewTip} + onSelect={onSelect} + onDelete={onDelete} + /> + </div> + </div> </div> </div> - </div> - </div>} + )} - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves ready</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`add new reserve`}> - - <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"> - {withFunds.length > 0 ? - <Table instances={withFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} /> : - <EmptyTable /> - } + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-cash" /> + </span> + <Translate>Reserves ready</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`add new reserve`}> + <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"> + {withFunds.length > 0 ? ( + <Table + instances={withFunds} + onNewTip={onNewTip} + onSelect={onSelect} + onDelete={onDelete} + /> + ) : ( + <EmptyTable /> + )} + </div> </div> </div> </div> - </div> - </Fragment> + </Fragment> + ); } interface TableProps { instances: Entity[]; @@ -96,89 +127,181 @@ interface TableProps { } function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode { - const i18n = useTranslator() + const i18n = useTranslator(); return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> - <th><Translate>Created at</Translate></th> - <th><Translate>Expires at</Translate></th> - <th><Translate>Initial</Translate></th> - <th><Translate>Picked up</Translate></th> - <th><Translate>Committed</Translate></th> + <th> + <Translate>Created at</Translate> + </th> + <th> + <Translate>Expires at</Translate> + </th> + <th> + <Translate>Initial</Translate> + </th> + <th> + <Translate>Picked up</Translate> + </th> + <th> + <Translate>Committed</Translate> + </th> <th /> </tr> </thead> <tbody> - {instances.map(i => { - return <tr key={i.id}> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.exchange_initial_amount}</td> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.pickup_amount}</td> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.committed_amount}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button class="button is-small is-danger has-tooltip-left" - data-tooltip={i18n`delete selected reserve from the database`} - type="button" onClick={(): void => onDelete(i)}> - Delete - </button> - <button class="button is-small is-info has-tooltip-left" - data-tooltip={i18n`authorize new tip from selected reserve`} - type="button" onClick={(): void => onNewTip(i)}> - New Tip - </button> - </div> - </td> - </tr> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.creation_time.t_s === "never" + ? "never" + : format(i.creation_time.t_s, "yyyy/MM/dd HH:mm:ss")} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.expiration_time.t_s === "never" + ? "never" + : format(i.expiration_time.t_s, "yyyy/MM/dd HH:mm:ss")} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.exchange_initial_amount} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.pickup_amount} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.committed_amount} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-small is-danger has-tooltip-left" + data-tooltip={i18n`delete selected reserve from the database`} + type="button" + onClick={(): void => onDelete(i)} + > + Delete + </button> + <button + class="button is-small is-info has-tooltip-left" + data-tooltip={i18n`authorize new tip from selected reserve`} + type="button" + onClick={(): void => onNewTip(i)} + > + New Tip + </button> + </div> + </td> + </tr> + ); })} - </tbody> - </table></div>) + </table> + </div> + ); } function EmptyTable(): VNode { - 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><Translate>There is no ready reserves yet, add more pressing the + sign or fund them</Translate></p> - </div> + 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> + <Translate> + There is no ready reserves yet, add more pressing the + sign or fund + them + </Translate> + </p> + </div> + ); } -function TableWithoutFund({ instances, onSelect, onDelete }: TableProps): VNode { - const i18n = useTranslator() +function TableWithoutFund({ + instances, + onSelect, + onDelete, +}: TableProps): VNode { + const i18n = useTranslator(); return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> - <th><Translate>Created at</Translate></th> - <th><Translate>Expires at</Translate></th> - <th><Translate>Expected Balance</Translate></th> + <th> + <Translate>Created at</Translate> + </th> + <th> + <Translate>Expires at</Translate> + </th> + <th> + <Translate>Expected Balance</Translate> + </th> <th /> </tr> </thead> <tbody> - {instances.map(i => { - return <tr key={i.id}> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.merchant_initial_amount}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button class="button is-small is-danger jb-modal has-tooltip-left" type="button" - data-tooltip={i18n`delete selected reserve from the database`} - onClick={(): void => onDelete(i)}> - Delete - </button> - </div> - </td> - </tr> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.creation_time.t_s === "never" + ? "never" + : format(i.creation_time.t_s, "yyyy/MM/dd HH:mm:ss")} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.expiration_time.t_s === "never" + ? "never" + : format(i.expiration_time.t_s, "yyyy/MM/dd HH:mm:ss")} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.merchant_initial_amount} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-small is-danger jb-modal has-tooltip-left" + type="button" + data-tooltip={i18n`delete selected reserve from the database`} + onClick={(): void => onDelete(i)} + > + Delete + </button> + </div> + </td> + </tr> + ); })} - </tbody> - </table></div>) + </table> + </div> + ); } diff --git a/packages/merchant-backoffice/src/paths/instance/transfers/list/List.stories.tsx b/packages/merchant-backoffice/src/paths/instance/transfers/list/List.stories.tsx @@ -15,73 +15,79 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h, VNode, FunctionalComponent } from 'preact'; -import { ListPage as TestedComponent } from './ListPage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode, FunctionalComponent } from "preact"; +import { ListPage as TestedComponent } from "./ListPage"; export default { - title: 'Pages/Transfer/List', + title: "Pages/Transfer/List", component: TestedComponent, argTypes: { - onCreate: { action: 'onCreate' }, - onDelete: { action: 'onDelete' }, - onLoadMoreBefore: { action: 'onLoadMoreBefore' }, - onLoadMoreAfter: { action: 'onLoadMoreAfter' }, - onShowAll: { action: 'onShowAll' }, - onShowVerified: { action: 'onShowVerified' }, - onShowUnverified: { action: 'onShowUnverified' }, - onChangePayTo: { action: 'onChangePayTo' } + onCreate: { action: "onCreate" }, + onDelete: { action: "onDelete" }, + onLoadMoreBefore: { action: "onLoadMoreBefore" }, + onLoadMoreAfter: { action: "onLoadMoreAfter" }, + onShowAll: { action: "onShowAll" }, + onShowVerified: { action: "onShowVerified" }, + onShowUnverified: { action: "onShowUnverified" }, + onChangePayTo: { action: "onChangePayTo" }, }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r +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, { - transfers: [{ - exchange_url: 'http://exchange.url/', - credit_amount: 'TESTKUDOS:10', - payto_uri: 'payto//x-taler-bank/bank:8080/account', - transfer_serial_id: 123123123, - wtid: '!@KJELQKWEJ!L@K#!J@', - confirmed: true, - execution_time: { - t_ms: new Date().getTime() + transfers: [ + { + exchange_url: "http://exchange.url/", + credit_amount: "TESTKUDOS:10", + payto_uri: "payto//x-taler-bank/bank:8080/account", + transfer_serial_id: 123123123, + wtid: "!@KJELQKWEJ!L@K#!J@", + confirmed: true, + execution_time: { + t_s: new Date().getTime(), + }, + verified: false, }, - verified: false, - }, { - exchange_url: 'http://exchange.url/', - credit_amount: 'TESTKUDOS:10', - payto_uri: 'payto//x-taler-bank/bank:8080/account', - transfer_serial_id: 123123123, - wtid: '!@KJELQKWEJ!L@K#!J@', - confirmed: true, - execution_time: { - t_ms: new Date().getTime() + { + exchange_url: "http://exchange.url/", + credit_amount: "TESTKUDOS:10", + payto_uri: "payto//x-taler-bank/bank:8080/account", + transfer_serial_id: 123123123, + wtid: "!@KJELQKWEJ!L@K#!J@", + confirmed: true, + execution_time: { + t_s: new Date().getTime(), + }, + verified: false, }, - verified: false, - }, { - exchange_url: 'http://exchange.url/', - credit_amount: 'TESTKUDOS:10', - payto_uri: 'payto//x-taler-bank/bank:8080/account', - transfer_serial_id: 123123123, - wtid: '!@KJELQKWEJ!L@K#!J@', - confirmed: true, - execution_time: { - t_ms: new Date().getTime() + { + exchange_url: "http://exchange.url/", + credit_amount: "TESTKUDOS:10", + payto_uri: "payto//x-taler-bank/bank:8080/account", + transfer_serial_id: 123123123, + wtid: "!@KJELQKWEJ!L@K#!J@", + confirmed: true, + execution_time: { + t_s: new Date().getTime(), + }, + verified: false, }, - verified: false, - }], - accounts: ['payto://x-taler-bank/bank/some_account'] + ], + accounts: ["payto://x-taler-bank/bank/some_account"], }); export const Empty = createExample(TestedComponent, { transfers: [], - accounts: [] + accounts: [], }); diff --git a/packages/merchant-backoffice/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice/src/paths/instance/transfers/list/Table.tsx @@ -15,17 +15,17 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { format } from "date-fns" -import { h, VNode } from "preact" -import { StateUpdater, useState } from "preact/hooks" -import { MerchantBackend, WithId } from "../../../../declaration" -import { Translate, useTranslator } from "../../../../i18n" +import { format } from "date-fns"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend, WithId } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; -type Entity = MerchantBackend.Transfers.TransferDetails & WithId +type Entity = MerchantBackend.Transfers.TransferDetails & WithId; interface Props { transfers: Entity[]; @@ -38,39 +38,60 @@ interface Props { onLoadMoreAfter?: () => void; } -export function CardTable({ transfers, onCreate, onDelete, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode { - const [rowSelection, rowSelectionHandler] = useState<string[]>([]) - - const i18n = useTranslator() +export function CardTable({ + transfers, + onCreate, + onDelete, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - return <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-bank" /></span><Translate>Transfers</Translate></p> - <div class="card-header-icon" aria-label="more options"> - <span class="has-tooltip-left" data-tooltip={i18n`add new transfer`}> - <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> + const i18n = useTranslator(); - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {transfers.length > 0 ? - <Table instances={transfers} - onDelete={onDelete} rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} hasMoreBefore={hasMoreBefore} - /> : - <EmptyTable /> - } + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-bank" /> + </span> + <Translate>Transfers</Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span class="has-tooltip-left" data-tooltip={i18n`add new transfer`}> + <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"> + {transfers.length > 0 ? ( + <Table + instances={transfers} + onDelete={onDelete} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> </div> </div> </div> - </div> + ); } interface TableProps { rowSelection: string[]; @@ -84,59 +105,118 @@ interface TableProps { } function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); } -function Table({ instances, onLoadMoreAfter, onDelete, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode { - const i18n = useTranslator() +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const i18n = useTranslator(); return ( <div class="table-container"> - {onLoadMoreBefore && <button class="button is-fullwidth" - data-tooltip={i18n`load more transfers before the first one`} - disabled={!hasMoreBefore} onClick={onLoadMoreBefore}><Translate>load newer transfers</Translate></button>} + {onLoadMoreBefore && ( + <button + class="button is-fullwidth" + data-tooltip={i18n`load more transfers before the first one`} + disabled={!hasMoreBefore} + onClick={onLoadMoreBefore} + > + <Translate>load newer transfers</Translate> + </button> + )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> - <th><Translate>ID</Translate></th> - <th><Translate>Credit</Translate></th> - <th><Translate>Address</Translate></th> - <th><Translate>Exchange URL</Translate></th> - <th><Translate>Confirmed</Translate></th> - <th><Translate>Verified</Translate></th> - <th><Translate>Executed at</Translate></th> + <th> + <Translate>ID</Translate> + </th> + <th> + <Translate>Credit</Translate> + </th> + <th> + <Translate>Address</Translate> + </th> + <th> + <Translate>Exchange URL</Translate> + </th> + <th> + <Translate>Confirmed</Translate> + </th> + <th> + <Translate>Verified</Translate> + </th> + <th> + <Translate>Executed at</Translate> + </th> <th /> </tr> </thead> <tbody> - {instances.map(i => { - return <tr key={i.id}> - <td>{i.id}</td> - <td>{i.credit_amount}</td> - <td>{i.payto_uri}</td> - <td>{i.exchange_url}</td> - <td>{i.confirmed ? i18n`yes` : i18n`no`}</td> - <td>{i.verified ? i18n`yes` : i18n`no`}</td> - <td>{i.execution_time ? (i.execution_time.t_ms == 'never' ? i18n`never` : format(i.execution_time.t_ms, 'yyyy/MM/dd HH:mm:ss')) : i18n`unknown`}</td> - <td> - {i.verified === undefined ? <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n`delete selected transfer from the database`} - onClick={() => onDelete(i)}>Delete</button> : undefined} - </td> - </tr> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td>{i.id}</td> + <td>{i.credit_amount}</td> + <td>{i.payto_uri}</td> + <td>{i.exchange_url}</td> + <td>{i.confirmed ? i18n`yes` : i18n`no`}</td> + <td>{i.verified ? i18n`yes` : i18n`no`}</td> + <td> + {i.execution_time + ? i.execution_time.t_s == "never" + ? i18n`never` + : format(i.execution_time.t_s, "yyyy/MM/dd HH:mm:ss") + : i18n`unknown`} + </td> + <td> + {i.verified === undefined ? ( + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n`delete selected transfer from the database`} + onClick={() => onDelete(i)} + > + Delete + </button> + ) : undefined} + </td> + </tr> + ); })} </tbody> </table> - {onLoadMoreAfter && <button class="button is-fullwidth" data-tooltip={i18n`load more transfer after the last one`} disabled={!hasMoreAfter} onClick={onLoadMoreAfter}><Translate>load older transfers</Translate></button>} - </div>) + {onLoadMoreAfter && ( + <button + class="button is-fullwidth" + data-tooltip={i18n`load more transfer after the last one`} + disabled={!hasMoreAfter} + onClick={onLoadMoreAfter} + > + <Translate>load older transfers</Translate> + </button> + )} + </div> + ); } function EmptyTable(): VNode { - 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><Translate>There is no transfer yet, add more pressing the + sign</Translate></p> - </div> + 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> + <Translate> + There is no transfer yet, add more pressing the + sign + </Translate> + </p> + </div> + ); } - - diff --git a/packages/merchant-backoffice/src/paths/instance/update/Update.stories.tsx b/packages/merchant-backoffice/src/paths/instance/update/Update.stories.tsx @@ -15,45 +15,47 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h, VNode, FunctionalComponent } from 'preact'; -import { UpdatePage as TestedComponent } from './UpdatePage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage"; export default { - title: 'Pages/Instance/Update', + title: "Pages/Instance/Update", component: TestedComponent, argTypes: { - onUpdate: { action: 'onUpdate' }, - onBack: { action: 'onBack' }, + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r +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, { selected: { accounts: [], - name: 'name', - auth: {method:'external'}, + name: "name", + auth: { method: "external" }, address: {}, jurisdiction: {}, - default_max_deposit_fee: 'TESTKUDOS:2', - default_max_wire_fee: 'TESTKUDOS:1', + default_max_deposit_fee: "TESTKUDOS:2", + default_max_wire_fee: "TESTKUDOS:1", default_pay_delay: { - d_ms: 1000000, + d_us: 1000000, }, default_wire_fee_amortization: 1, default_wire_transfer_delay: { - d_ms: 100000, + d_us: 100000, }, - merchant_pub: 'ASDWQEKASJDKSADJ' - } + merchant_pub: "ASDWQEKASJDKSADJ", + }, }); diff --git a/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx @@ -58,8 +58,8 @@ function convert( const payto_uris = accounts.filter((a) => a.active).map((a) => a.payto_uri); const defaults = { default_wire_fee_amortization: 1, - default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour - default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours + default_pay_delay: { d_us: 1000 * 60 * 60 }, //one hour + default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 }, //two hours }; return { ...defaults, ...rest, payto_uris }; } diff --git a/packages/merchant-backoffice/src/schemas/index.ts b/packages/merchant-backoffice/src/schemas/index.ts @@ -99,12 +99,12 @@ export const InstanceSchema = yup.object().shape({ country_subdivision: yup.string().optional(), }).meta({ type: 'group' }), // default_pay_delay: yup.object() - // .shape({ d_ms: yup.number() }) + // .shape({ d_us: yup.number() }) // .required() // .meta({ type: 'duration' }), // .transform(numberToDuration), default_wire_transfer_delay: yup.object() - .shape({ d_ms: yup.number() }) + .shape({ d_us: yup.number() }) .required() .meta({ type: 'duration' }), // .transform(numberToDuration), diff --git a/packages/merchant-backoffice/src/utils/amount.ts b/packages/merchant-backoffice/src/utils/amount.ts @@ -38,10 +38,10 @@ export function mergeRefunds(prev: MerchantBackend.Orders.RefundDetails[], cur: let tail; if (prev.length === 0 || //empty list - cur.timestamp.t_ms === 'never' || //current doesnt have timestamp - (tail = prev[prev.length - 1]).timestamp.t_ms === 'never' || // last doesnt have timestamp + cur.timestamp.t_s === 'never' || //current doesnt have timestamp + (tail = prev[prev.length - 1]).timestamp.t_s === 'never' || // last doesnt have timestamp cur.reason !== tail.reason || //different reason - Math.abs(cur.timestamp.t_ms - tail.timestamp.t_ms) > 1000 * 60) {//more than 1 minute difference + Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60) {//more than 1 minute difference prev.push(cur) return prev diff --git a/packages/merchant-backoffice/tests/hooks/swr/reserve.test.ts b/packages/merchant-backoffice/tests/hooks/swr/reserve.test.ts @@ -288,7 +288,7 @@ describe("reserve api interaction with details", () => { response: { tip_id: "id2", taler_tip_uri: "uri", - tip_expiration: { t_ms: 1 }, + tip_expiration: { t_s: 1 }, tip_status_url: "url", }, }); @@ -381,7 +381,7 @@ describe("reserve api interaction with details", () => { response: { tip_id: "id2", taler_tip_uri: "uri", - tip_expiration: { t_ms: 1 }, + tip_expiration: { t_s: 1 }, tip_status_url: "url", }, });