From 06976f7972d7333fe8b3089d3dff9cd09d6f5f86 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sat, 14 Feb 2026 19:01:59 +0100 Subject: [PATCH 1/5] added pasword input to admin panel --- admin/src/Layout/Login.tsx | 4 +- admin/src/components/ui/password-input.tsx | 159 +++++++++++++++++++++ admin/tsconfig.app.json | 10 +- 3 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 admin/src/components/ui/password-input.tsx diff --git a/admin/src/Layout/Login.tsx b/admin/src/Layout/Login.tsx index 813a7e6..590826c 100644 --- a/admin/src/Layout/Login.tsx +++ b/admin/src/Layout/Login.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { loginFunc } from "@/utils/loginUser"; import MyAlert from "../components/myChakra/MyAlert"; import { Button, Card, Field, Input, Stack } from "@chakra-ui/react"; +import { PasswordInput } from "@/components/ui/password-input"; const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => { const [username, setUsername] = useState(""); @@ -43,8 +44,7 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => { password - setPassword(e.target.value)} /> diff --git a/admin/src/components/ui/password-input.tsx b/admin/src/components/ui/password-input.tsx new file mode 100644 index 0000000..3b9af80 --- /dev/null +++ b/admin/src/components/ui/password-input.tsx @@ -0,0 +1,159 @@ +"use client" + +import type { + ButtonProps, + GroupProps, + InputProps, + StackProps, +} from "@chakra-ui/react" +import { + Box, + HStack, + IconButton, + Input, + InputGroup, + Stack, + mergeRefs, + useControllableState, +} from "@chakra-ui/react" +import * as React from "react" +import { LuEye, LuEyeOff } from "react-icons/lu" + +export interface PasswordVisibilityProps { + /** + * The default visibility state of the password input. + */ + defaultVisible?: boolean + /** + * The controlled visibility state of the password input. + */ + visible?: boolean + /** + * Callback invoked when the visibility state changes. + */ + onVisibleChange?: (visible: boolean) => void + /** + * Custom icons for the visibility toggle button. + */ + visibilityIcon?: { on: React.ReactNode; off: React.ReactNode } +} + +export interface PasswordInputProps + extends InputProps, + PasswordVisibilityProps { + rootProps?: GroupProps +} + +export const PasswordInput = React.forwardRef< + HTMLInputElement, + PasswordInputProps +>(function PasswordInput(props, ref) { + const { + rootProps, + defaultVisible, + visible: visibleProp, + onVisibleChange, + visibilityIcon = { on: , off: }, + ...rest + } = props + + const [visible, setVisible] = useControllableState({ + value: visibleProp, + defaultValue: defaultVisible || false, + onChange: onVisibleChange, + }) + + const inputRef = React.useRef(null) + + return ( + { + if (rest.disabled) return + if (e.button !== 0) return + e.preventDefault() + setVisible(!visible) + }} + > + {visible ? visibilityIcon.off : visibilityIcon.on} + + } + {...rootProps} + > + + + ) +}) + +const VisibilityTrigger = React.forwardRef( + function VisibilityTrigger(props, ref) { + return ( + + ) + }, +) + +interface PasswordStrengthMeterProps extends StackProps { + max?: number + value: number +} + +export const PasswordStrengthMeter = React.forwardRef< + HTMLDivElement, + PasswordStrengthMeterProps +>(function PasswordStrengthMeter(props, ref) { + const { max = 4, value, ...rest } = props + + const percent = (value / max) * 100 + const { label, colorPalette } = getColorPalette(percent) + + return ( + + + {Array.from({ length: max }).map((_, index) => ( + + ))} + + {label && {label}} + + ) +}) + +function getColorPalette(percent: number) { + switch (true) { + case percent < 33: + return { label: "Low", colorPalette: "red" } + case percent < 66: + return { label: "Medium", colorPalette: "orange" } + default: + return { label: "High", colorPalette: "green" } + } +} diff --git a/admin/tsconfig.app.json b/admin/tsconfig.app.json index 8ad072c..a824e0e 100644 --- a/admin/tsconfig.app.json +++ b/admin/tsconfig.app.json @@ -1,10 +1,11 @@ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ESNext", + "target": "ES2022", "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", + "types": ["vite/client"], "skipLibCheck": true, /* Bundler mode */ @@ -23,13 +24,10 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, - /* Chakra / Pfad Aliases */ - "baseUrl": ".", + /* Path aliases */ "paths": { "@/*": ["./src/*"] - }, - - "forceConsistentCasingInFileNames": true + } }, "include": ["src"] } From a8dab549af06ee38082c7519013f197f14e2fe21 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Fri, 20 Feb 2026 12:02:33 +0100 Subject: [PATCH 2/5] fixed bug: cannot return loan --- FrontendV2/src/utils/Fetcher.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/FrontendV2/src/utils/Fetcher.ts b/FrontendV2/src/utils/Fetcher.ts index c6be318..a684490 100644 --- a/FrontendV2/src/utils/Fetcher.ts +++ b/FrontendV2/src/utils/Fetcher.ts @@ -1,13 +1,10 @@ import Cookies from "js-cookie"; import { API_BASE } from "@/config/api.config"; -import { useTranslation } from "react-i18next"; export const getBorrowableItems = async ( startDate: string, endDate: string, ) => { - const { t } = useTranslation(); - try { const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, { method: "POST", @@ -24,7 +21,8 @@ export const getBorrowableItems = async ( data: null, status: "error", title: "Server error", - description: t("serverError"), + description: + "An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.", }; } From ee54d51f8bac0c4aa0d69efeb12440a5624a3c26 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Fri, 20 Feb 2026 12:14:56 +0100 Subject: [PATCH 3/5] enhanced loan management: added note field to loan creation and email templates --- backendV2/routes/api/api.database.js | 63 +++++++++++++------------ backendV2/routes/app/loanMgmt.route.js | 1 + backendV2/routes/app/services/mailer.js | 44 +++++++++++++++-- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/backendV2/routes/api/api.database.js b/backendV2/routes/api/api.database.js index d80365c..b824ad4 100644 --- a/backendV2/routes/api/api.database.js +++ b/backendV2/routes/api/api.database.js @@ -22,7 +22,7 @@ export const getItemsFromDatabaseV2 = async () => { export const getLoanByCodeV2 = async (loan_code) => { const [result] = await pool.query( "SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;", - [loan_code] + [loan_code], ); if (result.length > 0) { return { success: true, data: result[0] }; @@ -33,7 +33,7 @@ export const getLoanByCodeV2 = async (loan_code) => { export const changeInSafeStateV2 = async (itemId) => { const [result] = await pool.query( "UPDATE items SET in_safe = NOT in_safe WHERE id = ?", - [itemId] + [itemId], ); if (result.affectedRows > 0) { return { success: true }; @@ -42,47 +42,48 @@ export const changeInSafeStateV2 = async (itemId) => { }; export const setReturnDateV2 = async (loanCode) => { - const [items] = await pool.query( - "SELECT loaned_items_id FROM loans WHERE loan_code = ?", - [loanCode] - ); + try { + const [items] = await pool.query( + "SELECT loaned_items_id, username FROM loans WHERE loan_code = ?", + [loanCode], + ); - const [owner] = await pool.query( - "SELECT username FROM loans WHERE loan_code = ?", - [loanCode] - ); + if (items.length === 0) return { success: false }; - if (items.length === 0) return { success: false }; + const itemIds = Array.isArray(items[0].loaned_items_id) + ? items[0].loaned_items_id + : JSON.parse(items[0].loaned_items_id || "[]"); - const itemIds = Array.isArray(items[0].loaned_items_id) - ? items[0].loaned_items_id - : JSON.parse(items[0].loaned_items_id || "[]"); + const [result] = await pool.query( + "UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL", + [loanCode], + ); - const [setItemStates] = await pool.query( - "UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)", - [owner[0].username, itemIds] - ); + if (result.affectedRows === 0) return { success: false }; - const [result] = await pool.query( - "UPDATE loans SET returned_date = NOW() WHERE loan_code = ?", - [loanCode] - ); + if (itemIds.length > 0) { + await pool.query( + "UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = ? WHERE id IN (?)", + [items[0].username, itemIds], + ); + } - if (result.affectedRows > 0 && setItemStates.affectedRows > 0) { - return { success: true }; + return { success: true, data: { returned: true } }; + } catch (error) { + console.error("setReturnDateV2 error:", error); + return { success: false }; } - return { success: false }; }; export const setTakeDateV2 = async (loanCode) => { const [items] = await pool.query( "SELECT loaned_items_id FROM loans WHERE loan_code = ?", - [loanCode] + [loanCode], ); const [owner] = await pool.query( "SELECT username FROM loans WHERE loan_code = ?", - [loanCode] + [loanCode], ); if (items.length === 0) return { success: false }; @@ -93,12 +94,12 @@ export const setTakeDateV2 = async (loanCode) => { const [setItemStates] = await pool.query( "UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)", - [owner[0].username, itemIds] + [owner[0].username, itemIds], ); const [result] = await pool.query( "UPDATE loans SET take_date = NOW() WHERE loan_code = ?", - [loanCode] + [loanCode], ); if (result.affectedRows > 0 && setItemStates.affectedRows > 0) { @@ -118,12 +119,12 @@ export const getAllLoansV2 = async () => { export const openDoor = async (doorKey) => { const [result] = await pool.query( "SELECT safe_nr, id FROM items WHERE door_key = ?;", - [doorKey] + [doorKey], ); if (result.length > 0) { const [changeItemSate] = await pool.query( "UPDATE items SET in_safe = NOT in_safe WHERE id = ?", - [result[0].id] + [result[0].id], ); if (changeItemSate.affectedRows > 0) { return { success: true, data: result[0] }; diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index 3ec5599..7e0c0ce 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -62,6 +62,7 @@ router.post("/createLoan", authenticate, async (req, res) => { mailInfo.data.start_date, mailInfo.data.end_date, mailInfo.data.created_at, + mailInfo.data.note, ); return res.status(201).json({ message: "Loan created successfully", diff --git a/backendV2/routes/app/services/mailer.js b/backendV2/routes/app/services/mailer.js index 1357d31..3a081d6 100644 --- a/backendV2/routes/app/services/mailer.js +++ b/backendV2/routes/app/services/mailer.js @@ -34,7 +34,14 @@ const formatDateTime = (value) => { return "N/A"; }; -function buildLoanEmail({ user, items, startDate, endDate, createdDate }) { +function buildLoanEmail({ + user, + items, + startDate, + endDate, + createdDate, + note, +}) { const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9"; const itemsList = Array.isArray(items) && items.length @@ -116,6 +123,12 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) { createdDate, )} + + Notiz + ${ + note || "Keine Notiz" + } +

@@ -134,7 +147,14 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) { `; } -function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) { +function buildLoanEmailText({ + user, + items, + startDate, + endDate, + createdDate, + note, +}) { const itemsText = Array.isArray(items) && items.length ? items.join(", ") : "N/A"; return [ @@ -145,10 +165,18 @@ function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) { `Start: ${formatDateTime(startDate)}`, `Ende: ${formatDateTime(endDate)}`, `Erstellt am: ${formatDateTime(createdDate)}`, + `Notiz: ${note || "Keine Notiz"}`, ].join("\n"); } -export function sendMailLoan(user, items, startDate, endDate, createdDate) { +export function sendMailLoan( + user, + items, + startDate, + endDate, + createdDate, + note, +) { const transporter = nodemailer.createTransport({ host: process.env.MAIL_HOST, port: process.env.MAIL_PORT, @@ -170,8 +198,16 @@ export function sendMailLoan(user, items, startDate, endDate, createdDate) { startDate, endDate, createdDate, + note, + }), + html: buildLoanEmail({ + user, + items, + startDate, + endDate, + createdDate, + note, }), - html: buildLoanEmail({ user, items, startDate, endDate, createdDate }), }); console.log("Loan message sent:", info.messageId); From 3ba3c1c0cb2999550d47ef385779f0f1f9d4080a Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Fri, 20 Feb 2026 16:22:13 +0100 Subject: [PATCH 4/5] improved error logging for the api route to return or take loans --- backendV2/routes/api/api.database.js | 21 ++++++++++++++++----- backendV2/routes/api/api.route.js | 12 ++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/backendV2/routes/api/api.database.js b/backendV2/routes/api/api.database.js index b824ad4..d0a13d3 100644 --- a/backendV2/routes/api/api.database.js +++ b/backendV2/routes/api/api.database.js @@ -48,7 +48,8 @@ export const setReturnDateV2 = async (loanCode) => { [loanCode], ); - if (items.length === 0) return { success: false }; + if (items.length === 0) + return { success: false, message: "No items found for loan" }; const itemIds = Array.isArray(items[0].loaned_items_id) ? items[0].loaned_items_id @@ -71,11 +72,20 @@ export const setReturnDateV2 = async (loanCode) => { return { success: true, data: { returned: true } }; } catch (error) { console.error("setReturnDateV2 error:", error); - return { success: false }; + return { success: false, message: "Failed to set return date" }; } }; export const setTakeDateV2 = async (loanCode) => { + const [isTaken] = await pool.query( + "SELECT take_date FROM loans WHERE loan_code = ?", + [loanCode], + ); + + if (isTaken.length === 0 || isTaken[0].take_date !== null) { + return { success: false, message: "Loan not found or already taken" }; + } + const [items] = await pool.query( "SELECT loaned_items_id FROM loans WHERE loan_code = ?", [loanCode], @@ -86,7 +96,8 @@ export const setTakeDateV2 = async (loanCode) => { [loanCode], ); - if (items.length === 0) return { success: false }; + if (items.length === 0) + return { success: false, message: "No items found for loan" }; const itemIds = Array.isArray(items[0].loaned_items_id) ? items[0].loaned_items_id @@ -98,14 +109,14 @@ export const setTakeDateV2 = async (loanCode) => { ); const [result] = await pool.query( - "UPDATE loans SET take_date = NOW() WHERE loan_code = ?", + "UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL", [loanCode], ); if (result.affectedRows > 0 && setItemStates.affectedRows > 0) { return { success: true }; } - return { success: false }; + return { message: "Failed to set take date", success: false }; }; export const getAllLoansV2 = async () => { diff --git a/backendV2/routes/api/api.route.js b/backendV2/routes/api/api.route.js index 56a289d..fc638d8 100644 --- a/backendV2/routes/api/api.route.js +++ b/backendV2/routes/api/api.route.js @@ -47,7 +47,7 @@ router.get( } else { res.status(404).json({ message: "Loan not found" }); } - } + }, ); // Route for API to set the return date by the loan code @@ -58,11 +58,11 @@ router.post( const loanCode = req.params.loan_code; const result = await setReturnDateV2(loanCode); if (result.success) { - res.status(200).json({ data: result.data }); + res.status(200).json({}); } else { res.status(500).json({ message: "Failed to set return date" }); } - } + }, ); // Route for API to set the take away date by the loan code @@ -73,11 +73,11 @@ router.post( const loanCode = req.params.loan_code; const result = await setTakeDateV2(loanCode); if (result.success) { - res.status(200).json({ data: result.data }); + res.status(200).json({}); } else { - res.status(500).json({ message: "Failed to set take date" }); + res.status(500).json({ message: result.message }); } - } + }, ); // Route for API to open a door From 1fa8b4a9a722b2c0b985def0c6ca1aee808985f3 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Fri, 20 Feb 2026 16:30:20 +0100 Subject: [PATCH 5/5] refactor: clean up layout components and improve footer styling --- FrontendV2/src/App.tsx | 8 ++++---- FrontendV2/src/components/footer/Footer.tsx | 9 ++++++++- FrontendV2/src/pages/LoginPage.tsx | 4 +--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/FrontendV2/src/App.tsx b/FrontendV2/src/App.tsx index 400fe4f..58d81eb 100644 --- a/FrontendV2/src/App.tsx +++ b/FrontendV2/src/App.tsx @@ -12,7 +12,7 @@ import { triggerLogoutAtom } from "@/states/Atoms"; import { MyLoansPage } from "./pages/MyLoansPage"; import Landingpage from "./pages/Landingpage"; import { changeLanguage } from "i18next"; -import { Box, Flex } from "@chakra-ui/react"; +import { Flex } from "@chakra-ui/react"; import { Footer } from "./components/footer/Footer"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { API_BASE } from "@/config/api.config"; @@ -72,8 +72,8 @@ function App() { return ( - - + + @@ -88,7 +88,7 @@ function App() { - +