refactor: update ui and make it modern
This commit is contained in:
+191
-269
@@ -1,18 +1,36 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getStorages } from "../utils/api/storages.ts";
|
||||
import { Box, Button, Chip, CircularProgress, Divider, Input, Option, Select, Typography, } from "@mui/joy";
|
||||
import { Button, CircularProgress, Input, Option, Select, Typography } from "@mui/joy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
deleteSelectedProducts,
|
||||
getProductDetails,
|
||||
mutateProduct,
|
||||
} from "../utils/api/products.ts";
|
||||
import { toInputDate } from "../utils/uxFncs";
|
||||
import type { AlertInterface, productDetailsInterface, ProductFormValues, Storage, } from "../misc/interfaces";
|
||||
import type {
|
||||
AlertInterface,
|
||||
productDetailsInterface,
|
||||
ProductFormValues,
|
||||
Storage,
|
||||
} from "../misc/interfaces";
|
||||
import type { ApiError } from "../utils/api/apiError";
|
||||
import QrCodeIcon from "@mui/icons-material/QrCode";
|
||||
import { QrCode, Trash2 } from "lucide-react";
|
||||
import Cookies from "js-cookie";
|
||||
import QRCode from "qrcode";
|
||||
import { AmountStepper } from "../components/AmountStepper.tsx";
|
||||
import { ExpiryBadge } from "../components/ProductStatusBar.tsx";
|
||||
import { MyAlert } from "../components/MyAlert.tsx";
|
||||
|
||||
const cardSx = {
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
bgcolor: "background.surface",
|
||||
} as const;
|
||||
|
||||
interface ViewProductProps {
|
||||
uuid: string;
|
||||
}
|
||||
@@ -20,8 +38,8 @@ interface ViewProductProps {
|
||||
export const ViewProduct = (props: ViewProductProps) => {
|
||||
const uuid = props.uuid;
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [alert, setAlert] = useState<AlertInterface>({
|
||||
isAlert: false,
|
||||
type: "neutral",
|
||||
@@ -91,48 +109,43 @@ export const ViewProduct = (props: ViewProductProps) => {
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: ({
|
||||
values,
|
||||
uuid,
|
||||
}: {
|
||||
values: ProductFormValues;
|
||||
uuid: string;
|
||||
}) => mutateProduct(values, uuid),
|
||||
mutationFn: ({ values, uuid }: { values: ProductFormValues; uuid: string }) =>
|
||||
mutateProduct(values, uuid),
|
||||
onSuccess: (_data, variables) => {
|
||||
setSuccess(true);
|
||||
setAlert({ isAlert: true, type: "success", header: t("success"), text: t("save") });
|
||||
queryClient.invalidateQueries({ queryKey: ["product", variables.uuid] });
|
||||
},
|
||||
onError: showError,
|
||||
});
|
||||
|
||||
const { mutate: deleteProduct, isPending: isDeleting } = useMutation({
|
||||
mutationFn: () => deleteSelectedProducts([uuid]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["products"] });
|
||||
void navigate({ to: "/app/inventory" });
|
||||
},
|
||||
onError: showError,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!productDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setFieldValue("amount", productDetails.amount ?? 0);
|
||||
form.setFieldValue(
|
||||
"bottling_date",
|
||||
toInputDate(productDetails.bottling_date),
|
||||
);
|
||||
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.setFieldValue("storage_location_uuid", productDetails.storage_location_uuid ?? "");
|
||||
}, [form, productDetails]);
|
||||
|
||||
const downloadQRcode = async () => {
|
||||
const baseUrl: string = `${window.location.protocol}//${window.location.host}`;
|
||||
const url = `${baseUrl}/app/quick-view/product?uuid=${uuid}`;
|
||||
|
||||
const dataUrl = await QRCode.toDataURL(url, {
|
||||
width: 300,
|
||||
margin: 2,
|
||||
});
|
||||
const dataUrl = await QRCode.toDataURL(url, { width: 300, margin: 2 });
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.download = "qrcode.png";
|
||||
@@ -141,52 +154,42 @@ export const ViewProduct = (props: ViewProductProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="h2"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
{t("product-details")}
|
||||
</Typography>
|
||||
</div>
|
||||
<Chip
|
||||
variant="soft"
|
||||
color="primary"
|
||||
className="ml-auto rounded-full px-3"
|
||||
>
|
||||
{t("details")}
|
||||
</Chip>
|
||||
</div>
|
||||
{productDetailsLoading && <CircularProgress size="sm" />}
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Typography level="h2" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{isSuccess ? productDetails.name : t("product-details")}
|
||||
</Typography>
|
||||
{isSuccess && <ExpiryBadge expiryDate={productDetails.expiry_date} />}
|
||||
<Button
|
||||
startDecorator={<Trash2 size={16} />}
|
||||
color="danger"
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
className="ml-auto rounded-2xl"
|
||||
loading={isDeleting}
|
||||
onClick={() => deleteProduct()}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</div>
|
||||
{productDetailsLoading && <CircularProgress size="sm" />}
|
||||
|
||||
{isSuccess && (
|
||||
<Box
|
||||
className="mt-6 rounded-3xl p-6 backdrop-blur"
|
||||
sx={{
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
bgcolor: "background.surface",
|
||||
boxShadow:
|
||||
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 12%, transparent)",
|
||||
<form
|
||||
className="space-y-6"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<form
|
||||
className="space-y-6"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-5 lg:grid-cols-2">
|
||||
<div className="rounded-3xl p-6" style={cardSx}>
|
||||
<Typography level="title-lg" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("product")}
|
||||
</Typography>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="title-md"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("product-name")}
|
||||
</Typography>
|
||||
<form.Field name="name">
|
||||
@@ -199,19 +202,12 @@ export const ViewProduct = (props: ViewProductProps) => {
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
sx={{
|
||||
bgcolor: "var(--joy-palette-background-surface)",
|
||||
boxShadow: "0 10px 24px var(--joy-palette-divider)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="title-md"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("description")}
|
||||
</Typography>
|
||||
<form.Field name="description">
|
||||
@@ -224,44 +220,13 @@ export const ViewProduct = (props: ViewProductProps) => {
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
sx={{
|
||||
bgcolor: "var(--joy-palette-background-surface)",
|
||||
boxShadow: "0 10px 24px var(--joy-palette-divider)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="title-md"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
{t("expiry-date")}
|
||||
</Typography>
|
||||
<form.Field name="expiry_date">
|
||||
{(field) => (
|
||||
<Input
|
||||
type="date"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
sx={{
|
||||
bgcolor: "var(--joy-palette-background-surface)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="title-md"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("bottling-date")}
|
||||
</Typography>
|
||||
<form.Field name="bottling_date">
|
||||
@@ -274,180 +239,137 @@ export const ViewProduct = (props: ViewProductProps) => {
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
sx={{
|
||||
bgcolor: "var(--joy-palette-background-surface)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("expiry-date")}
|
||||
</Typography>
|
||||
<form.Field name="expiry_date">
|
||||
{(field) => (
|
||||
<Input
|
||||
type="date"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="rounded-2xl p-5"
|
||||
style={{
|
||||
border: "1px solid var(--joy-palette-divider)",
|
||||
background:
|
||||
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
|
||||
boxShadow: "0 16px 40px var(--joy-palette-divider)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level="title-lg"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
{t("inventory")}
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl p-6" style={cardSx}>
|
||||
<Typography level="title-lg" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("inventory")}
|
||||
</Typography>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("amount")}
|
||||
</Typography>
|
||||
<Divider className="my-3" />
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="title-md"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
<form.Field name="amount">
|
||||
{(field) => (
|
||||
<AmountStepper
|
||||
value={field.state.value}
|
||||
onChange={field.handleChange}
|
||||
onBlur={field.handleBlur}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("price")}
|
||||
</Typography>
|
||||
<form.Field name="price">
|
||||
{(field) => (
|
||||
<Input
|
||||
type="text"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
endDecorator={
|
||||
<Typography level="body-sm" sx={{ color: "var(--joy-palette-text-tertiary)" }}>
|
||||
{Cookies.get("currency")}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("storage-place")}
|
||||
</Typography>
|
||||
<form.Field name="storage_location_uuid">
|
||||
{(field) => (
|
||||
<Select
|
||||
value={field.state.value}
|
||||
onChange={(_event, value) => field.handleChange(value ?? "")}
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
>
|
||||
{t("amount")}
|
||||
</Typography>
|
||||
<form.Field name="amount">
|
||||
{(field) => (
|
||||
<Input
|
||||
type="number"
|
||||
color="neutral"
|
||||
id="amountInput"
|
||||
placeholder={t("amount")}
|
||||
value={field.state.value}
|
||||
variant="soft"
|
||||
size="lg"
|
||||
onChange={(e) => {
|
||||
const nextValue = Number(e.target.value);
|
||||
field.handleChange(
|
||||
Number.isNaN(nextValue) ? 0 : nextValue,
|
||||
);
|
||||
}}
|
||||
onBlur={field.handleBlur}
|
||||
className="rounded-2xl"
|
||||
sx={{
|
||||
bgcolor: "var(--joy-palette-background-surface)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="title-md"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
{t("price")}
|
||||
</Typography>
|
||||
<form.Field name="price">
|
||||
{(field) => (
|
||||
<Input
|
||||
type="text"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
sx={{
|
||||
bgcolor: "var(--joy-palette-background-surface)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
<Typography
|
||||
level="body-sm"
|
||||
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||
>
|
||||
{Cookies.get("currency")}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Typography
|
||||
level="title-md"
|
||||
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||
>
|
||||
{t("storage-place")}
|
||||
</Typography>
|
||||
<form.Field name="storage_location_uuid">
|
||||
{(field) => (
|
||||
<Select
|
||||
value={field.state.value}
|
||||
onChange={(_event, value) =>
|
||||
field.handleChange(value ?? "")
|
||||
}
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
className="rounded-2xl"
|
||||
sx={{
|
||||
bgcolor: "var(--joy-palette-background-surface)",
|
||||
}}
|
||||
>
|
||||
{storages?.map((storage) => (
|
||||
<Option key={storage.uuid} value={storage.uuid}>
|
||||
{storage.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
{storages?.map((storage) => (
|
||||
<Option key={storage.uuid} value={storage.uuid}>
|
||||
{storage.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-4 flex items-center gap-3 rounded-2xl p-4"
|
||||
style={{ backgroundColor: "var(--joy-palette-background-level1)" }}
|
||||
>
|
||||
<QrCode size={20} color="var(--joy-palette-text-tertiary)" className="shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<Typography level="title-md" sx={{ color: "var(--joy-palette-text-primary)" }}>
|
||||
{t("jar-label")}
|
||||
</Typography>
|
||||
<Typography level="body-sm" className="normal-case font-normal" sx={{ color: "var(--joy-palette-text-tertiary)" }}>
|
||||
{t("jar-label-hint")}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Typography
|
||||
level="body-sm"
|
||||
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||
>
|
||||
{t("product-details")}
|
||||
</Typography>
|
||||
<div className="grow"></div>
|
||||
<Button
|
||||
startDecorator={<QrCodeIcon />}
|
||||
loading={isPending}
|
||||
onClick={() => downloadQRcode()}
|
||||
size="lg"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
className="rounded-2xl transition hover:-translate-y-0.5"
|
||||
sx={{
|
||||
boxShadow:
|
||||
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
|
||||
}}
|
||||
>
|
||||
{t("download-qr-code")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isPending}
|
||||
size="lg"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
className="rounded-2xl transition hover:-translate-y-0.5"
|
||||
sx={{
|
||||
boxShadow:
|
||||
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
|
||||
}}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{alert.isAlert && (
|
||||
<MyAlert
|
||||
type={alert.type}
|
||||
header={alert.header}
|
||||
text={alert.text}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Box>
|
||||
<div className="flex flex-wrap items-center justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
startDecorator={<QrCode size={16} />}
|
||||
onClick={() => downloadQRcode()}
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
color="neutral"
|
||||
className="rounded-2xl"
|
||||
>
|
||||
{t("download-qr-code")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending} size="lg" color="primary" variant="solid" className="btn-lift rounded-2xl">
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{alert.isAlert && (
|
||||
<MyAlert type={alert.type} header={alert.header} text={alert.text} />
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user