diff --git a/backend/routes/app/database/products.database.js b/backend/routes/app/database/products.database.js index 43c4ac2..b91dfe8 100644 --- a/backend/routes/app/database/products.database.js +++ b/backend/routes/app/database/products.database.js @@ -91,3 +91,42 @@ export const allProducts = async () => { return { code: "ep002" }; } }; + +export const setAmount = async (itemUUID, amount) => { + const [result] = await pool.query( + ` + UPDATE products SET amount = ? WHERE uuid = UUID_TO_BIN(?) + `, + [amount, itemUUID], + ); + + if (result.affectedRows > 0) { + return { code: "sp004" }; // success + } else { + return { code: "ep004" }; // error + } +}; + +export const updateItem = async (itemUUID, newValues) => { + const [result] = await pool.query( + ` + UPDATE products SET name = ?, description = ?, price = ?, amount = ?, storage_location = UUID_TO_BIN(?), expiry_date = ?, bottling_date = ? WHERE uuid = UUID_TO_BIN(?); + `, + [ + newValues.name, + newValues.description, + newValues.price, + newValues.amount, + newValues.storage_location_uuid, + newValues.expiry_date, + newValues.bottling_date, + itemUUID, + ], + ); + + if (result.affectedRows > 0) { + return { code: "sp005" }; // success + } else { + return { code: "ep005" }; // error + } +}; diff --git a/backend/routes/app/products.route.js b/backend/routes/app/products.route.js index 167cf90..d6d7410 100644 --- a/backend/routes/app/products.route.js +++ b/backend/routes/app/products.route.js @@ -5,6 +5,8 @@ import { allProducts, newProduct, productDetails, + setAmount, + updateItem, } from "./database/products.database.js"; dotenv.config(); const router = express.Router(); @@ -95,4 +97,54 @@ router.get("/view", authenticate, async (req, res) => { } }); +router.put("/mutate/set-amount", authenticate, async (req, res) => { + const amount = req.query.amount; + const itemUUID = req.query.item; + + const result = await setAmount(itemUUID, amount); + + if (result.code === "ep004") { + res.status(406).json({ + success: false, + code: "ep004", + data: null, + message: "Error while updating product amount", + }); + } + + if (result.code === "sp004") { + res.status(200).json({ + success: true, + code: "sp004", + data: null, + message: "", + }); + } +}); + +router.post("/mutate/update-item", authenticate, async (req, res) => { + const itemUUID = req.query.item; + const newValues = req.body; + + const result = await updateItem(itemUUID, newValues); + + if (result.code === "ep005") { + res.status(406).json({ + success: false, + code: "ep005", + data: null, + message: "Error while updating product", + }); + } + + if (result.code === "sp005") { + res.status(200).json({ + success: true, + code: "sp005", + data: null, + message: "", + }); + } +}); + export default router; diff --git a/frontend/src/misc/interfaces.ts b/frontend/src/misc/interfaces.ts new file mode 100644 index 0000000..10f4a25 --- /dev/null +++ b/frontend/src/misc/interfaces.ts @@ -0,0 +1,22 @@ +export interface productDetailsInterface { + amount: number; + bottling_date: string; + description: string; + expiry_date: string; + name: string; + picture: string | null; + price: string; + storage_location_name: string; + storage_location_uuid: string; + uuid: string; +} + +export type ProductFormValues = { + amount: number; + bottling_date: string; + description: string; + expiry_date: string; + name: string; + price: string; + storage_location_uuid: string; +}; diff --git a/frontend/src/pages/ViewProduct.tsx b/frontend/src/pages/ViewProduct.tsx index 6c715c5..4821430 100644 --- a/frontend/src/pages/ViewProduct.tsx +++ b/frontend/src/pages/ViewProduct.tsx @@ -1,22 +1,20 @@ -import { useQuery } from "@tanstack/react-query"; -import { getProductDetails } from "../utils/uxFncs"; -import { CircularProgress, Typography } from "@mui/joy"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { getProductDetails, getStorages } from "../utils/uxFncs"; +import { + CircularProgress, + Typography, + Select, + Option, + Input, + Button, +} from "@mui/joy"; import { useTranslation } from "react-i18next"; import { useEffect } from "react"; -import { formatDate } from "../utils/uxFncs"; - -interface productDetailsInterface { - amount: number; - bottling_date: string; - description: string; - expiry_date: string; - name: string; - picture: string | null; - price: string; - storage_location_name: string; - storage_location_uuid: string; - uuid: string; -} +import { useForm } from "@tanstack/react-form"; +import { mutateProduct } from "../utils/uxFncs"; +import { toInputDate } from "../utils/uxFncs"; +import type { ProductFormValues } from "../misc/interfaces"; +import type { productDetailsInterface } from "../misc/interfaces"; interface ViewProductProps { uuid: string; @@ -25,6 +23,7 @@ interface ViewProductProps { export const ViewProduct = (props: ViewProductProps) => { const uuid = props.uuid; const { t } = useTranslation(); + const queryClient = useQueryClient(); const { data: productDetails, @@ -35,9 +34,62 @@ export const ViewProduct = (props: ViewProductProps) => { queryFn: () => getProductDetails(uuid), }); + 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 }) => { + if (!productDetails?.uuid) { + return; + } + + mutate({ values: value, uuid: productDetails.uuid }); + }, + }); + + const { mutate, isPending } = useMutation({ + mutationFn: ({ + values, + uuid, + }: { + values: ProductFormValues; + uuid: string; + }) => mutateProduct(values, uuid), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ["product", variables.uuid] }); + }, + }); + useEffect(() => { - console.log(productDetails); - }, [isSuccess]); + if (!productDetails) { + return; + } + + form.setFieldValue("amount", productDetails.amount ?? 0); + form.setFieldValue( + "bottling_date", + toInputDate(productDetails.bottling_date), + ); + form.setFieldValue("description", productDetails.description ?? ""); + form.setFieldValue("expiry_date", toInputDate(productDetails.expiry_date)); + form.setFieldValue("name", productDetails.name ?? ""); + form.setFieldValue("price", productDetails.price ?? ""); + form.setFieldValue( + "storage_location_uuid", + productDetails.storage_location_uuid ?? "", + ); + }, [form, productDetails]); return ( <> @@ -45,11 +97,100 @@ export const ViewProduct = (props: ViewProductProps) => { {productDetailsLoading && } {isSuccess && ( <> - {productDetails.name} - {productDetails.description} - - {t("expiry-date") + " " + formatDate(productDetails.expiry_date)} - +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + )} + + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + )} + + {t("expiry-date")} + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + )} + + {t("bottling-date")} + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + )} + + + {(field) => ( + { + const nextValue = Number(e.target.value); + field.handleChange(Number.isNaN(nextValue) ? 0 : nextValue); + }} + onBlur={field.handleBlur} + /> + )} + + + {(field) => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + )} + + {t("currency")} + + {(field) => ( + + )} + + +
)} diff --git a/frontend/src/utils/uxFncs.ts b/frontend/src/utils/uxFncs.ts index a4f3a1d..5b8d1c9 100644 --- a/frontend/src/utils/uxFncs.ts +++ b/frontend/src/utils/uxFncs.ts @@ -1,5 +1,6 @@ import { API_BASE } from "../config/api.config"; import Cookies from "js-cookie"; +import type { ProductFormValues } from "../misc/interfaces"; export const getProducts = async () => { const result = await fetch(`${API_BASE}/products/all-products`, { @@ -40,10 +41,73 @@ export const getProductDetails = async (uuid: string) => { } }; -export const formatDate = (isoString: string) => { - const date = new Date(isoString); - const day = String(date.getDate()).padStart(2, "0"); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const year = date.getFullYear(); - return `${day}.${month}.${year}`; +export const toInputDate = (value?: string) => { + if (!value) { + return ""; + } + + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return value; + } + + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return ""; + } + + return date.toISOString().slice(0, 10); +}; + +export const mutateProduct = async ( + values: ProductFormValues, + itemUUID: string, +) => { + const payload = { + ...values, + expiry_date: values.expiry_date || null, + bottling_date: values.bottling_date || null, + }; + + const result = await fetch( + `${API_BASE}/products/mutate/update-item?item=${itemUUID}`, + { + 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 === "ep004") { + return { success: false, code: response.code }; + } + + if (response.code === "sp004") { + return response.data; + } +}; + +export const getStorages = async () => { + const result = await fetch(`${API_BASE}/storage/all-storages`, { + headers: { + Authorization: `Bearer ${Cookies.get("token") || ""}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + const response = await result.json(); + + if (response.code === "es001") { + return { success: false, code: response.code }; + } + + if (response.code === "ss001") { + return response.data; + } };