From f1a10b07910170bb041401ae1bc04a96b1590236 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sat, 30 May 2026 11:53:54 +0200 Subject: [PATCH] updated inventory table --- frontend/src/misc/interfaces.ts | 16 + frontend/src/pages/Inventory.tsx | 505 ++++++------------------------- 2 files changed, 111 insertions(+), 410 deletions(-) diff --git a/frontend/src/misc/interfaces.ts b/frontend/src/misc/interfaces.ts index 8bd9859..ea82231 100644 --- a/frontend/src/misc/interfaces.ts +++ b/frontend/src/misc/interfaces.ts @@ -45,3 +45,19 @@ export interface SettingsIntf { ["app-name"]: string; currency: string; } + +export type ProductRow = { + id: string; + uuid: string; + name: string; + description: string; + imageUrl?: string; + price: string; + stock: string; + stockLabel: string; + stockStatus: "ok" | "low" | "missing"; + location: string; + locationDetail: string; + expiryDate: string; + refillDate: string; +}; \ No newline at end of file diff --git a/frontend/src/pages/Inventory.tsx b/frontend/src/pages/Inventory.tsx index 0ee16a3..be8c0c0 100644 --- a/frontend/src/pages/Inventory.tsx +++ b/frontend/src/pages/Inventory.tsx @@ -1,271 +1,27 @@ -import * as React from "react"; +import { useState } from "react"; import { Typography, Button, CircularProgress, Sheet, Table, - Chip, Avatar, - Box, - IconButton, + Chip, Checkbox, - FormControl, - FormLabel, - Link, - Tooltip, - Select, - Option, } from "@mui/joy"; import { useNavigate } from "@tanstack/react-router"; import { useTranslation } from "react-i18next"; import AddIcon from "@mui/icons-material/Add"; -import DeleteIcon from "@mui/icons-material/Delete"; -import FilterListIcon from "@mui/icons-material/FilterList"; -import KeyboardArrowLeftIcon from "@mui/icons-material/KeyboardArrowLeft"; -import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; -import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { getProducts, deleteSelectedProducts } from "../utils/api/products"; -import { visuallyHidden } from "@mui/utils"; +import { deleteSelectedProducts, getProducts } from "../utils/api/products"; import { formatDate } from "../utils/uxFncs"; import Cookies from "js-cookie"; - -type Order = "asc" | "desc"; - -type ProductRow = { - id: string; - uuid: string; - name: string; - description: string; - imageUrl?: string; - price: string; - stock: string; - stockLabel: string; - stockStatus: "ok" | "low" | "missing"; - location: string; - locationDetail: string; - expiryDate: string; - refillDate: string; -}; - -const descendingComparator = (a: T, b: T, orderBy: keyof T) => { - const aValue = (a[orderBy] ?? "") as string | number; - const bValue = (b[orderBy] ?? "") as string | number; - if (bValue < aValue) { - return -1; - } - if (bValue > aValue) { - return 1; - } - return 0; -}; - -const getComparator = (order: Order, orderBy: keyof ProductRow) => { - return order === "desc" - ? (a: ProductRow, b: ProductRow) => descendingComparator(a, b, orderBy) - : (a: ProductRow, b: ProductRow) => -descendingComparator(a, b, orderBy); -}; - -type EnhancedTableHeadProps = { - numSelected: number; - onRequestSort: ( - event: React.MouseEvent, - property: keyof ProductRow, - ) => void; - onSelectAllClick: (event: React.ChangeEvent) => void; - order: Order; - orderBy: string; - rowCount: number; -}; - -const EnhancedTableHead = (props: EnhancedTableHeadProps) => { - const { t } = useTranslation(); - - const { - onSelectAllClick, - order, - orderBy, - numSelected, - rowCount, - onRequestSort, - } = props; - const createSortHandler = - (property: keyof ProductRow) => (event: React.MouseEvent) => { - onRequestSort(event, property); - }; - - const headCells: readonly { - id: keyof ProductRow; - label: string; - numeric: boolean; - }[] = [ - { id: "name", label: t("product-name"), numeric: false }, - { id: "price", label: t("price"), numeric: true }, - { id: "stock", label: t("stock"), numeric: true }, - { id: "location", label: t("storage-place"), numeric: false }, - { id: "expiryDate", label: t("expiry-date"), numeric: false }, - { id: "refillDate", label: t("bottling-date"), numeric: false }, - ]; - - return ( - - - - 0 && numSelected < rowCount} - checked={rowCount > 0 && numSelected === rowCount} - onChange={onSelectAllClick} - slotProps={{ input: { "aria-label": "select all products" } }} - sx={{ verticalAlign: "sub" }} - /> - - {headCells.map((headCell) => { - const active = orderBy === headCell.id; - return ( - - - ) : null - } - endDecorator={ - !headCell.numeric ? ( - - ) : null - } - sx={{ - fontWeight: "lg", - "& svg": { - transition: "0.2s", - transform: - active && order === "desc" - ? "rotate(0deg)" - : "rotate(180deg)", - }, - "&:hover": { "& svg": { opacity: 1 } }, - }} - > - {headCell.label} - {active ? ( - - {order === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - ); - })} - {t("actions")} - - - ); -}; - -const EnhancedTableToolbar = ({ - numSelected, - selected, -}: { - numSelected: number; - selected: readonly string[]; -}) => { - const { t } = useTranslation(); - const queryClient = useQueryClient(); - - const { mutate } = useMutation({ - mutationFn: (values: string[]) => deleteSelectedProducts(values), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["products"] }); - }, - }); - - return ( - 0 && { - bgcolor: "background.level1", - }, - ]} - className="text-slate-700" - > - {numSelected > 0 ? ( - - {numSelected} {t("selected")} - - ) : ( - - {t("inventory")} - - )} - {numSelected > 0 ? ( - - mutate([...selected])} - size="sm" - color="danger" - variant="solid" - > - - - - ) : ( - - - - - - )} - - ); -}; +import type { ProductRow } from "../misc/interfaces"; export const InventoryPage = () => { const { t } = useTranslation(); const navigate = useNavigate(); - - const labelDisplayedRows = ({ - from, - to, - count, - }: { - from: number; - to: number; - count: number; - }) => `${from}-${to} ${t("of")} ${count}`; + const queryClient = useQueryClient(); const { data: productsData, isLoading: productsIsLoading } = useQuery({ queryKey: ["products"], @@ -279,7 +35,7 @@ export const InventoryPage = () => { name: product?.name ?? t("product-name"), description: product?.description ?? "", imageUrl: product?.picture ?? undefined, - price: product?.price ?? "-", + price: product?.price ? String(product.price) : "-", stock: `${product?.amount ?? 0} ${t("pcs")}`, location: product?.storage_location_name ?? "-", locationDetail: "", @@ -288,25 +44,19 @@ export const InventoryPage = () => { }), ); - const [order, setOrder] = React.useState("asc"); - const [orderBy, setOrderBy] = React.useState("name"); - const [selected, setSelected] = React.useState([]); - const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(5); + const [selected, setSelected] = useState([]); - const handleRequestSort = ( - _event: React.MouseEvent, - property: keyof ProductRow, - ) => { - const isAsc = orderBy === property && order === "asc"; - setOrder(isAsc ? "desc" : "asc"); - setOrderBy(property); - }; + const { mutate: deleteSelection, isPending: isDeleting } = useMutation({ + mutationFn: (values: string[]) => deleteSelectedProducts(values), + onSuccess: () => { + setSelected([]); + queryClient.invalidateQueries({ queryKey: ["products"] }); + }, + }); const handleSelectAllClick = (event: React.ChangeEvent) => { if (event.target.checked) { - const newSelected = rows.map((row) => row.id); - setSelected(newSelected); + setSelected(rows.map((row) => row.id)); return; } setSelected([]); @@ -330,27 +80,6 @@ export const InventoryPage = () => { setSelected(newSelected); }; - const handleChangePage = (newPage: number) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (_event: any, newValue: number | null) => { - setRowsPerPage(parseInt(newValue!.toString(), 10)); - setPage(0); - }; - - const getLabelDisplayedRowsTo = () => { - if (rows.length === -1) { - return (page + 1) * rowsPerPage; - } - return rowsPerPage === -1 - ? rows.length - : Math.min(rows.length, (page + 1) * rowsPerPage); - }; - - const emptyRows = - page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0; - return ( <> {t("inventory")} @@ -368,63 +97,90 @@ export const InventoryPage = () => { - - - theme.vars.palette.success.softBg, - "& thead th:nth-child(1)": { - width: "40px", - }, - "& thead th:nth-child(2)": { - width: "30%", - }, - "& tr > *:nth-child(n+3)": { textAlign: "left" }, - }} - > - - - {[...rows] - .sort(getComparator(order, orderBy)) - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row, index) => { +
+ + {t("inventory")} + + +
+
+
*:nth-child(n+4)": { textAlign: "left" }, + }} + > + + + + + + + + + + + + + + {rows.map((row, index) => { const isItemSelected = selected.includes(row.id); - const labelId = `enhanced-table-checkbox-${index}`; - + const labelId = `inventory-checkbox-${index}`; return ( handleClick(event, row.id)} role="checkbox" aria-checked={isItemSelected} tabIndex={-1} - className="border-t border-slate-200" - style={ - isItemSelected - ? ({ - "--TableCell-dataBackground": - "var(--TableCell-selectedBackground)", - "--TableCell-headBackground": - "var(--TableCell-selectedBackground)", - } as React.CSSProperties) - : {} - } > ); })} - {emptyRows > 0 && ( - - - )} - - - - - - -
+ 0 && selected.length === rows.length} + indeterminate={ + selected.length > 0 && selected.length < rows.length + } + onChange={handleSelectAllClick} + slotProps={{ input: { "aria-label": "select all" } }} + sx={{ verticalAlign: "sub" }} + /> + {t("product-name")}{t("price")}{t("stock")}{t("storage-place")}{t("expiry-date")}{t("bottling-date")}{t("actions")}
{
-
+
{
@@ -496,80 +252,9 @@ export const InventoryPage = () => {
-
- - - {t("rows-per-page")} - - - - {labelDisplayedRows({ - from: rows.length === 0 ? 0 : page * rowsPerPage + 1, - to: getLabelDisplayedRowsTo(), - count: rows.length === -1 ? -1 : rows.length, - })} - - - handleChangePage(page - 1)} - sx={{ bgcolor: "background.surface" }} - > - - - = Math.ceil(rows.length / rowsPerPage) - 1 - : false - } - onClick={() => handleChangePage(page + 1)} - sx={{ bgcolor: "background.surface" }} - > - - - - -
+ + +
);