merchant-backoffice

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

commit 520e1d886faeeda6dd56ca5451baf1e6329ab1ea
parent a75d12bb47ceaf17648a8fcc7c9d0e515ae2fcae
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 22 Jun 2021 12:31:30 -0300

joint inventory product and custom products

Diffstat:
Mpackages/frontend/src/components/form/useField.tsx | 4----
Apackages/frontend/src/components/product/InventoryProductForm.stories.tsx | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/components/product/InventoryProductForm.tsx | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/components/product/NonInventoryProductForm.tsx | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/components/product/ProductList.tsx | 7+++----
Mpackages/frontend/src/paths/instance/orders/create/CreatePage.tsx | 80++++++++++++++++++++++++++++++++++---------------------------------------------
Dpackages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx | 86-------------------------------------------------------------------------------
Dpackages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx | 148-------------------------------------------------------------------------------
8 files changed, 334 insertions(+), 288 deletions(-)

diff --git a/packages/frontend/src/components/form/useField.tsx b/packages/frontend/src/components/form/useField.tsx @@ -49,10 +49,6 @@ export function useField<T>(name: keyof T): Use<T[typeof name]> { const initial = readField(initialObject, String(name)) const isDirty = value !== initial const hasError = readField(errors, String(name)) - if (name == 'pricing.order_price') { - - console.log(value, initial, value === initial) - } return { error: isDirty ? hasError : undefined, required: !isDirty && hasError, diff --git a/packages/frontend/src/components/product/InventoryProductForm.stories.tsx b/packages/frontend/src/components/product/InventoryProductForm.stories.tsx @@ -0,0 +1,58 @@ +/* + 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, FunctionalComponent } from 'preact'; +import { InventoryProductForm as TestedComponent } from './InventoryProductForm'; + + +export default { + title: 'Components/Product/Add', + component: TestedComponent, + argTypes: { + onAddProduct: { action: 'onAddProduct' }, + }, +}; + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const WithASimpleList = createExample(TestedComponent, { + inventory:[{ + id: 'this id', + description: 'this is the description', + } as any] +}); + +export const WithAProductSelected = createExample(TestedComponent, { + inventory:[], + currentProducts: { + thisid: { + quantity: 1, + product: { + id: 'asd', + description: 'asdsadsad', + } as any + } + } +}); diff --git a/packages/frontend/src/components/product/InventoryProductForm.tsx b/packages/frontend/src/components/product/InventoryProductForm.tsx @@ -0,0 +1,95 @@ +/* + 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, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider, FormErrors } from "../form/FormProvider"; +import { InputNumber } from "../form/InputNumber"; +import { InputSearchProduct } from "../form/InputSearchProduct"; +import { MerchantBackend, WithId } from "../../declaration"; +import { Translate, useTranslator } from "../../i18n"; +import { ProductMap } from "../../paths/instance/orders/create/CreatePage"; + +type Form = { + product: MerchantBackend.Products.ProductDetail & WithId, + quantity: number; +} + +interface Props { + currentProducts: ProductMap, + onAddProduct: (product: MerchantBackend.Products.ProductDetail & WithId, quantity: number) => void, + inventory: (MerchantBackend.Products.ProductDetail & WithId)[], +} + +export function InventoryProductForm({ currentProducts, onAddProduct, inventory }: Props): VNode { + const initialState = { quantity: 1 } + const [state, setState] = useState<Partial<Form>>(initialState) + const [errors, setErrors] = useState<FormErrors<Form>>({}) + + const i18n = useTranslator() + + const productWithInfiniteStock = state.product && state.product.total_stock === -1 + + const submit = (): void => { + if (!state.product) { + setErrors({ product: i18n`You must enter a valid product identifier.` }); + return; + } + if (productWithInfiniteStock) { + onAddProduct(state.product, 1) + } else { + if (!state.quantity || state.quantity <= 0) { + setErrors({ quantity: i18n`Quantity must 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) { + const left = currentStock - p.quantity; + setErrors({ quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` }); + return; + } + onAddProduct(state.product, state.quantity + p.quantity) + } else { + if (state.quantity > currentStock) { + const left = currentStock; + setErrors({ quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` }); + return; + } + onAddProduct(state.product, state.quantity) + } + } + + setState(initialState) + } + + return <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> + <InputSearchProduct selected={state.product} onChange={(p) => setState(v => ({ ...v, product: p }))} products={inventory} /> + { state.product && <div class="columns mt-5"> + <div class="column is-four-fifths"> + {!productWithInfiniteStock && + <InputNumber<Form> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> + } + </div> + <div class="column"> + <div class="buttons is-right"> + <button class="button is-success" onClick={submit}><Translate>Add</Translate></button> + </div> + </div> + </div> } + + </FormProvider> +} diff --git a/packages/frontend/src/components/product/NonInventoryProductForm.tsx b/packages/frontend/src/components/product/NonInventoryProductForm.tsx @@ -0,0 +1,144 @@ +/* + 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 { Fragment, h, VNode } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import * as yup from 'yup'; +import { FormErrors, FormProvider } from "../form/FormProvider"; +import { Input } from "../form/Input"; +import { InputCurrency } from "../form/InputCurrency"; +import { InputImage } from "../form/InputImage"; +import { InputNumber } from "../form/InputNumber"; +import { InputTaxes } from "../form/InputTaxes"; +import { MerchantBackend } from "../../declaration"; +import { useListener } from "../../hooks/listener"; +import { Translate, useTranslator } from "../../i18n"; +import { + NonInventoryProductSchema as schema +} from '../../schemas'; + + +type Entity = MerchantBackend.Product + +interface Props { + onAddProduct: (p: Entity) => Promise<void>; + productToEdit?: Entity; +} +export function NonInventoryProductFrom({ productToEdit, onAddProduct }: Props): VNode { + const [showCreateProduct, setShowCreateProduct] = useState(false) + + const isEditing = !!productToEdit + + useEffect(() => { + setShowCreateProduct(isEditing) + }, [isEditing]) + + const [submitForm, addFormSubmitter] = useListener<Partial<MerchantBackend.Product> | undefined>((result) => { + if (result) { + setShowCreateProduct(false) + return onAddProduct({ + quantity: result.quantity || 0, + taxes: result.taxes || [], + description: result.description || '', + image: result.image || '', + price: result.price || '', + unit: result.unit || '' + }) + } + return Promise.resolve() + }) + + const i18n = useTranslator() + + return <Fragment> + <div class="buttons"> + <button class="button is-success" onClick={() => setShowCreateProduct(true)} ><Translate>add custom product</Translate></button> + </div> + {showCreateProduct && <div class="modal is-active"> + <div class="modal-background " onClick={() => setShowCreateProduct(false)} /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">{i18n`Complete information of the product`}</p> + <button class="delete " aria-label="close" onClick={() => setShowCreateProduct(false)} /> + </header> + <section class="modal-card-body"> + <ProductForm initial={productToEdit} onSubscribe={addFormSubmitter} /> + </section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: '100%' }}> + <button class="button " onClick={() => setShowCreateProduct(false)} ><Translate>Cancel</Translate></button> + <button class="button is-info " disabled={!submitForm} onClick={submitForm} ><Translate>Confirm</Translate></button> + </div> + </footer> + </div> + <button class="modal-close is-large " aria-label="close" onClick={() => setShowCreateProduct(false)} /> + </div>} + </Fragment> +} + +interface ProductProps { + onSubscribe: (c?: () => Entity | undefined) => void; + initial?: Partial<Entity>; +} + +interface NonInventoryProduct { + quantity: number; + description: string; + unit: string; + price: string; + image: string; + taxes: MerchantBackend.Tax[]; +} + +export function ProductForm({ onSubscribe, initial }: ProductProps) { + const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ + taxes: [], + ...initial, + }) + let errors: FormErrors<Entity> = {} + try { + schema.validateSync(value, { abortEarly: false }) + } catch (err) { + const yupErrors = err.inner as yup.ValidationError[] + errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) + } + + const submit = useCallback((): Entity | undefined => { + return value as MerchantBackend.Product + }, [value]) + + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + + useEffect(() => { + onSubscribe(hasErrors ? undefined : submit) + }, [submit, hasErrors]) + + const i18n = useTranslator() + + return <div> + <FormProvider<NonInventoryProduct> name="product" errors={errors} object={value} valueHandler={valueHandler} > + + <InputImage<NonInventoryProduct> name="image" label={i18n`Image`} tooltip={i18n`photo of the product`} /> + <Input<NonInventoryProduct> name="description" inputType="multiline" label={i18n`Description`} tooltip={i18n`full product description`} /> + <Input<NonInventoryProduct> name="unit" label={i18n`Unit`} tooltip={i18n`name of the product unit`} /> + <InputCurrency<NonInventoryProduct> name="price" label={i18n`Price`} tooltip={i18n`amount in the current currency`} /> + + <InputNumber<NonInventoryProduct> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> + + <InputTaxes<NonInventoryProduct> name="taxes" label={i18n`Taxes`} /> + + </FormProvider> + </div> +} diff --git a/packages/frontend/src/components/product/ProductList.tsx b/packages/frontend/src/components/product/ProductList.tsx @@ -15,9 +15,9 @@ */ import { h, VNode } from "preact" import { MerchantBackend } from "../../declaration" -import { multiplyPrice } from "../../utils/amount" +import { Amounts } from "@gnu-taler/taler-util"; import emptyImage from "../../assets/empty.png"; -import { Translate, useTranslator } from "../../i18n"; +import { Translate } from "../../i18n"; interface Props { list: MerchantBackend.Product[], @@ -28,7 +28,6 @@ interface Props { }[] } export function ProductList({ list, actions = [] }: Props): VNode { - const i18n = useTranslator() return <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -52,7 +51,7 @@ export function ProductList({ list, actions = [] }: Props): VNode { {entry.quantity === 0 ? '--' : `${entry.quantity} ${entry.unit}`} </td> <td >{entry.price}</td> - <td >{multiplyPrice(entry.price, entry.quantity)}</td> + <td >{Amounts.stringify(Amounts.mult(Amounts.parseOrThrow(entry.price), entry.quantity).amount)}</td> <td class="is-actions-cell right-sticky"> {actions.map((a, i) => { return <div key={i} class="buttons is-right"> diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx @@ -20,7 +20,7 @@ */ import { add, isBefore, isFuture } from "date-fns"; -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { Amounts } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { FormProvider, FormErrors } from "../../../../components/form/FormProvider"; @@ -35,8 +35,8 @@ import { Duration, MerchantBackend, WithId } from "../../../../declaration"; import { Translate, useTranslator } from "../../../../i18n"; import { OrderCreateSchema as schema } from '../../../../schemas/index'; import { rate } from "../../../../utils/amount"; -import { InventoryProductForm } from "./InventoryProductForm"; -import { NonInventoryProductFrom } from "./NonInventoryProductForm"; +import { InventoryProductForm } from "../../../../components/product/InventoryProductForm"; +import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm"; interface Props { onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; @@ -113,6 +113,9 @@ const stringIsValidJSON = (value: string) => { } } +function undefinedIfEmpty<T>(obj: T): T | undefined { + return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj : undefined +} export function CreatePage({ onCreate, onBack, instanceConfig, instanceInventory }: Props): VNode { const config = useConfigContext() @@ -124,19 +127,15 @@ export function CreatePage({ onCreate, onBack, instanceConfig, instanceInventory const i18n = useTranslator() - function check<T>(obj: T): T | undefined { - return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj : undefined - } - const errors: FormErrors<Entity> = { - pricing: { - summary: !value.pricing?.summary ? i18n`required`:undefined, - order_price: !value.pricing?.order_price ? i18n`required`: ( + pricing: undefinedIfEmpty({ + summary: !value.pricing?.summary ? i18n`required` : undefined, + order_price: !value.pricing?.order_price ? i18n`required` : ( (Amounts.parse(value.pricing.order_price)?.value || 0) <= 0 ? i18n`must be greater than 0` : undefined ) - }, + }), extra: value.extra && !stringIsValidJSON(value.extra) ? i18n`not a valid json` : undefined, - payments: check({ + payments: undefinedIfEmpty({ refund_deadline: !value.payments?.refund_deadline ? i18n`required` : ( !isFuture(value.payments.refund_deadline) ? i18n`should be in the future` : ( value.payments.pay_deadline && value.payments.refund_deadline && isBefore(value.payments.refund_deadline, value.payments.pay_deadline) ? @@ -230,21 +229,24 @@ export function CreatePage({ onCreate, onBack, instanceConfig, instanceInventory const hasProducts = inventoryList.length > 0 || productList.length > 0 const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts) + const totalAsString = Amounts.stringify(totalPrice.amount); + const allProducts = productList.concat(inventoryList.map(asProduct)) + useEffect(() => { valueHandler(v => { return ({ ...v, pricing: { - ...v.pricing!, - // products_price: (Amounts.isZero(totalPrice.amount) ? undefined : Amounts.stringify(totalPrice.amount))!, - // order_price: (Amounts.isZero(totalPrice.amount) ? undefined : Amounts.stringify(totalPrice.amount))!, + ...v.pricing, + products_price: hasProducts ? totalAsString : undefined, + order_price: hasProducts ? totalAsString : undefined, // products_price: Amounts.stringify(totalPrice.amount), // order_price: Amounts.stringify(totalPrice.amount), } }) }) - }, [hasProducts, totalPrice]) + }, [hasProducts, totalAsString]) - const discountOrRise = rate(value.pricing?.order_price || `${config.currency}:0`, Amounts.stringify(totalPrice.amount)) + const discountOrRise = rate(value.pricing?.order_price || `${config.currency}:0`, totalAsString) // useEffect(() => { // valueHandler(v => { @@ -263,54 +265,40 @@ export function CreatePage({ onCreate, onBack, instanceConfig, instanceInventory <div class="column" /> <div class="column is-four-fifths"> - <InputGroup name="inventory_products" label={i18n`Manage products from inventory in order`} alternative={ - inventoryList.length > 0 && <p> - {/* // FIXME: translating plural singular */} - {inventoryList.length} products - with a total price of {Amounts.stringify(totalPriceInventory)}. + {/* // FIXME: translating plural singular */} + <InputGroup name="inventory_products" label={i18n`Manage products in order`} alternative={ + allProducts.length > 0 && <p> + {allProducts.length} products + with a total price of {totalAsString}. </p> - } tooltip={i18n`Manage list of products from managed inventory included in the order.`}> + } tooltip={i18n`Manage list of products in the order.`}> + <InventoryProductForm currentProducts={value.inventoryProducts || {}} onAddProduct={addProductToTheInventoryList} inventory={instanceInventory} /> - {inventoryList.length > 0 && - <ProductList list={inventoryList.map(asProduct)} - actions={[{ - name: i18n`Remove`, - tooltip: i18n`Remove this product from the order.`, - handler: (e) => removeProductFromTheInventoryList(e.product_id!) - }]} - /> - } - </InputGroup> - - <InputGroup name="products" label={i18n`Manage products outside of inventory in order`} alternative={ - productList.length > 0 && <p> - {/* // FIXME: translating plural singular */} - {productList.length} products - with a total price of {Amounts.stringify(totalPriceProducts)}. - </p> - } tooltip={i18n`Manage list of products without inventory management included in the order.`}> <NonInventoryProductFrom productToEdit={editingProduct} onAddProduct={(p) => { setEditingProduct(undefined) return addNewProduct(p) }} /> - {productList.length > 0 && - <ProductList list={productList} + {allProducts.length > 0 && + <ProductList list={allProducts} actions={[{ name: i18n`Remove`, tooltip: i18n`Remove this product from the order.`, handler: (e, index) => { - removeFromNewProduct(index); - setEditingProduct(e); + if (e.product_id) { + removeProductFromTheInventoryList(e.product_id) + } else { + removeFromNewProduct(index); + setEditingProduct(e); + } } }]} /> - } </InputGroup> diff --git a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx @@ -1,86 +0,0 @@ -/* - 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, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { FormProvider, FormErrors } from "../../../../components/form/FormProvider"; -import { InputNumber } from "../../../../components/form/InputNumber"; -import { InputSearchProduct } from "../../../../components/form/InputSearchProduct"; -import { MerchantBackend, WithId } from "../../../../declaration"; -import { Translate, useTranslator } from "../../../../i18n"; -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, - inventory: (MerchantBackend.Products.ProductDetail & WithId)[], -} - -export function InventoryProductForm({ currentProducts, onAddProduct, inventory }: Props): VNode { - const initialState = { quantity: 1 } - const [state, setState] = useState<Partial<Form>>(initialState) - const [errors, setErrors] = useState<FormErrors<Form>>({}) - - const i18n = useTranslator() - - const productWithInfiniteStock = state.product && state.product.total_stock === -1 - - const submit = (): void => { - if (!state.product) { - setErrors({ product: i18n`You must enter a valid product identifier.` }); - return; - } - if (productWithInfiniteStock) { - onAddProduct(state.product, 1) - } else { - if (!state.quantity || state.quantity <= 0) { - setErrors({ quantity: i18n`Quantity must 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) { - const left = currentStock - p.quantity; - setErrors({ quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` }); - return; - } - onAddProduct(state.product, state.quantity + p.quantity) - } else { - if (state.quantity > currentStock) { - const left = currentStock; - setErrors({ quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` }); - return; - } - onAddProduct(state.product, state.quantity) - } - } - - setState(initialState) - } - - return <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> - <InputSearchProduct selected={state.product} onChange={(p) => setState(v => ({ ...v, product: p }))} products={inventory} /> - { state.product && !productWithInfiniteStock && <InputNumber<Form> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> } - <div class="buttons is-right mt-5"> - <button class="button is-success" disabled={!state.product} onClick={submit}><Translate>Add</Translate></button> - </div> - </FormProvider> -} diff --git a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx @@ -1,148 +0,0 @@ -/* - 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 { Fragment, h, VNode } from "preact"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { FormProvider, FormErrors } from "../../../../components/form/FormProvider"; -import { Input } from "../../../../components/form/Input"; -import { InputCurrency } from "../../../../components/form/InputCurrency"; -import { InputImage } from "../../../../components/form/InputImage"; -import { InputNumber } from "../../../../components/form/InputNumber"; -import { InputTaxes } from "../../../../components/form/InputTaxes"; -import { ConfirmModal } from "../../../../components/modal"; -import { MerchantBackend } from "../../../../declaration"; -import { useListener } from "../../../../hooks/listener"; - -import { - NonInventoryProductSchema as schema -} from '../../../../schemas'; -import * as yup from 'yup'; -import { Translate, useTranslator } from "../../../../i18n"; - -type Entity = MerchantBackend.Product - -interface Props { - onAddProduct: (p: Entity) => Promise<void>; - productToEdit?: Entity; -} -export function NonInventoryProductFrom({ productToEdit, onAddProduct }: Props): VNode { - const [showCreateProduct, setShowCreateProduct] = useState(false) - - const isEditing = !!productToEdit - - useEffect(() => { - setShowCreateProduct(isEditing) - }, [isEditing]) - - const [submitForm, addFormSubmitter] = useListener<Partial<MerchantBackend.Product> | undefined>((result) => { - if (result) { - setShowCreateProduct(false) - return onAddProduct({ - quantity: result.quantity || 0, - taxes: result.taxes || [], - description: result.description || '', - image: result.image || '', - price: result.price || '', - unit: result.unit || '' - }) - } - return Promise.resolve() - }) - - const i18n = useTranslator() - - console.log('submit form', submitForm) - - return <Fragment> - <div class="buttons"> - <button class="button is-success" onClick={() => setShowCreateProduct(true)} ><Translate>add product</Translate></button> - </div> - {showCreateProduct && <div class="modal is-active"> - <div class="modal-background " onClick={() => setShowCreateProduct(false)} /> - <div class="modal-card"> - <header class="modal-card-head"> - <p class="modal-card-title">{i18n`Complete information of the product`}</p> - <button class="delete " aria-label="close" onClick={() => setShowCreateProduct(false)} /> - </header> - <section class="modal-card-body"> - <ProductForm initial={productToEdit} onSubscribe={addFormSubmitter} /> - </section> - <footer class="modal-card-foot"> - <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button " onClick={() => setShowCreateProduct(false)} ><Translate>Cancel</Translate></button> - <button class="button is-info " disabled={!submitForm} onClick={submitForm} ><Translate>Confirm</Translate></button> - </div> - </footer> - </div> - <button class="modal-close is-large " aria-label="close" onClick={() => setShowCreateProduct(false)} /> - </div>} - </Fragment> -} - -interface ProductProps { - onSubscribe: (c?: () => Entity | undefined) => void; - initial?: Partial<Entity>; -} - -interface NonInventoryProduct { - quantity: number; - description: string; - unit: string; - price: string; - image: string; - taxes: MerchantBackend.Tax[]; -} - -export function ProductForm({ onSubscribe, initial }: ProductProps) { - const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ - taxes: [], - ...initial, - }) - let errors: FormErrors<Entity> = {} - try { - schema.validateSync(value, { abortEarly: false }) - } catch (err) { - const yupErrors = err.inner as yup.ValidationError[] - errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) - } - - const submit = useCallback((): Entity | undefined => { - return value as MerchantBackend.Product - }, [value]) - - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) - - useEffect(() => { - console.log('has errors', hasErrors) - onSubscribe(hasErrors ? undefined : submit) - }, [submit, hasErrors]) - - const i18n = useTranslator() - - return <div> - <FormProvider<NonInventoryProduct> name="product" errors={errors} object={value} valueHandler={valueHandler} > - - <InputImage<NonInventoryProduct> name="image" label={i18n`Image`} tooltip={i18n`photo of the product`} /> - <Input<NonInventoryProduct> name="description" inputType="multiline" label={i18n`Description`} tooltip={i18n`full product description`} /> - <Input<NonInventoryProduct> name="unit" label={i18n`Unit`} tooltip={i18n`name of the product unit`} /> - <InputCurrency<NonInventoryProduct> name="price" label={i18n`Price`} tooltip={i18n`amount in the current currency`} /> - - <InputNumber<NonInventoryProduct> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> - - <InputTaxes<NonInventoryProduct> name="taxes" label={i18n`Taxes`} /> - - </FormProvider> - </div> -}