commit e9a4228ef2d7210e67aded9559eeed79dfa4624f parent 311086e0dd8522b21d023eb408d53d68d45ec72e Author: Sebastian <sebasjm@gmail.com> Date: Wed, 31 Mar 2021 10:16:17 -0300 order detail Diffstat:
15 files changed, 452 insertions(+), 116 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -27,14 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - product detail: we could have some button that brings us to the detailed screen for the product - order id field to go -created -wired (if wired === true) -refund +frontend, too many redirects -error details - -fatal error: exchange error -warning: exchange repotred problem - navigation to another instance should not do full refresh - cleanup instance and token management, because code is a mess and can be refactored ## [Unreleased] diff --git a/packages/frontend/package.json b/packages/frontend/package.json @@ -77,6 +77,7 @@ "bulma-radio": "^1.1.1", "bulma-responsive-tables": "^1.2.3", "bulma-switch-control": "^1.1.1", + "bulma-timeline": "^3.0.4", "bulma-upload-control": "^1.2.0", "dotenv": "^8.2.0", "enzyme": "^3.11.0", diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -204,37 +204,6 @@ export function InstanceRoutes({ id, admin }: Props): VNode { } <Route path="/" component={Redirect} to={InstancePaths.order_list} /> - {/* - component={DetailPage} - - onUnauthorized={() => { - return <Fragment> - <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - }} - - onNotFound={() => { - if (admin) { - return <Fragment> - <NotificationCard notification={{ - message: 'No default instance', - description: 'in order to use merchant backoffice, you should create the default instance', - type: 'INFO' - }} /> - <InstanceCreatePage onError={() => null} onConfirm={() => null} /> - </Fragment> - } - return <NotFoundPage /> - }} - - onLoadError={(error: SwrError) => { - return <Fragment> - <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - }} - /> */} <Route path={InstancePaths.update} component={InstanceUpdatePage} @@ -286,12 +255,10 @@ export function InstanceRoutes({ id, admin }: Props): VNode { }} onConfirm={() => { - // pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); route(`/`); }} onUpdateError={(e: Error) => { - // pushNotification({ message: i18n`update_error`, type: 'ERROR' }); }} /> diff --git a/packages/frontend/src/components/form/Input.tsx b/packages/frontend/src/components/form/Input.tsx @@ -59,7 +59,7 @@ export function Input<T>({ name, readonly, expand, inputType, fromStr = defaultF <TextInput error={error} inputType={inputType} placeholder={placeholder} readonly={readonly} - name={String(name)} value={toStr(value)} disabled={readonly} + name={String(name)} value={toStr(value)} onChange={(e:h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} /> <Message id={`fields.instance.${name}.help`}> </Message> </p> diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx b/packages/frontend/src/components/form/InputWithAddon.tsx @@ -59,7 +59,7 @@ export function InputWithAddon<T>({ name, readonly, addonBefore, expand, inputTy </div>} <p class={ expand ? "control is-expanded" : "control" }> <input class={error ? "input is-danger" : "input"} type={inputType} - placeholder={placeholder} readonly={readonly} disabled={readonly} + placeholder={placeholder} readonly={readonly} name={String(name)} value={toStr(value)} onChange={(e): void => onChange(fromStr(e.currentTarget.value))} /> <Message id={`fields.instance.${name}.help`}> </Message> diff --git a/packages/frontend/src/components/menu/SideBar.tsx b/packages/frontend/src/components/menu/SideBar.tsx @@ -74,12 +74,12 @@ export function Sidebar({ mobile, instance, onLogout, admin }: Props): VNode { <span class="menu-item-label">Transfers</span> </a> </li> - <li> + {/* <li> <a href="/tips" class="has-icon"> <span class="icon"><i class="mdi mdi-cash" /></span> <span class="menu-item-label">Tips</span> </a> - </li> + </li> */} </ul> <p class="menu-label">Connection</p> <ul class="menu-list"> diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po @@ -311,50 +311,41 @@ msgstr "Description" msgid "fields.instance.description.placeholder" msgstr "add more information about the refund" -# msgid "fields.instance.reason.tooltip" -# msgstr "" msgid "fields.instance.order_status.placeholder" msgstr "" -# msgid "fields.instance.order_status.tooltip" -# msgstr "" - msgid "fields.instance.order_status.label" msgstr "Order status" - -# msgid "fields.instance.order_status_url.placeholder" -# msgstr "" - -# msgid "fields.instance.order_status_url.tooltip" -# msgstr "" - msgid "fields.instance.order_status_url.label" msgstr "Order status URL" -# msgid "fields.instance.taler_pay_uri.placeholder" -# msgstr "" - -# msgid "fields.instance.taler_pay_uri.tooltip" -# msgstr "" - msgid "fields.instance.taler_pay_uri.label" msgstr "Taler Pay URI" -# msgid "fields.instance.amount.placeholder" -# msgstr "" - -# msgid "fields.instance.amount.tooltip" -# msgstr "" msgid "fields.instance.amount.label" msgstr "Amount" -# msgid "fields.instance.summary.placeholder" -# msgstr "" +msgid "fields.instance.summary.label" +msgstr "Summary" -# msgid "fields.instance.summary.tooltip" -# msgstr "" -msgid "fields.instance.summary.label" +msgid "fields.instance.contract_terms.amount.label" +msgstr "Amount" + +msgid "fields.instance.contract_terms.summary.label" msgstr "Summary" + +msgid "fields.instance.refund_amount.label" +msgstr "Refund Amount" + +msgid "fields.instance.deposit_total.label" +msgstr "Deposit Total" + +msgid "fields.instance.contract_terms.max_fee.label" +msgstr "Max Fee" + +msgid "fields.instance.contract_terms.max_wire_fee.label" +msgstr "Max Wire Fee" + diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx @@ -25,12 +25,19 @@ import { MerchantBackend } from "../../../../declaration"; import { Input } from "../../../../components/form/Input"; import { FormProvider } from "../../../../components/form/Field"; import { NotificationCard } from "../../../../components/menu"; +import { useConfigContext } from "../../../../context/backend"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { copyToClipboard } from "../../../../utils/functions"; +import { format } from "date-fns"; +import { Event, Timeline } from "./Timeline"; +import { RefundModal } from "../list/Table"; type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; interface Props { onBack: () => void; selected: Entity; - + id: string; + onRefund: (id:string, value: MerchantBackend.Orders.RefundRequest) => void; } interface KeyValue { @@ -41,41 +48,316 @@ type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse -export function DetailPage({ selected }: Props): VNode { - const [value, valueHandler] = useState<Partial<Entity>>(selected) + +function ClaimedPage({ id, order }: { id: string; order: MerchantBackend.Orders.CheckPaymentClaimedResponse }) { + const events: Event[] = [] + events.push({ + when: new Date(), + description: 'now', + type: 'now' + }) + events.push({ + when: new Date(order.contract_terms.timestamp.t_ms), + description: 'order created', + type: 'start' + }) + events.push({ + when: new Date(order.contract_terms.pay_deadline.t_ms), + description: 'pay deadline', + type: 'deadline' + }) + events.push({ + when: new Date(order.contract_terms.refund_deadline.t_ms), + description: 'refund deadline', + type: 'deadline' + }) + events.push({ + when: new Date(order.contract_terms.wire_transfer_deadline.t_ms), + description: 'wire deadline', + type: 'deadline' + }) + if (order.contract_terms.delivery_date) events.push({ + when: new Date(order.contract_terms.delivery_date?.t_ms), + description: 'delivery', + type: 'delivery' + }) + + events.sort((a, b) => a.when.getTime() - b.when.getTime()) + const [value, valueHandler] = useState<Partial<Claimed>>(order) const [errors, setErrors] = useState<KeyValue>({}) + const config = useConfigContext() + + 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"> + Order #{id} + <div class="tag is-info ml-4">claimed</div> + </div> + </div> + </div> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title"> + {order.contract_terms.amount} + </h1> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <h1 class="title"> + <button class="button is-info" onClick={() => { + if (order.contract_terms.fulfillment_url) copyToClipboard(order.contract_terms.fulfillment_url) + }}>copy url</button> + </h1> + </div> + </div> + </div> + + <div class="level"> + <div class="level-left" style={{ maxWidth: '100%' }}> + <div class="level-item" style={{ maxWidth: '100%' }}> + <div class="content" style={{ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + // maxWidth: '100%', + }}> + <p>pay at: <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> + </div> + </div> + </div> + </div> + </section> + + <section class="section"> + <div class="columns"> + <div class="column is-4"> + <div class="title">Timeline</div> + <Timeline events={events} /> + </div> + <div class="column is-8" > + <div class="title">Payment details</div> + <FormProvider<Claimed> errors={errors} object={value} valueHandler={valueHandler} > + <Input name="contract_terms.summary" readonly inputType="multiline" /> + <InputCurrency name="contract_terms.amount" readonly currency={config.currency} /> + <Input<Claimed> name="order_status" readonly /> + </FormProvider> + </div> + </div> + </section> + + <section class="section"> + <div class="columns"> + <div class="column is-2" /> + <div class="column is-8" > + <div class="title">Payment details</div> + <FormProvider<Claimed> errors={errors} object={value} valueHandler={valueHandler} > + <Input name="contract_terms.summary" readonly inputType="multiline" /> + <InputCurrency name="contract_terms.amount" readonly currency={config.currency} /> + <Input<Claimed> name="order_status" readonly /> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + + </div> + <div class="column" /> + </div> + </section> + </div> +} +function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend.Orders.CheckPaymentPaidResponse, onRefund: (id:string) => void }) { + const events: Event[] = [] + events.push({ + when: new Date(), + description: 'now', + type: 'now' + }) + events.push({ + when: new Date(order.contract_terms.timestamp.t_ms), + description: 'order created', + type: 'start' + }) + events.push({ + when: new Date(order.contract_terms.pay_deadline.t_ms), + description: 'pay deadline', + type: 'deadline' + }) + events.push({ + when: new Date(order.contract_terms.refund_deadline.t_ms), + description: 'refund deadline', + type: 'deadline' + }) + events.push({ + when: new Date(order.contract_terms.wire_transfer_deadline.t_ms), + description: 'wire deadline', + type: 'deadline' + }) + if (order.contract_terms.delivery_date) events.push({ + when: new Date(order.contract_terms.delivery_date?.t_ms), + description: 'delivery', + type: 'delivery' + }) + order.refund_details.forEach(e => { + events.push({ + when: new Date(e.timestamp.t_ms), + description: `refund: ${e.amount}`, + type: 'refund', + }) + }) + order.wire_details.forEach(e => { + events.push({ + when: new Date(e.execution_time.t_ms), + description: `wired`, + type: 'wired', + }) + }) + if (order.contract_terms.wire_transfer_deadline.t_ms !== 'never' && + order.contract_terms.wire_transfer_deadline.t_ms < new Date().getTime() ) events.push({ + when: new Date(order.contract_terms.wire_transfer_deadline.t_ms - 1000*10), + description: `wired (faked)`, + type: 'wired', + }) + + events.sort((a, b) => a.when.getTime() - b.when.getTime()) + const [value, valueHandler] = useState<Partial<Paid>>({...order, fee: 'COL:0.1'} as any) + const [errors, setErrors] = useState<KeyValue>({}) + const config = useConfigContext() + + const refundable = !order.refunded && + new Date().getTime() <order.contract_terms.refund_deadline.t_ms return <div> - <NotificationCard notification={{ - message: 'DEMO WARNING', - type:'WARN', - description: <Fragment> - <p>UNDER CONSTRUCTION: for now we are showing some field of the order depending on the state</p> - <p><b>unpaid:</b> status_url and pay_uri</p> - <p><b>claimed:</b> contractTerms.amount</p> - <p><b>paid:</b> deposit_total</p> - </Fragment> - }} /> + <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"> + Order #{id} + <div class="tag is-success ml-4">paid</div> + {order.wired ? + <div class="tag is-success ml-4">wired</div> : null + } + {order.refunded ? + <div class="tag is-danger ml-4">refunded</div> : null + } + </div> + </div> + </div> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title"> + {order.contract_terms.amount} + </h1> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <h1 class="title"> + <div class="buttons"> + { refundable && <button class="button is-danger" onClick={() => onRefund(id) }>refund</button> } + <button class="button is-info" onClick={() => { + if (order.contract_terms.fulfillment_url) copyToClipboard(order.contract_terms.fulfillment_url) + }}>copy url</button> + </div> + </h1> + </div> + </div> + </div> + + <div class="level"> + <div class="level-left" style={{ maxWidth: '100%' }}> + <div class="level-item" style={{ maxWidth: '100%' }}> + <div class="content" style={{ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + // 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> + </div> + </div> + </div> + </div> + </section> + + <section class="section"> + <div class="columns"> + <div class="column is-4"> + <div class="title">Timeline</div> + <Timeline events={events} /> + </div> + <div class="column is-8" > + <div class="title">Payment details</div> + <FormProvider<Paid> errors={errors} object={value} valueHandler={valueHandler} > + <Input name="contract_terms.summary" readonly inputType="multiline" /> + <InputCurrency name="contract_terms.amount" readonly currency={config.currency} /> + <InputCurrency name="fee" readonly currency={config.currency} /> + <InputCurrency<Paid> name="refund_amount" readonly currency={config.currency} /> + <InputCurrency<Paid> name="deposit_total" readonly currency={config.currency} /> + <Input<Paid> name="order_status" readonly /> + </FormProvider> + </div> + </div> + </section> + + </div> + <div class="column" /> + </div> + </section> + </div> +} + +function UnpaidPage({ id, order }: { id: string; order: MerchantBackend.Orders.CheckPaymentUnpaidResponse }) { + const [value, valueHandler] = useState<Partial<Unpaid>>(order) + const [errors, setErrors] = useState<KeyValue>({}) + 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"> + Order #{id} + </h1> + </div> + <div class="tag is-dark">unpaid</div> + </div> + </div> + </div> + </section> <section class="section is-main-section"> <div class="columns"> <div class="column" /> <div class="column is-6"> - <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > - {selected.order_status === 'unpaid' && <Fragment> - <Input<Unpaid> name="order_status" readonly /> - <Input<Unpaid> name="order_status_url" readonly /> - <Input<Unpaid> name="taler_pay_uri" readonly /> - </Fragment>} - {selected.order_status === 'claimed' && <Fragment> - <Input<Claimed> name="order_status" readonly /> - <Input name="contract_terms.amount" readonly /> - </Fragment>} - {selected.order_status === 'paid' && <Fragment> - <Input<Paid> name="order_status" readonly /> - <Input name="contract_terms.deposit_total" readonly /> - </Fragment>} + <FormProvider<Unpaid> errors={errors} object={value} valueHandler={valueHandler} > + <Input<Unpaid> name="order_status" readonly /> + <Input<Unpaid> name="order_status_url" readonly /> + <Input<Unpaid> name="taler_pay_uri" readonly /> </FormProvider> </div> <div class="column" /> @@ -83,5 +365,37 @@ export function DetailPage({ selected }: Props): VNode { </section> </div> +} + +export function DetailPage({ id, selected, onRefund }: Props): VNode { + 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={(order) => setShowRefund(id)} /> + case 'unpaid': return <UnpaidPage id={id} order={selected} /> + default: return <div>unknown order status</div> + } + } + + return <Fragment> + <NotificationCard notification={{ + message: 'DEMO WARNING', + type: 'WARN', + description: <ul> + <li>wired event is faked</li> + <li>fee value is fake, is not being calculated</li> + </ul> + }} /> + {DetailByStatus()} + {showRefund && <RefundModal + onCancel={() => setShowRefund(undefined)} + onConfirm={(value) => { + onRefund(showRefund, value) + setShowRefund(undefined) + }} + />} + </Fragment> } \ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/details/Timeline.tsx b/packages/frontend/src/paths/instance/orders/details/Timeline.tsx @@ -0,0 +1,35 @@ +import { format } from "date-fns"; +import { h } from "preact"; + +interface Props { + events: Event[] +} + +export function Timeline({ events }: Props) { + return <div class="timeline"> + {events.map(e => { + return <div class="timeline-item"> + {(() => { + switch (e.type) { + case "deadline": return <div class="timeline-marker is-icon "><i class="mdi mdi-flag" /></div> + case "delivery": return <div class="timeline-marker is-icon "><i class="mdi mdi-delivery" /></div> + case "start": return <div class="timeline-marker is-icon is-success"><i class="mdi mdi-flag " /></div> + case "wired": return <div class="timeline-marker is-icon is-success"><i class="mdi mdi-cash" /></div> + case "refund": return <div class="timeline-marker is-icon is-danger"><i class="mdi mdi-cash" /></div> + case "now": return <div class="timeline-marker is-icon is-info"><i class="mdi mdi-clock" /></div> + } + })()} + <div class="timeline-content"> + <p class="heading">{format(e.when, 'yyyy/MM/dd HH:mm:ss')}</p> + <p>{e.description}</p> + </div> + </div> + })} + </div >; + +} +export interface Event { + when: Date; + description: string; + type: 'start' | 'refund' | 'wired' | 'deadline' | 'delivery' | 'now' +} diff --git a/packages/frontend/src/paths/instance/orders/details/index.tsx b/packages/frontend/src/paths/instance/orders/details/index.tsx @@ -14,9 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { Loading } from "../../../../components/exception/loading"; import { SwrError, useOrderDetails, useOrderMutateAPI } from "../../../../hooks/backend"; - import { DetailPage } from "./DetailPage"; +import { Notification } from "../../../../utils/types"; +import { DetailPage } from "./DetailPage"; export interface Props { oid: string; @@ -30,6 +32,7 @@ export interface Props { export default function Update({ oid, onBack, onLoadError, onNotFound, onUnauthorized }: Props): VNode { const { refundOrder } = useOrderMutateAPI(); const details = useOrderDetails(oid) + const [notif, setNotif] = useState<Notification | undefined>(undefined) if (details.unauthorized) return onUnauthorized() if (details.notfound) return onNotFound(); @@ -42,7 +45,18 @@ export default function Update({ oid, onBack, onLoadError, onNotFound, onUnautho return <Fragment> <DetailPage onBack={onBack} + id={oid} + onRefund={(id, value) => refundOrder(id, value) + .then(() => setNotif({ + message: 'refund created successfully', + type: "SUCCESS" + })).catch((error) => setNotif({ + message: 'could not create the refund', + type: "ERROR", + description: error.message + })) + } selected={details.data} - /> + /> </Fragment> } \ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx @@ -127,12 +127,12 @@ function Table({ instances, onSelect, onRefund, onCopyURL, onLoadMoreAfter, onLo <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 || pos === 0) && + {(i.refundable) && <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onRefund(i)}> Refund </button> } - {(!i.paid || pos === 0) && + {(!i.paid) && <button class="button is-small is-info jb-modal" type="button" onClick={(): void => onCopyURL(i)}> copy url </button> @@ -192,8 +192,5 @@ export function RefundModal({ onCancel, onConfirm }: RefundModalProps): VNode { <InputSelector name="mainReason" values={['duplicated', 'requested by the customer', 'other']} /> {form.mainReason && <Input<State> name="description" />} </FormProvider> - <div class="block"> - You are going to refund the order - </div> </ConfirmModal> } diff --git a/packages/frontend/src/paths/instance/orders/list/index.tsx b/packages/frontend/src/paths/instance/orders/list/index.tsx @@ -29,6 +29,7 @@ import { format } from 'date-fns'; import { DatePicker } from '../../../../components/form/DatePicker'; import { NotificationCard } from '../../../../components/menu'; import { Notification } from '../../../../utils/types'; +import { copyToClipboard } from '../../../../utils/functions'; interface Props { onUnauthorized: () => VNode; @@ -47,7 +48,7 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo const result = useInstanceOrders(filter, setNewDate) const { createOrder, refundOrder, getPaymentURL } = useOrderMutateAPI() - const { currency } = useConfigContext() + // const { currency } = useConfigContext() let instances: (MerchantBackend.Orders.OrderHistoryEntry & { id: string })[]; @@ -119,9 +120,7 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo <CardTable instances={instances} onCreate={onCreate} onSelect={(order) => onSelect(order.id)} - onCopyURL={(id) => getPaymentURL(id).then(url => { - navigator.clipboard.writeText(url) - })} + onCopyURL={(id) => getPaymentURL(id).then(copyToClipboard)} onRefund={(id, value) => refundOrder(id, value) .then(() => setNotif({ message: 'refund created successfully', diff --git a/packages/frontend/src/scss/main.scss b/packages/frontend/src/scss/main.scss @@ -5,22 +5,22 @@ GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. - + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - + /** * * @author Sebastian Javier Marchano (sebasjm) */ - -/* Theme style (colors & sizes) */ -@import "theme-default"; + + /* Theme style (colors & sizes) */ + @import "theme-default"; /* Core Libs & Lib configs */ @import "libs/all"; @@ -48,6 +48,19 @@ @import "icons/materialdesignicons-4.9.95.min.css"; @import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css"; +@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css"; + +.timeline .timeline-item .timeline-content { + padding-top: 0; +} + +.timeline .timeline-item:last-child::before { + display: none; +} + +.timeline .timeline-item .timeline-marker { + top: 0; +} .toast { position: absolute; diff --git a/packages/frontend/src/utils/functions.ts b/packages/frontend/src/utils/functions.ts @@ -35,3 +35,8 @@ export function onTranslationError(error: MessageError) { if (typeof window === "undefined") return; window.MerchantBackoffice.missing_locales = window.MerchantBackoffice.missing_locales.concat(error.path.join()) } + +export async function copyToClipboard(text:string) { + return navigator.clipboard.writeText(text) +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -37,6 +37,7 @@ importers: bulma-radio: 1.1.1 bulma-responsive-tables: 1.2.3 bulma-switch-control: 1.1.1 + bulma-timeline: 3.0.4 bulma-upload-control: 1.2.0 dotenv: 8.2.0 enzyme: 3.11.0 @@ -86,6 +87,7 @@ importers: bulma-radio: ^1.1.1 bulma-responsive-tables: ^1.2.3 bulma-switch-control: ^1.1.1 + bulma-timeline: ^3.0.4 bulma-upload-control: ^1.2.0 date-fns: ^2.19.0 dotenv: ^8.2.0 @@ -4966,6 +4968,10 @@ packages: dev: true resolution: integrity: sha512-uvPhLeiip1P/JZf9nidbA+7cQmUYKzS5vVbzhEAUk0oz6H3hPhHDIef/rUwqig1veRUd7vXBZ1hOcsM9gLxv/A== + /bulma-timeline/3.0.4: + dev: true + resolution: + integrity: sha512-gCUOcSUuzHoeVMkCpLF49j5Z5yl78XQ+KgJcT+1ju5WIGgBgVytRUob/dw5NHAxPLO2rmcvwYNbCJFp7w4WT4Q== /bulma-upload-control/1.2.0: dependencies: bulma: 0.9.2