merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 14b76f2c318bf483bd7534c8761aec720d067532
parent dd03dc5877bbeca17efb4dc5007f62a7dd90eb16
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 14 Apr 2021 17:10:48 -0300

create and update product form

Diffstat:
Mpackages/frontend/src/InstanceRoutes.tsx | 5+++++
Apackages/frontend/src/assets/empty.png | 0
Mpackages/frontend/src/components/form/Field.tsx | 27---------------------------
Mpackages/frontend/src/components/form/Input.tsx | 8+++++---
Apackages/frontend/src/components/form/InputImage.tsx | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/components/product/ProductForm.tsx | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/custom.d.ts | 4++++
Mpackages/frontend/src/hooks/product.ts | 20+++++++++++++++++++-
Mpackages/frontend/src/messages/en.po | 21++++++++++++++++++---
Mpackages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx | 2+-
Mpackages/frontend/src/paths/instance/products/create/CreatePage.tsx | 29++---------------------------
Mpackages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx | 14++++++--------
Dpackages/frontend/src/paths/instance/products/create/ProductForm.tsx | 49-------------------------------------------------
Mpackages/frontend/src/paths/instance/products/create/index.tsx | 1+
Mpackages/frontend/src/paths/instance/products/list/index.tsx | 23++---------------------
Apackages/frontend/src/paths/instance/products/update/UpdatePage.tsx | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/paths/instance/products/update/index.tsx | 49++++++++++++++++++++++++++++++++++++++++++++++---
Mpackages/frontend/src/schemas/index.ts | 10++++++++++
18 files changed, 345 insertions(+), 143 deletions(-)

diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -195,6 +195,11 @@ export function InstanceRoutes({ id, admin }: Props): VNode { onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route path={InstancePaths.product_update} component={ProductUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={LoginPageServerError} + onConfirm={() => { route(InstancePaths.product_list); }} + onBack={() => { route(InstancePaths.product_list); }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route path={InstancePaths.product_new} component={ProductCreatePage} /> diff --git a/packages/frontend/src/assets/empty.png b/packages/frontend/src/assets/empty.png Binary files differ. diff --git a/packages/frontend/src/components/form/Field.tsx b/packages/frontend/src/components/form/Field.tsx @@ -125,30 +125,3 @@ export function useGroupField<T>(name: keyof T) { } } -// export function Field<T>({ name, info, readonly }: Props<T>): VNode { -// const {errors, object, valueHandler, updateField} = useForm<T>() - -// const backend = useContext(BackendContext) -// const config = useContext(ConfigContext) - -// switch (info.meta?.type) { - // case 'group': { - // return <InputObject name={name} readonly={readonly} - - // onChange={(updater: any): void => valueHandler((prev: any) => ({ ...prev, [name]: updater(prev[name]) }))} - // /> - // } - // case 'array': return <InputArray name={name} readonly={readonly} />; - // case 'amount': { - // if (config.currency) { - // return <InputWithAddon name={name} readonly={readonly} addon={config.currency} onChange={(v: string): void => values.onChange(`${config.currency}:${v}`)} value={values.value?.split(':')[1]} /> - // } - // return <Input name={name} readonly={readonly} />; - // } - // case 'url': return <InputWithAddon name={name} readonly={readonly} addon={`${backend.url}/private/instances/`} />; - // case 'secured': return <InputSecured name={name} readonly={readonly} />; - // case 'duration': return <InputWithAddon name={name} readonly={readonly} addon={readableDuration(values.value?.d_ms)} atTheEnd value={`${values.value?.d_ms / 1000 || ''}`} onChange={(v: string): void => values.onChange({ d_ms: (parseInt(v, 10) * 1000) || undefined } as any)} />; - // default: return <Input name={name} readonly={readonly} />; - - // } -// } diff --git a/packages/frontend/src/components/form/Input.tsx b/packages/frontend/src/components/form/Input.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"; @@ -30,6 +30,7 @@ interface Props<T> { toStr?: (v?: any) => string; fromStr?: (s: string) => any; inputExtra?: any, + children?: ComponentChildren; } const defaultToString = (f?: any): string => f || '' @@ -39,7 +40,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, inputExtra, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { +export function Input<T>({ name, readonly, expand, children, 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,13 +57,14 @@ export function Input<T>({ name, readonly, expand, inputType, inputExtra, fromSt </div> <div class="field-body is-flex-grow-3"> <div class="field"> - <p class={ expand ? "control is-expanded" : "control" }> + <p class={expand ? "control is-expanded" : "control"}> <TextInput error={error} {...inputExtra} inputType={inputType} placeholder={placeholder} readonly={readonly} name={String(name)} value={toStr(value)} onChange={(e:h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} /> <Message id={`fields.instance.${name}.help`}> </Message> + {children} </p> {error ? <p class="help is-danger"> <Message id={`validation.${error.type}`} fields={error.params}>{error.message} </Message> diff --git a/packages/frontend/src/components/form/InputImage.tsx b/packages/frontend/src/components/form/InputImage.tsx @@ -0,0 +1,80 @@ +/* + 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 { ComponentChildren, Fragment, h } from "preact"; +import { useField } from "./Field"; +import emptyImage from "../../assets/empty.png"; +import { Message, useMessage } from "preact-messages"; +import { useRef } from "preact/hooks"; + +export interface Props<T> { + name: keyof T; + readonly?: boolean; + expand?: boolean; + addonAfter?: ComponentChildren; + children?: ComponentChildren; +} + +export function InputImage<T>({ name, readonly, children, expand }: Props<T>) { + const { error, value, onChange } = useField<T>(name); + + const placeholder = useMessage(`fields.instance.${name}.placeholder`); + const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const image = useRef<HTMLInputElement>(null) + + return <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <Message id={`fields.instance.${name}.label`} /> + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class={expand ? "control is-expanded" : "control"}> + <img src={!value ? emptyImage : value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} /> + <input + ref={image} style={{ display: 'none' }} + type="file" name={String(name)} + placeholder={placeholder} readonly={readonly} + onChange={e => { + const f: FileList | null = e.currentTarget.files + if (!f || f.length != 1 || f[0].size > 10000000) return onChange(emptyImage) + f[0].arrayBuffer().then(b => { + const b64 = btoa( + new Uint8Array(b) + .reduce((data, byte) => data + String.fromCharCode(byte), '') + ) + onChange(`data:${f[0].type};base64,${b64}` as any) + }) + }} /> + <Message id={`fields.instance.${name}.help`}> </Message> + {children} + </p> + {error ? <p class="help is-danger"> + <Message id={`validation.${error.type}`} fields={error.params}>{error.message} </Message> + </p> : null} + </div> + </div> + </div> +} + diff --git a/packages/frontend/src/components/product/ProductForm.tsx b/packages/frontend/src/components/product/ProductForm.tsx @@ -0,0 +1,82 @@ +import { h } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { FormErrors, FormProvider } from "../form/Field"; +import { Input } from "../form/Input"; +import { InputCurrency } from "../form/InputCurrency"; +import { useBackendContext, useConfigContext } from "../../context/backend"; +import { MerchantBackend } from "../../declaration"; +import { + ProductUpdateSchema as updateSchema, + ProductCreateSchema as createSchema, + } from '../../schemas' +import * as yup from 'yup'; +import { InputGroup } from "../form/InputGroup"; +import { useBackendURL } from "../../hooks"; +import { InputWithAddon } from "../form/InputWithAddon"; +import { InputImage } from "../form/InputImage"; + +type Entity = MerchantBackend.Products.ProductAddDetail + +interface Props { + onSubscribe: (c: () => Entity | undefined) => void; + initial?: Partial<Entity>; + showId?: boolean; +} + +export function ProductForm({ onSubscribe, initial, showId }: Props) { + const [value, valueHandler] = useState<Partial<Entity>>(initial || { + address: {}, + description_i18n: {}, + taxes: [], + next_restock: { t_ms: 'never' } + }) + const [errors, setErrors] = useState<FormErrors<Entity>>({}) + + const submit = useCallback((): Entity | undefined => { + try { + (showId ? createSchema : updateSchema).validateSync(value, { abortEarly: false }) + return value as MerchantBackend.Products.ProductAddDetail + } 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]) + + const backend = useBackendContext(); + return <div> + <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > + + { showId ? <InputWithAddon<Entity> name="product_id" addonBefore={`${backend.url}/product/`} /> : undefined } + <InputImage<Entity> name="image" /> + <Input<Entity> name="description" inputType="multiline" /> + <Input<Entity> name="unit" /> + <InputCurrency<Entity> name="price" currency={config.currency} /> + + <Input<Entity> name="total_stock" inputType="number" fromStr={(v) => parseInt(v, 10)} toStr={(v) => "" + v} inputExtra={{ min: 0 }} /> + + <InputGroup<Entity> name="address"> + <Input name="address.country" /> + <Input name="address.address_lines" inputType="multiline" + toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} + fromStr={(v: string) => v.split('\n')} + /> + <Input name="address.building_number" /> + <Input name="address.building_name" /> + <Input name="address.street" /> + <Input name="address.post_code" /> + <Input name="address.town_location" /> + <Input name="address.town" /> + <Input name="address.district" /> + <Input name="address.country_subdivision" /> + </InputGroup> + + </FormProvider> + </div> +} diff --git a/packages/frontend/src/custom.d.ts b/packages/frontend/src/custom.d.ts @@ -21,6 +21,10 @@ declare module "*.jpeg" { const content: any; export default content; } +declare module "*.png" { + const content: any; + export default content; +} declare module '*.svg' { const content: any; export default content; diff --git a/packages/frontend/src/hooks/product.ts b/packages/frontend/src/hooks/product.ts @@ -28,7 +28,10 @@ export function useProductAPI(): ProductAPI { await request(`${url}/private/products`, { method: 'post', token, - data + data: { + ...data, + image: {} + } }); mutateAll(/@"\/private\/products"@/); @@ -108,3 +111,18 @@ export function useInstanceProducts(): HttpResponse<(MerchantBackend.Products.Pr return { loading: true } } +export function useProductDetails(productId: string): HttpResponse<MerchantBackend.Products.ProductDetail> { + const { url: baseUrl } = useBackendContext(); + const { token, id: instanceId, admin } = useInstanceContext(); + + const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}` + + const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Products.ProductDetail>, HttpError>( + [`/private/products/${productId}`, token, url], fetcher + ) + + if (isValidating) return { loading: true, data: data?.data } + if (data) return data + if (error) return error + return { loading: true } +} diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po @@ -254,9 +254,6 @@ msgstr "Exchange Initial Amount" msgid "fields.tips.merchant_initial_amount.label" msgstr "Merchant Initial Amount" -msgid "fields.instance.paid.placeholder" -msgstr "" - msgid "fields.instance.paid.tooltip" msgstr "three state boolean" @@ -507,3 +504,20 @@ msgstr "Extra information" msgid "fields.instance.extra.tooltip" msgstr "Must be a JSON formatted string" + + +msgid "fields.instance.product_id.label" +msgstr "ID" + + +msgid "fields.instance.image.label" +msgstr "Image" + + +msgid "fields.instance.unit.label" +msgstr "Unit" + + + +msgid "fields.instance.total_stock.label" +msgstr "Total Stock" +\ 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 @@ -3,7 +3,7 @@ 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"; +import { ProductForm } from "../../../../components/product/ProductForm"; type Entity = MerchantBackend.Product diff --git a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx @@ -20,21 +20,9 @@ */ import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; import { MerchantBackend } from "../../../../declaration"; -import * as yup from 'yup'; -import { FormErrors, FormProvider } from "../../../../components/form/Field" -import { ProductCreateSchema as schema } from '../../../../schemas' import { Message } from "preact-messages"; -import { Input } from "../../../../components/form/Input"; -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 { ProductForm } from "../../../../components/product/ProductForm"; import { useListener } from "../../../../hooks"; type Entity = MerchantBackend.Products.ProductAddDetail @@ -45,12 +33,6 @@ interface Props { } -function with_defaults(id?: string): Partial<Entity> { - return { - - }; -} - export function CreatePage({ onCreate, onBack }: Props): VNode { const [submitForm, addFormSubmitter] = useListener<Entity | undefined>((result) => { @@ -62,14 +44,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <div class="columns"> <div class="column" /> <div class="column is-two-thirds"> - {/* <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> */} - <ProductForm onSubscribe={addFormSubmitter} /> + <ProductForm onSubscribe={addFormSubmitter} showId /> <div class="buttons is-right mt-5"> {onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></button>} diff --git a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx @@ -14,9 +14,7 @@ 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 { @@ -30,36 +28,36 @@ export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Prop return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Amount</label> + <label class="label">Image</label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input class="input" readonly value={entity.price} /> + <img src={entity.image} /> </p> </div> </div> </div> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Summary</label> + <label class="label">Description</label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input class="input" readonly value={entity.description} /> + <textarea 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> + <label class="label">Price</label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input class="input" readonly value={entity.total_stock} /> + <input class="input" readonly value={entity.price} /> </p> </div> </div> diff --git a/packages/frontend/src/paths/instance/products/create/ProductForm.tsx b/packages/frontend/src/paths/instance/products/create/ProductForm.tsx @@ -1,48 +0,0 @@ -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?: Partial<Entity>; -} - -export function ProductForm({onSubscribe, initial}:Props) { - const [value, valueHandler] = useState<Partial<Entity>>(initial||{}) - const [errors, setErrors] = useState<FormErrors<Entity>>({}) - - const submit = useCallback((): Entity|undefined => { - try { - schema.validateSync(value, { abortEarly: false }) - return value as MerchantBackend.Products.ProductAddDetail - } 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" fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v} inputExtra={{min:0}} /> - - </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 @@ -44,6 +44,7 @@ export default function ({ onConfirm, onBack }: Props): VNode { } return <Fragment> + <NotificationCard notification={notif} /> <CreatePage onBack={onBack} onCreate={(request: MerchantBackend.Products.ProductAddDetail) => { diff --git a/packages/frontend/src/paths/instance/products/list/index.tsx b/packages/frontend/src/paths/instance/products/list/index.tsx @@ -40,8 +40,7 @@ interface Props { } export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNotFound }: Props): VNode { const result = useInstanceProducts() - const { createProduct, deleteProduct, updateProduct } = useProductAPI() - const { currency } = useConfigContext() + const { deleteProduct, updateProduct } = useProductAPI() const [notif, setNotif] = useState<Notification | undefined>(undefined) if (result.clientError && result.isUnauthorized) return onUnauthorized() @@ -60,25 +59,7 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo <NotificationCard notification={notif} /> <CardTable instances={result.data} - // onCreate={onCreate} - onCreate={() => { - const product_id = `${Math.floor(Math.random() * 999999 + 1)}` - const price = `${currency}:${Math.floor(Math.random() * 20 + 1)}` - return createProduct({ - product_id, - address: {}, - description: `product with id ${product_id} and price ${price}`, - description_i18n: { - en: '', es: '' - }, - image: {} as string, //WTF? - price, - taxes: [], - total_stock: Math.floor(Math.random() * 20 + 10), - unit: 'units', - next_restock: { t_ms: 'never' }, //WTF? should not be required - }) - }} + onCreate={onCreate} onUpdate={(id, prod) => updateProduct(id, prod) .then(() => setNotif({ message: 'product updated successfully', diff --git a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx @@ -0,0 +1,63 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h, VNode } from "preact"; +import { MerchantBackend, WithId } from "../../../../declaration"; +import { Message } from "preact-messages"; +import { ProductForm } from "../../../../components/product/ProductForm"; +import { useListener } from "../../../../hooks"; + +type Entity = MerchantBackend.Products.ProductPatchDetail & WithId + +interface Props { + onUpdate: (d: Entity) => void; + onBack?: () => void; + product: Entity; +} + +export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { + const [submitForm, addFormSubmitter] = useListener<Entity | undefined>((result) => { + if (result) onUpdate(result) + }) + + return <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + <ProductForm initial={product} onSubscribe={(a) => { + addFormSubmitter(() => { + const p = a() + return p as any + }) + }} /> + + <div class="buttons is-right mt-5"> + {onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></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/update/index.tsx b/packages/frontend/src/paths/instance/products/update/index.tsx @@ -19,8 +19,51 @@ * @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 { UpdatePage } from './UpdatePage'; +import { useProductAPI, useProductDetails } from '../../../../hooks/product'; +import { HttpError } from '../../../../hooks/backend'; +import { Loading } from '../../../../components/exception/loading'; -export default function ():VNode { - return <div>product update page</div> +export type Entity = MerchantBackend.Products.ProductAddDetail +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError) => VNode; + pid: string; +} +export default function ({ pid, onConfirm, onBack, onUnauthorized, onNotFound, onLoadError }: Props): VNode { + const { updateProduct } = useProductAPI() + const result = useProductDetails(pid) + const [notif, setNotif] = useState<Notification | undefined>(undefined) + + if (result.clientError && result.isUnauthorized) return onUnauthorized() + if (result.clientError && result.isNotfound) return onNotFound() + if (result.loading) return <Loading /> + if (!result.ok) return onLoadError(result) + + return <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + product={{ ...result.data, id: pid }} + onBack={onBack} + onUpdate={(data) => { + updateProduct(pid, data) + .then(onConfirm) + .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/schemas/index.ts b/packages/frontend/src/schemas/index.ts @@ -167,6 +167,16 @@ export const OrderCreateSchema = yup.object().shape({ }) export const ProductCreateSchema = yup.object().shape({ + product_id: yup.string().ensure().required(), + description: yup.string().required(), + unit: yup.string().ensure().required(), + price: yup.string() + .required() + .test('amount', 'the amount is not valid', currencyWithAmountIsValid), + total_stock: yup.number().required(), +}) + +export const ProductUpdateSchema = yup.object().shape({ description: yup.string().required(), price: yup.string() .required()