[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] branch master updated: new order (WIP)
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] branch master updated: new order (WIP) |
Date: |
Thu, 08 Apr 2021 15:01:30 +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 f2dfda1 new order (WIP)
f2dfda1 is described below
commit f2dfda17f27d5a842eaa77711e411313ddfc6039
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Apr 8 09:34:25 2021 -0300
new order (WIP)
---
DESIGN.md | 58 +++++
packages/frontend/src/components/form/Input.tsx | 5 +-
.../frontend/src/components/form/InputGroup.tsx | 17 +-
.../src/components/form/InputSearchProduct.tsx | 111 +++++++++
.../frontend/src/components/form/InputSelector.tsx | 1 -
.../src/components/form/InputWithAddon.tsx | 10 +-
packages/frontend/src/components/menu/index.tsx | 3 +-
packages/frontend/src/components/modal/index.tsx | 4 +-
packages/frontend/src/declaration.d.ts | 30 ++-
packages/frontend/src/hooks/index.ts | 14 ++
packages/frontend/src/hooks/product.ts | 4 +-
packages/frontend/src/messages/en.po | 27 ++-
.../paths/instance/orders/create/CreatePage.tsx | 263 ++++++++++++++++++---
.../orders/create/InventoryProductForm.tsx | 60 +++++
.../orders/create/NonInventoryProductForm.tsx | 38 +++
.../src/paths/instance/orders/list/Table.tsx | 4 +-
.../{orders => products}/create/CreatePage.tsx | 56 ++---
.../products/create/CreatedSuccessfully.tsx | 68 ++++++
.../paths/instance/products/create/ProductForm.tsx | 51 ++++
.../src/paths/instance/products/create/index.tsx | 41 +++-
.../src/paths/instance/products/list/Table.tsx | 4 +-
.../src/paths/instance/products/list/index.tsx | 4 +-
.../src/paths/instance/tips/list/Table.tsx | 4 +-
.../src/paths/instance/transfers/list/Table.tsx | 4 +-
packages/frontend/src/schemas/index.ts | 8 +
packages/frontend/src/scss/main.scss | 4 +
packages/frontend/src/utils/table.ts | 6 +-
27 files changed, 791 insertions(+), 108 deletions(-)
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..6e71776
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,58 @@
+# Page internal routing
+
+* The SPA is loaded from the BACKOFFICE_URL
+
+* The view to be rendered is decided by the URL fragment
+
+* Query parameters that may affect routing
+
+ - instance: use from the default instance to mimic another instance
management
+
+* The user must provide BACKEND_URL or BACKOFFICE_URL will use as default
+
+* Token for querying the backend will be saved in localStorage under
backend-token-${name}
+
+# HTTP queries to the backend
+
+HTTP queries will have 4 states:
+
+* loading: request did not end yet. data and error are undefined
+
+* ok: data has information, http response status == 200
+
+* clientError: http response status is between 400 and 499
+
+ - notfound: http status 404
+
+ - unauthorized: http status 401
+
+* serverError: http response status is grater than 500
+
+There are categories of queries:
+
+ * sync: getting information for the page rendering
+
+ * async: performing an CRUD operation
+
+## Loading the page information (sync)
+
+In this scenario, a failed request will make the app flow to break.
+
+When receiving an notfound error a generic not found page will be shown. If
the BACKEND_URL points to a default instance it should send the user to create
the instance.
+
+When receiving an unauthorized error, the user should be prompted with a login
form.
+
+When receiving an another error (400 < http status < 600), the login form
should be shown with an error message using the hint from the backend.
+
+On other unexpected error (like network error), the login form should be shown
with and error message.
+
+## CRUD operation (async)
+
+In this scenario, a failed request does not break the flow but a message will
be prompted.
+
+# Forms
+
+# Custom Hooks
+
+# Contexts
+
diff --git a/packages/frontend/src/components/form/Input.tsx
b/packages/frontend/src/components/form/Input.tsx
index c3ffa25..a3201e9 100644
--- a/packages/frontend/src/components/form/Input.tsx
+++ b/packages/frontend/src/components/form/Input.tsx
@@ -29,6 +29,7 @@ interface Props<T> {
expand?: boolean;
toStr?: (v?: any) => string;
fromStr?: (s: string) => any;
+ inputExtra?: any,
}
const defaultToString = (f?: any): string => f || ''
@@ -38,7 +39,7 @@ const TextInput = ({inputType, error, ...rest}:any) =>
inputType === 'multiline'
<textarea {...rest} class={error ? "textarea is-danger" : "textarea"}
rows="3" /> :
<input {...rest} class={error ? "input is-danger" : "input"}
type={inputType} />;
-export function Input<T>({ name, readonly, expand, inputType, fromStr =
defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode {
+export function Input<T>({ name, readonly, expand, inputType, inputExtra,
fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode {
const { error, value, onChange } = useField<T>(name);
const placeholder = useMessage(`fields.instance.${name}.placeholder`);
@@ -56,7 +57,7 @@ export function Input<T>({ name, readonly, expand, inputType,
fromStr = defaultF
<div class="field-body is-flex-grow-3">
<div class="field">
<p class={ expand ? "control is-expanded" : "control" }>
- <TextInput error={error}
+ <TextInput error={error} {...inputExtra}
inputType={inputType}
placeholder={placeholder} readonly={readonly}
name={String(name)} value={toStr(value)}
diff --git a/packages/frontend/src/components/form/InputGroup.tsx
b/packages/frontend/src/components/form/InputGroup.tsx
index 0a3f26d..2e5217b 100644
--- a/packages/frontend/src/components/form/InputGroup.tsx
+++ b/packages/frontend/src/components/form/InputGroup.tsx
@@ -18,16 +18,17 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { Message } from "preact-messages";
import { useState } from "preact/hooks";
export interface Props<T> {
name: keyof T;
- children: VNode[] | VNode;
+ children: ComponentChildren;
+ alternative?: ComponentChildren;
}
-export function InputGroup<T>({ name, children }: Props<T>): VNode {
+export function InputGroup<T>({ name, children, alternative}: Props<T>): VNode
{
const [active, setActive] = useState(false);
return <div class="card">
<header class="card-header">
@@ -42,10 +43,16 @@ export function InputGroup<T>({ name, children }:
Props<T>): VNode {
</span>
</button>
</header>
- <div class={active ? "card-content" : "is-hidden"}>
+ { active ? <div class="card-content">
<div class="content">
{children}
</div>
- </div>
+ </div> : (
+ alternative ? <div class="card-content">
+ <div class="content">
+ {alternative}
+ </div>
+ </div> : undefined
+ ) }
</div>;
}
diff --git a/packages/frontend/src/components/form/InputSearchProduct.tsx
b/packages/frontend/src/components/form/InputSearchProduct.tsx
new file mode 100644
index 0000000..9f16340
--- /dev/null
+++ b/packages/frontend/src/components/form/InputSearchProduct.tsx
@@ -0,0 +1,111 @@
+/*
+ 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 } from "preact";
+import { MerchantBackend, WithId } from "../../declaration";
+import { InputWithAddon } from "./InputWithAddon";
+import { FormErrors, FormProvider, useField } from "./Field";
+import { useInstanceProducts } from "../../hooks/product";
+import { useState } from "preact/hooks";
+
+type Entity = MerchantBackend.Products.ProductDetail & WithId
+
+export interface Props {
+ selected?: Entity;
+ onChange: (p?: Entity) => void;
+}
+
+interface ProductSearch {
+ name: string;
+}
+
+export function InputSearchProduct<T>({ selected, onChange }: Props) {
+ const [prodForm, setProdName] = useState<Partial<ProductSearch>>({ name: ''
})
+
+ const errors: FormErrors<ProductSearch> = {
+ name: undefined
+ }
+
+ if (selected) {
+ return <article class="media">
+ <figure class="media-left">
+ <p class="image is-128x128">
+ <img src="https://avatars.dicebear.com/v2/gridy/Ms.-Lora-Kiehn.svg"
/>
+ </p>
+ </figure>
+ <div class="media-content">
+ <div class="content">
+ <p class="media-meta">Product #{selected.id}</p>
+ <p>{selected.description}</p>
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={() =>
onChange(undefined)}>clear</button>
+ </div>
+ </div>
+ </div>
+ </article>
+ }
+
+ return <FormProvider<ProductSearch> errors={errors} object={prodForm}
valueHandler={setProdName} >
+
+ <InputWithAddon<ProductSearch>
+ name="name"
+ addonBefore={<span class="icon" ><i class="mdi mdi-magnify" /></span>}
+ >
+ <ProductList name={prodForm.name} onSelect={(p) => {
+ setProdName({ name: '' })
+ onChange(p)
+ }} />
+ </InputWithAddon>
+
+ </FormProvider>
+
+}
+
+interface ProductListProps {
+ name?: string;
+ onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void;
+}
+
+function ProductList({ name, onSelect }: ProductListProps) {
+ const result = useInstanceProducts();
+
+ const re = new RegExp(`.*${name}.*`)
+
+ let products = <div />
+
+ if (result.loading) {
+ products = <div class="dropdown-content">
+ <div class="dropdown-item">loading...</div>
+ </div>
+ } else if (result.ok && !!name) {
+ products = <div class="dropdown-content">
+ {result.data.filter(p => re.test(p.description)).map(p => (
+ <div class="dropdown-item" onClick={() => onSelect(p)}>
+ {p.description}
+ </div>
+ ))}
+ </div>
+ }
+ return <div class="dropdown is-active">
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
+ {products}
+ </div>
+ </div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/components/form/InputSelector.tsx
b/packages/frontend/src/components/form/InputSelector.tsx
index 8914a99..48ec0a6 100644
--- a/packages/frontend/src/components/form/InputSelector.tsx
+++ b/packages/frontend/src/components/form/InputSelector.tsx
@@ -57,7 +57,6 @@ export function InputSelector<T>({ name, readonly, expand,
values, fromStr = def
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>
diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx
b/packages/frontend/src/components/form/InputWithAddon.tsx
index 35bc2a7..7124778 100644
--- a/packages/frontend/src/components/form/InputWithAddon.tsx
+++ b/packages/frontend/src/components/form/InputWithAddon.tsx
@@ -18,7 +18,7 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { Message, useMessage } from "preact-messages";
import { useField } from "./Field";
@@ -27,17 +27,18 @@ export interface Props<T> {
readonly?: boolean;
expand?: boolean;
inputType?: 'text' | 'number';
- addonBefore?: string;
- addonAfter?: string;
+ addonBefore?: string | VNode;
+ addonAfter?: string | VNode;
toStr?: (v?: any) => string;
fromStr?: (s: string) => any;
inputExtra?: any,
+ children?: ComponentChildren,
}
const defaultToString = (f?: any):string => f || ''
const defaultFromString = (v: string):any => v as any
-export function InputWithAddon<T>({ name, readonly, addonBefore, expand,
inputType, inputExtra, addonAfter, toStr = defaultToString, fromStr =
defaultFromString }: Props<T>): VNode {
+export function InputWithAddon<T>({ name, readonly, addonBefore, children,
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`);
@@ -64,6 +65,7 @@ export function InputWithAddon<T>({ name, readonly,
addonBefore, expand, inputTy
name={String(name)} value={toStr(value)}
onChange={(e): void => onChange(fromStr(e.currentTarget.value))}
/>
<Message id={`fields.instance.${name}.help`}> </Message>
+ {children}
</p>
{addonAfter && <div class="control">
<a class="button is-static">{addonAfter}</a>
diff --git a/packages/frontend/src/components/menu/index.tsx
b/packages/frontend/src/components/menu/index.tsx
index 36b5639..be5ac1e 100644
--- a/packages/frontend/src/components/menu/index.tsx
+++ b/packages/frontend/src/components/menu/index.tsx
@@ -107,9 +107,10 @@ export function NotificationCard({ notification: n }:
NotifProps) {
<div class="message-header">
<p>{n.message}</p>
</div>
+ { n.description &&
<div class="message-body">
{n.description}
- </div>
+ </div> }
</article>
</div>
</div>
diff --git a/packages/frontend/src/components/modal/index.tsx
b/packages/frontend/src/components/modal/index.tsx
index 5bc3eb6..cd38edc 100644
--- a/packages/frontend/src/components/modal/index.tsx
+++ b/packages/frontend/src/components/modal/index.tsx
@@ -20,7 +20,7 @@
*/
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { Message } from "preact-messages";
import { useState } from "preact/hooks";
import { MerchantBackend } from "../../declaration";
@@ -32,7 +32,7 @@ interface Props {
description?: string;
onCancel?: () => void;
onConfirm?: () => void;
- children?: VNode[];
+ children?: ComponentChildren;
danger?: boolean;
disabled?: boolean;
}
diff --git a/packages/frontend/src/declaration.d.ts
b/packages/frontend/src/declaration.d.ts
index 6f258c4..fdecfe3 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -27,6 +27,10 @@ type WireTransferIdentifierRawP = string;
type RelativeTime = Duration;
type ImageDataUrl = string;
+export interface WithId {
+ id: string
+}
+
interface Timestamp {
// Milliseconds since epoch, or the special
// value "forever" to represent an event that will
@@ -39,7 +43,7 @@ interface Duration {
d_ms: number | "forever";
}
-interface WidthId {
+interface WithId {
id: string;
}
@@ -53,39 +57,39 @@ export namespace MerchantBackend {
// Numeric error code unique to the condition.
// The other arguments are specific to the error value reported here.
code: number;
-
+
// Human-readable description of the error, i.e. "missing parameter",
"commitment violation", ...
// Should give a human-readable hint about the error's nature.
Optional, may change without notice!
hint?: string;
-
+
// Optional detail about the specific input value that failed. May
change without notice!
detail?: string;
-
+
// Name of the parameter that was bogus (if applicable).
parameter?: string;
-
+
// Path to the argument that was bogus (if applicable).
path?: string;
-
+
// Offset of the argument that was bogus (if applicable).
offset?: string;
-
+
// Index of the argument that was bogus (if applicable).
index?: string;
-
+
// Name of the object that was bogus (if applicable).
object?: string;
-
+
// Name of the currency than was problematic (if applicable).
currency?: string;
-
+
// Expected type (if applicable).
type_expected?: string;
-
+
// Type that was provided instead (if applicable).
type_actual?: string;
- }
-
+ }
+
// Delivery location, loosely modeled as a subset of
// ISO20022's PostalAddress25.
diff --git a/packages/frontend/src/hooks/index.ts
b/packages/frontend/src/hooks/index.ts
index ab268e9..5ccb6c6 100644
--- a/packages/frontend/src/hooks/index.ts
+++ b/packages/frontend/src/hooks/index.ts
@@ -129,3 +129,17 @@ export function useNotNullLocalStorage(key: string,
initialValue: string): [stri
return [storedValue, setValue];
}
+export function useListener<T>(onCall: (r: T) => void): [() => void,
(listener: () => T) => void] {
+ const [state, setState] = useState({ run: () => { } })
+
+ const subscriber = (listener: () => T) => {
+ setState({
+ run: () => onCall(listener())
+ })
+ }
+
+ const activator = () => state.run()
+
+ return [activator, subscriber]
+}
+
diff --git a/packages/frontend/src/hooks/product.ts
b/packages/frontend/src/hooks/product.ts
index 6568a05..2208d53 100644
--- a/packages/frontend/src/hooks/product.ts
+++ b/packages/frontend/src/hooks/product.ts
@@ -1,7 +1,7 @@
import { useEffect } from 'preact/hooks';
import useSWR, { useSWRInfinite } from 'swr';
import { useBackendContext, useInstanceContext } from '../context/backend';
-import { MerchantBackend } from '../declaration';
+import { MerchantBackend, WithId } from '../declaration';
import { fetcher, HttpError, HttpResponse, HttpResponseOk, mutateAll, request,
SwrError } from './backend';
@@ -67,7 +67,7 @@ export function useProductAPI(): ProductAPI {
}
-export function useInstanceProducts():
HttpResponse<(MerchantBackend.Products.ProductDetail & { id: string })[]> {
+export function useInstanceProducts():
HttpResponse<(MerchantBackend.Products.ProductDetail & WithId)[]> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
diff --git a/packages/frontend/src/messages/en.po
b/packages/frontend/src/messages/en.po
index 8d6820d..b59ebc6 100644
--- a/packages/frontend/src/messages/en.po
+++ b/packages/frontend/src/messages/en.po
@@ -384,4 +384,29 @@ msgid "fields.instance.lost.label"
msgstr "add stock lost"
msgid "fields.instance.price.label"
-msgstr "new price"
\ No newline at end of file
+msgstr "new price"
+
+msgid "fields.instance.inventory_products.label"
+msgstr "Products from inventory"
+
+msgid "fields.instance.products.label"
+msgstr "Products outside inventory"
+
+msgid "fields.instance.quantity.label"
+msgstr "Quantity"
+
+
+msgid "fields.instance.pricing.order_price.label"
+msgstr "Order Price"
+
+msgid "fields.instance.pricing.summary.label"
+msgstr "Summary"
+
+msgid "fields.instance.pricing.products_price.label"
+msgstr "Products Price"
+
+msgid "fields.instance.pricing.products_taxes.label"
+msgstr "Products Taxes"
+
+msgid "fields.instance.pricing.net.label"
+msgstr "Net"
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index 408bca7..f3bdfa9 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -19,66 +19,252 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { MerchantBackend } from "../../../../declaration";
-import * as yup from 'yup';
+import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend, WithId } from "../../../../declaration";
import { FormErrors, FormProvider } from "../../../../components/form/Field"
-import { OrderCreateSchema as schema } from "../../../../schemas"
import { Message } from "preact-messages";
-import { Input } from "../../../../components/form/Input";
import { useConfigContext } from "../../../../context/backend";
+import { InputGroup } from "../../../../components/form/InputGroup";
+import { InventoryProductForm } from "./InventoryProductForm";
+import { NonInventoryProductFrom } from "./NonInventoryProductForm";
import { InputCurrency } from "../../../../components/form/InputCurrency";
-
-type Entity = MerchantBackend.Orders.MinimalOrderDetail
+import { Input } from "../../../../components/form/Input";
interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
onBack?: () => void;
}
-interface KeyValue {
- [key: string]: string;
+function with_defaults(): Entity {
+ return {
+ inventoryProducts: {},
+ products: [],
+ pricing: {} as any
+ };
}
-function with_defaults(id?: string): Partial<Entity> {
- return {};
+export interface ProductMap {
+ [id: string]: {
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number;
+ }
+}
+
+interface Pricing {
+ products_price: string;
+ products_taxes: string;
+ net: string;
+ order_price: string;
+ summary: string;
+}
+interface Entity {
+ inventoryProducts: ProductMap,
+ products: MerchantBackend.Products.ProductAddDetail[],
+ pricing: Pricing;
}
export function CreatePage({ onCreate, onBack }: Props): VNode {
const [value, valueHandler] = useState(with_defaults())
const [errors, setErrors] = useState<FormErrors<Entity>>({})
- const submit = (): void => {
- try {
- schema.validateSync(value, { abortEarly: false })
- const order = schema.cast(value) as Entity
- onCreate({ order });
- } catch (err) {
- const errors = err.inner as yup.ValidationError[]
- const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message
} }), {})
- setErrors(pathMessages)
- }
- }
+ // const submit = (): void => {
+ // try {
+ // // schema.validateSync(value, { abortEarly: false })
+ // // const order = schema.cast(value) as Entity
+ // // onCreate({ order });
+ // } catch (err) {
+ // const errors = err.inner as yup.ValidationError[]
+ // const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev :
({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message:
cur.message } }), {})
+ // setErrors(pathMessages)
+ // }
+ // }
const config = useConfigContext()
+ const addProductToTheInventoryList = (product:
MerchantBackend.Products.ProductDetail & WithId, quantity: number) => {
+ valueHandler(v => {
+ const inventoryProducts = { ...v.inventoryProducts }
+ inventoryProducts[product.id] = { product, quantity }
+ return ({ ...v, inventoryProducts })
+ })
+ }
+
+ const removeProductFromTheInventoryList = (id: string) => {
+ valueHandler(v => {
+ const inventoryProducts = { ...v.inventoryProducts }
+ delete inventoryProducts[id]
+ return ({ ...v, inventoryProducts })
+ })
+ }
+
+ const addNewProduct = (product: MerchantBackend.Products.ProductAddDetail)
=> {
+ valueHandler(v => {
+ const products = [...v.products, product]
+ return ({ ...v, products })
+ })
+ }
+
+ const removeFromNewProduct = (index: number) => {
+ valueHandler(v => {
+ const products = [...v.products]
+ products.splice(index, 1)
+ return ({ ...v, products })
+ })
+ }
+
+ const [editingProduct, setEditingProduct] =
useState<MerchantBackend.Products.ProductAddDetail | undefined>(undefined)
+
+ const inventoryList = Object.values(value.inventoryProducts)
+ const productList = Object.values(value.products)
+
+ const totalPriceInventory = inventoryList.reduce((prev, cur) =>
sumPrices(multiplyPrice(cur.product.price, cur.quantity), prev), ':0')
+ const totalPriceProducts = productList.reduce((prev, cur) =>
sumPrices(multiplyPrice(cur.price, cur.total_stock), prev), ':0')
+
+ const totalTaxInventory = inventoryList.reduce((prev, cur) =>
sumPrices(multiplyPrice(cur.product.taxes.reduce((prev, cur) =>
sumPrices(cur.tax, prev), ':0'), cur.quantity), prev), ':0')
+ const totalTaxProducts = productList.reduce((prev, cur) =>
sumPrices(multiplyPrice(cur.taxes.reduce((prev, cur) => sumPrices(cur.tax,
prev), ':0'), cur.total_stock), prev), ':0')
+
+ const hasProducts = inventoryList.length > 0 || productList.length > 0
+ const totalPrice = sumPrices(totalPriceInventory, totalPriceProducts)
+ const totalTax = sumPrices(totalTaxInventory, totalTaxProducts)
+
+ useEffect(() => {
+ valueHandler(v => {
+ return ({...v, pricing: {
+ ...v.pricing,
+ products_price: totalPrice,
+ products_taxes: totalTax,
+ order_price: totalPrice,
+ net: subtractPrices(totalPrice, totalTax),
+ }})
+ })
+ }, [hasProducts, totalPrice, totalTax, value.pricing])
+
return <div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
- <div class="column is-two-thirds">
- <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler} >
+ <div class="column is-four-fifths">
+
+ <InputGroup name="inventory_products" alternative={
+ inventoryList.length > 0 && <p>
+ {inventoryList.length} products,
+ in {inventoryList.reduce((prev, cur) => cur.quantity + prev, 0)}
units,
+ with a total price of {totalPriceInventory}
+ </p>
+ }>
+ <InventoryProductForm
+ currentProducts={value.inventoryProducts}
+ onAddProduct={addProductToTheInventoryList}
+ />
+
+ {inventoryList.length > 0 && <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable
is-fullwidth">
+ <thead>
+ <tr>
+ <th>image</th>
+ <th>description</th>
+ <th>quantity</th>
+ <th>unit price</th>
+ <th>total price</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {inventoryList.map((entry, index) => {
+ return <tr>
+ <td>image</td>
+ <td >{entry.product.description}</td>
+ <td >
+ {entry.quantity} {entry.product.unit}
+ </td>
+ <td >{entry.product.price}</td>
+ <td >{multiplyPrice(entry.product.price,
entry.quantity)}</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 =>
removeProductFromTheInventoryList(entry.product.id)}>
+ Remove
+ </button>
+ </div>
+ </td>
+ </tr>
+ })}
+
+ </tbody>
+ </table>
+ </div>}
+ </InputGroup>
+
+ <InputGroup name="products" alternative={
+ productList.length > 0 && <p>
+ {productList.length} products,
+ in {productList.reduce((prev, cur) => cur.total_stock + prev,
0)} units,
+ with a total price of {totalPriceProducts}
+ </p>
+ }>
+ <NonInventoryProductFrom value={editingProduct} onAddProduct={(p)
=> {
+ setEditingProduct(undefined)
+ addNewProduct(p)
+ }} />
+ {productList.length > 0 && <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable
is-fullwidth">
+ <thead>
+ <tr>
+ <th>image</th>
+ <th>description</th>
+ <th>quantity</th>
+ <th>unit price</th>
+ <th>total price</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {productList.map((entry, index) => {
+ return <tr>
+ <td>image</td>
+ <td >{entry.description}</td>
+ <td >
+ {entry.total_stock} {entry.unit}
+ </td>
+ <td >{entry.price}</td>
+ <td >{multiplyPrice(entry.price, entry.total_stock)}</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 => {
+ removeFromNewProduct(index)
+ setEditingProduct(entry)
+ }}>
+ Edit
+ </button>
+ <button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => removeFromNewProduct(index)}>
+ Remove
+ </button>
+ </div>
+ </td>
+ </tr>
+ })}
+ </tbody>
+ </table>
+ </div>}
+ </InputGroup>
- <InputCurrency<Entity> name="amount" currency={config.currency} />
+ <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler as any}>
+ {hasProducts ? <Fragment>
+ <InputCurrency name="pricing.products_price" readonly
currency={config.currency}/>
+ <InputCurrency name="pricing.products_taxes" readonly
currency={config.currency}/>
+ <InputCurrency name="pricing.order_price"
currency={config.currency} />
+ <InputCurrency name="pricing.net" readonly
currency={config.currency} />
+ </Fragment> : <Fragment>
+ <InputCurrency name="pricing.order_price"
currency={config.currency} />
+ </Fragment>}
- <Input<Entity> name="summary" />
+ <Input name="pricing.summary" />
</FormProvider>
<div class="buttons is-right mt-5">
{onBack && <button class="button" onClick={onBack} ><Message
id="Cancel" /></button>}
- <button class="button is-success" onClick={submit} ><Message
id="Confirm" /></button>
+ {/* <button class="button is-success" onClick={submit} ><Message
id="Confirm" /></button> */}
</div>
</div>
@@ -87,4 +273,23 @@ export function CreatePage({ onCreate, onBack }: Props):
VNode {
</section>
</div>
-}
\ No newline at end of file
+}
+
+
+const multiplyPrice = (price: string, q: number) => {
+ const [currency, value] = price.split(':')
+ const total = parseInt(value, 10) * q
+ return `${currency}:${total}`
+}
+
+const sumPrices = (one: string, two: string) => {
+ const [currency, valueOne] = one.split(':')
+ const [, valueTwo] = two.split(':')
+ return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
+}
+
+const subtractPrices = (one: string, two: string) => {
+ const [currency, valueOne] = one.split(':')
+ const [, valueTwo] = two.split(':')
+ return `${currency}:${parseInt(valueOne, 10) - parseInt(valueTwo, 10)}`
+}
diff --git
a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
new file mode 100644
index 0000000..73be77b
--- /dev/null
+++
b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
@@ -0,0 +1,60 @@
+import { h } from "preact";
+import { Message } from "preact-messages";
+import { useState } from "preact/hooks";
+import { FormErrors, FormProvider } from "../../../../components/form/Field";
+import { Input } from "../../../../components/form/Input";
+import { InputSearchProduct } from
"../../../../components/form/InputSearchProduct";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon";
+import { MerchantBackend, WithId } from "../../../../declaration";
+import { ProductMap } from "./CreatePage";
+
+type Form = {
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number;
+}
+
+interface Props {
+ currentProducts: ProductMap,
+ onAddProduct: (product: MerchantBackend.Products.ProductDetail & WithId,
quantity: number) => void
+}
+
+export function InventoryProductForm({ currentProducts, onAddProduct }: Props)
{
+ const [state, setState] = useState<Partial<Form>>({})
+ const [errors, setErrors] = useState<FormErrors<Form>>({})
+
+ const submit = (): void => {
+ if (!state.product) {
+ setErrors({ product: { message: 'select a product first' } });
+ return;
+ }
+ if (!state.quantity || state.quantity <= 0) {
+ setErrors({ quantity: { message: 'should be greater than 0' } });
+ return;
+ }
+ const currentStock = state.product.total_stock - state.product.total_lost
- state.product.total_sold
+ const p = currentProducts[state.product.id]
+ if (p) {
+ if (state.quantity + p.quantity > currentStock) {
+ setErrors({ quantity: { message: `cannot be greater than current stock
and quantity previously added. max: ${currentStock - p.quantity}` } });
+ return;
+ }
+ onAddProduct(state.product, state.quantity + p.quantity)
+ } else {
+ if (state.quantity > currentStock) {
+ setErrors({ quantity: { message: `cannot be greater than current stock
${currentStock}` } });
+ return;
+ }
+ onAddProduct(state.product, state.quantity)
+ }
+
+ setState({})
+ }
+
+ return <FormProvider<Form> errors={errors} object={state}
valueHandler={setState}>
+ <InputSearchProduct selected={state.product} onChange={(p) => setState(v
=> ({ ...v, product: p }))} />
+ <Input<Form> name="quantity" inputType="number" fromStr={(v) =>
parseInt(v, 10)} toStr={(v) => ""+v} inputExtra={{min:0}}/>
+ <div class="buttons is-right mt-5">
+ <button class="button is-success" onClick={submit} >add</button>
+ </div>
+ </FormProvider>
+}
\ No newline at end of file
diff --git
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
new file mode 100644
index 0000000..36e8bac
--- /dev/null
+++
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
@@ -0,0 +1,38 @@
+import { Fragment, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { ConfirmModal } from "../../../../components/modal";
+import { MerchantBackend } from "../../../../declaration";
+import { useListener } from "../../../../hooks";
+import { ProductForm } from "../../products/create/ProductForm";
+
+type Entity = MerchantBackend.Products.ProductAddDetail
+
+interface Props {
+ onAddProduct: (p: Entity) => void;
+ value?: Entity;
+}
+export function NonInventoryProductFrom({ value, onAddProduct }: Props) {
+ const [showCreateProduct, setShowCreateProduct] = useState(false)
+
+ const editing = !!value
+
+ useEffect(() => {
+ setShowCreateProduct(editing)
+ }, [editing])
+
+ const [ submitForm, addFormSubmitter ] = useListener<Entity |
undefined>((result) => {
+ if (result) {
+ setShowCreateProduct(false)
+ onAddProduct(result)
+ }
+ })
+
+ return <Fragment>
+ <div class="buttons">
+ <button class="button is-success" onClick={() =>
setShowCreateProduct(true)} >add new product</button>
+ </div>
+ {showCreateProduct && <ConfirmModal active onCancel={() =>
setShowCreateProduct(false)} onConfirm={submitForm}>
+ <ProductForm initial={value} onSubscribe={addFormSubmitter} />
+ </ConfirmModal>}
+ </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 a35281d..9538d55 100644
--- a/packages/frontend/src/paths/instance/orders/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -29,12 +29,12 @@ 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 { MerchantBackend, WithId } 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 }
+type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId
interface Props {
instances: Entity[];
onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
similarity index 56%
copy from packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
copy to packages/frontend/src/paths/instance/products/create/CreatePage.tsx
index 408bca7..b19e5a5 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
@@ -24,67 +24,61 @@ import { useState } from "preact/hooks";
import { MerchantBackend } from "../../../../declaration";
import * as yup from 'yup';
import { FormErrors, FormProvider } from "../../../../components/form/Field"
-import { OrderCreateSchema as schema } from "../../../../schemas"
+import { ProductCreateSchema as schema } from '../../../../schemas'
import { Message } from "preact-messages";
import { Input } from "../../../../components/form/Input";
-import { useConfigContext } from "../../../../context/backend";
+import { InputSecured } from "../../../../components/form/InputSecured";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon";
+import { InputGroup } from "../../../../components/form/InputGroup";
+import { useConfigContext, useBackendContext } from
"../../../../context/backend";
+import { InputDuration } from "../../../../components/form/InputDuration";
import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { InputPayto } from "../../../../components/form/InputPayto";
+import { ProductForm } from "./ProductForm";
+import { useListener } from "../../../../hooks";
-type Entity = MerchantBackend.Orders.MinimalOrderDetail
+type Entity = MerchantBackend.Products.ProductAddDetail
interface Props {
- onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
+ onCreate: (d: Entity) => void;
onBack?: () => void;
}
-interface KeyValue {
- [key: string]: string;
-}
function with_defaults(id?: string): Partial<Entity> {
- return {};
+ return {
+
+ };
}
export function CreatePage({ onCreate, onBack }: Props): VNode {
- const [value, valueHandler] = useState(with_defaults())
- const [errors, setErrors] = useState<FormErrors<Entity>>({})
-
- const submit = (): void => {
- try {
- schema.validateSync(value, { abortEarly: false })
- const order = schema.cast(value) as Entity
- onCreate({ order });
- } catch (err) {
- const errors = err.inner as yup.ValidationError[]
- const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message
} }), {})
- setErrors(pathMessages)
- }
- }
- const config = useConfigContext()
- return <div>
+ const [submitForm, addFormSubmitter] = useListener<Entity |
undefined>((result) => {
+ if (result) onCreate(result)
+ })
+ return <div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-two-thirds">
- <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler} >
-
- <InputCurrency<Entity> name="amount" currency={config.currency} />
+ {/* <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler} >
- <Input<Entity> name="summary" />
+ <Input<Entity> name="description" />
+ <InputCurrency<Entity> name="price" currency={config.currency} />
+ <Input<Entity> name="total_stock" inputType="number" />
- </FormProvider>
+ </FormProvider> */}
+ <ProductForm onSubscribe={addFormSubmitter} />
<div class="buttons is-right mt-5">
{onBack && <button class="button" onClick={onBack} ><Message
id="Cancel" /></button>}
- <button class="button is-success" onClick={submit} ><Message
id="Confirm" /></button>
+ <button class="button is-success" onClick={submitForm} ><Message
id="Confirm" /></button>
</div>
</div>
<div class="column" />
</div>
</section>
-
</div>
}
\ No newline at end of file
diff --git
a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx
b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx
new file mode 100644
index 0000000..17d716f
--- /dev/null
+++
b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx
@@ -0,0 +1,68 @@
+/*
+ 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 { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { CreatedSuccessfully as Template } from
"../../../../components/notifications/CreatedSuccessfully";
+import { useOrderAPI } from "../../../../hooks/order";
+import { Entity } from "./index";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }:
Props) {
+
+ return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.price} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Summary</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.description} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Order ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.total_stock} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Template>;
+}
diff --git
a/packages/frontend/src/paths/instance/products/create/ProductForm.tsx
b/packages/frontend/src/paths/instance/products/create/ProductForm.tsx
new file mode 100644
index 0000000..6a1cbff
--- /dev/null
+++ b/packages/frontend/src/paths/instance/products/create/ProductForm.tsx
@@ -0,0 +1,51 @@
+import { h } from "preact";
+import { useCallback, 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 { ProductCreateSchema as schema } from '../../../../schemas'
+import * as yup from 'yup';
+
+type Entity = MerchantBackend.Products.ProductAddDetail
+
+interface Props {
+ onSubscribe: (c:() => Entity|undefined) => void;
+ initial?: Entity;
+}
+
+export function ProductForm({onSubscribe, initial}:Props) {
+ const [value, valueHandler] = useState<Partial<Entity>>(initial || {
+ taxes:[]
+ })
+ const [errors, setErrors] = useState<FormErrors<Entity>>({})
+
+ const submit = useCallback((): Entity|undefined => {
+ try {
+ schema.validateSync(value, { abortEarly: false })
+ return schema.cast(value) as any as Entity
+ // onCreate(schema.cast(value) as any as Entity );
+ } catch (err) {
+ const errors = err.inner as yup.ValidationError[]
+ const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message
} }), {})
+ setErrors(pathMessages)
+ }
+ },[value])
+
+ const config = useConfigContext()
+
+ useEffect(()=> {
+ onSubscribe(submit)
+ },[submit])
+
+ return <div>
+ <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler} >
+
+ <Input<Entity> name="description" />
+ <InputCurrency<Entity> name="price" currency={config.currency} />
+ <Input<Entity> name="total_stock" inputType="number" />
+
+ </FormProvider>
+ </div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/products/create/index.tsx
b/packages/frontend/src/paths/instance/products/create/index.tsx
index 382b58a..1ac80a7 100644
--- a/packages/frontend/src/paths/instance/products/create/index.tsx
+++ b/packages/frontend/src/paths/instance/products/create/index.tsx
@@ -19,8 +19,43 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from 'preact';
+import { Fragment, h, VNode } from 'preact';
+import { useState } from 'preact/hooks';
+import { NotificationCard } from '../../../../components/menu';
+import { MerchantBackend } from '../../../../declaration';
+import { useOrderAPI } from '../../../../hooks/order';
+import { Notification } from '../../../../utils/types';
+import { CreatePage } from './CreatePage';
+import { CreatedSuccessfully } from './CreatedSuccessfully';
+import { useProductAPI } from '../../../../hooks/product';
-export default function ():VNode {
- return <div>product list page</div>
+export type Entity = MerchantBackend.Products.ProductAddDetail
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+export default function ({ onConfirm, onBack }: Props): VNode {
+ const { createProduct } = useProductAPI()
+ const [notif, setNotif] = useState<Notification | undefined>(undefined)
+ const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined);
+
+ if (createdOk) {
+ return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm}
onCreateAnother={() => setCreatedOk(undefined)} />
+ }
+
+ return <Fragment>
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Products.ProductAddDetail) => {
+ createProduct(request).then(() => {
+ setCreatedOk(request)
+ }).catch((error) => {
+ setNotif({
+ message: 'could not create product',
+ type: "ERROR",
+ description: error.message
+ })
+ })
+ }} />
+ </Fragment>
}
\ 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 e8bf19b..f097648 100644
--- a/packages/frontend/src/paths/instance/products/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/products/list/Table.tsx
@@ -26,11 +26,11 @@ 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 { MerchantBackend, WithId } from "../../../../declaration"
import { useProductAPI } from "../../../../hooks/product"
import { Actions, buildActions } from "../../../../utils/table"
-type Entity = MerchantBackend.Products.ProductDetail & { id: string }
+type Entity = MerchantBackend.Products.ProductDetail & WithId
interface Props {
instances: Entity[];
diff --git a/packages/frontend/src/paths/instance/products/list/index.tsx
b/packages/frontend/src/paths/instance/products/list/index.tsx
index 2a28376..4502276 100644
--- a/packages/frontend/src/paths/instance/products/list/index.tsx
+++ b/packages/frontend/src/paths/instance/products/list/index.tsx
@@ -26,7 +26,7 @@ import { useProductAPI } from "../../../../hooks/product";
import { CardTable } from './Table';
import logo from '../../../../assets/logo.jpeg';
import { useConfigContext } from '../../../../context/backend';
-import { MerchantBackend } from '../../../../declaration';
+import { MerchantBackend, WithId } from '../../../../declaration';
import { Loading } from '../../../../components/exception/loading';
import { useInstanceProducts } from '../../../../hooks/product';
import { NotificationCard } from '../../../../components/menu';
@@ -92,7 +92,7 @@ export default function ({ onUnauthorized, onLoadError,
onCreate, onSelect, onNo
}))
}
onSelect={(product) => onSelect(product.id)}
- onDelete={(prod: (MerchantBackend.Products.ProductDetail & { id: string
})) => deleteProduct(prod.id)}
+ onDelete={(prod: (MerchantBackend.Products.ProductDetail & WithId)) =>
deleteProduct(prod.id)}
/>
</section>
}
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/tips/list/Table.tsx
b/packages/frontend/src/paths/instance/tips/list/Table.tsx
index 5684c02..5a9f9a6 100644
--- a/packages/frontend/src/paths/instance/tips/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/tips/list/Table.tsx
@@ -22,10 +22,10 @@
import { h, VNode } from "preact"
import { Message } from "preact-messages"
import { StateUpdater, useEffect, useState } from "preact/hooks"
-import { MerchantBackend } from "../../../../declaration"
+import { MerchantBackend, WithId } from "../../../../declaration"
import { Actions, buildActions } from "../../../../utils/table"
-type Entity = MerchantBackend.Tips.ReserveStatusEntry & { id: string }
+type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId
interface Props {
instances: Entity[];
diff --git a/packages/frontend/src/paths/instance/transfers/list/Table.tsx
b/packages/frontend/src/paths/instance/transfers/list/Table.tsx
index 9d513ff..2b750be 100644
--- a/packages/frontend/src/paths/instance/transfers/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/transfers/list/Table.tsx
@@ -22,10 +22,10 @@
import { h, VNode } from "preact"
import { Message } from "preact-messages"
import { StateUpdater, useEffect, useState } from "preact/hooks"
-import { MerchantBackend } from "../../../../declaration"
+import { MerchantBackend, WithId } from "../../../../declaration"
import { Actions, buildActions } from "../../../../utils/table"
-type Entity = MerchantBackend.Transfers.TransferDetails & { id: string }
+type Entity = MerchantBackend.Transfers.TransferDetails & WithId
interface Props {
instances: Entity[];
diff --git a/packages/frontend/src/schemas/index.ts
b/packages/frontend/src/schemas/index.ts
index 008be42..6e6288b 100644
--- a/packages/frontend/src/schemas/index.ts
+++ b/packages/frontend/src/schemas/index.ts
@@ -120,3 +120,11 @@ export const OrderCreateSchema = yup.object().shape({
.required()
.test('amount', 'the amount is not valid', currencyWithAmountIsValid),
})
+
+export const ProductCreateSchema = yup.object().shape({
+ description: yup.string().required(),
+ price:yup.string()
+ .required()
+ .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
+ total_stock: yup.number().required(),
+})
diff --git a/packages/frontend/src/scss/main.scss
b/packages/frontend/src/scss/main.scss
index 369571e..b39664a 100644
--- a/packages/frontend/src/scss/main.scss
+++ b/packages/frontend/src/scss/main.scss
@@ -50,6 +50,10 @@
@import
"../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
+.notification {
+ background-color: transparent;
+}
+
.timeline .timeline-item .timeline-content {
padding-top: 0;
}
diff --git a/packages/frontend/src/utils/table.ts
b/packages/frontend/src/utils/table.ts
index 87ab600..3d713a6 100644
--- a/packages/frontend/src/utils/table.ts
+++ b/packages/frontend/src/utils/table.ts
@@ -14,6 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { WithId } from "../declaration";
+
/**
*
* @author Sebastian Javier Marchano (sebasjm)
@@ -28,10 +30,6 @@ function notEmpty<TValue>(value: TValue | null | undefined):
value is TValue {
return value !== null && value !== undefined;
}
-interface WithId {
- id: string
-}
-
export function buildActions<T extends WithId>(intances: T[], selected:
string[], action: 'DELETE'): Actions<T>[] {
return selected.map(id => intances.find(i => i.id === id))
.filter(notEmpty)
--
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: new order (WIP),
gnunet <=