added MyLoansPage component and integrated loan deletion functionality; updated routing in App and added Header component
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
99
FrontendV2/src/components/Header.tsx
Normal file
99
FrontendV2/src/components/Header.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
165
FrontendV2/src/pages/MyLoansPage.tsx
Normal file
165
FrontendV2/src/pages/MyLoansPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user