feat: add profile route and sidebar navigation; implement inventory and view product pages
- Added a new profile route under the hidden layout. - Introduced a Sidebar component for navigation between inventory, add product, and profile pages. - Created InventoryPage to display a list of products with sorting and pagination. - Implemented ViewProduct page to show details of a selected product. - Integrated API calls for fetching products and product details. - Updated route tree to include new routes and components.
This commit is contained in:
@@ -40,6 +40,32 @@ export const newProduct = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const productDetails = async (uuid) => {
|
||||
const [result] = await pool.query(
|
||||
`SELECT
|
||||
BIN_TO_UUID(p.uuid) AS uuid,
|
||||
p.name,
|
||||
p.description,
|
||||
p.price,
|
||||
p.amount,
|
||||
BIN_TO_UUID(s.uuid) AS storage_location_uuid,
|
||||
s.name AS storage_location_name,
|
||||
p.expiry_date,
|
||||
p.bottling_date,
|
||||
p.picture
|
||||
FROM products p
|
||||
JOIN storage_locations s ON p.storage_location = s.uuid
|
||||
WHERE p.uuid = UUID_TO_BIN(?)`,
|
||||
[uuid],
|
||||
);
|
||||
|
||||
if (result.length > 0) {
|
||||
return { code: "sp003", data: result[0] };
|
||||
} else {
|
||||
return { code: "ep003" };
|
||||
}
|
||||
};
|
||||
|
||||
export const allProducts = async () => {
|
||||
const [result] = await pool.query(`
|
||||
SELECT
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import express from "express";
|
||||
import dotenv from "dotenv";
|
||||
import { authenticate } from "../../services/tokenService.js";
|
||||
import { allProducts, newProduct } from "./database/products.database.js";
|
||||
import {
|
||||
allProducts,
|
||||
newProduct,
|
||||
productDetails,
|
||||
} from "./database/products.database.js";
|
||||
dotenv.config();
|
||||
const router = express.Router();
|
||||
|
||||
@@ -67,4 +71,28 @@ router.get("/all-products", authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/view", authenticate, async (req, res) => {
|
||||
const uuid = req.query.uuid;
|
||||
|
||||
const result = await productDetails(uuid);
|
||||
|
||||
if (result.code === "ep003") {
|
||||
res.status(406).json({
|
||||
success: false,
|
||||
code: "ep003",
|
||||
data: null,
|
||||
message: "Error while fetching product",
|
||||
});
|
||||
}
|
||||
|
||||
if (result.code === "sp003") {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: "sp003",
|
||||
data: result.data,
|
||||
message: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Generated
+279
-1
@@ -11,6 +11,7 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mui/icons-material": "^9.0.1",
|
||||
"@mui/joy": "^5.0.0-beta.52",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@tanstack/react-form": "^1.32.0",
|
||||
@@ -845,6 +846,32 @@
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/icons-material": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.0.1.tgz",
|
||||
"integrity": "sha512-5PRpQjVLTNLyV/2J9J53Yz4R0tVbodG0BQDN2zQI1QBG1OPYM25ar+4N20eyFOfJT6zKglLzsnU70+zdVLaTkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mui/material": "^9.0.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/joy": {
|
||||
"version": "5.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.52.tgz",
|
||||
@@ -886,6 +913,220 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.1.tgz",
|
||||
"integrity": "sha512-voyCpeUxcSWLN7KPZuq0pGCIt726T9K6kiVM3XUcywZDAlZSarLHaUxJVQpospbjjOzN53hwyjo8s6KoWl6utw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@mui/core-downloads-tracker": "^9.0.1",
|
||||
"@mui/system": "^9.0.1",
|
||||
"@mui/types": "^9.0.0",
|
||||
"@mui/utils": "^9.0.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.2.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.2.4",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^9.0.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
},
|
||||
"@mui/material-pigment-css": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.1.tgz",
|
||||
"integrity": "sha512-GzamIIhZ1bH77dq7eKaeyRgJdkypsxin4jBFq2EMs4lBWRR0LFO1CSVMsoebn/VvjcNrnrOrjy48MkrkQUK2iw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/private-theming": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.1.tgz",
|
||||
"integrity": "sha512-pSIGq4Yw749KHEwlkYZWVERgHgwJELP6ODtBNUfV8V4oIb5H+h7IQDFXuk/b2oQccODK1enJAtiEzlgLZmq+8g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@mui/utils": "^9.0.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/styled-engine": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz",
|
||||
"integrity": "sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"@emotion/sheet": "^1.4.0",
|
||||
"csstype": "^3.2.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/system": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-9.0.1.tgz",
|
||||
"integrity": "sha512-WvlioaLxk6ewUIOfh0StxUvOPDS1mCfzaulcudsL1brZNXuh0N9FMk7RpH7ImJKjEz412SEy/V/yvqmtxbqxCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@mui/private-theming": "^9.0.1",
|
||||
"@mui/styled-engine": "^9.0.0",
|
||||
"@mui/types": "^9.0.0",
|
||||
"@mui/utils": "^9.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.2.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/types": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz",
|
||||
"integrity": "sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/utils": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.1.tgz",
|
||||
"integrity": "sha512-f3UO3jNN1pYg5zxqXC81Bvv8hx5ACcYc0387382ZI7M5ono1heIwHYLrKsz85myguWdeVKPRZGmDdynWUBjK2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@mui/types": "^9.0.0",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz",
|
||||
@@ -2050,7 +2291,6 @@
|
||||
"version": "19.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
|
||||
"integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2066,6 +2306,16 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz",
|
||||
@@ -2650,6 +2900,17 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.361",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
|
||||
@@ -4006,6 +4267,23 @@
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mui/icons-material": "^9.0.1",
|
||||
"@mui/joy": "^5.0.0-beta.52",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@tanstack/react-form": "^1.32.0",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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 AccountBoxIcon from "@mui/icons-material/AccountBox";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Sidebar = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const currentURL = window.location.href;
|
||||
const [currentPage, setCurrentPage] = useState(
|
||||
currentURL.substring(currentURL.lastIndexOf("/") + 1),
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-h-screen w-full max-w-70 flex-col gap-8 border-r border-white/80 bg-linear-to-b from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] px-6 py-8 shadow-[0_20px_60px_rgba(11,107,203,0.08)]">
|
||||
<div className="space-y-2">
|
||||
<Typography
|
||||
level="h2"
|
||||
className="text-[22px] font-semibold text-[#0b6bcb]"
|
||||
>
|
||||
{t("app-title")}
|
||||
</Typography>
|
||||
<Typography
|
||||
level="body-lg"
|
||||
className="text-sm font-medium text-slate-500"
|
||||
>
|
||||
{t("app-subtitle")}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate({ to: "/app/inventory" });
|
||||
setCurrentPage("inventory");
|
||||
}}
|
||||
variant={currentPage === "inventory" ? "soft" : "plain"}
|
||||
startDecorator={<InventoryIcon />}
|
||||
className="h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold text-slate-700 transition hover:bg-white/80 hover:text-[#0b6bcb] [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!"
|
||||
>
|
||||
{t("inventory")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate({ to: "/app/add-product" });
|
||||
setCurrentPage("add-product");
|
||||
}}
|
||||
variant={currentPage === "add-product" ? "soft" : "plain"}
|
||||
startDecorator={<AddBoxIcon />}
|
||||
className="h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold text-slate-700 transition hover:bg-white/80 hover:text-[#0b6bcb] [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!"
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate({ to: "/app/profile" });
|
||||
setCurrentPage("profile");
|
||||
}}
|
||||
variant={currentPage === "profile" ? "soft" : "plain"}
|
||||
startDecorator={<AccountBoxIcon />}
|
||||
className="h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold text-slate-700 transition hover:bg-white/80 hover:text-[#0b6bcb] [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!"
|
||||
>
|
||||
{t("profile")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/70 bg-white/80 px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em] text-[#0b6bcb] shadow-[0_12px_30px_rgba(12,38,78,0.12)]">
|
||||
Stockhome
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,572 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Sheet,
|
||||
Table,
|
||||
Chip,
|
||||
Avatar,
|
||||
Box,
|
||||
IconButton,
|
||||
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 { useQuery } from "@tanstack/react-query";
|
||||
import { getProducts } from "../utils/uxFncs";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
|
||||
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 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;
|
||||
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<unknown>,
|
||||
property: keyof ProductRow,
|
||||
) => void;
|
||||
onSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => 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<unknown>) => {
|
||||
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("refill-date"), numeric: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr className="bg-slate-50 text-slate-600">
|
||||
<th className="px-4 py-4">
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
onChange={onSelectAllClick}
|
||||
slotProps={{ input: { "aria-label": "select all products" } }}
|
||||
sx={{ verticalAlign: "sub" }}
|
||||
/>
|
||||
</th>
|
||||
{headCells.map((headCell) => {
|
||||
const active = orderBy === headCell.id;
|
||||
return (
|
||||
<th
|
||||
key={headCell.id}
|
||||
className="px-6 py-4"
|
||||
aria-sort={
|
||||
active
|
||||
? ({ asc: "ascending", desc: "descending" } as const)[order]
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Link
|
||||
underline="none"
|
||||
color="neutral"
|
||||
textColor={active ? "primary.plainColor" : undefined}
|
||||
component="button"
|
||||
onClick={createSortHandler(headCell.id)}
|
||||
startDecorator={
|
||||
headCell.numeric ? (
|
||||
<ArrowDownwardIcon
|
||||
sx={[active ? { opacity: 1 } : { opacity: 0 }]}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
endDecorator={
|
||||
!headCell.numeric ? (
|
||||
<ArrowDownwardIcon
|
||||
sx={[active ? { opacity: 1 } : { opacity: 0 }]}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
sx={{
|
||||
fontWeight: "lg",
|
||||
"& svg": {
|
||||
transition: "0.2s",
|
||||
transform:
|
||||
active && order === "desc"
|
||||
? "rotate(0deg)"
|
||||
: "rotate(180deg)",
|
||||
},
|
||||
"&:hover": { "& svg": { opacity: 1 } },
|
||||
}}
|
||||
>
|
||||
{headCell.label}
|
||||
{active ? (
|
||||
<Box component="span" sx={visuallyHidden}>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</Box>
|
||||
) : null}
|
||||
</Link>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th className="px-6 py-4 text-right">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
|
||||
const EnhancedTableToolbar = ({ numSelected }: { numSelected: number }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={[
|
||||
{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
pl: { sm: 2 },
|
||||
pr: { xs: 1, sm: 1 },
|
||||
borderTopLeftRadius: "var(--unstable_actionRadius)",
|
||||
borderTopRightRadius: "var(--unstable_actionRadius)",
|
||||
},
|
||||
numSelected > 0 && {
|
||||
bgcolor: "background.level1",
|
||||
},
|
||||
]}
|
||||
className="text-slate-700"
|
||||
>
|
||||
{numSelected > 0 ? (
|
||||
<Typography sx={{ flex: "1 1 100%" }} component="div">
|
||||
{numSelected} ausgewahlt
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography level="body-lg" sx={{ flex: "1 1 100%" }} component="div">
|
||||
{t("inventory")}
|
||||
</Typography>
|
||||
)}
|
||||
{numSelected > 0 ? (
|
||||
<Tooltip title="Delete">
|
||||
<IconButton size="sm" color="danger" variant="solid">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Filter list">
|
||||
<IconButton size="sm" variant="outlined" color="neutral">
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
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 { data: productsData, isLoading: productsIsLoading } = useQuery({
|
||||
queryKey: ["products"],
|
||||
queryFn: getProducts,
|
||||
});
|
||||
|
||||
const rows: ProductRow[] = (productsData ?? []).map(
|
||||
(product: any, index: number) => ({
|
||||
id: String(product?.uuid ?? index),
|
||||
uuid: String(product?.uuid ?? ""),
|
||||
name: product?.name ?? "Produktname",
|
||||
description: product?.description ?? "",
|
||||
imageUrl: product?.picture ?? undefined,
|
||||
price: product?.price ?? "-",
|
||||
stock: `${product?.amount ?? 0} Stk.`,
|
||||
stockLabel: product?.amount === 0 ? "FEHLT" : "OK",
|
||||
stockStatus: product?.amount === 0 ? "missing" : "ok",
|
||||
location: product?.storage_location_name ?? "-",
|
||||
locationDetail: "",
|
||||
expiryDate: formatDate(product?.expiry_date),
|
||||
refillDate: formatDate(product?.bottling_date),
|
||||
}),
|
||||
);
|
||||
|
||||
const [order, setOrder] = React.useState<Order>("asc");
|
||||
const [orderBy, setOrderBy] = React.useState<keyof ProductRow>("name");
|
||||
const [selected, setSelected] = React.useState<readonly string[]>([]);
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(5);
|
||||
|
||||
const handleRequestSort = (
|
||||
event: React.MouseEvent<unknown>,
|
||||
property: keyof ProductRow,
|
||||
) => {
|
||||
const isAsc = orderBy === property && order === "asc";
|
||||
setOrder(isAsc ? "desc" : "asc");
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
const newSelected = rows.map((row) => row.id);
|
||||
setSelected(newSelected);
|
||||
return;
|
||||
}
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
const handleClick = (event: React.MouseEvent<unknown>, id: string) => {
|
||||
const selectedIndex = selected.indexOf(id);
|
||||
let newSelected: readonly string[] = [];
|
||||
if (selectedIndex === -1) {
|
||||
newSelected = newSelected.concat(selected, id);
|
||||
} else if (selectedIndex === 0) {
|
||||
newSelected = newSelected.concat(selected.slice(1));
|
||||
} else if (selectedIndex === selected.length - 1) {
|
||||
newSelected = newSelected.concat(selected.slice(0, -1));
|
||||
} else if (selectedIndex > 0) {
|
||||
newSelected = newSelected.concat(
|
||||
selected.slice(0, selectedIndex),
|
||||
selected.slice(selectedIndex + 1),
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<>
|
||||
<Typography level="h2">{t("inventory-subtitle")}</Typography>
|
||||
<Typography level="body-lg">{t("inventory-header")}</Typography>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Button
|
||||
startDecorator={<AddIcon />}
|
||||
onClick={() => navigate({ to: "/app/add-product" })}
|
||||
variant="solid"
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
{productsIsLoading && <CircularProgress size="sm" />}
|
||||
</div>
|
||||
|
||||
<Sheet
|
||||
variant="outlined"
|
||||
className="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm"
|
||||
>
|
||||
<EnhancedTableToolbar numSelected={selected.length} />
|
||||
<Table
|
||||
aria-labelledby="tableTitle"
|
||||
hoverRow
|
||||
className="min-w-[960px] text-slate-700"
|
||||
sx={{
|
||||
"--TableCell-headBackground": "transparent",
|
||||
"--TableCell-selectedBackground": (theme) =>
|
||||
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" },
|
||||
}}
|
||||
>
|
||||
<EnhancedTableHead
|
||||
numSelected={selected.length}
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onSelectAllClick={handleSelectAllClick}
|
||||
onRequestSort={handleRequestSort}
|
||||
rowCount={rows.length}
|
||||
/>
|
||||
<tbody>
|
||||
{[...rows]
|
||||
.sort(getComparator(order, orderBy))
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map((row, index) => {
|
||||
const isItemSelected = selected.includes(row.id);
|
||||
const labelId = `enhanced-table-checkbox-${index}`;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
onClick={(event) => 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)
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<th scope="row" className="px-4 py-5">
|
||||
<Checkbox
|
||||
checked={isItemSelected}
|
||||
slotProps={{ input: { "aria-labelledby": labelId } }}
|
||||
sx={{ verticalAlign: "top" }}
|
||||
/>
|
||||
</th>
|
||||
<th id={labelId} scope="row" className="px-6 py-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar size="lg" variant="soft" src={row.imageUrl} />
|
||||
<div>
|
||||
<Typography
|
||||
level="title-md"
|
||||
className="text-slate-900"
|
||||
>
|
||||
{row.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
level="body-sm"
|
||||
className="text-slate-500"
|
||||
>
|
||||
{row.description}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td className="px-6 py-5">
|
||||
<Typography level="title-md">{row.price}</Typography>
|
||||
<Typography level="body-sm" className="text-slate-400">
|
||||
{t("currency")}
|
||||
</Typography>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<Chip
|
||||
variant="soft"
|
||||
color={
|
||||
row.stockStatus === "low" ? "warning" : "success"
|
||||
}
|
||||
size="lg"
|
||||
className="px-3"
|
||||
>
|
||||
{row.stock}
|
||||
</Chip>
|
||||
<Chip
|
||||
variant="soft"
|
||||
color={
|
||||
row.stockStatus === "missing" ? "danger" : "success"
|
||||
}
|
||||
size="md"
|
||||
className="ml-2 mt-2"
|
||||
>
|
||||
{row.stockLabel}
|
||||
</Chip>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<Typography level="title-md">{row.location}</Typography>
|
||||
<Typography level="body-sm" className="text-slate-500">
|
||||
{row.locationDetail}
|
||||
</Typography>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<Typography level="title-md">{row.expiryDate}</Typography>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<Typography level="title-md">{row.refillDate}</Typography>
|
||||
</td>
|
||||
<td className="px-6 py-5 text-right">
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: `/app/view-product?product=${row.uuid}`,
|
||||
})
|
||||
}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
>
|
||||
{t("details")}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<tr
|
||||
style={
|
||||
{
|
||||
height: `calc(${emptyRows} * 40px)`,
|
||||
"--TableRow-hoverBackground": "transparent",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<td colSpan={8} aria-hidden />
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={8}>
|
||||
<Box
|
||||
className="px-6 py-4 text-slate-600"
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<FormControl orientation="horizontal" size="sm">
|
||||
<FormLabel>{t("rows-per-page")}</FormLabel>
|
||||
<Select
|
||||
onChange={handleChangeRowsPerPage}
|
||||
value={rowsPerPage}
|
||||
>
|
||||
<Option value={5}>5</Option>
|
||||
<Option value={10}>10</Option>
|
||||
<Option value={25}>25</Option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography sx={{ textAlign: "center", minWidth: 120 }}>
|
||||
{labelDisplayedRows({
|
||||
from: rows.length === 0 ? 0 : page * rowsPerPage + 1,
|
||||
to: getLabelDisplayedRowsTo(),
|
||||
count: rows.length === -1 ? -1 : rows.length,
|
||||
})}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
color="neutral"
|
||||
variant="outlined"
|
||||
disabled={page === 0}
|
||||
onClick={() => handleChangePage(page - 1)}
|
||||
sx={{ bgcolor: "background.surface" }}
|
||||
>
|
||||
<KeyboardArrowLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="sm"
|
||||
color="neutral"
|
||||
variant="outlined"
|
||||
disabled={
|
||||
rows.length !== -1
|
||||
? page >= Math.ceil(rows.length / rowsPerPage) - 1
|
||||
: false
|
||||
}
|
||||
onClick={() => handleChangePage(page + 1)}
|
||||
sx={{ bgcolor: "background.surface" }}
|
||||
>
|
||||
<KeyboardArrowRightIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getProductDetails } from "../utils/uxFncs";
|
||||
import { CircularProgress, Typography } from "@mui/joy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ViewProductProps {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export const ViewProduct = (props: ViewProductProps) => {
|
||||
const uuid = props.uuid;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: productDetails, isLoading: productDetailsLoading } = useQuery({
|
||||
queryKey: ["product", uuid],
|
||||
queryFn: () => getProductDetails(uuid),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>{t("product-details")}</Typography>
|
||||
{productDetailsLoading && <CircularProgress size="sm" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 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'
|
||||
|
||||
@@ -37,6 +38,11 @@ const AppHiddenLayoutViewProductRoute =
|
||||
path: '/view-product',
|
||||
getParentRoute: () => AppHiddenLayoutRoute,
|
||||
} as any)
|
||||
const AppHiddenLayoutProfileRoute = AppHiddenLayoutProfileRouteImport.update({
|
||||
id: '/profile',
|
||||
path: '/profile',
|
||||
getParentRoute: () => AppHiddenLayoutRoute,
|
||||
} as any)
|
||||
const AppHiddenLayoutInventoryRoute =
|
||||
AppHiddenLayoutInventoryRouteImport.update({
|
||||
id: '/inventory',
|
||||
@@ -56,6 +62,7 @@ export interface FileRoutesByFullPath {
|
||||
'/app': typeof AppHiddenLayoutRouteWithChildren
|
||||
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
|
||||
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
|
||||
'/app/profile': typeof AppHiddenLayoutProfileRoute
|
||||
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
@@ -64,6 +71,7 @@ export interface FileRoutesByTo {
|
||||
'/app': typeof AppHiddenLayoutRouteWithChildren
|
||||
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
|
||||
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
|
||||
'/app/profile': typeof AppHiddenLayoutProfileRoute
|
||||
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
@@ -73,6 +81,7 @@ export interface FileRoutesById {
|
||||
'/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren
|
||||
'/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute
|
||||
'/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute
|
||||
'/app/_hiddenLayout/profile': typeof AppHiddenLayoutProfileRoute
|
||||
'/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
@@ -83,6 +92,7 @@ export interface FileRouteTypes {
|
||||
| '/app'
|
||||
| '/app/add-product'
|
||||
| '/app/inventory'
|
||||
| '/app/profile'
|
||||
| '/app/view-product'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
@@ -91,6 +101,7 @@ export interface FileRouteTypes {
|
||||
| '/app'
|
||||
| '/app/add-product'
|
||||
| '/app/inventory'
|
||||
| '/app/profile'
|
||||
| '/app/view-product'
|
||||
id:
|
||||
| '__root__'
|
||||
@@ -99,6 +110,7 @@ export interface FileRouteTypes {
|
||||
| '/app/_hiddenLayout'
|
||||
| '/app/_hiddenLayout/add-product'
|
||||
| '/app/_hiddenLayout/inventory'
|
||||
| '/app/_hiddenLayout/profile'
|
||||
| '/app/_hiddenLayout/view-product'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
@@ -138,6 +150,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppHiddenLayoutViewProductRouteImport
|
||||
parentRoute: typeof AppHiddenLayoutRoute
|
||||
}
|
||||
'/app/_hiddenLayout/profile': {
|
||||
id: '/app/_hiddenLayout/profile'
|
||||
path: '/profile'
|
||||
fullPath: '/app/profile'
|
||||
preLoaderRoute: typeof AppHiddenLayoutProfileRouteImport
|
||||
parentRoute: typeof AppHiddenLayoutRoute
|
||||
}
|
||||
'/app/_hiddenLayout/inventory': {
|
||||
id: '/app/_hiddenLayout/inventory'
|
||||
path: '/inventory'
|
||||
@@ -158,12 +177,14 @@ declare module '@tanstack/react-router' {
|
||||
interface AppHiddenLayoutRouteChildren {
|
||||
AppHiddenLayoutAddProductRoute: typeof AppHiddenLayoutAddProductRoute
|
||||
AppHiddenLayoutInventoryRoute: typeof AppHiddenLayoutInventoryRoute
|
||||
AppHiddenLayoutProfileRoute: typeof AppHiddenLayoutProfileRoute
|
||||
AppHiddenLayoutViewProductRoute: typeof AppHiddenLayoutViewProductRoute
|
||||
}
|
||||
|
||||
const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = {
|
||||
AppHiddenLayoutAddProductRoute: AppHiddenLayoutAddProductRoute,
|
||||
AppHiddenLayoutInventoryRoute: AppHiddenLayoutInventoryRoute,
|
||||
AppHiddenLayoutProfileRoute: AppHiddenLayoutProfileRoute,
|
||||
AppHiddenLayoutViewProductRoute: AppHiddenLayoutViewProductRoute,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// routes/app/_layout.tsx (oder app.tsx als Parent)
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
import { Sidebar } from "../../components/Sidebar";
|
||||
|
||||
export const Route = createFileRoute("/app/_hiddenLayout")({
|
||||
component: AppLayout,
|
||||
@@ -7,9 +7,11 @@ export const Route = createFileRoute("/app/_hiddenLayout")({
|
||||
|
||||
function AppLayout() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Layout</h1>
|
||||
<div className="flex min-h-screen w-full bg-[#f7f9fc]">
|
||||
<Sidebar />
|
||||
<main className="flex-1 px-8 py-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { isAuthenticated } from "../../../utils/auth";
|
||||
import { InventoryPage } from "../../../pages/Inventory";
|
||||
|
||||
export const Route = createFileRoute("/app/_hiddenLayout/inventory")({
|
||||
beforeLoad: async () => {
|
||||
@@ -13,9 +14,5 @@ export const Route = createFileRoute("/app/_hiddenLayout/inventory")({
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<>
|
||||
<p>Inventar</p>
|
||||
</>
|
||||
);
|
||||
return <InventoryPage />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/app/_hiddenLayout/profile')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/app/_hiddenLayout/profile"!</div>
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
import { isAuthenticated } from "../../../utils/auth";
|
||||
import { ViewProduct } from "../../../pages/ViewProduct";
|
||||
|
||||
export const Route = createFileRoute("/app/_hiddenLayout/view-product")({
|
||||
beforeLoad: async () => {
|
||||
@@ -9,9 +11,13 @@ export const Route = createFileRoute("/app/_hiddenLayout/view-product")({
|
||||
});
|
||||
}
|
||||
},
|
||||
validateSearch: z.object({
|
||||
product: z.string(),
|
||||
}),
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/app/view-product"!</div>;
|
||||
const { product } = Route.useSearch();
|
||||
return <ViewProduct uuid={product} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { API_BASE } from "../config/api.config";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
export const getProducts = async () => {
|
||||
const result = await fetch(`${API_BASE}/products/all-products`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const response = await result.json();
|
||||
|
||||
if (response.code === "ep002") {
|
||||
return { success: false, code: response.code };
|
||||
}
|
||||
|
||||
if (response.code === "sp002") {
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const getProductDetails = async (uuid: string) => {
|
||||
const result = await fetch(`${API_BASE}/products/view?uuid=${uuid}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await result.json();
|
||||
|
||||
if (response.code === "ep003") {
|
||||
return { success: false, code: response.code };
|
||||
}
|
||||
|
||||
if (response.code === "sp003") {
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user