gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]