improved product details page

This commit is contained in:
2026-05-27 18:07:56 +02:00
parent 8ac83520a9
commit 11b490b2e5
5 changed files with 348 additions and 30 deletions
@@ -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
}
};
+52
View File
@@ -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;
+22
View File
@@ -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;
};
+165 -24
View File
@@ -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 && <CircularProgress size="sm" />}
{isSuccess && (
<>
<Typography level="h3">{productDetails.name}</Typography>
<Typography level="body-lg">{productDetails.description}</Typography>
<Typography level="body-lg">
{t("expiry-date") + " " + formatDate(productDetails.expiry_date)}
</Typography>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field name="name">
{(field) => (
<Input
type="text"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>
<form.Field name="description">
{(field) => (
<Input
type="text"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>
{t("expiry-date")}
<form.Field name="expiry_date">
{(field) => (
<Input
type="date"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>
{t("bottling-date")}
<form.Field name="bottling_date">
{(field) => (
<Input
type="date"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>
<form.Field name="amount">
{(field) => (
<Input
type="number"
color="neutral"
id="amountInput"
placeholder={t("amount")}
value={field.state.value}
variant="soft"
onChange={(e) => {
const nextValue = Number(e.target.value);
field.handleChange(Number.isNaN(nextValue) ? 0 : nextValue);
}}
onBlur={field.handleBlur}
/>
)}
</form.Field>
<form.Field name="price">
{(field) => (
<Input
type="text"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>
{t("currency")}
<form.Field name="storage_location_uuid">
{(field) => (
<Select
value={field.state.value}
onChange={(_event, value) => field.handleChange(value ?? "")}
>
{storages?.map((storage: { uuid: string; name: string }) => (
<Option key={storage.uuid} value={storage.uuid}>
{storage.name}
</Option>
))}
</Select>
)}
</form.Field>
<Button type="submit" loading={isPending}>
{t("save")}
</Button>
</form>
</>
)}
</>
+70 -6
View File
@@ -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;
}
};