7 Commits

6 changed files with 256 additions and 70 deletions

View File

@@ -4,9 +4,7 @@ import Layout from "./Layout/Layout";
function App() {
return (
<>
<Layout>
<p></p>
</Layout>
<Layout />
</>
);
}

View File

@@ -3,15 +3,20 @@ import { useEffect } from "react";
import Dashboard from "./Dashboard";
import Login from "./Login";
import Cookies from "js-cookie";
import Landingpage from "@/components/API/Landingpage";
type LayoutProps = {
children: React.ReactNode;
};
const Layout: React.FC<LayoutProps> = ({ children }) => {
const Layout: React.FC = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [showAPI, setShowAPI] = useState(false);
useEffect(() => {
const path = window.location.pathname.replace(/\/+$/, ""); // remove trailing slash
if (path === "/api") {
setShowAPI(true);
console.log("signal");
return;
}
if (Cookies.get("token")) {
const verifyToken = async () => {
const response = await fetch("https://backend.insta.the1s.de/api/verifyToken", {
@@ -37,8 +42,15 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
setIsLoggedIn(false);
};
if (showAPI) {
return (
<main>
<Landingpage />
</main>
);
}
return (
<>
<main>
{isLoggedIn ? (
<Dashboard onLogout={() => handleLogout()} />
@@ -46,8 +58,6 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
<Login onSuccess={() => setIsLoggedIn(true)} />
)}
</main>
{children}
</>
);
};

View File

@@ -0,0 +1,181 @@
import React, { useEffect, useState } from "react";
import {
Spinner,
Text,
VStack,
Table,
Heading,
HStack,
IconButton,
} from "@chakra-ui/react";
import { Tooltip } from "@/components/ui/tooltip";
import { RefreshCcwDot } from "lucide-react";
import MyAlert from "../myChakra/MyAlert";
import { formatDateTime } from "@/utils/userFuncs";
type Loan = {
id: number;
username: string;
start_date: string;
end_date: string;
returned_date: string | null;
take_date: string | null;
loaned_items_name: string[] | string;
};
const Landingpage: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [loans, setLoans] = useState<Loan[]>([]);
const [isError, setIsError] = useState(false);
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
const [errorMessage, setErrorMessage] = useState("");
const [errorDsc, setErrorDsc] = useState("");
const [reload, setReload] = useState(false);
const setError = (
status: "error" | "success",
message: string,
description: string
) => {
setIsError(false);
setErrorStatus(status);
setErrorMessage(message);
setErrorDsc(description);
setIsError(true);
};
useEffect(() => {
const fetchLoans = async () => {
setIsLoading(true);
try {
const res = await fetch("http://localhost:8002/apiV2/allLoans");
const data = await res.json();
if (Array.isArray(data)) {
setLoans(data);
} else {
setError(
"error",
"Fehler beim Laden",
"Unerwartetes Datenformat erhalten."
);
}
} catch (e) {
setError(
"error",
"Fehler beim Laden",
"Die Ausleihen konnten nicht geladen werden."
);
} finally {
setIsLoading(false);
}
};
fetchLoans();
}, [reload]);
return (
<>
<Heading as="h1" size="lg" mb={2}>
Matthias-Claudius-Schule Technik
</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 */}
<Heading as="h2" size="md" mb={4}>
Alle Ausleihen
</Heading>
{isError && (
<MyAlert
status={errorStatus}
description={errorDsc}
title={errorMessage}
/>
)}
{isLoading && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">Loading...</Text>
</VStack>
)}
{!isLoading && (
<Table.Root size="sm" striped>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>
<strong>#</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Benutzername</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Startdatum</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Enddatum</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Ausgeliehene Artikel</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Rückgabedatum</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Ausleihdatum</strong>
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{loans.map((loan) => (
<Table.Row key={loan.id}>
<Table.Cell>{loan.id}</Table.Cell>
<Table.Cell>{loan.username}</Table.Cell>
<Table.Cell>{formatDateTime(loan.start_date)}</Table.Cell>
<Table.Cell>{formatDateTime(loan.end_date)}</Table.Cell>
<Table.Cell>
{Array.isArray(loan.loaned_items_name)
? loan.loaned_items_name.join(", ")
: loan.loaned_items_name}
</Table.Cell>
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
)}
{!isLoading && loans.length === 0 && !isError && (
<Text color="gray.500" mt={2}>
Keine Ausleihen vorhanden.
</Text>
)}
</>
);
};
export default Landingpage;

View File

@@ -9,7 +9,6 @@ import {
IconButton,
Heading,
Icon,
Tag,
Input,
} from "@chakra-ui/react";
import { Tooltip } from "@/components/ui/tooltip";
@@ -219,57 +218,34 @@ const ItemTable: React.FC = () => {
onClick={() =>
changeSafeState(item.id).then(() => setReload(!reload))
}
size="sm"
>
{item.inSafe ? (
<Tag.Root
size="md"
bg="green.500"
color="white"
px={4}
py={1.5}
size="xs"
rounded="full"
display="inline-flex"
alignItems="center"
px={3}
py={1}
gap={2}
shadow="sm"
_hover={{ shadow: "md" }}
variant="ghost"
color={item.inSafe ? "green.600" : "red.600"}
borderWidth="1px"
borderColor={item.inSafe ? "green.300" : "red.300"}
_hover={{
bg: item.inSafe ? "green.50" : "red.50",
borderColor: item.inSafe ? "green.400" : "red.400",
transform: "translateY(-1px)",
shadow: "sm",
}}
_active={{ transform: "translateY(0)" }}
aria-label={
item.inSafe ? "Mark as not in safe" : "Mark as in safe"
}
>
<Icon as={CheckCircle2} boxSize={4} />
<Text
as="span"
fontSize="xs"
letterSpacing="wide"
textTransform="uppercase"
>
Yes
<Icon
as={item.inSafe ? CheckCircle2 : XCircle}
boxSize={3.5}
mr={2}
/>
<Text as="span" fontSize="xs" fontWeight="semibold">
{item.inSafe ? "Yes" : "No"}
</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>
)}
</Button>
</Table.Cell>
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>

View File

@@ -6,6 +6,7 @@ import {
setReturnDateV2,
setTakeDateV2,
getLoanByCodeV2,
getAllLoansV2,
} from "../services/database.js";
dotenv.config();
@@ -45,6 +46,7 @@ router.post("/controlInSafe/:key/:itemId/:state", async (req, res) => {
}
});
// Route for API to get a loan by its code
router.get("/getLoanByCode/:key/:loan_code", async (req, res) => {
if (req.params.key === process.env.ADMIN_ID) {
const loan_code = req.params.loan_code;
@@ -58,7 +60,7 @@ router.get("/getLoanByCode/:key/:loan_code", async (req, res) => {
}
});
// Route for API to set the return date
// Route for API to set the return date by the loan code
router.post("/setReturnDate/:key/:loan_code", async (req, res) => {
if (req.params.key === process.env.ADMIN_ID) {
const loanCode = req.params.loan_code;
@@ -74,7 +76,7 @@ router.post("/setReturnDate/:key/:loan_code", async (req, res) => {
}
});
// Route for API to set the take away date
// Route for API to set the take away date by the loan code
router.post("/setTakeDate/:key/:loan_code", async (req, res) => {
if (req.params.key === process.env.ADMIN_ID) {
const loanCode = req.params.loan_code;
@@ -90,4 +92,13 @@ router.post("/setTakeDate/:key/:loan_code", async (req, res) => {
}
});
// Route for API to get ALL loans from the database without sensitive info
router.get("/allLoans", async (req, res) => {
const result = await getAllLoansV2();
if (result.success) {
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to fetch loans" });
});
export default router;

View File

@@ -448,3 +448,13 @@ export const updateItemByID = async (itemId, item_name, can_borrow_role) => {
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const getAllLoansV2 = async () => {
const [rows] = await pool.query(
"SELECT id, username, start_date, end_date, loaned_items_name, returned_date, take_date FROM loans"
);
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};