gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated: product update


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: product update
Date: Mon, 05 Apr 2021 23:20:21 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

The following commit(s) were added to refs/heads/master by this push:
     new 1b61402  product update
1b61402 is described below

commit 1b6140287ad5102ce66a7f3be4e9a977192530c6
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Apr 5 18:20:04 2021 -0300

    product update
---
 CHANGELOG.md                                       |   2 +-
 packages/frontend/src/InstanceRoutes.tsx           |   4 +-
 .../frontend/src/components/form/InputCurrency.tsx |   3 +-
 .../src/components/form/InputWithAddon.tsx         |   5 +-
 packages/frontend/src/messages/en.po               |  11 +-
 .../src/paths/instance/products/list/Table.tsx     | 142 +++++++++++++--------
 .../src/paths/instance/products/list/index.tsx     |  55 +++++---
 7 files changed, 144 insertions(+), 78 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee6c160..fc7b7b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,7 @@ 
https://git.taler.net/anastasis.git/tree/src/cli/test_anastasis_reducer_enter_se
 ## [Unreleased]
  - fixed bug when updating token and not admin
  - showing a yellow bar on non-default instance navigation (admin)
- - 
+
 ## [0.0.6] - 2021-03-25
  - complete order list information (#6793)
  - complete product list information (#6792)
diff --git a/packages/frontend/src/InstanceRoutes.tsx 
b/packages/frontend/src/InstanceRoutes.tsx
index afffd51..18f97e8 100644
--- a/packages/frontend/src/InstanceRoutes.tsx
+++ b/packages/frontend/src/InstanceRoutes.tsx
@@ -53,7 +53,6 @@ import InstanceListPage from './paths/admin/list';
 import InstanceCreatePage from "./paths/admin/create";
 import { NotificationCard } from './components/menu';
 import { Loading } from './components/exception/loading';
-import { MerchantBackend } from './declaration';
 
 export enum InstancePaths {
   // details = '/',
@@ -66,7 +65,6 @@ export enum InstancePaths {
   order_list = '/orders',
   order_new = '/order/new',
   order_details = '/order/:oid/details',
-  // order_new = '/order/new',
 
   // tips_list = '/tips',
   // tips_update = '/tip/:rid/update',
@@ -192,6 +190,8 @@ export function InstanceRoutes({ id, admin }: Props): VNode 
{
       <Route path={InstancePaths.product_list} component={ProductListPage}
         onUnauthorized={LoginPageAccessDenied}
         onLoadError={LoginPageServerError}
+        onCreate={() => { route(InstancePaths.product_new) }}
+        onSelect={(id: string) => { 
route(InstancePaths.product_update.replace(':pid', id)) }}
         onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
       />
       <Route path={InstancePaths.product_update} component={ProductUpdatePage}
diff --git a/packages/frontend/src/components/form/InputCurrency.tsx 
b/packages/frontend/src/components/form/InputCurrency.tsx
index 72334b7..69fa2a8 100644
--- a/packages/frontend/src/components/form/InputCurrency.tsx
+++ b/packages/frontend/src/components/form/InputCurrency.tsx
@@ -33,7 +33,8 @@ export function InputCurrency<T>({ name, readonly, expand, 
currency }: Props<T>)
   return <InputWithAddon<T> name={name} readonly={readonly} 
addonBefore={currency}
     inputType='number' expand={expand}
     toStr={(v?: Amount) => v?.split(':')[1] || ''}
-    fromStr={(v: string) => `${currency}:${v}`}
+    fromStr={(v: string) => !v ? '' : `${currency}:${v}`}
+    inputExtra={{min:0}}
   />
 }
 
diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx 
b/packages/frontend/src/components/form/InputWithAddon.tsx
index 6b194ab..fb96ae9 100644
--- a/packages/frontend/src/components/form/InputWithAddon.tsx
+++ b/packages/frontend/src/components/form/InputWithAddon.tsx
@@ -31,12 +31,13 @@ export interface Props<T> {
   addonAfter?: string;
   toStr?: (v?: any) => string;
   fromStr?: (s: string) => any;
+  inputExtra: any,
 }
 
 const defaultToString = (f?: any):string => f || ''
 const defaultFromString = (v: string):any => v as any
 
-export function InputWithAddon<T>({ name, readonly, addonBefore, expand, 
inputType, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: 
Props<T>): VNode {
+export function InputWithAddon<T>({ name, readonly, addonBefore, expand, 
inputType, inputExtra, addonAfter, toStr = defaultToString, fromStr = 
defaultFromString }: Props<T>): VNode {
   const { error, value, onChange } = useField<T>(name);
 
   const placeholder = useMessage(`fields.instance.${name}.placeholder`);
@@ -58,7 +59,7 @@ export function InputWithAddon<T>({ name, readonly, 
addonBefore, expand, inputTy
             <a class="button is-static">{addonBefore}</a>
           </div>}
           <p class={ expand ? "control is-expanded" : "control" }>
-            <input class={error ? "input is-danger" : "input"} type={inputType}
+            <input {...(inputExtra||{})} class={error ? "input is-danger" : 
"input"} type={inputType}
               placeholder={placeholder} readonly={readonly} 
               name={String(name)} value={toStr(value)}
               onChange={(e): void => onChange(fromStr(e.currentTarget.value))} 
/>
diff --git a/packages/frontend/src/messages/en.po 
b/packages/frontend/src/messages/en.po
index a21f4cd..8d6820d 100644
--- a/packages/frontend/src/messages/en.po
+++ b/packages/frontend/src/messages/en.po
@@ -375,4 +375,13 @@ msgid "fields.product.stock.label"
 msgstr "Stock"
 
 msgid "fields.product.sold.label"
-msgstr "Sold"
\ No newline at end of file
+msgstr "Sold"
+
+msgid "fields.instance.stock.label"
+msgstr "add stock"
+
+msgid "fields.instance.lost.label"
+msgstr "add stock lost"
+
+msgid "fields.instance.price.label"
+msgstr "new price"
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/products/list/Table.tsx 
b/packages/frontend/src/paths/instance/products/list/Table.tsx
index 55fb572..e8bf19b 100644
--- a/packages/frontend/src/paths/instance/products/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/products/list/Table.tsx
@@ -19,9 +19,13 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 
-import { h, VNode } from "preact"
+import { Fragment, h, VNode } from "preact"
 import { Message } from "preact-messages"
 import { StateUpdater, useEffect, useState } from "preact/hooks"
+import { FormErrors, FormProvider } from "../../../../components/form/Field"
+import { Input } from "../../../../components/form/Input"
+import { InputCurrency } from "../../../../components/form/InputCurrency"
+import { useConfigContext } from "../../../../context/backend"
 import { MerchantBackend } from "../../../../declaration"
 import { useProductAPI } from "../../../../hooks/product"
 import { Actions, buildActions } from "../../../../utils/table"
@@ -30,30 +34,15 @@ type Entity = MerchantBackend.Products.ProductDetail & { 
id: string }
 
 interface Props {
   instances: Entity[];
-  onUpdate: (id: string) => void;
   onDelete: (id: Entity) => void;
+  onSelect: (product: Entity) => void;
+  onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => 
void;
   onCreate: () => void;
   selected?: boolean;
 }
 
-export function CardTable({ instances, onCreate, onUpdate, onDelete, selected 
}: 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])
-
+export function CardTable({ instances, onCreate, onSelect, onUpdate, onDelete 
}: Props): VNode {
+  const [rowSelection, rowSelectionHandler] = useState<string | 
undefined>(undefined)
 
   return <div class="card has-table">
     <header class="card-header">
@@ -61,10 +50,6 @@ export function CardTable({ instances, onCreate, onUpdate, 
onDelete, selected }:
 
       <div class="card-header-icon" aria-label="more options">
 
-        <button class={rowSelection.length > 0 ? "button is-danger" : 
"is-hidden"}
-          type="button" onClick={(): void => 
actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} >
-          Delete
-        </button>
       </div>
       <div class="card-header-icon" aria-label="more options">
         <button class="button is-info" type="button" onClick={onCreate}>
@@ -77,7 +62,7 @@ export function CardTable({ instances, onCreate, onUpdate, 
onDelete, selected }:
       <div class="b-table has-pagination">
         <div class="table-wrapper has-mobile-cards">
           {instances.length > 0 ?
-            <Table instances={instances} onUpdate={onUpdate} 
onDelete={onDelete} rowSelection={rowSelection} 
rowSelectionHandler={rowSelectionHandler} /> :
+            <Table instances={instances} onSelect={onSelect} 
onDelete={onDelete} onUpdate={onUpdate} rowSelection={rowSelection} 
rowSelectionHandler={rowSelectionHandler} /> :
             <EmptyTable />
           }
         </div>
@@ -86,30 +71,21 @@ export function CardTable({ instances, onCreate, onUpdate, 
onDelete, selected }:
   </div>
 }
 interface TableProps {
-  rowSelection: string[];
+  rowSelection: string | undefined;
   instances: Entity[];
-  onUpdate: (id: string) => void;
+  onSelect: (id: Entity) => void;
+  onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => 
void;
   onDelete: (id: Entity) => void;
-  rowSelectionHandler: StateUpdater<string[]>;
-}
-
-function toggleSelected<T>(id: T): (prev: T[]) => T[] {
-  return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : 
prev.filter(e => e != id)
+  rowSelectionHandler: StateUpdater<string | undefined>;
 }
 
-function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, 
onDelete }: TableProps): VNode {
+function Table({ rowSelection, rowSelectionHandler, instances, onSelect, 
onUpdate, onDelete }: TableProps): VNode {
   const { } = useProductAPI()
   return (
     <div class="table-container">
       <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
         <thead>
           <tr>
-            <th class="is-checkbox-cell">
-              <label class="b-checkbox checkbox">
-                <input type="checkbox" checked={rowSelection.length === 
instances.length} onClick={(): void => rowSelectionHandler(rowSelection.length 
=== instances.length ? [] : instances.map(i => i.id))} />
-                <span class="check" />
-              </label>
-            </th>
             <th><Message id="fields.product.image.label" /></th>
             <th><Message id="fields.product.description.label" /></th>
             <th><Message id="fields.product.sell.label" /></th>
@@ -122,28 +98,31 @@ function Table({ rowSelection, rowSelectionHandler, 
instances, onUpdate, onDelet
         </thead>
         <tbody>
           {instances.map(i => {
-            return <tr>
-              <td class="is-checkbox-cell">
-                <label class="b-checkbox checkbox">
-                  <input type="checkbox" checked={rowSelection.indexOf(i.id) 
!= -1} onClick={(): void => rowSelectionHandler(toggleSelected(i.id))} />
-                  <span class="check" />
-                </label>
-              </td>
-              <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 
'pointer' }} >{JSON.stringify(i.image)}</td>
-              <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 
'pointer' }} >{i.description}</td>
-              <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 
'pointer' }} >{i.price} / {i.unit}</td>
-              <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 
'pointer' }} >{sum(i.taxes)}</td>
-              <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 
'pointer' }} >{difference(i.price, sum(i.taxes))}</td>
-              <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 
'pointer' }} >{i.total_stock} {i.unit} ({i.next_restock?.t_ms})</td>
-              <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 
'pointer' }} >{i.total_sold} {i.unit}</td>
+            return <Fragment><tr>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} 
>{JSON.stringify(i.image)}</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.description}</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.price} / 
{i.unit}</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{sum(i.taxes)}</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{difference(i.price, 
sum(i.taxes))}</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_stock} 
{i.unit} ({i.next_restock?.t_ms})</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_sold} 
{i.unit}</td>
               <td class="is-actions-cell right-sticky">
                 <div class="buttons is-right">
+                  <button class="button is-small is-success jb-modal" 
type="button" onClick={(): void => onSelect(i)}>
+                    Update
+                  </button>
                   <button class="button is-small is-danger jb-modal" 
type="button" onClick={(): void => onDelete(i)}>
                     Delete
-                </button>
+                  </button>
                 </div>
               </td>
             </tr>
+              {rowSelection === i.id && <tr>
+                <td colSpan={10} >
+                  <FastProductUpdateForm product={i} onUpdate={(prod) => 
onUpdate(i.id, prod)} onCancel={() => rowSelectionHandler(undefined)} />
+                </td>
+              </tr>}
+            </Fragment>
           })}
 
         </tbody>
@@ -151,6 +130,61 @@ function Table({ rowSelection, rowSelectionHandler, 
instances, onUpdate, onDelet
     </div>)
 }
 
+interface FastProductUpdateFormProps {
+  product: Entity;
+  onUpdate: (data: MerchantBackend.Products.ProductPatchDetail) => void;
+  onCancel: () => void;
+}
+interface FastProductUpdate {
+  stock?: number;
+  lost?: number;
+  price?: string;
+}
+
+function FastProductUpdateForm({ product, onUpdate, onCancel }: 
FastProductUpdateFormProps) {
+  const [value, valueHandler] = useState<FastProductUpdate>({})
+  const config = useConfigContext()
+
+  const errors:FormErrors<FastProductUpdate> = {
+    lost: !value.lost ? undefined : (value.lost < product.total_lost ? 
{message: `should be greater than ${product.total_lost}`} : undefined),
+    stock: !value.stock ? undefined : (value.stock < product.total_stock ? 
{message: `should be greater than ${product.total_stock}`} : undefined),
+    price: undefined,
+  }
+
+  const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 
undefined)
+  const isDirty = Object.keys(value).some(k => !!(value as any)[k])
+
+  return <Fragment>
+    <FormProvider<FastProductUpdate> errors={errors} object={value} 
valueHandler={valueHandler} >
+      <div class="columns">
+        <div class="column">
+          <Input<FastProductUpdate> name="stock" inputType="number" 
fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v}/>
+        </div>
+        <div class="column">
+          <Input<FastProductUpdate> name="lost" inputType="number" 
fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v}/>
+        </div>
+        <div class="column">
+          <InputCurrency<FastProductUpdate> name="price" 
currency={config.currency} />
+        </div>
+      </div>
+    </FormProvider>
+
+    <div class="buttons is-right mt-5">
+      <button class="button" onClick={onCancel} ><Message id="Cancel" 
/></button>
+      <button class="button is-info" disabled={hasErrors || !isDirty} 
onClick={() => {
+        return onUpdate({
+          ...product,
+          total_stock: value.stock || product.total_stock,
+          total_lost: value.lost || product.total_lost,
+          price: value.price || product.price,
+        })
+      }}><Message id="Confirm" /></button>
+    </div>
+    
+  </Fragment>
+
+}
+
 function EmptyTable(): VNode {
   return <div class="content has-text-grey has-text-centered">
     <p>
diff --git a/packages/frontend/src/paths/instance/products/list/index.tsx 
b/packages/frontend/src/paths/instance/products/list/index.tsx
index 9eff8f4..2a28376 100644
--- a/packages/frontend/src/paths/instance/products/list/index.tsx
+++ b/packages/frontend/src/paths/instance/products/list/index.tsx
@@ -30,16 +30,21 @@ import { MerchantBackend } from '../../../../declaration';
 import { Loading } from '../../../../components/exception/loading';
 import { useInstanceProducts } from '../../../../hooks/product';
 import { NotificationCard } from '../../../../components/menu';
+import { useState } from 'preact/hooks';
+import { Notification } from '../../../../utils/types';
 
 interface Props {
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
+  onCreate: () => void;
+  onSelect: (id: string) => void;
   onLoadError: (e: HttpError) => VNode;
 }
-export default function ({ onUnauthorized, onLoadError, onNotFound }: Props): 
VNode {
+export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, 
onNotFound }: Props): VNode {
   const result = useInstanceProducts()
-  const { createProduct, deleteProduct } = useProductAPI()
+  const { createProduct, deleteProduct, updateProduct } = useProductAPI()
   const { currency } = useConfigContext()
+  const [notif, setNotif] = useState<Notification | undefined>(undefined)
 
   if (result.clientError && result.isUnauthorized) return onUnauthorized()
   if (result.clientError && result.isNotfound) return onNotFound()
@@ -54,24 +59,40 @@ export default function ({ onUnauthorized, onLoadError, 
onNotFound }: Props): VN
         <li>image return object when api says string</li>
       </ul>
     }} />
+    <NotificationCard notification={notif} />
 
     <CardTable instances={result.data}
-      onCreate={() => createProduct({
-        product_id: `${Math.floor(Math.random() * 999999 + 1)}`,
-        address: {},
-        description: '',
-        description_i18n: {
-          en: '', es: ''
-        },
-        image: {} as string, //WTF? 
-        price: `${currency}:${Math.floor(Math.random() * 20 + 1)}`,
-        taxes: [],
-        total_stock: Math.floor(Math.random() * 20 + 1),
-        unit: 'units',
-        next_restock: { t_ms: 'never' }, //WTF? should not be required
-      })}
+      // onCreate={onCreate}
+      onCreate={() => {
+        const product_id = `${Math.floor(Math.random() * 999999 + 1)}`
+        const price = `${currency}:${Math.floor(Math.random() * 20 + 1)}`
+        return createProduct({
+          product_id,
+          address: {},
+          description: `product with id ${product_id} and price ${price}`,
+          description_i18n: {
+            en: '', es: ''
+          },
+          image: {} as string, //WTF? 
+          price,
+          taxes: [],
+          total_stock: Math.floor(Math.random() * 20 + 10),
+          unit: 'units',
+          next_restock: { t_ms: 'never' }, //WTF? should not be required
+        })
+      }}
+      onUpdate={(id, prod) => updateProduct(id, prod)
+        .then(() => setNotif({
+          message: 'product updated successfully',
+          type: "SUCCESS"
+        })).catch((error) => setNotif({
+          message: 'could not update the product',
+          type: "ERROR",
+          description: error.message
+        }))
+      }
+      onSelect={(product) => onSelect(product.id)}
       onDelete={(prod: (MerchantBackend.Products.ProductDetail & { id: string 
})) => deleteProduct(prod.id)}
-      onUpdate={() => null}
     />
   </section>
 }
\ No newline at end of file

-- 
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]