+
-
);
};
diff --git a/FrontendV2/src/utils/Fetcher.ts b/FrontendV2/src/utils/Fetcher.ts
index 035de1f..a684490 100644
--- a/FrontendV2/src/utils/Fetcher.ts
+++ b/FrontendV2/src/utils/Fetcher.ts
@@ -3,7 +3,7 @@ import { API_BASE } from "@/config/api.config";
export const getBorrowableItems = async (
startDate: string,
- endDate: string
+ endDate: string,
) => {
try {
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
@@ -22,7 +22,7 @@ export const getBorrowableItems = async (
status: "error",
title: "Server error",
description:
- "Ein Fehler ist auf dem Server aufgetreten. Manchmal hilft es, die Seite neu zu laden.",
+ "An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.",
};
}
@@ -48,7 +48,7 @@ export const createLoan = async (
itemIds: number[],
startDate: string,
endDate: string,
- note: string | null
+ note: string | null,
) => {
const response = await fetch(`${API_BASE}/api/loans/createLoan`, {
method: "POST",
diff --git a/FrontendV2/src/utils/i18n/locales/de/de.json b/FrontendV2/src/utils/i18n/locales/de/de.json
index 5d39c80..f6ed2dc 100644
--- a/FrontendV2/src/utils/i18n/locales/de/de.json
+++ b/FrontendV2/src/utils/i18n/locales/de/de.json
@@ -84,6 +84,7 @@
"contact": "Kontakt",
"take": "Abholen",
"return": "Zurückgeben",
+ "serverError": "Serverfehler. Bitte versuchen Sie es später erneut, oder laden Sie die Seite neu.",
"take-loan-success": "Ausleihe erfolgreich abgeholt",
"return-loan-success": "Ausleihe erfolgreich zurückgegeben",
"network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.",
diff --git a/FrontendV2/src/utils/i18n/locales/en/en.json b/FrontendV2/src/utils/i18n/locales/en/en.json
index 13dec04..6a52916 100644
--- a/FrontendV2/src/utils/i18n/locales/en/en.json
+++ b/FrontendV2/src/utils/i18n/locales/en/en.json
@@ -82,6 +82,7 @@
"contactPage_messagePlaceholder": "Enter your message here...",
"contactPage_messageErrorText": "This field cannot be empty.",
"contact": "Contact",
+ "serverError": "Server error. Please try again later, or refresh the page.",
"take": "Take",
"return": "Return",
"take-loan-success": "Loan taken successfully",
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"]
}
diff --git a/backendV2/routes/api/api.database.js b/backendV2/routes/api/api.database.js
index d80365c..d0a13d3 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,50 +42,62 @@ 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, message: "No items found for loan" };
- 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, message: "Failed to set return date" };
}
- return { success: false };
};
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]
+ [loanCode],
);
const [owner] = await pool.query(
"SELECT username FROM loans WHERE loan_code = ?",
- [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
@@ -93,18 +105,18 @@ 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]
+ "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 () => {
@@ -118,12 +130,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/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
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 }) {