[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] branch master updated: refund modal
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] branch master updated: refund modal |
Date: |
Thu, 25 Mar 2021 14:42:43 +0100 |
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 1f89d3a refund modal
1f89d3a is described below
commit 1f89d3a791d201a1cbfe0a0993e648c4bcb61853
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Mar 25 10:42:30 2021 -0300
refund modal
---
CHANGELOG.md | 9 +-
packages/frontend/src/ApplicationReadyRoutes.tsx | 7 +-
packages/frontend/src/InstanceRoutes.tsx | 50 +++++++-
.../frontend/src/components/form/InputSelector.tsx | 71 +++++++++++
packages/frontend/src/components/menu/SideBar.tsx | 13 ++-
packages/frontend/src/components/menu/index.tsx | 6 +-
packages/frontend/src/components/modal/index.tsx | 18 +--
packages/frontend/src/declaration.d.ts | 130 +++++++++++++++++++++
packages/frontend/src/hooks/backend.ts | 124 ++++++++++++++------
packages/frontend/src/messages/en.po | 62 +++++++++-
.../paths/instance/orders/details/DetailPage.tsx | 87 ++++++++++++++
.../src/paths/instance/orders/details/index.tsx | 48 ++++++++
.../src/paths/instance/orders/list/Table.tsx | 112 ++++++++++++------
.../src/paths/instance/orders/list/index.tsx | 30 ++++-
.../src/paths/instance/orders/update/index.tsx | 26 -----
.../src/paths/instance/update/UpdatePage.tsx | 6 +-
packages/frontend/src/schemas/index.ts | 9 +-
packages/frontend/src/scss/_custom-calendar.scss | 8 +-
packages/frontend/src/scss/_theme-default.scss | 2 +-
packages/frontend/src/utils/constants.ts | 4 +-
packages/frontend/src/utils/types.ts | 4 +-
21 files changed, 678 insertions(+), 148 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5e2e4a..59e78d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,6 @@ The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).
## [Future work]
- - complete product list information (#6792)
- - complete order list information (#6793)
- gettext templates should be generated from the source code (#6791)
- date format (error handling)
@@ -27,9 +25,14 @@ 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 thedetailed
screen for the product
- -
+
+ - BUG: updating instance, shoot POST /instance/default/private/auth with
token method:token token:secret-token:undefined
## [Unreleased]
+ - complete order list information (#6793)
+ - complete product list information (#6792)
+ - missing fields in the instance update
+
## [0.0.5] - 2021-03-18
- change the admin title to "instances" if we are listing the instances and
"settings: $ID" on updating instances (#6790)
- update title with: Taler Backoffice: $PAGE_TITLE (#6790)
diff --git a/packages/frontend/src/ApplicationReadyRoutes.tsx
b/packages/frontend/src/ApplicationReadyRoutes.tsx
index 607a6fe..d8a92ce 100644
--- a/packages/frontend/src/ApplicationReadyRoutes.tsx
+++ b/packages/frontend/src/ApplicationReadyRoutes.tsx
@@ -20,7 +20,6 @@
*/
import { Fragment, h, VNode } from 'preact';
import { route } from 'preact-router';
-import { Notification } from "./utils/types";
import { useBackendContext } from './context/backend';
import { useBackendInstancesTestForAdmin } from "./hooks/backend";
import { InstanceRoutes } from "./InstanceRoutes";
@@ -63,13 +62,13 @@ export function ApplicationReadyRoutes(): VNode {
const path = new URL(backendURL).pathname
const match = INSTANCE_ID_LOOKUP.exec(path)
if (!match || !match[1]) {
- // this should be rare becuase
+ // this should be rare because
// query to /config is ok but the URL
// doest not match with our pattern
return <Fragment>
<NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} />
<NotificationCard notification={{
- message: i18n`Couldnt access the server`,
+ message: i18n`Couldn't access the server`,
description: i18n`Could not infer instance id from url
${backendURL}`,
type: 'ERROR',
}}
@@ -88,7 +87,7 @@ export function ApplicationReadyRoutes(): VNode {
return <Fragment>
<NotYetReadyAppMenu title="Error" />
<NotificationCard notification={{
- message: i18n`Couldnt access the server`,
+ message: i18n`Couldn't access the server`,
description: list.error.message,
type: 'ERROR'
}} />
diff --git a/packages/frontend/src/InstanceRoutes.tsx
b/packages/frontend/src/InstanceRoutes.tsx
index 01f6204..7f16416 100644
--- a/packages/frontend/src/InstanceRoutes.tsx
+++ b/packages/frontend/src/InstanceRoutes.tsx
@@ -40,7 +40,7 @@ import ProductUpdatePage from
'./paths/instance/products/update'
import OrderListPage from './paths/instance/orders/list'
import OrderCreatePage from './paths/instance/orders/create'
-import OrderUpdatePage from './paths/instance/orders/update'
+import OrderDetailsPage from './paths/instance/orders/details'
import TipListPage from './paths/instance/tips/list'
import TipCreatePage from './paths/instance/tips/create'
@@ -63,8 +63,8 @@ export enum InstancePaths {
product_new = '/product/new',
order_list = '/orders',
- order_update = '/order/:oid/update',
- // order_new = '/oreder/new',
+ order_details = '/order/:oid/details',
+ // order_new = '/order/new',
// tips_list = '/tips',
// tips_update = '/tip/:rid/update',
@@ -241,6 +241,18 @@ export function InstanceRoutes({ id, admin }: Props):
VNode {
}}
onLoadError={(error: SwrError) => {
+ 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} forceId="default"
onConfirm={() => {
+ route(AdminPaths.list_instances)
+ }} />
+ </Fragment>
+ }
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} />
@@ -320,6 +332,10 @@ export function InstanceRoutes({ id, admin }: Props):
VNode {
<Route path={InstancePaths.order_list}
component={OrderListPage}
+ onSelect={ (id:string) => {
+ route(InstancePaths.order_details.replace(':oid',id))
+ }}
+
onUnauthorized={() => {
return <Fragment>
<NotificationCard notification={{ message: i18n`Access denied`,
description: i18n`Check your token is valid`, type: 'ERROR', }} />
@@ -350,8 +366,30 @@ export function InstanceRoutes({ id, admin }: Props):
VNode {
</Fragment>
}}
/>
- <Route path={InstancePaths.order_update}
- component={OrderUpdatePage}
+ <Route path={InstancePaths.order_details}
+ component={OrderDetailsPage}
+
+ onUnauthorized={() => {
+ return <Fragment>
+ <NotificationCard notification={{ message: i18n`Access denied`,
description: i18n`Check your token is valid`, type: 'ERROR', }} />
+ <LoginPage onConfirm={updateLoginStatus} />
+ </Fragment>
+ }}
+
+ onNotFound={() => {
+ 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>
+ }}
+
+ onBack={() => {
+ route(InstancePaths.order_list)
+ }}
/>
{/*
<Route path={InstancePaths.tips_list}
@@ -432,7 +470,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode
{
{/* example of loading page*/}
<Route path="/loading" component={Loading} />
- <Route default component={NotFoundPage} />
+ <Route default component={NotFoundPage} />
</Router>
</InstanceContextProvider>;
diff --git a/packages/frontend/src/components/form/InputSelector.tsx
b/packages/frontend/src/components/form/InputSelector.tsx
new file mode 100644
index 0000000..8914a99
--- /dev/null
+++ b/packages/frontend/src/components/form/InputSelector.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { Message, useMessage } from "preact-messages";
+import { useField } from "./Field";
+
+interface Props<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ values: string[];
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || ''
+const defaultFromString = (v: string): any => v as any
+
+export function InputSelector<T>({ name, readonly, expand, values, fromStr =
defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const placeholder = useMessage(`fields.instance.${name}.placeholder`);
+ const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <Message id={`fields.instance.${name}.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">
+ <p class={expand ? "control is-expanded select" : "control select"}>
+ <select class={error ? "select is-danger" : "select"}
+ name={String(name)} disabled={readonly} readonly={readonly}
+ onChange={(e) => { onChange(fromStr(e.currentTarget.value)) }}>
+ <option>{placeholder}</option>
+ {values
+ // .filter((l) => l !== value)
+ .map(v => <option value={toStr(v)}>{toStr(v)}</option>)}
+ </select>
+ <Message id={`fields.instance.${name}.help`}> </Message>
+ </p>
+ {error ? <p class="help is-danger">
+ <Message id={`validation.${error.type}`}
fields={error.params}>{error.message} </Message>
+ </p> : null}
+ </div>
+ </div>
+ </div>;
+}
diff --git a/packages/frontend/src/components/menu/SideBar.tsx
b/packages/frontend/src/components/menu/SideBar.tsx
index cd69f01..92e0f46 100644
--- a/packages/frontend/src/components/menu/SideBar.tsx
+++ b/packages/frontend/src/components/menu/SideBar.tsx
@@ -105,14 +105,21 @@ export function Sidebar({ mobile, instance, onLogout,
admin }: Props): VNode {
</span>
</div>
</li>
- {admin &&
+ {admin && <Fragment>
+ <p class="menu-label">Instances</p>
<li>
<a href="/instance/new" class="has-icon">
<span class="icon"><i class="mdi mdi-plus" /></span>
- <span class="menu-item-label">New Instance</span>
+ <span class="menu-item-label">New</span>
</a>
</li>
- }
+ <li>
+ <a href="/instances" class="has-icon">
+ <span class="icon"><i class="mdi mdi-format-list-bulleted"
/></span>
+ <span class="menu-item-label">List</span>
+ </a>
+ </li>
+ </Fragment>}
<li>
<a class="has-icon is-state-info is-hoverable" onClick={(): void
=> onLogout()}>
<span class="icon"><i class="mdi mdi-logout default" /></span>
diff --git a/packages/frontend/src/components/menu/index.tsx
b/packages/frontend/src/components/menu/index.tsx
index ce80248..af0b074 100644
--- a/packages/frontend/src/components/menu/index.tsx
+++ b/packages/frontend/src/components/menu/index.tsx
@@ -31,7 +31,7 @@ function getInstanceTitle(path: string, id: string): string {
case InstancePaths.update: return `${id}: Settings`
case InstancePaths.order_list: return `${id}: Orders`
// case InstancePaths.order_new: return `${id}: New order`
- case InstancePaths.order_update: return `${id}: Update order`
+ case InstancePaths.order_details: return `${id}: Detail of the order`
case InstancePaths.product_list: return `${id}: Products`
case InstancePaths.product_new: return `${id}: New product`
case InstancePaths.product_update: return `${id}: Update product`
@@ -93,15 +93,13 @@ interface NotifProps {
notification?: Notification;
}
export function NotificationCard({ notification:n }: NotifProps) {
- // const [n, setNotif] = useState(notification)
if (!n) return null
return <div class="notification">
<div class="columns is-vcentered">
<div class="column is-12">
- <article class={n.type === 'ERROR' ? "message is-danger" : "message
is-info"}>
+ <article class={n.type === 'ERROR' ? "message is-danger" : (n.type ===
'WARN' ? "message is-warning" : "message is-info")}>
<div class="message-header">
<p>{n.message}</p>
- {/* {n.type !== 'ERROR' && <button class="delete"
aria-label="delete" onClick={() => setNotif(undefined)}></button> } */}
</div>
<div class="message-body">
{n.description}
diff --git a/packages/frontend/src/components/modal/index.tsx
b/packages/frontend/src/components/modal/index.tsx
index aaa50fc..5bc3eb6 100644
--- a/packages/frontend/src/components/modal/index.tsx
+++ b/packages/frontend/src/components/modal/index.tsx
@@ -42,15 +42,17 @@ export function ConfirmModal({ active, description,
onCancel, onConfirm, childre
<div class="modal-background " onClick={onCancel} />
<div class="modal-card">
<header class="modal-card-head">
- {!description ? null : <p class="modal-card-title"> <Message
id={description} /></p> }
+ {!description ? null : <p class="modal-card-title"> <Message
id={description} /></p>}
<button class="delete " aria-label="close" onClick={onCancel} />
</header>
<section class="modal-card-body">
{children}
</section>
<footer class="modal-card-foot">
- <button class="button " onClick={onCancel} ><Message id="Cancel"
/></button>
- <button class={danger ? "button is-danger " : "button is-info "}
disabled={disabled} onClick={onConfirm} ><Message id="Confirm" /></button>
+ <div class="buttons is-right" style={{width: '100%'}}>
+ <button class="button " onClick={onCancel} ><Message id="Cancel"
/></button>
+ <button class={danger ? "button is-danger " : "button is-info "}
disabled={disabled} onClick={onConfirm} ><Message id="Confirm" /></button>
+ </div>
</footer>
</div>
<button class="modal-close is-large " aria-label="close"
onClick={onCancel} />
@@ -62,7 +64,7 @@ export function ClearConfirmModal({ description, onCancel,
onClear, onConfirm, c
<div class="modal-background " onClick={onCancel} />
<div class="modal-card">
<header class="modal-card-head">
- {!description ? null : <p class="modal-card-title"> <Message
id={description} /></p> }
+ {!description ? null : <p class="modal-card-title"> <Message
id={description} /></p>}
<button class="delete " aria-label="close" onClick={onCancel} />
</header>
<section class="modal-card-body">
@@ -100,16 +102,16 @@ interface UpdateTokenModalProps {
}
export function UpdateTokenModal({ element, onCancel, onClear, onConfirm,
oldToken }: UpdateTokenModalProps): VNode {
- type State = {old_token: string, new_token: string}
+ type State = { old_token: string, new_token: string }
const [form, setValue] = useState<Partial<State>>({
old_token: '', new_token: ''
})
const errors = {
old_token: oldToken && oldToken !== form.old_token ? { message: 'should be
the same' } : undefined,
- new_token: !form.new_token ? { message: 'should be the same' } : (
form.new_token === form.old_token ? { message: 'cant repeat' } : undefined ),
+ new_token: !form.new_token ? { message: 'should be the same' } :
(form.new_token === form.old_token ? { message: 'cant repeat' } : undefined),
}
-
+
return <ClearConfirmModal description="update_token"
onCancel={onCancel}
onConfirm={() => onConfirm(form.new_token!)}
@@ -124,3 +126,5 @@ export function UpdateTokenModal({ element, onCancel,
onClear, onConfirm, oldTok
<p>Clearing the auth token will mean public access to the instance</p>
</ClearConfirmModal>
}
+
+
diff --git a/packages/frontend/src/declaration.d.ts
b/packages/frontend/src/declaration.d.ts
index 8a882f6..cf639b1 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -528,6 +528,136 @@ export namespace MerchantBackend {
}
namespace Orders {
+
+ type MerchantOrderStatusResponse = CheckPaymentPaidResponse |
+ CheckPaymentClaimedResponse |
+ CheckPaymentUnpaidResponse;
+ interface CheckPaymentPaidResponse {
+ // The customer paid for this contract.
+ order_status: "paid";
+
+ // Was the payment refunded (even partially)?
+ refunded: boolean;
+
+ // True if there are any approved refunds that the wallet has
+ // not yet obtained.
+ refund_pending: boolean;
+
+ // Did the exchange wire us the funds?
+ wired: boolean;
+
+ // Total amount the exchange deposited into our bank account
+ // for this contract, excluding fees.
+ deposit_total: Amount;
+
+ // Numeric error code indicating errors the exchange
+ // encountered tracking the wire transfer for this purchase (before
+ // we even got to specific coin issues).
+ // 0 if there were no issues.
+ exchange_ec: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information to track the wire transfer for this purchase.
+ // 0 if there were no issues.
+ exchange_hc: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: Amount;
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+
+ // The wire transfer status from the exchange for this order if
+ // available, otherwise empty array.
+ wire_details: TransactionWireTransfer[];
+
+ // Reports about trouble obtaining wire transfer details,
+ // empty array if no trouble were encountered.
+ wire_reports: TransactionWireReport[];
+
+ // The refund details for this order. One entry per
+ // refunded coin; empty array if there are no refunds.
+ refund_details: RefundDetails[];
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+ }
+ interface CheckPaymentClaimedResponse {
+ // A wallet claimed the order, but did not yet pay for the
contract.
+ order_status: "claimed";
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+
+ }
+ interface CheckPaymentUnpaidResponse {
+ // The order was neither claimed nor paid.
+ order_status: "unpaid";
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // Alternative order ID which was paid for already in the same
session.
+ // Only given if the same product was purchased before in the same
session.
+ already_paid_order_id?: string;
+
+ // Fulfillment URL of an already paid order. Only given if under
this
+ // session an already paid order with a fulfillment URL exists.
+ already_paid_fulfillment_url?: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+
+ // We do we NOT return the contract terms here because they may not
+ // exist in case the wallet did not yet claim them.
+ }
+ interface RefundDetails {
+ // Reason given for the refund.
+ reason: string;
+
+ // When was the refund approved.
+ timestamp: Timestamp;
+
+ // Total amount that was refunded (minus a refund fee).
+ amount: Amount;
+ }
+ interface TransactionWireTransfer {
+ // Responsible exchange.
+ exchange_url: string;
+
+ // 32-byte wire transfer identifier.
+ wtid: Base32;
+
+ // Execution time of the wire transfer.
+ execution_time: Timestamp;
+
+ // Total amount that has been wire transferred
+ // to the merchant.
+ amount: Amount;
+
+ // Was this transfer confirmed by the merchant via the
+ // POST /transfers API, or is it merely claimed by the exchange?
+ confirmed: boolean;
+ }
+ interface TransactionWireReport {
+ // Numerical error code.
+ code: number;
+
+ // Human-readable error description.
+ hint: string;
+
+ // Numerical error code from the exchange.
+ exchange_ec: number;
+
+ // HTTP status code received from the exchange.
+ exchange_hc: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKey;
+ }
+
interface OrderHistory {
// timestamp-sorted array of all orders matching the query.
// The order of the sorting depends on the sign of delta.
diff --git a/packages/frontend/src/hooks/backend.ts
b/packages/frontend/src/hooks/backend.ts
index 540ed43..59bcdd0 100644
--- a/packages/frontend/src/hooks/backend.ts
+++ b/packages/frontend/src/hooks/backend.ts
@@ -25,7 +25,7 @@ import { MerchantBackend } from '../declaration';
import { useBackendContext, useInstanceContext } from '../context/backend';
import { useEffect, useMemo, useState } from 'preact/hooks';
import { MAX_RESULT_SIZE, PAGE_SIZE } from '../utils/constants';
-import { format, max } from 'date-fns';
+import { add, addHours, addSeconds, format, max } from 'date-fns';
function mutateAll(re: RegExp) {
cache.keys().filter(key => re.test(key)).forEach(key => mutate(key, null))
@@ -99,7 +99,8 @@ async function request(url: string, options: RequestOptions =
{}): Promise<any>
} catch (e) {
const info = e.response?.data
const status = e.response?.status
- throw { info, status, message: e.message, backend: url, hasToken:
!!options.token }
+ const hint = info?.hint
+ throw { info, status, message: hint || e.message, backend: url, hasToken:
!!options.token }
}
}
@@ -110,8 +111,12 @@ function fetcher(url: string, token: string, backend:
string) {
type YesOrNo = 'yes' | 'no';
-function orderFetcher(url: string, token: string, backend: string, paid?:
YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, date?: Date, delta?: number) {
- return request(`${backend}${url}`, { token, params: { paid, refunded, wired,
delta, date: date? format(date, 'yyyy-MM-dd HH:mm:ss'): undefined } })
+function orderFetcher(url: string, token: string, backend: string, paid?:
YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, searchDate?: Date, delta?:
number) {
+ const newDate = searchDate && addHours(searchDate, 3) // remove this, locale
+ // if we are
+ const newDatePlus1SecIfNeeded = delta && delta < 0 && newDate ?
addSeconds(newDate, 1) : newDate
+ const date = newDatePlus1SecIfNeeded ? format(newDatePlus1SecIfNeeded,
'yyyy-MM-dd HH:mm:ss') : undefined
+ return request(`${backend}${url}`, { token, params: { paid, refunded, wired,
delta, date } })
}
function transferFetcher(url: string, token: string, backend: string) {
@@ -213,6 +218,7 @@ interface OrderMutateAPI {
//FIXME: add OutOfStockResponse on 410
createOrder: (data: MerchantBackend.Orders.PostOrderRequest) =>
Promise<MerchantBackend.Orders.PostOrderResponse>;
forgetOrder: (id: string, data: MerchantBackend.Orders.ForgetRequest) =>
Promise<void>;
+ refundOrder: (id: string, data: MerchantBackend.Orders.RefundRequest) =>
Promise<MerchantBackend.Orders.MerchantRefundResponse>;
deleteOrder: (id: string) => Promise<void>;
}
@@ -236,6 +242,17 @@ export function useOrderMutateAPI(): OrderMutateAPI {
mutateAll(/@"\/private\/orders"@/)
return res
}
+ const refundOrder = async (orderId: string, data:
MerchantBackend.Orders.RefundRequest):
Promise<MerchantBackend.Orders.MerchantRefundResponse> => {
+ const res = await request(`${url}/private/orders/${orderId}/refund`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ mutateAll(/@"\/private\/orders"@/)
+ return res
+ }
+
const forgetOrder = async (orderId: string, data:
MerchantBackend.Orders.ForgetRequest): Promise<void> => {
await request(`${url}/private/orders/${orderId}/forget`, {
method: 'patch',
@@ -253,9 +270,21 @@ export function useOrderMutateAPI(): OrderMutateAPI {
mutateAll(/@"\/private\/orders"@/)
}
- return { createOrder, forgetOrder, deleteOrder }
+ return { createOrder, forgetOrder, deleteOrder, refundOrder }
+}
+
+export function useOrderDetails(oderId:string):
HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> {
+ const { url: baseUrl } = useBackendContext();
+ const { token, id: instanceId, admin } = useInstanceContext();
+
+ const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
+
+ const { data, error } =
useSWR<MerchantBackend.Orders.MerchantOrderStatusResponse,
SwrError>([`/private/orders/${oderId}`, token, url], fetcher)
+
+ return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
}
+
interface TransferMutateAPI {
informTransfer: (data: MerchantBackend.Transfers.TransferInformation) =>
Promise<MerchantBackend.Transfers.MerchantTrackTransferResponse>;
}
@@ -411,26 +440,6 @@ export function useInstanceMutateAPI(): InstaceMutateAPI {
return { updateInstance, deleteInstance, setNewToken, clearToken }
}
-export function useBackendInstancesTestForAdmin():
HttpResponse<MerchantBackend.Instances.InstancesResponse> {
- const { url, token } = useBackendContext()
- interface Result {
- data?: MerchantBackend.Instances.InstancesResponse;
- error?: SwrError;
- }
- const [result, setResult] = useState<Result | undefined>(undefined)
-
- useEffect(() => {
- request(`${url}/private/instances`, { token })
- .then(data => setResult({ data }))
- .catch(error => setResult({ error }))
- }, [url, token])
-
- const data = result?.data
- const error = result?.error
-
- return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
-}
-
export function useBackendInstances():
HttpResponse<MerchantBackend.Instances.InstancesResponse> {
const { url, token } = useBackendContext()
const { data, error } = useSWR<MerchantBackend.Instances.InstancesResponse,
SwrError>(['/private/instances', token, url], fetcher)
@@ -485,7 +494,7 @@ export function useInstanceOrders(args:
InstanceOrderFilter, updateFilter: (d:Da
const [pageAfter, setPageAfter] = useState(1)
const totalAfter = pageAfter * PAGE_SIZE;
- const totalBefore = pageBefore * PAGE_SIZE;
+ const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0;
const { data:beforeData, error:beforeError } =
useSWR<MerchantBackend.Orders.OrderHistory, SwrError>(
[`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired,
args?.date, totalBefore],
@@ -506,31 +515,34 @@ export function useInstanceOrders(args:
InstanceOrderFilter, updateFilter: (d:Da
// this has problems when there are some ids missing
const isReachingEnd = afterData && afterData.orders.length < totalAfter;
- const isReachingStart = beforeData && beforeData.orders.length < totalBefore;
+ const isReachingStart = (!args?.date) || (beforeData &&
beforeData.orders.length < totalBefore);
+
+ const orders = !beforeData || !afterData ? undefined : (beforeData ||
lastBefore).orders.slice().reverse().concat((afterData || lastAfter).orders)
+ const unauthorized = beforeError?.status === 401 || afterError?.status ===
401
+ const notfound = beforeError?.status === 404 || afterError?.status === 404
const loadMore = () => {
- if (totalAfter < MAX_RESULT_SIZE) {
+ if (!orders) return
+ if (orders.length < MAX_RESULT_SIZE) {
setPageAfter(pageAfter + 1)
- } else {
- const from = afterData?.orders?.[PAGE_SIZE]?.timestamp?.t_ms
+ } else {
+ const from =
afterData?.orders?.[afterData?.orders?.length-1]?.timestamp?.t_ms
+ // console.log(afterData?.orders?.map(d => d.row_id), PAGE_SIZE, from &&
format(new Date(from), 'yyyy/MM/dd HH:mm:ss'))
if (from) updateFilter(new Date(from))
}
}
const loadMorePrev = () => {
- if (totalBefore < MAX_RESULT_SIZE) {
+ if (!orders) return
+ if (orders.length < MAX_RESULT_SIZE) {
setPageBefore(pageBefore + 1)
} else {
- const from = beforeData?.orders?.[PAGE_SIZE-1]?.timestamp?.t_ms
+ const from =
beforeData?.orders?.[beforeData?.orders?.length-1]?.timestamp?.t_ms
if (from) updateFilter(new Date(from))
}
}
- const orders = (beforeData || lastBefore ||
{orders:[]}).orders.slice().reverse().concat((afterData || lastAfter ||
{orders:[]}).orders)
- const unauthorized = beforeError?.status === 401 || afterError?.status ===
401
- const notfound = beforeError?.status === 404 || afterError?.status === 404
-
- return { data: {orders}, loadMorePrev, loadMore, isReachingEnd,
isReachingStart, unauthorized, notfound }
+ return { data: orders ? {orders} : undefined, loadMorePrev, loadMore,
isReachingEnd, isReachingStart, unauthorized, notfound, error: beforeError ?
beforeError : afterError }
}
export function useInstanceTips():
HttpResponse<MerchantBackend.Tips.TippingReserveStatus> {
@@ -563,9 +575,45 @@ export function useInstanceTransfers():
HttpResponse<MerchantBackend.Transfers.T
return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
}
+
+export function useBackendInstancesTestForAdmin():
HttpResponse<MerchantBackend.Instances.InstancesResponse> {
+ const { url, token } = useBackendContext()
+ interface Result {
+ data?: MerchantBackend.Instances.InstancesResponse;
+ error?: SwrError;
+ }
+ const [result, setResult] = useState<Result | undefined>(undefined)
+
+ useEffect(() => {
+ request(`${url}/private/instances`, { token })
+ .then(data => setResult({ data }))
+ .catch(error => setResult({ error }))
+ }, [url, token])
+
+ const data = result?.data
+ const error = result?.error
+
+ return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
+}
+
+
export function useBackendConfig():
HttpResponse<MerchantBackend.VersionResponse> {
const { url, token } = useBackendContext()
- const { data, error } = useSWR<MerchantBackend.VersionResponse,
SwrError>(['/config', token, url], fetcher)
+ interface Result {
+ data?: MerchantBackend.VersionResponse;
+ error?: SwrError;
+ }
+ const [result, setResult] = useState<Result | undefined>(undefined)
+
+ useEffect(() => {
+ request(`${url}/config`, { token })
+ .then(data => setResult({ data }))
+ .catch(error => setResult({ error }))
+ }, [url, token])
+
+ const data = result?.data
+ const error = result?.error
+
return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
}
diff --git a/packages/frontend/src/messages/en.po
b/packages/frontend/src/messages/en.po
index 8f19d50..8a52eec 100644
--- a/packages/frontend/src/messages/en.po
+++ b/packages/frontend/src/messages/en.po
@@ -58,8 +58,8 @@ msgstr "Use this token to secure an instance with a password"
msgid "fields.instance.payto_uris.label"
msgstr "Account address"
-# msgid "fields.instance.payto_uris.help"
-# msgstr "x-taler-bank/bank.taler:5882/blogger"
+msgid "fields.instance.payto_uris.help"
+msgstr "x-taler-bank/bank.taler:5882/blogger"
msgid "fields.instance.default_max_deposit_fee.label"
msgstr "Max deposit fee label"
@@ -283,3 +283,61 @@ msgstr "Creation succeed"
msgid "create_error"
msgstr "Creation failed"
+
+msgid "delete_instance"
+msgstr "Delete instance"
+
+# msgid "fields.instance.refund.placeholder"
+# msgstr ""
+
+# msgid "fields.instance.refund.tooltip"
+# msgstr ""
+
+msgid "fields.instance.refund.label"
+msgstr "Amount"
+
+msgid "fields.instance.mainReason.placeholder"
+msgstr "select an option"
+
+# msgid "fields.instance.reason.tooltip"
+# msgstr ""
+
+msgid "fields.instance.mainReason.label"
+msgstr "Reason"
+
+msgid "fields.instance.description.label"
+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"
+
diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
new file mode 100644
index 0000000..4c81302
--- /dev/null
+++ b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration";
+import { Input } from "../../../../components/form/Input";
+import { FormProvider } from "../../../../components/form/Field";
+import { NotificationCard } from "../../../../components/menu";
+
+type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
+interface Props {
+ onBack: () => void;
+ selected: Entity;
+
+}
+
+interface KeyValue {
+ [key: string]: string;
+}
+
+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)
+ const [errors, setErrors] = useState<KeyValue>({})
+
+
+ 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 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>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+
+ </div>
+
+}
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/orders/details/index.tsx
b/packages/frontend/src/paths/instance/orders/details/index.tsx
new file mode 100644
index 0000000..eae4f15
--- /dev/null
+++ b/packages/frontend/src/paths/instance/orders/details/index.tsx
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { Fragment, h, VNode } from "preact";
+import { Loading } from "../../../../components/exception/loading";
+import { SwrError, useOrderDetails, useOrderMutateAPI } from
"../../../../hooks/backend";
+ import { DetailPage } from "./DetailPage";
+
+export interface Props {
+ oid: string;
+
+ onBack: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: SwrError) => VNode;
+}
+
+export default function Update({ oid, onBack, onLoadError, onNotFound,
onUnauthorized }: Props): VNode {
+ const { refundOrder } = useOrderMutateAPI();
+ const details = useOrderDetails(oid)
+
+ if (details.unauthorized) return onUnauthorized()
+ if (details.notfound) return onNotFound();
+
+ if (!details.data) {
+ if (details.error) return onLoadError(details.error)
+ return <Loading />
+ }
+
+ return <Fragment>
+ <DetailPage
+ onBack={onBack}
+ 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
index 6f202d7..aa1f1cf 100644
--- a/packages/frontend/src/paths/instance/orders/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -23,16 +23,23 @@ import { format } from "date-fns";
import { Fragment, h, VNode } from "preact"
import { Message } from "preact-messages"
import { StateUpdater, useCallback, useEffect, useRef, useState } from
"preact/hooks"
+import { FormErrors, FormProvider } from "../../../../components/form/Field";
+import { Input } from "../../../../components/form/Input";
+import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { InputSelector } from "../../../../components/form/InputSelector";
+import { ConfirmModal } from "../../../../components/modal";
+import { useConfigContext } from "../../../../context/backend";
import { MerchantBackend, WidthId } from "../../../../declaration"
+import { RefoundSchema } from "../../../../schemas";
+import { AMOUNT_REGEX } from "../../../../utils/constants";
import { Actions, buildActions } from "../../../../utils/table";
type Entity = MerchantBackend.Orders.OrderHistoryEntry & { id: string }
interface Props {
instances: Entity[];
- onUpdate: (id: string) => void;
- onDelete: (id: Entity) => void;
+ onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
onCreate: () => void;
- selected?: boolean;
+ onSelect: (order: Entity) => void;
onLoadMoreBefore?: () => void;
hasMoreBefore?: boolean;
hasMoreAfter?: boolean;
@@ -42,23 +49,11 @@ interface Props {
// onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd}
-export function CardTable({ instances, onCreate, onUpdate, onDelete, selected,
onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode
{
+export function CardTable({ instances, onCreate, onRefund, onSelect,
onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode
{
const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]);
const [rowSelection, rowSelectionHandler] = useState<string[]>([])
- useEffect(() => {
- if (actionQueue.length > 0 && !selected && actionQueue[0].type ==
'DELETE') {
- onDelete(actionQueue[0].element)
- actionQueueHandler(actionQueue.slice(1))
- }
- }, [actionQueue, selected, onDelete])
-
- useEffect(() => {
- if (actionQueue.length > 0 && !selected && actionQueue[0].type ==
'UPDATE') {
- onUpdate(actionQueue[0].element.id)
- actionQueueHandler(actionQueue.slice(1))
- }
- }, [actionQueue, selected, onUpdate])
+ const [showRefund, setShowRefund] = useState<string | undefined>(undefined)
return <div class="card has-table">
@@ -66,7 +61,7 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected, o
<p class="card-header-title"><span class="icon"><i class="mdi
mdi-cash-register" /></span><Message id="Orders" /></p>
<div class="card-header-icon" aria-label="more options" />
-
+
<div class="card-header-icon" aria-label="more options">
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small" ><i class="mdi mdi-plus mdi-36px"
/></span>
@@ -78,7 +73,7 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected, o
<div class="b-table has-pagination">
<div class="table-wrapper has-mobile-cards">
{instances.length > 0 ?
- <Table instances={instances} onUpdate={onUpdate}
onDelete={onDelete}
+ <Table instances={instances} onSelect={onSelect} onRefund={(order)
=> setShowRefund(order.id)}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
hasMoreAfter={hasMoreAfter} hasMoreBefore={hasMoreBefore}
@@ -88,15 +83,20 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected, o
</div>
</div>
</div>
+ {showRefund && <RefundModal
+ onCancel={() => setShowRefund(undefined)}
+ onConfirm={(value) => {
+ onRefund(showRefund, value)
+ setShowRefund(undefined)
+ }}
+ />}
</div>
-
-
}
interface TableProps {
rowSelection: string[];
instances: Entity[];
- onUpdate: (id: string) => void;
- onDelete: (id: Entity) => void;
+ onRefund: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
hasMoreBefore?: boolean;
@@ -104,13 +104,9 @@ interface TableProps {
onLoadMoreAfter?: () => void;
}
-function toggleSelected<T>(id: T): (prev: T[]) => T[] {
- return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] :
prev.filter(e => e != id)
-}
-
-function Table({ rowSelection, rowSelectionHandler, instances, onUpdate,
onDelete, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }:
TableProps): VNode {
+function Table({ instances, onSelect, onRefund, onLoadMoreAfter,
onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode {
return <div class="table-container">
- {onLoadMoreBefore && <button class="button is-fullwidth"
disabled={!hasMoreBefore} onClick={onLoadMoreBefore}> load more before
</button>}
+ {onLoadMoreBefore && <button class="button is-fullwidth"
disabled={!hasMoreBefore} onClick={onLoadMoreBefore}> load more after </button>}
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>
@@ -121,23 +117,25 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
</tr>
</thead>
<tbody>
- {instances.map(i => {
+ {instances.map((i,pos) => {
return <tr>
- <td onClick={(): void => onUpdate(i.id)} style={{ cursor:
'pointer' }} >{format(new Date(i.timestamp.t_ms), 'dd/MM/yyyy HH:mm:ss')}</td>
- <td onClick={(): void => onUpdate(i.id)} style={{ cursor:
'pointer' }} >{i.amount}</td>
- <td onClick={(): void => onUpdate(i.id)} style={{ cursor:
'pointer' }} >{i.row_id}</td>
+ <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">
- <button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => onDelete(i)}>
- Refund
- </button>
+ {(i.refundable || pos === 0) &&
+ <button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => onRefund(i)}>
+ Refund
+ </button>
+ }
</div>
</td>
</tr>
})}
</tbody>
</table>
- {onLoadMoreAfter && <button class="button is-fullwidth"
disabled={!hasMoreAfter} onClick={onLoadMoreAfter}> load more after </button>}
+ {onLoadMoreAfter && <button class="button is-fullwidth"
disabled={!hasMoreAfter} onClick={onLoadMoreAfter}> load more before </button>}
</div>
}
@@ -150,4 +148,44 @@ function EmptyTable(): VNode {
</div>
}
+interface RefundModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
+}
+
+export function RefundModal({ onCancel, onConfirm }: RefundModalProps): VNode {
+ const config = useConfigContext()
+ type State = { mainReason?: string, description?: string, refund?: string }
+ const [form, setValue] = useState<State>({})
+
+ const [errors, setErrors] = useState<FormErrors<State>>({})
+
+ const validateAndConfirm = () => {
+ try {
+ RefoundSchema.validateSync(form, { abortEarly: false })
+ if (!form.refund) return;
+ onConfirm({
+ refund: form.refund,
+ reason: `${form.mainReason}: ${form.description}`
+ })
+ } catch (err) {
+ const errors = err.inner as any[]
+ const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message
} }), {})
+ setErrors(pathMessages)
+ }
+ }
+ 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} />
+ <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
index b5a8770..e3a4538 100644
--- a/packages/frontend/src/paths/instance/orders/list/index.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/index.tsx
@@ -27,31 +27,36 @@ import { InstanceOrderFilter, SwrError, useInstanceOrders,
useOrderMutateAPI, us
import { CardTable } from './Table';
import { format } from 'date-fns';
import { DatePicker } from '../../../../components/form/DatePicker';
+import { NotificationCard } from '../../../../components/menu';
+import { Notification } from '../../../../utils/types';
interface Props {
onUnauthorized: () => VNode;
onLoadError: (e: SwrError) => VNode;
onNotFound: () => VNode;
+ onSelect: (id: string) => void;
onCreate: () => void;
}
-export default function ({ onUnauthorized, onLoadError, onCreate, onNotFound
}: Props): VNode {
+export default function ({ onUnauthorized, onLoadError, onCreate, onSelect,
onNotFound }: Props): VNode {
const [filter, setFilter] = useState<InstanceOrderFilter>({paid:'yes'})
const [pickDate, setPickDate] = useState(false)
const setNewDate = (date:Date) => setFilter(prev => ({...prev,date}))
+
const result = useInstanceOrders(filter, setNewDate)
- const { createOrder, deleteOrder } = useOrderMutateAPI()
+ const { createOrder, refundOrder } = useOrderMutateAPI()
const { currency } = useConfigContext()
let instances: (MerchantBackend.Orders.OrderHistoryEntry & { id: string })[];
+ const [notif, setNotif] = useState<Notification | undefined>(undefined)
+
if (result.unauthorized) return onUnauthorized()
if (result.notfound) return onNotFound()
if (!result.data) {
if (result.error) return onLoadError(result.error)
- //if loading assume empty list
instances = []
} else {
instances = result.data.orders.map(o => ({ ...o, id: o.order_id }))
@@ -66,6 +71,12 @@ export default function ({ onUnauthorized, onLoadError,
onCreate, onNotFound }:
const isAllActive = filter.paid === undefined && filter.refunded ===
undefined && filter.wired === undefined ? 'is-active' : ''
return <section class="section is-main-section">
+ <NotificationCard notification={{
+ message: 'DEMO WARNING',
+ type:'WARN',
+ description: 'refund button is being forced in the first row, other
depends on the refundable property'
+ }} />
+ <NotificationCard notification={notif} />
<DatePicker
opened={pickDate}
@@ -112,8 +123,17 @@ export default function ({ onUnauthorized, onLoadError,
onCreate, onNotFound }:
summary: `some summary with a random number
${Math.floor(Math.random() * 20 + 1)}`,
}
})}
- onDelete={(order: MerchantBackend.Orders.OrderHistoryEntry) =>
deleteOrder(order.order_id)}
- onUpdate={() => null}
+ onSelect={(order) => onSelect(order.id)}
+ 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
+ }))
+ }
onLoadMoreBefore={result.loadMorePrev}
hasMoreBefore={!result.isReachingStart}
onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd}
/>
diff --git a/packages/frontend/src/paths/instance/orders/update/index.tsx
b/packages/frontend/src/paths/instance/orders/update/index.tsx
deleted file mode 100644
index c1b69e3..0000000
--- a/packages/frontend/src/paths/instance/orders/update/index.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode } from 'preact';
-
-export default function ():VNode {
- return <div>order update page</div>
-}
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx
b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
index ac6eec4..40a2726 100644
--- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
@@ -74,8 +74,12 @@ export function UpdatePage({ onUpdate, isLoading, selected,
onBack }: Props): VN
// use conversion instead of this
const newToken = value.auth_token;
value.auth_token = undefined;
+
+ //if new token was not set or has been set to the actual current token
+ //it is not needed to send a change
+ //otherwise, checked where we are setting a new token or removing it
const auth: MerchantBackend.Instances.InstanceAuthConfigurationMessage |
undefined =
- newToken === currentTokenValue ? undefined : (newToken === null ?
+ newToken === undefined || newToken === currentTokenValue ? undefined :
(newToken === null ?
{ method: "external" } :
{ method: "token", token: `secret-token:${newToken}` });
diff --git a/packages/frontend/src/schemas/index.ts
b/packages/frontend/src/schemas/index.ts
index c595ea5..02d3f15 100644
--- a/packages/frontend/src/schemas/index.ts
+++ b/packages/frontend/src/schemas/index.ts
@@ -58,7 +58,7 @@ export const InstanceSchema = yup.object().shape({
.test('payto', '{path} is not valid', listOfPayToUrisAreValid),
default_max_deposit_fee: yup.string()
.required()
- .test('amount', '{path} is not valid', currencyWithAmountIsValid)
+ .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
.meta({type: 'amount'}),
default_max_wire_fee: yup.string()
.required()
@@ -105,3 +105,10 @@ export const InstanceSchema = yup.object().shape({
export const InstanceUpdateSchema = InstanceSchema.clone().omit(['id']);
export const InstanceCreateSchema = InstanceSchema.clone();
+export const RefoundSchema = yup.object().shape({
+ mainReason: yup.string().required(),
+ description: yup.string().required(),
+ refund: yup.string()
+ .required()
+ .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
+})
\ No newline at end of file
diff --git a/packages/frontend/src/scss/_custom-calendar.scss
b/packages/frontend/src/scss/_custom-calendar.scss
index 10dbb76..32746ed 100644
--- a/packages/frontend/src/scss/_custom-calendar.scss
+++ b/packages/frontend/src/scss/_custom-calendar.scss
@@ -1,11 +1,5 @@
:root {
- --primary-color: #673ab7;
- --primary-color-light: #9a67ea;
- --primary-color-dark: #320b86;
-
- --secondary-color: #ffc400;
- --secondary-color-light: #fff64f;
- --secondary-color-dark: #c79400;
+ --primary-color: #3298dc;
--primary-text-color-dark: rgba(0,0,0,.87);
--secondary-text-color-dark: rgba(0,0,0,.57);
diff --git a/packages/frontend/src/scss/_theme-default.scss
b/packages/frontend/src/scss/_theme-default.scss
index 091c42d..538dfd4 100644
--- a/packages/frontend/src/scss/_theme-default.scss
+++ b/packages/frontend/src/scss/_theme-default.scss
@@ -102,7 +102,7 @@ $modal-card-head-border-bottom: 1px solid $white-ter;
$modal-card-foot-border-top: 0;
/* Modal card: specifics */
-$modal-card-width: 40vw;
+$modal-card-width: 80vw;
$modal-card-width-mobile: 90vw;
$modal-card-foot-background-color: $white-ter;
diff --git a/packages/frontend/src/utils/constants.ts
b/packages/frontend/src/utils/constants.ts
index 148275b..a65f58b 100644
--- a/packages/frontend/src/utils/constants.ts
+++ b/packages/frontend/src/utils/constants.ts
@@ -27,7 +27,7 @@ export const
AMOUNT_REGEX=/^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/
// how much rows we add every time user hit load more
-export const PAGE_SIZE = 10
+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 = 19;
\ No newline at end of file
+export const MAX_RESULT_SIZE = 39;
\ No newline at end of file
diff --git a/packages/frontend/src/utils/types.ts
b/packages/frontend/src/utils/types.ts
index c018e11..9e49d39 100644
--- a/packages/frontend/src/utils/types.ts
+++ b/packages/frontend/src/utils/types.ts
@@ -14,13 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { VNode } from "preact"
+
export interface KeyValue {
[key: string]: string;
}
export interface Notification {
message: string;
- description?: string;
+ description?: string | VNode;
type: MessageType;
}
--
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: refund modal,
gnunet <=