diff --git a/backend/routes/app/database/products.database.js b/backend/routes/app/database/products.database.js index b91dfe8..515f354 100644 --- a/backend/routes/app/database/products.database.js +++ b/backend/routes/app/database/products.database.js @@ -20,12 +20,19 @@ export const newProduct = async ( expiry_date, bottling_date, ) => { + let newPrice; + if (price == "") { + newPrice = null; + } else { + newPrice = price; + } + const [result] = await pool.query( "INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES (?, ?, ?, ?, UUID_TO_BIN(?), ?, ?)", [ name, description, - price, + newPrice, amount, storage_location, expiry_date, diff --git a/frontend/src/pages/AddProduct.tsx b/frontend/src/pages/AddProduct.tsx new file mode 100644 index 0000000..5a644ac --- /dev/null +++ b/frontend/src/pages/AddProduct.tsx @@ -0,0 +1,271 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + Alert, + Box, + Button, + Chip, + Divider, + Input, + Option, + Select, + Typography, +} from "@mui/joy"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import { useForm } from "@tanstack/react-form"; +import { createProduct, getStorages } from "../utils/uxFncs"; +import type { ProductFormValues } from "../misc/interfaces"; + +export const AddProduct = () => { + const { t } = useTranslation(); + const [success, setSuccess] = useState(false); + + const { data: storages } = useQuery({ + queryKey: ["storages"], + queryFn: () => getStorages(), + }); + + const form = useForm({ + defaultValues: { + amount: 0, + bottling_date: "", + description: "", + expiry_date: "", + name: "", + price: "", + storage_location_uuid: "", + }, + onSubmit: async ({ value }) => { + setSuccess(false); + mutate(value); + }, + }); + + const { mutate, isPending } = useMutation({ + mutationFn: (values: ProductFormValues) => createProduct(values), + onSuccess: () => { + setSuccess(true); + }, + }); + + return ( + <> +
+
+
+ + {t("add-product")} + + + {t("inventory-header")} + +
+ + {t("details")} + +
+
+ +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > +
+
+
+ + {t("product-name")} + + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + size="lg" + variant="outlined" + className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" + /> + )} + +
+
+ + {t("description")} + + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + size="lg" + variant="outlined" + className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" + /> + )} + +
+
+
+ + {t("expiry-date")} + + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + size="lg" + variant="outlined" + className="rounded-2xl bg-white/90" + /> + )} + +
+
+ + {t("bottling-date")} + + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + size="lg" + variant="outlined" + className="rounded-2xl bg-white/90" + /> + )} + +
+
+
+
+
+ + {t("inventory")} + + +
+
+ + {t("amount")} + + + {(field) => ( + { + const nextValue = Number(e.target.value); + field.handleChange( + Number.isNaN(nextValue) ? 0 : nextValue, + ); + }} + onBlur={field.handleBlur} + className="rounded-2xl bg-white/80" + /> + )} + +
+
+ + {t("price")} + + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + size="lg" + variant="outlined" + className="rounded-2xl bg-white/90" + /> + )} + + + {t("currency")} + +
+
+ + {t("storage-place")} + + + {(field) => ( + + )} + +
+
+
+
+
+
+ + {t("product-details")} + + +
+ {success && ( + +
+ + {t("success")} + +
+
+ )} +
+
+ + ); +}; diff --git a/frontend/src/routes/app/_hiddenLayout/add-product.tsx b/frontend/src/routes/app/_hiddenLayout/add-product.tsx index f2307b9..f069f08 100644 --- a/frontend/src/routes/app/_hiddenLayout/add-product.tsx +++ b/frontend/src/routes/app/_hiddenLayout/add-product.tsx @@ -1,5 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { isAuthenticated } from "../../../utils/auth"; +import { AddProduct } from "../../../pages/AddProduct"; export const Route = createFileRoute("/app/_hiddenLayout/add-product")({ beforeLoad: async () => { @@ -13,5 +14,5 @@ export const Route = createFileRoute("/app/_hiddenLayout/add-product")({ }); function RouteComponent() { - return
Hello "/app/add-product"!
; + return ; } diff --git a/frontend/src/utils/uxFncs.ts b/frontend/src/utils/uxFncs.ts index 5b8d1c9..7b9ee85 100644 --- a/frontend/src/utils/uxFncs.ts +++ b/frontend/src/utils/uxFncs.ts @@ -111,3 +111,35 @@ export const getStorages = async () => { return response.data; } }; + +export const createProduct = async (values: ProductFormValues) => { + const payload = { + name: values.name, + description: values.description, + price: values.price, + amount: values.amount, + storage_location: values.storage_location_uuid, + expiry_date: values.expiry_date || null, + bottling_date: values.bottling_date || null, + }; + + const result = await fetch(`${API_BASE}/products/new-product`, { + method: "POST", + body: JSON.stringify(payload), + headers: { + Authorization: `Bearer ${Cookies.get("token") || ""}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + const response = await result.json(); + + if (response.code === "ep001") { + return { success: false, code: response.code }; + } + + if (response.code === "sp001") { + return response.data; + } +};