refactor: remove LockerTable component, enhance ItemTable and LoanTable with CRUD functionality, and implement AddItemForm for item creation

This commit is contained in:
2025-09-02 20:30:29 +02:00
parent b217769961
commit 48c16350b7
9 changed files with 472 additions and 24 deletions

View File

@@ -4,7 +4,6 @@ import { Box, Heading, Text, Flex, Button } from "@chakra-ui/react";
import Sidebar from "./Sidebar";
import UserTable from "../components/UserTable";
import ItemTable from "../components/ItemTable";
import LockerTable from "../components/LockerTable";
import LoanTable from "../components/LoanTable";
import { MoveLeft } from "lucide-react";
@@ -67,7 +66,6 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
{activeView === "User" && <UserTable />}
{activeView === "Ausleihen" && <LoanTable />}
{activeView === "Gegenstände" && <ItemTable />}
{activeView === "Schließfächer" && <LockerTable />}
</Box>
</Box>
</Flex>

View File

@@ -59,15 +59,6 @@ const Sidebar: React.FC<SidebarProps> = ({
>
Gegenstände
</Link>
<Link
px={3}
py={2}
rounded="md"
_hover={{ bg: "gray.700", textDecoration: "none" }}
onClick={viewSchliessfaecher}
>
Schließfächer
</Link>
</VStack>
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">

View File

@@ -0,0 +1,84 @@
import React from "react";
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
import { createItem } from "@/utils/userActions";
type AddItemFormProps = {
onClose: () => void;
alert: (
status: "success" | "error",
message: string,
description: string
) => void;
};
const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<Card.Root maxW="sm">
<Card.Header>
<Card.Title>Neuen Gegenstand erstellen</Card.Title>
<Card.Description>
Füllen Sie das folgende Formular aus, um einen Gegenstand zu
erstellen.
</Card.Description>
</Card.Header>
<Card.Body>
<Stack gap="4" w="full">
<Field.Root>
<Field.Label>Gegenstandsname</Field.Label>
<Input id="item_name" placeholder="z.B. Laptop" />
</Field.Root>
<Field.Root>
<Field.Label>Ausleih-Berechtigung (Rolle)</Field.Label>
<Input
id="can_borrow_role"
type="number"
placeholder="Zahl (z.B. 2)"
/>
</Field.Root>
</Stack>
</Card.Body>
<Card.Footer justifyContent="flex-end" gap="2">
<Button variant="outline" onClick={onClose}>
Abbrechen
</Button>
<Button
variant="solid"
onClick={async () => {
const name =
(
document.getElementById("item_name") as HTMLInputElement
)?.value.trim() || "";
const role = Number(
(document.getElementById("can_borrow_role") as HTMLInputElement)
?.value
);
if (!name || Number.isNaN(role)) return;
const res = await createItem(name, role);
if (res.success) {
alert(
"success",
"Gegenstand erstellt",
"Der Gegenstand wurde erfolgreich erstellt."
);
onClose();
} else {
alert(
"error",
"Fehler",
"Der Gegenstand konnte nicht erstellt werden."
);
}
}}
>
Erstellen
</Button>
</Card.Footer>
</Card.Root>
</div>
);
};
export default AddItemForm;

View File

@@ -1,7 +1,265 @@
import React from "react";
import {
Table,
Spinner,
Text,
VStack,
Button,
HStack,
IconButton,
Heading,
Icon,
Tag,
} from "@chakra-ui/react";
import { Tooltip } from "@/components/ui/tooltip";
import MyAlert from "./myChakra/MyAlert";
import {
Trash2,
RefreshCcwDot,
CirclePlus,
CheckCircle2,
XCircle,
} from "lucide-react";
import Cookies from "js-cookie";
import { useState, useEffect } from "react";
import { deleteItem } from "@/utils/userActions";
import AddItemForm from "./AddItemForm";
type Items = {
id: number;
item_name: string;
can_borrow_role: string;
inSafe: boolean;
entry_created_at: string;
};
const ItemTable: React.FC = () => {
return <>Item Table</>;
const [items, setItems] = useState<Items[]>([]);
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
const [errorMessage, setErrorMessage] = useState("");
const [errorDsc, setErrorDsc] = useState("");
const [isError, setIsError] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [reload, setReload] = useState(false);
const [addForm, setAddForm] = useState(false);
const setError = (
status: "error" | "success",
message: string,
description: string
) => {
setIsError(false);
setErrorStatus(status);
setErrorMessage(message);
setErrorDsc(description);
setIsError(true);
};
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch("http://localhost:8002/api/allItems", {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
const data = await response.json();
return data;
} catch (error) {
setError("error", "Failed to fetch items", "There is an error");
} finally {
setIsLoading(false);
}
};
fetchData().then((data) => {
if (Array.isArray(data)) {
setItems(data);
}
});
}, [reload]);
return (
<>
{/* Action toolbar */}
<HStack
mb={4}
gap={3}
justify="flex-start"
align="center"
flexWrap="wrap"
>
<Tooltip content="Gegenstände neu laden" openDelay={300}>
<IconButton
aria-label="Refresh items"
size="sm"
variant="outline"
rounded="md"
shadow="sm"
_hover={{ shadow: "md", transform: "translateY(-2px)" }}
_active={{ transform: "translateY(0)" }}
onClick={() => setReload(!reload)}
>
<RefreshCcwDot size={18} />
</IconButton>
</Tooltip>
<Tooltip content="Neuen Gegenstand hinzufügen" openDelay={300}>
<Button
size="sm"
colorPalette="teal"
variant="solid"
rounded="md"
fontWeight="semibold"
shadow="sm"
_hover={{ shadow: "md", bg: "colorPalette.600" }}
_active={{ bg: "colorPalette.700" }}
onClick={() => {
setAddForm(true);
}}
>
<CirclePlus size={18} style={{ marginRight: 6 }} />
Neuen Gegenstand hinzufügen
</Button>
</Tooltip>
</HStack>
{/* End action toolbar */}
<Heading marginBottom={4} size="md">
Gegenstände
</Heading>
{isError && (
<MyAlert
status={errorStatus}
description={errorDsc}
title={errorMessage}
/>
)}
{isLoading && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">Loading...</Text>
</VStack>
)}
{addForm && (
<AddItemForm
onClose={() => {
setAddForm(false);
setReload(!reload);
}}
alert={setError}
/>
)}
<Table.Root size="sm" striped>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>
<strong>#</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Gegenstand</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Ausleih Berechtigung</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Im Schließfach</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Eintrag erstellt am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Aktionen</strong>
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{items.map((item) => (
<Table.Row key={item.id}>
<Table.Cell>{item.id}</Table.Cell>
<Table.Cell>{item.item_name}</Table.Cell>
<Table.Cell>{item.can_borrow_role}</Table.Cell>
<Table.Cell>
{item.inSafe ? (
<Tag.Root
size="md"
bg="green.500"
color="white"
px={4}
py={1.5}
rounded="full"
display="inline-flex"
alignItems="center"
gap={2}
shadow="sm"
_hover={{ shadow: "md" }}
>
<Icon as={CheckCircle2} boxSize={4} />
<Text
as="span"
fontSize="xs"
letterSpacing="wide"
textTransform="uppercase"
>
Yes
</Text>
</Tag.Root>
) : (
<Tag.Root
size="md"
bg="red.500"
color="white"
px={4}
py={1.5}
rounded="full"
display="inline-flex"
alignItems="center"
gap={2}
shadow="sm"
_hover={{ shadow: "md" }}
>
<Icon as={XCircle} boxSize={4} />
<Text
as="span"
fontSize="xs"
letterSpacing="wide"
textTransform="uppercase"
>
No
</Text>
</Tag.Root>
)}
</Table.Cell>
<Table.Cell>{item.entry_created_at}</Table.Cell>
<Table.Cell>
<Button
onClick={() =>
deleteItem(item.id).then((response) => {
if (response.success) {
setItems(items.filter((i) => i.id !== item.id));
setError(
"success",
"Gegenstand gelöscht",
"Der Gegenstand wurde erfolgreich gelöscht."
);
}
})
}
colorPalette="red"
size="sm"
ml={2}
>
<Trash2 />
</Button>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</>
);
};
export default ItemTable;

View File

@@ -1,18 +1,21 @@
import React from "react";
import {
Table,
Code,
VStack,
Spinner,
Text,
Heading,
VStack,
Button,
HStack,
IconButton,
Heading,
Code,
} from "@chakra-ui/react";
import { Tooltip } from "@/components/ui/tooltip";
import { useState, useEffect } from "react";
import Cookies from "js-cookie";
import MyAlert from "./myChakra/MyAlert";
import { formatDateTime } from "@/utils/userFuncs";
import { Trash2 } from "lucide-react";
import { Trash2, RefreshCcwDot } from "lucide-react";
import { deleteLoan } from "@/utils/userActions";
const LoanTable: React.FC = () => {
@@ -22,6 +25,7 @@ const LoanTable: React.FC = () => {
const [errorDsc, setErrorDsc] = useState("");
const [isError, setIsError] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [reload, setReload] = useState(false);
const setError = (
status: "error" | "success",
@@ -70,13 +74,39 @@ const LoanTable: React.FC = () => {
setItems(data);
}
});
}, []);
}, [reload]);
return (
<>
<Heading marginBottom={4} size="md">
Ausleihen
</Heading>
{/* Action toolbar */}
<HStack
mb={4}
gap={3}
justify="flex-start"
align="center"
flexWrap="wrap"
>
<Tooltip content="Ausleihen neu laden" openDelay={300}>
<IconButton
aria-label="Refresh loans"
size="sm"
variant="outline"
rounded="md"
shadow="sm"
_hover={{ shadow: "md", transform: "translateY(-2px)" }}
_active={{ transform: "translateY(0)" }}
onClick={() => setReload(!reload)}
>
<RefreshCcwDot size={18} />
</IconButton>
</Tooltip>
</HStack>
{/* End action toolbar */}
{isError && (
<MyAlert
status={errorStatus}

View File

@@ -1,7 +0,0 @@
import React from "react";
const LockerTable: React.FC = () => {
return <>Locker Table</>;
};
export default LockerTable;

View File

@@ -93,3 +93,47 @@ export const deleteLoan = async (loanId: number) => {
return { success: false };
}
};
export const deleteItem = async (itemId: number) => {
try {
const response = await fetch(
`http://localhost:8002/api/deleteItem/${itemId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
}
);
if (!response.ok) {
throw new Error("Failed to delete item");
}
return { success: true };
} catch (error) {
console.error("Error deleting item:", error);
return { success: false };
}
};
export const createItem = async (
item_name: string,
can_borrow_role: number
) => {
try {
const response = await fetch(`http://localhost:8002/api/createItem`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`,
},
body: JSON.stringify({ item_name, can_borrow_role }),
});
if (!response.ok) {
throw new Error("Failed to create item");
}
return { success: true };
} catch (error) {
console.error("Error creating item:", error);
return { success: false };
}
};

View File

@@ -15,6 +15,9 @@ import {
handleEdit,
createUser,
getAllLoans,
getAllItems,
deleteItemID,
createItem,
} from "../services/database.js";
import { authenticate, generateToken } from "../services/tokenService.js";
const router = express.Router();
@@ -247,4 +250,30 @@ router.get("/allLoans", authenticate, async (req, res) => {
return res.status(500).json({ message: "Failed to fetch loans" });
});
router.get("/allItems", authenticate, async (req, res) => {
const result = await getAllItems();
if (result.success) {
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to fetch items" });
});
router.delete("/deleteItem/:id", authenticate, async (req, res) => {
const itemId = req.params.id;
const result = await deleteItemID(itemId);
if (result.success) {
return res.status(200).json({ message: "Item deleted successfully" });
}
return res.status(500).json({ message: "Failed to delete item" });
});
router.post("/createItem", authenticate, async (req, res) => {
const { item_name, can_borrow_role } = req.body || {};
const result = await createItem(item_name, can_borrow_role);
if (result.success) {
return res.status(201).json({ message: "Item created successfully" });
}
return res.status(500).json({ message: "Failed to create item" });
});
export default router;

View File

@@ -364,3 +364,24 @@ export const getAllLoans = async () => {
if (result.length > 0) return { success: true, data: result };
return { success: false };
};
export const getAllItems = async () => {
const [result] = await pool.query("SELECT * FROM items");
if (result.length > 0) return { success: true, data: result };
return { success: false };
};
export const deleteItemID = async (itemId) => {
const [result] = await pool.query("DELETE FROM items WHERE id = ?", [itemId]);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const createItem = async (item_name, can_borrow_role) => {
const [result] = await pool.query(
"INSERT INTO items (item_name, can_borrow_role) VALUES (?, ?)",
[item_name, can_borrow_role]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};