[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.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-merchant-backoffice] branch master updated: product update,
gnunet <=