added MyLoansPage component and integrated loan deletion functionality; updated routing in App and added Header component

This commit is contained in:
2025-10-25 21:27:08 +02:00
parent 7a79bf4436
commit cc0dcaf664
6 changed files with 295 additions and 62 deletions

View File

@@ -9,6 +9,7 @@ import { useAtom } from "jotai";
import { setIsLoggedInAtom } from "@/states/Atoms";
import { UserContext, type User } from "./states/Context";
import { triggerLogoutAtom } from "@/states/Atoms";
import { MyLoansPage } from "./pages/MyLoansPage";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
@@ -51,6 +52,7 @@ function App() {
<Routes>
<Route element={<ProtectedRoutes />}>
<Route path="/" element={<HomePage />} />
<Route path="/my-loans" element={<MyLoansPage />} />
</Route>
<Route path="/login" element={<LoginPage />} />

View File

@@ -0,0 +1,99 @@
import { Badge, Button, Flex, Heading, Stack, Text } from "@chakra-ui/react";
import Cookies from "js-cookie";
import { useAtom } from "jotai";
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
import { useNavigate } from "react-router-dom";
import {
CircleUserRound,
RotateCcwKey,
Code,
LifeBuoy,
LogOut,
CalendarPlus,
} from "lucide-react";
import { useUserContext } from "@/states/Context";
export const Header = () => {
const navigate = useNavigate();
const userData = useUserContext();
const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
return (
<Stack as="header" gap={3} className="mb-16">
<Flex
direction={{ base: "column", md: "row" }}
align={{ base: "stretch", md: "center" }}
justify="space-between"
gap={4}
>
<Stack gap={2}>
<Heading
size="2xl"
className="tracking-tight text-slate-900 dark:text-slate-100"
>
Home
</Heading>
<Stack
direction={{ base: "column", sm: "row" }}
gap={2}
alignItems="start"
>
<Text fontSize="md" className="text-slate-600 dark:text-slate-400">
Willkommen zurück,{" "}
{userData.username.replace(
/^./,
userData.username[0].toUpperCase()
)}
!
</Text>
<Badge variant="subtle" px={2} py={1} borderRadius="full">
Rolle: {userData.role}
</Badge>
</Stack>
</Stack>
<Button onClick={() => navigate("/", { replace: true })}>
<CalendarPlus /> Ausleihe erstellen
</Button>
<Button onClick={() => navigate("/my-loans", { replace: true })}>
<CircleUserRound /> Meine Ausleihen
</Button>
<Button>
<RotateCcwKey /> Passwort ändern
</Button>
<a
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
target="_blank"
>
<Button>
<LifeBuoy /> Hilfe
</Button>
</a>
<a
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
target="_blank"
>
<Button>
<Code /> Source Code
</Button>
</a>
<Button
onClick={() => {
Cookies.remove("token");
setIsLoggedIn(false);
setTriggerLogout(true);
}}
variant="solid"
size="sm"
className="self-start md:self-auto"
>
<LogOut /> Logout
</Button>
</Flex>
</Stack>
);
};

View File

@@ -1,25 +1,20 @@
import { useUserContext } from "@/states/Context";
import {
Container,
Stack,
Heading,
Text,
Badge,
Flex,
Button,
Input,
Spinner,
VStack,
Table,
} from "@chakra-ui/react";
import { triggerLogoutAtom, setIsLoggedInAtom } from "@/states/Atoms";
import { useAtom } from "jotai";
import Cookies from "js-cookie";
import { getBorrowableItems } from "@/utils/Fetcher";
import { useState } from "react";
import MyAlert from "@/components/myChakra/MyAlert";
import { borrowAbleItemsAtom } from "@/states/Atoms";
import { createLoan } from "@/utils/Fetcher";
import { Header } from "@/components/Header";
export interface User {
username: string;
@@ -27,9 +22,6 @@ export interface User {
}
export const HomePage = () => {
const userData = useUserContext();
const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
@@ -52,56 +44,7 @@ export const HomePage = () => {
return (
<Container maxW="7xl" className="px-6 sm:px-8 pt-10">
<Stack as="header" gap={3} className="mb-16">
<Flex
direction={{ base: "column", md: "row" }}
align={{ base: "stretch", md: "center" }}
justify="space-between"
gap={4}
>
<Stack gap={2}>
<Heading
size="2xl"
className="tracking-tight text-slate-900 dark:text-slate-100"
>
Home
</Heading>
<Stack
direction={{ base: "column", sm: "row" }}
gap={2}
alignItems="start"
>
<Text
fontSize="md"
className="text-slate-600 dark:text-slate-400"
>
Willkommen zurück,{" "}
{userData.username.replace(
/^./,
userData.username[0].toUpperCase()
)}
!
</Text>
<Badge variant="subtle" px={2} py={1} borderRadius="full">
Rolle: {userData.role}
</Badge>
</Stack>
</Stack>
<Button
onClick={() => {
Cookies.remove("token");
setIsLoggedIn(false);
setTriggerLogout(true);
}}
variant="solid"
size="sm"
className="self-start md:self-auto"
>
Logout
</Button>
</Flex>
</Stack>
<Header />
{isMsg && (
<MyAlert
status={msgStatus}

View File

@@ -0,0 +1,165 @@
import { useEffect, useState } from "react";
import Cookies from "js-cookie";
import { useNavigate } from "react-router-dom";
import MyAlert from "@/components/myChakra/MyAlert";
import { Container, VStack, Spinner, Text, Table } from "@chakra-ui/react";
import { Header } from "@/components/Header";
import { Trash2 } from "lucide-react";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
export const MyLoansPage = () => {
const navigate = useNavigate();
const [loans, setLoans] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
// Error handling states
const [isMsg, setIsMsg] = useState(false);
const [msgStatus, setMsgStatus] = useState<"error" | "success">("error");
const [msgTitle, setMsgTitle] = useState("");
const [msgDescription, setMsgDescription] = useState("");
useEffect(() => {
if (!Cookies.get("token")) {
navigate("/login", { replace: true });
return;
}
const fetchLoans = async () => {
try {
setIsLoading(true);
const res = await fetch(`${API_BASE}/api/userLoans`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
if (!res.ok) {
setMsgStatus("error");
setMsgTitle("Fehler");
setMsgDescription(
"Beim Laden der Ausleihen ist ein Fehler aufgetreten."
);
setIsMsg(true);
return;
}
const data = await res.json();
setLoans(data);
console.log("Fetched loans:", data);
} catch (e) {
setMsgStatus("error");
setMsgTitle("Fehler");
setMsgDescription("Netzwerkfehler beim Laden der Ausleihen.");
setIsMsg(true);
} finally {
setIsLoading(false);
}
};
fetchLoans();
}, [navigate]);
const deleteLoan = async (loanId: number) => {
try {
const res = await fetch(`${API_BASE}/api/SETdeleteLoan/${loanId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
if (!res.ok) {
setMsgStatus("error");
setMsgTitle("Fehler");
setMsgDescription(
"Beim Löschen der Ausleihe ist ein Fehler aufgetreten."
);
setIsMsg(true);
return;
}
setLoans((prev) => prev.filter((loan) => loan.id !== loanId));
setMsgStatus("success");
setMsgTitle("Erfolg");
setMsgDescription("Ausleihe erfolgreich gelöscht.");
setIsMsg(true);
} catch (e) {
setMsgStatus("error");
setMsgTitle("Fehler");
setMsgDescription("Netzwerkfehler beim Löschen der Ausleihe.");
setIsMsg(true);
}
};
const formatDate = (iso: string | null) => {
if (!iso) return "-";
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
if (!m) return iso;
const [, y, M, d, h, min] = m;
return `${d}.${M}.${y} ${h}:${min}`;
};
return (
<>
<Container maxW="7xl" className="px-6 sm:px-8 pt-10">
<Header />
{isMsg && (
<MyAlert
status={msgStatus}
title={msgTitle}
description={msgDescription}
/>
)}
{isLoading && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">Loading...</Text>
</VStack>
)}
{loans && (
<Table.Root size="sm" variant="outline">
<Table.ColumnGroup>
<Table.Column htmlWidth="50%" />
<Table.Column htmlWidth="40%" />
<Table.Column />
</Table.ColumnGroup>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>Ausleihcode</Table.ColumnHeader>
<Table.ColumnHeader>Startdatum</Table.ColumnHeader>
<Table.ColumnHeader>Enddatum</Table.ColumnHeader>
<Table.ColumnHeader>Geräte</Table.ColumnHeader>
<Table.ColumnHeader>Ausleihdatum</Table.ColumnHeader>
<Table.ColumnHeader>Rückgabedatum</Table.ColumnHeader>
<Table.ColumnHeader>Aktionen</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{loans.map((loan) => (
<Table.Row key={loan.id}>
<Table.Cell>{loan.loan_code}</Table.Cell>
<Table.Cell>{formatDate(loan.start_date)}</Table.Cell>
<Table.Cell>{formatDate(loan.end_date)}</Table.Cell>
<Table.Cell>{loan.loaned_items_name}</Table.Cell>
<Table.Cell>{formatDate(loan.take_date)}</Table.Cell>
<Table.Cell>{formatDate(loan.returned_date)}</Table.Cell>
<Table.Cell>
<button onClick={() => deleteLoan(loan.id)}>
<Trash2 />
</button>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
)}
</Container>
</>
);
};

View File

@@ -26,6 +26,7 @@ import {
createAPIentry,
deleteAPKey,
getLoanInfoWithID,
SETdeleteLoanFromDatabase,
} from "../services/database.js";
import { authenticate, generateToken } from "../services/tokenService.js";
const router = express.Router();
@@ -261,6 +262,16 @@ router.delete("/deleteLoan/:id", authenticate, async (req, res) => {
}
});
router.delete("/SETdeleteLoan/:id", authenticate, async (req, res) => {
const loanId = req.params.id;
const result = await SETdeleteLoanFromDatabase(loanId);
if (result.success) {
res.status(200).json({ message: "Loan deleted successfully" });
} else {
res.status(500).json({ message: "Failed to delete loan" });
}
});
router.post("/borrowableItems", authenticate, async (req, res) => {
const { startDate, endDate } = req.body || {};
if (!startDate || !endDate) {

View File

@@ -126,9 +126,10 @@ export const getLoansFromDatabase = async () => {
};
export const getUserLoansFromDatabase = async (username) => {
const [result] = await pool.query("SELECT * FROM loans WHERE username = ?;", [
username,
]);
const [result] = await pool.query(
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
[username]
);
if (result.length > 0) {
return { success: true, data: result };
} else if (result.length == 0) {
@@ -149,6 +150,18 @@ export const deleteLoanFromDatabase = async (loanId) => {
}
};
export const SETdeleteLoanFromDatabase = async (loanId) => {
const [result] = await pool.query(
"UPDATE loans SET deleted = 1 WHERE id = ?;",
[loanId]
);
if (result.affectedRows > 0) {
return { success: true };
} else {
return { success: false };
}
};
export const getBorrowableItemsFromDatabase = async (
startDate,
endDate,