add storage management features including update functionality and storage listing page

This commit is contained in:
2026-05-27 22:20:12 +02:00
parent 96488a3137
commit b0731b22db
10 changed files with 252 additions and 13 deletions
@@ -35,3 +35,16 @@ export const newStorage = async (name, description) => {
return { code: "es002" };
}
};
export const updateStorage = async (uuid, values) => {
const [result] = await pool.query(
"UPDATE storage_locations SET name = ?, description = ? WHERE uuid = UUID_TO_BIN(?);",
[values.name, values.description, uuid],
);
if (result.affectedRows > 0) {
return { code: "ss003" };
} else {
return { code: "es003" };
}
};
+30 -1
View File
@@ -1,7 +1,11 @@
import express from "express";
import dotenv from "dotenv";
import { authenticate } from "../../services/tokenService.js";
import { allStorages, newStorage } from "./database/storage.database.js";
import {
allStorages,
newStorage,
updateStorage,
} from "./database/storage.database.js";
dotenv.config();
const router = express.Router();
@@ -51,4 +55,29 @@ router.post("/new-storage", authenticate, async (req, res) => {
}
});
router.post("/update-storage", authenticate, async (req, res) => {
const storageUUID = req.query.storageUUID;
const values = req.body;
const result = await updateStorage(storageUUID, values);
if (result.code === "es003") {
res.status(500).json({
success: false,
code: "es003",
data: null,
message: "unexpected server error",
});
}
if (result.code === "ss003") {
res.status(201).json({
success: true,
code: "ss003",
data: null,
message: "",
});
}
});
export default router;
+9
View File
@@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
import { Button, Typography } from "@mui/joy";
import InventoryIcon from "@mui/icons-material/Inventory";
import AddBoxIcon from "@mui/icons-material/AddBox";
import StorageIcon from "@mui/icons-material/Storage";
import AccountBoxIcon from "@mui/icons-material/AccountBox";
import { useNavigate, useMatchRoute } from "@tanstack/react-router";
@@ -50,6 +51,14 @@ export const Sidebar = () => {
>
{t("add")}
</Button>
<Button
onClick={() => navigate({ to: "/app/storages" })}
variant={variant("/app/storages")}
startDecorator={<StorageIcon />}
className={btnClass}
>
{t("storages")}
</Button>
<Button
onClick={() => navigate({ to: "/app/profile" })}
variant={variant("/app/profile")}
+77
View File
@@ -0,0 +1,77 @@
import { useQueryClient, useMutation } from "@tanstack/react-query";
import { updateStorage } from "../utils/uxFncs";
import { useForm } from "@tanstack/react-form";
import { useStore } from "@tanstack/react-store";
import { Input, Button } from "@mui/joy";
import type { Storage } from "../misc/interfaces";
import { formatDate } from "../utils/uxFncs";
interface StorageRowProps {
storage: Storage;
}
export const StorageRow = ({ storage }: StorageRowProps) => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (values: Pick<Storage, "name" | "description">) =>
updateStorage(storage.uuid, values),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["storages"] });
},
});
const form = useForm({
defaultValues: {
name: storage.name,
description: storage.description ?? "",
},
onSubmit: async ({ value }) => {
await mutation.mutateAsync(value);
},
});
const values = useStore(form.baseStore, (state) => state.values);
const isDirty =
values.name !== storage.name ||
(values.description ?? "") !== (storage.description ?? "");
return (
<tr key={storage.uuid}>
<td>
<form.Field name="name">
{(field) => (
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
size="sm"
/>
)}
</form.Field>
</td>
<td>
<form.Field name="description">
{(field) => (
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
size="sm"
/>
)}
</form.Field>
</td>
<td>{formatDate(storage.created_at)}</td>
<td>{formatDate(storage.updated_at)}</td>
<td>
<Button
onClick={form.handleSubmit}
disabled={!isDirty || mutation.isPending}
>
{mutation.isPending ? "..." : "Save"}
</Button>
</td>
</tr>
);
};
+8
View File
@@ -20,3 +20,11 @@ export type ProductFormValues = {
price: string;
storage_location_uuid: string;
};
export interface Storage {
name: string;
description: string | null;
created_at: string;
updated_at: string;
uuid: string;
}
+1 -11
View File
@@ -28,6 +28,7 @@ import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { useQuery } from "@tanstack/react-query";
import { getProducts } from "../utils/uxFncs";
import { visuallyHidden } from "@mui/utils";
import { formatDate } from "../utils/uxFncs";
type Order = "asc" | "desc";
@@ -47,17 +48,6 @@ type ProductRow = {
refillDate: string;
};
const formatDate = (value?: string | null) => {
if (!value) {
return "-";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "-";
}
return date.toLocaleDateString("de-DE");
};
const descendingComparator = <T,>(a: T, b: T, orderBy: keyof T) => {
const aValue = (a[orderBy] ?? "") as string | number;
const bValue = (b[orderBy] ?? "") as string | number;
+43
View File
@@ -0,0 +1,43 @@
import { useQuery } from "@tanstack/react-query";
import { getStorages } from "../utils/uxFncs";
import { Sheet, Table, Button } from "@mui/joy";
import { useTranslation } from "react-i18next";
import type { Storage } from "../misc/interfaces";
import { StorageRow } from "../components/StorageRow";
export const Storages = () => {
const { t } = useTranslation();
const { data: storages, isLoading } = useQuery({
queryKey: ["storages"],
queryFn: () => getStorages(),
});
return (
<Sheet>
<Button>+</Button>
<Table
borderAxis="x"
color="neutral"
stickyHeader
stripe="odd"
variant="soft"
>
<thead>
<tr>
<th>{t("name")}</th>
<th>{t("description")}</th>
<th>{t("created-at")}</th>
<th>{t("updated-at")}</th>
<th></th>
</tr>
</thead>
<tbody>
{storages?.map((storage: Storage) => (
<StorageRow key={storage.uuid} storage={storage} />
))}
</tbody>
</Table>
</Sheet>
);
};
+21
View File
@@ -13,6 +13,7 @@ import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout'
import { Route as AppHiddenLayoutViewProductRouteImport } from './routes/app/_hiddenLayout/view-product'
import { Route as AppHiddenLayoutStoragesRouteImport } from './routes/app/_hiddenLayout/storages'
import { Route as AppHiddenLayoutProfileRouteImport } from './routes/app/_hiddenLayout/profile'
import { Route as AppHiddenLayoutInventoryRouteImport } from './routes/app/_hiddenLayout/inventory'
import { Route as AppHiddenLayoutAddProductRouteImport } from './routes/app/_hiddenLayout/add-product'
@@ -38,6 +39,11 @@ const AppHiddenLayoutViewProductRoute =
path: '/view-product',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutStoragesRoute = AppHiddenLayoutStoragesRouteImport.update({
id: '/storages',
path: '/storages',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutProfileRoute = AppHiddenLayoutProfileRouteImport.update({
id: '/profile',
path: '/profile',
@@ -63,6 +69,7 @@ export interface FileRoutesByFullPath {
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/profile': typeof AppHiddenLayoutProfileRoute
'/app/storages': typeof AppHiddenLayoutStoragesRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
}
export interface FileRoutesByTo {
@@ -72,6 +79,7 @@ export interface FileRoutesByTo {
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/profile': typeof AppHiddenLayoutProfileRoute
'/app/storages': typeof AppHiddenLayoutStoragesRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
}
export interface FileRoutesById {
@@ -82,6 +90,7 @@ export interface FileRoutesById {
'/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/_hiddenLayout/profile': typeof AppHiddenLayoutProfileRoute
'/app/_hiddenLayout/storages': typeof AppHiddenLayoutStoragesRoute
'/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute
}
export interface FileRouteTypes {
@@ -93,6 +102,7 @@ export interface FileRouteTypes {
| '/app/add-product'
| '/app/inventory'
| '/app/profile'
| '/app/storages'
| '/app/view-product'
fileRoutesByTo: FileRoutesByTo
to:
@@ -102,6 +112,7 @@ export interface FileRouteTypes {
| '/app/add-product'
| '/app/inventory'
| '/app/profile'
| '/app/storages'
| '/app/view-product'
id:
| '__root__'
@@ -111,6 +122,7 @@ export interface FileRouteTypes {
| '/app/_hiddenLayout/add-product'
| '/app/_hiddenLayout/inventory'
| '/app/_hiddenLayout/profile'
| '/app/_hiddenLayout/storages'
| '/app/_hiddenLayout/view-product'
fileRoutesById: FileRoutesById
}
@@ -150,6 +162,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppHiddenLayoutViewProductRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/storages': {
id: '/app/_hiddenLayout/storages'
path: '/storages'
fullPath: '/app/storages'
preLoaderRoute: typeof AppHiddenLayoutStoragesRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/profile': {
id: '/app/_hiddenLayout/profile'
path: '/profile'
@@ -178,6 +197,7 @@ interface AppHiddenLayoutRouteChildren {
AppHiddenLayoutAddProductRoute: typeof AppHiddenLayoutAddProductRoute
AppHiddenLayoutInventoryRoute: typeof AppHiddenLayoutInventoryRoute
AppHiddenLayoutProfileRoute: typeof AppHiddenLayoutProfileRoute
AppHiddenLayoutStoragesRoute: typeof AppHiddenLayoutStoragesRoute
AppHiddenLayoutViewProductRoute: typeof AppHiddenLayoutViewProductRoute
}
@@ -185,6 +205,7 @@ const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = {
AppHiddenLayoutAddProductRoute: AppHiddenLayoutAddProductRoute,
AppHiddenLayoutInventoryRoute: AppHiddenLayoutInventoryRoute,
AppHiddenLayoutProfileRoute: AppHiddenLayoutProfileRoute,
AppHiddenLayoutStoragesRoute: AppHiddenLayoutStoragesRoute,
AppHiddenLayoutViewProductRoute: AppHiddenLayoutViewProductRoute,
}
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { Storages } from "../../../pages/Storages";
export const Route = createFileRoute("/app/_hiddenLayout/storages")({
component: RouteComponent,
});
function RouteComponent() {
return <Storages />;
}
+40 -1
View File
@@ -1,6 +1,6 @@
import { API_BASE } from "../config/api.config";
import Cookies from "js-cookie";
import type { ProductFormValues } from "../misc/interfaces";
import type { ProductFormValues, Storage } from "../misc/interfaces";
export const getProducts = async () => {
const result = await fetch(`${API_BASE}/products/all-products`, {
@@ -143,3 +143,42 @@ export const createProduct = async (values: ProductFormValues) => {
return response.data;
}
};
export const updateStorage = async (
uuid: string,
values: Pick<Storage, "name" | "description">,
) => {
const result = await fetch(
`${API_BASE}/storage/update-storage?storageUUID=${uuid}`,
{
method: "POST",
body: JSON.stringify(values),
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;
}
};
export const formatDate = (value?: string | null) => {
if (!value) {
return "-";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "-";
}
return date.toLocaleDateString("de-DE");
};