[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] branch master updated: refunded table
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] branch master updated: refunded table |
Date: |
Tue, 13 Apr 2021 22:57:00 +0200 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository merchant-backoffice.
The following commit(s) were added to refs/heads/master by this push:
new 2444fb8 refunded table
2444fb8 is described below
commit 2444fb8a6d3160fe2ed639207a7856f026913fd4
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Apr 13 17:56:43 2021 -0300
refunded table
---
CHANGELOG.md | 1 -
.../frontend/src/components/form/InputCurrency.tsx | 4 +-
.../frontend/src/components/form/InputGroup.tsx | 5 +-
.../paths/instance/orders/create/CreatePage.tsx | 35 +++---------
.../paths/instance/orders/details/DetailPage.tsx | 9 ++--
.../src/paths/instance/orders/list/Table.tsx | 62 ++++++++++++++++++----
packages/frontend/src/utils/amount.ts | 62 ++++++++++++++++++++++
packages/frontend/src/utils/constants.ts | 18 ++++---
8 files changed, 143 insertions(+), 53 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29fcb9f..51e0f45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,7 +25,6 @@ and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0
- check if there is a way to remove auto async for /routes
/components/{async,routes} so it can be turned on when building
non-single-bundle
- product detail: we could have some button that brings us to the detailed
screen for the product
- - order id field to go
- input number
- navigation to another instance should not do full refresh
diff --git a/packages/frontend/src/components/form/InputCurrency.tsx
b/packages/frontend/src/components/form/InputCurrency.tsx
index fa1eba9..f932d34 100644
--- a/packages/frontend/src/components/form/InputCurrency.tsx
+++ b/packages/frontend/src/components/form/InputCurrency.tsx
@@ -28,15 +28,17 @@ export interface Props<T> {
expand?: boolean;
currency: string;
addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
}
-export function InputCurrency<T>({ name, readonly, expand, currency,
addonAfter }: Props<T>) {
+export function InputCurrency<T>({ name, readonly, expand, currency,
addonAfter, children }: Props<T>) {
return <InputWithAddon<T> name={name} readonly={readonly}
addonBefore={currency}
addonAfter={addonAfter}
inputType='number' expand={expand}
toStr={(v?: Amount) => v?.split(':')[1] || ''}
fromStr={(v: string) => !v ? '' : `${currency}:${v}`}
inputExtra={{ min: 0 }}
+ children={children}
/>
}
diff --git a/packages/frontend/src/components/form/InputGroup.tsx
b/packages/frontend/src/components/form/InputGroup.tsx
index 5d9e551..e80ef66 100644
--- a/packages/frontend/src/components/form/InputGroup.tsx
+++ b/packages/frontend/src/components/form/InputGroup.tsx
@@ -26,17 +26,18 @@ import { useField, useGroupField } from "./Field";
export interface Props<T> {
name: keyof T;
children: ComponentChildren;
+ description?: string;
alternative?: ComponentChildren;
}
-export function InputGroup<T>({ name, children, alternative}: Props<T>): VNode
{
+export function InputGroup<T>({ name, description, children, alternative}:
Props<T>): VNode {
const [active, setActive] = useState(false);
const group = useGroupField<T>(name);
return <div class="card">
<header class="card-header">
<p class={ !group?.hasError ? "card-header-title" : "card-header-title
has-text-danger"}>
- <Message id={`fields.instance.${String(name)}.label`} />
+ { description ? description : <Message
id={`fields.instance.${String(name)}.label`} /> }
</p>
<button class="card-header-icon" aria-label="more options" onClick={():
void => setActive(!active)}>
<span class="icon">
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index d5b4527..46dad20 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -35,6 +35,7 @@ import * as yup from 'yup';
import { InputDate } from "../../../../components/form/InputDate";
import { useInstanceDetails } from "../../../../hooks/instance";
import { add } from "date-fns";
+import { multiplyPrice, rate, subtractPrices, sumPrices } from
"../../../../utils/amount";
interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@@ -102,6 +103,13 @@ export function CreatePage({ onCreate, onBack }: Props):
VNode {
summary: order.pricing.summary,
products: productList,
extra: value.extra,
+ pay_deadline: value.payments.pay_deadline ? { t_ms:
value.payments.pay_deadline.getTime()*1000 } : undefined,
+ wire_transfer_deadline: value.payments.pay_deadline ? { t_ms:
value.payments.pay_deadline.getTime()*1000 } : undefined,
+ refund_deadline: value.payments.refund_deadline ? { t_ms:
value.payments.refund_deadline.getTime()*1000 } : undefined,
+ max_fee: value.payments.max_fee,
+ max_wire_fee: value.payments.max_wire_fee,
+ delivery_date: value.payments.delivery_date ? { t_ms:
value.payments.delivery_date.getTime()*1000 } : undefined,
+ delivery_location: value.payments.delivery_location,
},
inventory_products: inventoryList.map(p => ({
product_id: p.product.id,
@@ -382,30 +390,3 @@ export function CreatePage({ onCreate, onBack }: Props):
VNode {
}
-const multiplyPrice = (price: string, q: number) => {
- const [currency, value] = price.split(':')
- const total = parseInt(value, 10) * q
- return `${currency}:${total}`
-}
-
-const sumPrices = (one: string, two: string) => {
- const [currency, valueOne] = one.split(':')
- const [, valueTwo] = two.split(':')
- return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
-}
-
-const subtractPrices = (one: string, two: string) => {
- const [currency, valueOne] = one.split(':')
- const [, valueTwo] = two.split(':')
- return `${currency}:${parseInt(valueOne, 10) - parseInt(valueTwo, 10)}`
-}
-
-const rate = (one?: string, two?: string) => {
- const [, valueOne] = (one || '').split(':')
- const [, valueTwo] = (two || '').split(':')
- const intOne = parseInt(valueOne, 10)
- const intTwo = parseInt(valueTwo, 10)
- if (!intTwo) return intOne
- return intOne / intTwo
-}
-
diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
index f631858..85b8d8e 100644
--- a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
+++ b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
@@ -31,6 +31,7 @@ import { copyToClipboard } from "../../../../utils/functions";
import { format } from "date-fns";
import { Event, Timeline } from "./Timeline";
import { RefundModal } from "../list/Table";
+import { mergeRefunds } from "../../../../utils/amount";
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
interface Props {
@@ -212,10 +213,10 @@ function PaidPage({ id, order, onRefund }: { id: string;
order: MerchantBackend.
description: 'delivery',
type: 'delivery'
})
- order.refund_details.forEach(e => {
+ order.refund_details.reduce(mergeRefunds,[]).forEach(e => {
events.push({
when: new Date(e.timestamp.t_ms),
- description: `refund: ${e.amount}`,
+ description: `refund: ${e.amount}: ${e.reason}`,
type: 'refund',
})
})
@@ -238,8 +239,7 @@ function PaidPage({ id, order, onRefund }: { id: string;
order: MerchantBackend.
const [errors, setErrors] = useState<KeyValue>({})
const config = useConfigContext()
- const refundable = !order.refunded &&
- new Date().getTime() < order.contract_terms.refund_deadline.t_ms
+ const refundable = new Date().getTime() <
order.contract_terms.refund_deadline.t_ms
return <div>
<section class="section">
@@ -408,6 +408,7 @@ export function DetailPage({ id, selected, onRefund }:
Props): VNode {
{DetailByStatus()}
{showRefund && <RefundModal
+ id={id}
onCancel={() => setShowRefund(undefined)}
onConfirm={(value) => {
onRefund(showRefund, value)
diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx
b/packages/frontend/src/paths/instance/orders/list/Table.tsx
index 9538d55..e0646cc 100644
--- a/packages/frontend/src/paths/instance/orders/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -26,12 +26,15 @@ import { StateUpdater, useCallback, useEffect, useRef,
useState } from "preact/h
import { FormErrors, FormProvider } from "../../../../components/form/Field";
import { Input } from "../../../../components/form/Input";
import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { InputGroup } from "../../../../components/form/InputGroup";
import { InputSelector } from "../../../../components/form/InputSelector";
import { ConfirmModal } from "../../../../components/modal";
import { useConfigContext } from "../../../../context/backend";
import { MerchantBackend, WithId } from "../../../../declaration"
+import { useOrderDetails } from "../../../../hooks/order";
import { RefoundSchema } from "../../../../schemas";
-import { AMOUNT_REGEX } from "../../../../utils/constants";
+import { mergeRefunds, subtractPrices, sumPrices } from
"../../../../utils/amount";
+import { AMOUNT_ZERO_REGEX } from "../../../../utils/constants";
import { Actions, buildActions } from "../../../../utils/table";
type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId
@@ -86,6 +89,7 @@ export function CardTable({ instances, onCreate, onRefund,
onCopyURL, onSelect,
</div>
</div>
{showRefund && <RefundModal
+ id={showRefund}
onCancel={() => setShowRefund(undefined)}
onConfirm={(value) => {
onRefund(showRefund, value)
@@ -120,7 +124,7 @@ function Table({ instances, onSelect, onRefund, onCopyURL,
onLoadMoreAfter, onLo
</tr>
</thead>
<tbody>
- {instances.map((i,pos) => {
+ {instances.map((i, pos) => {
return <tr>
<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>
@@ -158,11 +162,13 @@ function EmptyTable(): VNode {
interface RefundModalProps {
onCancel: () => void;
+ id: string;
onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
}
-export function RefundModal({ onCancel, onConfirm }: RefundModalProps): VNode {
+export function RefundModal({ id, onCancel, onConfirm }: RefundModalProps):
VNode {
const config = useConfigContext()
+ const result = useOrderDetails(id)
type State = { mainReason?: string, description?: string, refund?: string }
const [form, setValue] = useState<State>({})
@@ -183,14 +189,50 @@ export function RefundModal({ onCancel, onConfirm }:
RefundModalProps): VNode {
}
}
- return <ConfirmModal description="delete_instance" danger active
onCancel={onCancel} onConfirm={validateAndConfirm}>
- <div class="block">
- You are going to refund the order
- </div>
- <FormProvider<State> errors={errors} object={form} valueHandler={(d) =>
setValue(d as any)}>
- <InputCurrency<State> name="refund" currency={config.currency} />
+ const refunds = (result.ok && result.data.order_status === 'paid' ?
result.data.refund_details : [])
+ .reduce(mergeRefunds, [])
+ const totalRefunded = refunds.map(r => r.amount).reduce((p, c) =>
sumPrices(c, p), ':0')
+ const orderPrice = (result.ok && result.data.order_status === 'paid' ?
result.data.contract_terms.amount : undefined)
+ const totalRefundable = !orderPrice ? undefined : (refunds.length ?
subtractPrices(orderPrice, totalRefunded) : orderPrice)
+
+ const isRefundable = totalRefundable &&
!AMOUNT_ZERO_REGEX.test(totalRefundable)
+
+ 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" description={`${totalRefunded} was already
refunded`}>
+ <table class="table is-fullwidth">
+ <thead>
+ <tr>
+ <th>date</th>
+ <th>amount</th>
+ <th>reason</th>
+ </tr>
+ </thead>
+ <tbody>
+ {refunds.map(r => {
+ return <tr>
+ <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" currency={config.currency}>
+ Max refundable: {totalRefundable}
+ </InputCurrency>
<InputSelector name="mainReason" values={['duplicated', 'requested by
the customer', 'other']} />
{form.mainReason && <Input<State> name="description" />}
- </FormProvider>
+ </FormProvider> }
+
</ConfirmModal>
}
+
diff --git a/packages/frontend/src/utils/amount.ts
b/packages/frontend/src/utils/amount.ts
new file mode 100644
index 0000000..ac98f0d
--- /dev/null
+++ b/packages/frontend/src/utils/amount.ts
@@ -0,0 +1,62 @@
+import { MerchantBackend } from "../declaration";
+
+/**
+ * sums two prices,
+ * @param one
+ * @param two
+ * @returns
+ */
+export const sumPrices = (one: string, two: string) => {
+ const [currency, valueOne] = one.split(':')
+ const [, valueTwo] = two.split(':')
+ return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
+}
+
+/**
+ * merge refund with the same description and a difference less than one minute
+ * @param prev list of refunds that will hold the merged refunds
+ * @param cur new refund to add to the list
+ * @returns list with the new refund, may be merged with the last
+ */
+export function mergeRefunds(prev: MerchantBackend.Orders.RefundDetails[],
cur: MerchantBackend.Orders.RefundDetails) {
+ 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.reason !== tail.reason || //different reason
+ Math.abs(cur.timestamp.t_ms - tail.timestamp.t_ms) > 1000 * 60) {//more
than 1 minute difference
+
+ prev.push(cur)
+ return prev
+ }
+
+ prev[prev.length - 1] = {
+ ...tail,
+ amount: sumPrices(tail.amount, cur.amount)
+ }
+
+ return prev
+}
+
+export const multiplyPrice = (price: string, q: number) => {
+ const [currency, value] = price.split(':')
+ const total = parseInt(value, 10) * q
+ return `${currency}:${total}`
+}
+
+export const subtractPrices = (one: string, two: string) => {
+ const [currency, valueOne] = one.split(':')
+ const [, valueTwo] = two.split(':')
+ return `${currency}:${parseInt(valueOne, 10) - parseInt(valueTwo, 10)}`
+}
+
+export const rate = (one?: string, two?: string) => {
+ const [, valueOne] = (one || '').split(':')
+ const [, valueTwo] = (two || '').split(':')
+ const intOne = parseInt(valueOne, 10)
+ const intTwo = parseInt(valueTwo, 10)
+ if (!intTwo) return intOne
+ return intOne / intTwo
+}
+
diff --git a/packages/frontend/src/utils/constants.ts
b/packages/frontend/src/utils/constants.ts
index 8d642d7..8ca284e 100644
--- a/packages/frontend/src/utils/constants.ts
+++ b/packages/frontend/src/utils/constants.ts
@@ -14,20 +14,22 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
- //https://tools.ietf.org/html/rfc8905
-export const
PAYTO_REGEX=/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
+//https://tools.ietf.org/html/rfc8905
+export const PAYTO_REGEX =
/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
-export const AMOUNT_REGEX=/^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
+export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/
+export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/
+
// how much rows we add every time user hit load more
export const PAGE_SIZE = 20
// how bigger can be the result set
// after this threshold, load more with move the cursor
-export const MAX_RESULT_SIZE = PAGE_SIZE*2-1;
\ No newline at end of file
+export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-merchant-backoffice] branch master updated: refunded table,
gnunet <=