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 { setIsLoggedInAtom } from "@/states/Atoms";
|
||||||
import { UserContext, type User } from "./states/Context";
|
import { UserContext, type User } from "./states/Context";
|
||||||
import { triggerLogoutAtom } from "@/states/Atoms";
|
import { triggerLogoutAtom } from "@/states/Atoms";
|
||||||
|
import { MyLoansPage } from "./pages/MyLoansPage";
|
||||||
|
|
||||||
const API_BASE =
|
const API_BASE =
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||||
@@ -51,6 +52,7 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<ProtectedRoutes />}>
|
<Route element={<ProtectedRoutes />}>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/my-loans" element={<MyLoansPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<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 {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Stack,
|
Stack,
|
||||||
Heading,
|
|
||||||
Text,
|
Text,
|
||||||
Badge,
|
|
||||||
Flex,
|
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Spinner,
|
Spinner,
|
||||||
VStack,
|
VStack,
|
||||||
Table,
|
Table,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { triggerLogoutAtom, setIsLoggedInAtom } from "@/states/Atoms";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { getBorrowableItems } from "@/utils/Fetcher";
|
import { getBorrowableItems } from "@/utils/Fetcher";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import MyAlert from "@/components/myChakra/MyAlert";
|
import MyAlert from "@/components/myChakra/MyAlert";
|
||||||
import { borrowAbleItemsAtom } from "@/states/Atoms";
|
import { borrowAbleItemsAtom } from "@/states/Atoms";
|
||||||
import { createLoan } from "@/utils/Fetcher";
|
import { createLoan } from "@/utils/Fetcher";
|
||||||
|
import { Header } from "@/components/Header";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -27,9 +22,6 @@ export interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const userData = useUserContext();
|
|
||||||
const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
|
||||||
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
|
||||||
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
||||||
const [startDate, setStartDate] = useState("");
|
const [startDate, setStartDate] = useState("");
|
||||||
const [endDate, setEndDate] = useState("");
|
const [endDate, setEndDate] = useState("");
|
||||||
@@ -52,56 +44,7 @@ export const HomePage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxW="7xl" className="px-6 sm:px-8 pt-10">
|
<Container maxW="7xl" className="px-6 sm:px-8 pt-10">
|
||||||
<Stack as="header" gap={3} className="mb-16">
|
<Header />
|
||||||
<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>
|
|
||||||
{isMsg && (
|
{isMsg && (
|
||||||
<MyAlert
|
<MyAlert
|
||||||
status={msgStatus}
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
createAPIentry,
|
createAPIentry,
|
||||||
deleteAPKey,
|
deleteAPKey,
|
||||||
getLoanInfoWithID,
|
getLoanInfoWithID,
|
||||||
|
SETdeleteLoanFromDatabase,
|
||||||
} from "../services/database.js";
|
} from "../services/database.js";
|
||||||
import { authenticate, generateToken } from "../services/tokenService.js";
|
import { authenticate, generateToken } from "../services/tokenService.js";
|
||||||
const router = express.Router();
|
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) => {
|
router.post("/borrowableItems", authenticate, async (req, res) => {
|
||||||
const { startDate, endDate } = req.body || {};
|
const { startDate, endDate } = req.body || {};
|
||||||
if (!startDate || !endDate) {
|
if (!startDate || !endDate) {
|
||||||
|
|||||||
@@ -126,9 +126,10 @@ export const getLoansFromDatabase = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getUserLoansFromDatabase = async (username) => {
|
export const getUserLoansFromDatabase = async (username) => {
|
||||||
const [result] = await pool.query("SELECT * FROM loans WHERE username = ?;", [
|
const [result] = await pool.query(
|
||||||
username,
|
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
|
||||||
]);
|
[username]
|
||||||
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
} else if (result.length == 0) {
|
} 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 (
|
export const getBorrowableItemsFromDatabase = async (
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
|||||||
Reference in New Issue
Block a user