aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/components/product
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-10-24 10:46:14 +0200
committerFlorian Dold <florian@dold.me>2022-10-24 10:46:14 +0200
commit3e060b80428943c6562250a6ff77eff10a0259b7 (patch)
treed08472bc5ca28621c62ac45b229207d8215a9ea7 /packages/merchant-backoffice-ui/src/components/product
parentfb52ced35ac872349b0e1062532313662552ff6c (diff)
downloadwallet-core-3e060b80428943c6562250a6ff77eff10a0259b7.tar.xz
repo: integrate packages from former merchant-backoffice.git
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components/product')
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx58
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx95
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx146
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx176
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductList.tsx105
5 files changed, 580 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
new file mode 100644
index 000000000..6504d85ba
--- /dev/null
+++ b/packages/merchant-backoffice-ui/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/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
new file mode 100644
index 000000000..8f05c9736
--- /dev/null
+++ b/packages/merchant-backoffice-ui/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-two-thirds">
+ {!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 from inventory</Translate></button>
+ </div>
+ </div>
+ </div> }
+
+ </FormProvider>
+}
diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
new file mode 100644
index 000000000..397efe616
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
@@ -0,0 +1,146 @@
+/*
+ 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" data-tooltip={i18n`describe and add a product that is not in the inventory list`} 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): 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 = 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/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
new file mode 100644
index 000000000..9434d3de8
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -0,0 +1,176 @@
+/*
+ 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 { useCallback, useEffect, useState } from "preact/hooks";
+import * as yup from "yup";
+import { useBackendContext } from "../../context/backend";
+import { MerchantBackend } from "../../declaration";
+import { useTranslator } from "../../i18n";
+import {
+ ProductCreateSchema as createSchema,
+ ProductUpdateSchema as updateSchema,
+} from "../../schemas";
+import { FormProvider, FormErrors } from "../form/FormProvider";
+import { Input } from "../form/Input";
+import { InputCurrency } from "../form/InputCurrency";
+import { InputImage } from "../form/InputImage";
+import { InputNumber } from "../form/InputNumber";
+import { InputStock, Stock } from "../form/InputStock";
+import { InputTaxes } from "../form/InputTaxes";
+import { InputWithAddon } from "../form/InputWithAddon";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onSubscribe: (c?: () => Entity | undefined) => void;
+ initial?: Partial<Entity>;
+ alreadyExist?: boolean;
+}
+
+export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
+ const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({
+ address: {},
+ description_i18n: {},
+ taxes: [],
+ next_restock: { t_s: "never" },
+ price: ":0",
+ ...initial,
+ stock:
+ !initial || initial.total_stock === -1
+ ? undefined
+ : {
+ current: initial.total_stock || 0,
+ lost: initial.total_lost || 0,
+ sold: initial.total_sold || 0,
+ address: initial.address,
+ nextRestock: initial.next_restock,
+ },
+ });
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ (alreadyExist ? updateSchema : createSchema).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 hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined
+ );
+
+ const submit = useCallback((): Entity | undefined => {
+ const stock: Stock = (value as any).stock;
+
+ if (!stock) {
+ value.total_stock = -1;
+ } else {
+ value.total_stock = stock.current;
+ value.total_lost = stock.lost;
+ value.next_restock =
+ stock.nextRestock instanceof Date
+ ? { t_s: stock.nextRestock.getTime() / 1000 }
+ : stock.nextRestock;
+ value.address = stock.address;
+ }
+ delete (value as any).stock;
+
+ if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) {
+ delete value.minimum_age;
+ }
+
+ return value as MerchantBackend.Products.ProductDetail & {
+ product_id: string;
+ };
+ }, [value]);
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const backend = useBackendContext();
+ const i18n = useTranslator();
+
+ return (
+ <div>
+ <FormProvider<Entity>
+ name="product"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ {alreadyExist ? undefined : (
+ <InputWithAddon<Entity>
+ name="product_id"
+ addonBefore={`${backend.url}/product/`}
+ label={i18n`ID`}
+ tooltip={i18n`product identification to use in URLs (for internal use only)`}
+ />
+ )}
+ <InputImage<Entity>
+ name="image"
+ label={i18n`Image`}
+ tooltip={i18n`illustration of the product for customers`}
+ />
+ <Input<Entity>
+ name="description"
+ inputType="multiline"
+ label={i18n`Description`}
+ tooltip={i18n`product description for customers`}
+ />
+ <InputNumber<Entity>
+ name="minimum_age"
+ label={i18n`Age restricted`}
+ tooltip={i18n`is this product restricted for customer below certain age?`}
+ />
+ <Input<Entity>
+ name="unit"
+ label={i18n`Unit`}
+ tooltip={i18n`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
+ />
+ <InputCurrency<Entity>
+ name="price"
+ label={i18n`Price`}
+ tooltip={i18n`sale price for customers, including taxes, for above units of the product`}
+ />
+ <InputStock
+ name="stock"
+ label={i18n`Stock`}
+ alreadyExist={alreadyExist}
+ tooltip={i18n`product inventory for products with finite supply (for internal use only)`}
+ />
+ <InputTaxes<Entity>
+ name="taxes"
+ label={i18n`Taxes`}
+ tooltip={i18n`taxes included in the product price, exposed to customers`}
+ />
+ </FormProvider>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
new file mode 100644
index 000000000..ff141bb39
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
@@ -0,0 +1,105 @@
+/*
+ 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 { Amounts } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import emptyImage from "../../assets/empty.png";
+import { MerchantBackend } from "../../declaration";
+import { Translate } from "../../i18n";
+
+interface Props {
+ list: MerchantBackend.Product[];
+ actions?: {
+ name: string;
+ tooltip: string;
+ handler: (d: MerchantBackend.Product, index: number) => void;
+ }[];
+}
+export function ProductList({ list, actions = [] }: Props): VNode {
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <Translate>image</Translate>
+ </th>
+ <th>
+ <Translate>description</Translate>
+ </th>
+ <th>
+ <Translate>quantity</Translate>
+ </th>
+ <th>
+ <Translate>unit price</Translate>
+ </th>
+ <th>
+ <Translate>total price</Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {list.map((entry, index) => {
+ const unitPrice = !entry.price ? "0" : entry.price;
+ const totalPrice = !entry.price
+ ? "0"
+ : Amounts.stringify(
+ Amounts.mult(
+ Amounts.parseOrThrow(entry.price),
+ entry.quantity
+ ).amount
+ );
+
+ return (
+ <tr key={index}>
+ <td>
+ <img
+ style={{ height: 32, width: 32 }}
+ src={entry.image ? entry.image : emptyImage}
+ />
+ </td>
+ <td>{entry.description}</td>
+ <td>
+ {entry.quantity === 0
+ ? "--"
+ : `${entry.quantity} ${entry.unit}`}
+ </td>
+ <td>{unitPrice}</td>
+ <td>{totalPrice}</td>
+ <td class="is-actions-cell right-sticky">
+ {actions.map((a, i) => {
+ return (
+ <div key={i} class="buttons is-right">
+ <button
+ class="button is-small is-danger has-tooltip-left"
+ data-tooltip={a.tooltip}
+ type="button"
+ onClick={() => a.handler(entry, index)}
+ >
+ {a.name}
+ </button>
+ </div>
+ );
+ })}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}