From 70f3d1fdcc3a16f8144e9cc69ba7905bdedb946c Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Wed, 28 Jan 2026 13:06:03 +0100 Subject: [PATCH] added: User can return loan from web panel --- FrontendV2/src/pages/MyLoansPage.tsx | 109 +++++++++++++++++- FrontendV2/src/utils/i18n/locales/de/de.json | 7 +- .../routes/app/database/loansMgmt.database.js | 88 ++++++++++++-- backendV2/routes/app/loanMgmt.route.js | 28 ++++- 4 files changed, 215 insertions(+), 17 deletions(-) diff --git a/FrontendV2/src/pages/MyLoansPage.tsx b/FrontendV2/src/pages/MyLoansPage.tsx index b7d2bca..8fa5904 100644 --- a/FrontendV2/src/pages/MyLoansPage.tsx +++ b/FrontendV2/src/pages/MyLoansPage.tsx @@ -112,6 +112,86 @@ export const MyLoansPage = () => { return `${d}.${M}.${y} ${h}:${min}`; }; + const handleTakeAction = async (loanCode: string) => { + try { + const res = await fetch( + `${API_BASE}/api/loans/set-take-date/${loanCode}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${Cookies.get("token")}`, + }, + }, + ); + + if (!res.ok) { + setMsgStatus("error"); + setMsgTitle(t("error")); + setMsgDescription(t("error-take-loan")); + setIsMsg(true); + return; + } + + // Update the loan in state + setLoans((prev) => + prev.map((loan) => + loan.loan_code === loanCode + ? { ...loan, take_date: new Date().toISOString() } + : loan, + ), + ); + setMsgStatus("success"); + setMsgTitle(t("success")); + setMsgDescription(t("take-loan-success")); + setIsMsg(true); + } catch (e) { + setMsgStatus("error"); + setMsgTitle(t("error")); + setMsgDescription(t("network-error")); + setIsMsg(true); + } + }; + + const handleReturnAction = async (loanCode: string) => { + try { + const res = await fetch( + `${API_BASE}/api/loans/set-return-date/${loanCode}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${Cookies.get("token")}`, + }, + }, + ); + + if (!res.ok) { + setMsgStatus("error"); + setMsgTitle(t("error")); + setMsgDescription(t("error-return-loan")); + setIsMsg(true); + return; + } + + // Update the loan in state + setLoans((prev) => + prev.map((loan) => + loan.loan_code === loanCode + ? { ...loan, returned_date: new Date().toISOString() } + : loan, + ), + ); + setMsgStatus("success"); + setMsgTitle(t("success")); + setMsgDescription(t("return-loan-success")); + setIsMsg(true); + } catch (e) { + setMsgStatus("error"); + setMsgTitle(t("error")); + setMsgDescription(t("network-error")); + setIsMsg(true); + } + }; + return ( <> @@ -190,8 +270,33 @@ export const MyLoansPage = () => { : "-"} - {formatDate(loan.take_date)} - {formatDate(loan.returned_date)} + + {loan.take_date ? ( + formatDate(loan.take_date) + ) : ( + + )} + + + {loan.returned_date ? ( + formatDate(loan.returned_date) + ) : ( + + )} + {loan.note} diff --git a/FrontendV2/src/utils/i18n/locales/de/de.json b/FrontendV2/src/utils/i18n/locales/de/de.json index 95fa174..92f7882 100644 --- a/FrontendV2/src/utils/i18n/locales/de/de.json +++ b/FrontendV2/src/utils/i18n/locales/de/de.json @@ -81,5 +81,10 @@ "contactPage_messageLabel": "Nachricht", "contactPage_messagePlaceholder": "Geben Sie hier Ihre Nachricht ein...", "contactPage_messageErrorText": "Dieses Feld darf nicht leer sein.", - "contact": "Kontakt" + "contact": "Kontakt", + "take": "Abholen", + "return": "Zurückgeben", + "take-loan-success": "Ausleihe erfolgreich abgeholt", + "return-loan-success": "Ausleihe erfolgreich zurückgegeben", + "network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator." } \ No newline at end of file diff --git a/backendV2/routes/app/database/loansMgmt.database.js b/backendV2/routes/app/database/loansMgmt.database.js index 99cdbaa..3fdc67e 100644 --- a/backendV2/routes/app/database/loansMgmt.database.js +++ b/backendV2/routes/app/database/loansMgmt.database.js @@ -16,7 +16,7 @@ export const createLoanInDatabase = async ( startDate, endDate, note, - itemIds + itemIds, ) => { if (!username) return { success: false, code: "BAD_REQUEST", message: "Missing username" }; @@ -52,7 +52,7 @@ export const createLoanInDatabase = async ( // Ensure all items exist and collect names + lockers const [itemsRows] = await conn.query( "SELECT id, item_name, safe_nr FROM items WHERE id IN (?)", - [itemIds] + [itemIds], ); if (!itemsRows || itemsRows.length !== itemIds.length) { await conn.rollback(); @@ -65,7 +65,7 @@ export const createLoanInDatabase = async ( const itemNames = itemIds .map( - (id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name + (id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name, ) .filter(Boolean); @@ -80,9 +80,9 @@ export const createLoanInDatabase = async ( sn !== undefined && Number.isInteger(Number(sn)) && Number(sn) >= 0 && - Number(sn) <= 99 + Number(sn) <= 99, ) - .map((sn) => Number(sn)) + .map((sn) => Number(sn)), ), ]; @@ -98,7 +98,7 @@ export const createLoanInDatabase = async ( AND l.start_date < ? AND COALESCE(l.returned_date, l.end_date) > ? `, - [itemIds, end, start] + [itemIds, end, start], ); if (confRows?.[0]?.conflicts > 0) { await conn.rollback(); @@ -115,7 +115,7 @@ export const createLoanInDatabase = async ( const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits const [exists] = await conn.query( "SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1", - [candidate] + [candidate], ); if (exists.length === 0) { loanCode = candidate; @@ -146,7 +146,7 @@ export const createLoanInDatabase = async ( JSON.stringify(itemIds.map((n) => Number(n))), JSON.stringify(itemNames), note, - ] + ], ); await conn.commit(); @@ -189,7 +189,7 @@ export const getLoanInfoWithID = async (loanId) => { export const getLoansFromDatabase = async (username) => { const [result] = await pool.query( "SELECT * FROM loans WHERE username = ? AND deleted = 0;", - [username] + [username], ); if (result.length > 0) { return { success: true, status: true, data: result }; @@ -202,7 +202,7 @@ export const getLoansFromDatabase = async (username) => { export const getBorrowableItemsFromDatabase = async ( startDate, endDate, - role = 0 + role = 0, ) => { // Overlap if: loan.start < end AND effective_end > start // effective_end is returned_date if set, otherwise end_date @@ -236,7 +236,7 @@ export const getBorrowableItemsFromDatabase = async ( export const SETdeleteLoanFromDatabase = async (loanId) => { const [result] = await pool.query( "UPDATE loans SET deleted = 1 WHERE id = ?;", - [loanId] + [loanId], ); if (result.affectedRows > 0) { return { success: true }; @@ -260,3 +260,69 @@ export const getItems = async () => { } return { success: false }; }; + +export const setReturnDate = async (loanCode) => { + const [items] = await pool.query( + "SELECT loaned_items_id 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 }; + + const itemIds = Array.isArray(items[0].loaned_items_id) + ? items[0].loaned_items_id + : JSON.parse(items[0].loaned_items_id || "[]"); + + const [setItemStates] = await pool.query( + "UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)", + [owner[0].username, itemIds], + ); + + const [result] = await pool.query( + "UPDATE loans SET returned_date = NOW() WHERE loan_code = ?", + [loanCode], + ); + + if (result.affectedRows > 0 && setItemStates.affectedRows > 0) { + return { success: true }; + } + return { success: false }; +}; + +export const setTakeDate = async (loanCode) => { + const [items] = await pool.query( + "SELECT loaned_items_id 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 }; + + const itemIds = Array.isArray(items[0].loaned_items_id) + ? items[0].loaned_items_id + : JSON.parse(items[0].loaned_items_id || "[]"); + + const [setItemStates] = await pool.query( + "UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)", + [owner[0].username, itemIds], + ); + + const [result] = await pool.query( + "UPDATE loans SET take_date = NOW() WHERE loan_code = ?", + [loanCode], + ); + + if (result.affectedRows > 0 && setItemStates.affectedRows > 0) { + return { success: true }; + } + return { success: false }; +}; diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index d30f0f9..3ec5599 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -13,6 +13,8 @@ import { getALLLoans, getItems, SETdeleteLoanFromDatabase, + setReturnDate, + setTakeDate, } from "./database/loansMgmt.database.js"; import { sendMailLoan } from "./services/mailer.js"; @@ -48,7 +50,7 @@ router.post("/createLoan", authenticate, async (req, res) => { start, end, note, - itemIds + itemIds, ); if (result.success) { @@ -59,7 +61,7 @@ router.post("/createLoan", authenticate, async (req, res) => { mailInfo.data.loaned_items_name, mailInfo.data.start_date, mailInfo.data.end_date, - mailInfo.data.created_at + mailInfo.data.created_at, ); return res.status(201).json({ message: "Loan created successfully", @@ -96,6 +98,26 @@ router.get("/loans", authenticate, async (req, res) => { } }); +router.post("/set-return-date/:loan_code", authenticate, async (req, res) => { + const loanCode = req.params.loan_code; + const result = await setReturnDate(loanCode); + if (result.success) { + res.status(200).json({ data: result.data }); + } else { + res.status(500).json({ message: "Failed to set return date" }); + } +}); + +router.post("/set-take-date/:loan_code", authenticate, async (req, res) => { + const loanCode = req.params.loan_code; + const result = await setTakeDate(loanCode); + if (result.success) { + res.status(200).json({ data: result.data }); + } else { + res.status(500).json({ message: "Failed to set take date" }); + } +}); + router.get("/all-items", authenticate, async (req, res) => { const result = await getItems(); if (result.success) { @@ -135,7 +157,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => { const result = await getBorrowableItemsFromDatabase( startDate, endDate, - req.user.role + req.user.role, ); if (result.success) { // return the array directly for consistency with /items