diff options
Diffstat (limited to 'packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx')
-rw-r--r-- | packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx new file mode 100644 index 000000000..c6d280f94 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -0,0 +1,215 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import * as yup from "yup"; +import { MerchantBackend } from "../../declaration.js"; +import { useListener } from "../../hooks/listener.js"; +import { NonInventoryProductSchema as schema } from "../../schemas/index.js"; +import { FormErrors, FormProvider } from "../form/FormProvider.js"; +import { Input } from "../form/Input.js"; +import { InputCurrency } from "../form/InputCurrency.js"; +import { InputImage } from "../form/InputImage.js"; +import { InputNumber } from "../form/InputNumber.js"; +import { InputTaxes } from "../form/InputTaxes.js"; + +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 } = useTranslationContext(); + + return ( + <Fragment> + <div class="buttons"> + <button + class="button is-success" + data-tooltip={i18n.str`describe and add a product that is not in the inventory list`} + onClick={() => setShowCreateProduct(true)} + > + <i18n.Translate>Add custom product</i18n.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.str`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)} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + <button + class="button is-info " + disabled={!submitForm} + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.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): VNode { + const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ + taxes: [], + ...initial, + }); + let errors: FormErrors<Entity> = {}; + try { + schema.validateSync(value, { abortEarly: false }); + } catch (err) { + if (err instanceof yup.ValidationError) { + 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 } = useTranslationContext(); + + return ( + <div> + <FormProvider<NonInventoryProduct> + name="product" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <InputImage<NonInventoryProduct> + name="image" + label={i18n.str`Image`} + tooltip={i18n.str`photo of the product`} + /> + <Input<NonInventoryProduct> + name="description" + inputType="multiline" + label={i18n.str`Description`} + tooltip={i18n.str`full product description`} + /> + <Input<NonInventoryProduct> + name="unit" + label={i18n.str`Unit`} + tooltip={i18n.str`name of the product unit`} + /> + <InputCurrency<NonInventoryProduct> + name="price" + label={i18n.str`Price`} + tooltip={i18n.str`amount in the current currency`} + /> + + <InputNumber<NonInventoryProduct> + name="quantity" + label={i18n.str`Quantity`} + tooltip={i18n.str`how many products will be added`} + /> + + <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} /> + </FormProvider> + </div> + ); +} |