diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 2269c2e..f0933ae 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -4,9 +4,7 @@ import Layout from "./Layout/Layout"; function App() { return ( <> - -

-
+ ); } diff --git a/admin/src/Layout/Layout.tsx b/admin/src/Layout/Layout.tsx index f096e02..d2d5c77 100644 --- a/admin/src/Layout/Layout.tsx +++ b/admin/src/Layout/Layout.tsx @@ -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 = ({ 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,17 +42,22 @@ const Layout: React.FC = ({ children }) => { setIsLoggedIn(false); }; - return ( - <> + if (showAPI) { + return (
- {isLoggedIn ? ( - handleLogout()} /> - ) : ( - setIsLoggedIn(true)} /> - )} +
- {children} - + ); + } + + return ( +
+ {isLoggedIn ? ( + handleLogout()} /> + ) : ( + setIsLoggedIn(true)} /> + )} +
); }; diff --git a/admin/src/components/API/Landingpage.tsx b/admin/src/components/API/Landingpage.tsx new file mode 100644 index 0000000..51fd700 --- /dev/null +++ b/admin/src/components/API/Landingpage.tsx @@ -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([]); + 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 ( + <> + + Matthias-Claudius-Schule Technik + + + {/* Action toolbar */} + + + setReload(!reload)} + > + + + + + {/* End action toolbar */} + + + Alle Ausleihen + + + {isError && ( + + )} + + {isLoading && ( + + + Loading... + + )} + + {!isLoading && ( + + + + + # + + + Benutzername + + + Startdatum + + + Enddatum + + + Ausgeliehene Artikel + + + Rückgabedatum + + + Ausleihdatum + + + + + {loans.map((loan) => ( + + {loan.id} + {loan.username} + {formatDateTime(loan.start_date)} + {formatDateTime(loan.end_date)} + + {Array.isArray(loan.loaned_items_name) + ? loan.loaned_items_name.join(", ") + : loan.loaned_items_name} + + {formatDateTime(loan.returned_date)} + {formatDateTime(loan.take_date)} + + ))} + + + )} + + {!isLoading && loans.length === 0 && !isError && ( + + Keine Ausleihen vorhanden. + + )} + + ); +}; + +export default Landingpage; diff --git a/admin/src/components/ItemTable.tsx b/admin/src/components/ItemTable.tsx index 79b372f..da45f16 100644 --- a/admin/src/components/ItemTable.tsx +++ b/admin/src/components/ItemTable.tsx @@ -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" + size="xs" + rounded="full" + px={3} + py={1} + gap={2} + 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" + } > - {item.inSafe ? ( - - - - Yes - - - ) : ( - - - - No - - - )} + + + {item.inSafe ? "Yes" : "No"} + {formatDateTime(item.entry_created_at)} diff --git a/backend/routes/apiV2.js b/backend/routes/apiV2.js index faecec2..1b81426 100644 --- a/backend/routes/apiV2.js +++ b/backend/routes/apiV2.js @@ -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; diff --git a/backend/services/database.js b/backend/services/database.js index 7bf442d..a102c2d 100644 --- a/backend/services/database.js +++ b/backend/services/database.js @@ -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 }; +};