gnunet-svn
[Top][All Lists]
Advanced

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



reply via email to

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