added: User can return loan from web panel

This commit is contained in:
2026-01-28 13:06:03 +01:00
parent 4b08a574d8
commit 70f3d1fdcc
4 changed files with 215 additions and 17 deletions

View File

@@ -112,6 +112,86 @@ export const MyLoansPage = () => {
return `${d}.${M}.${y} ${h}:${min}`; 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 ( return (
<> <>
<Container className="px-6 sm:px-8 pt-10"> <Container className="px-6 sm:px-8 pt-10">
@@ -190,8 +270,33 @@ export const MyLoansPage = () => {
: "-"} : "-"}
</Text> </Text>
</Table.Cell> </Table.Cell>
<Table.Cell>{formatDate(loan.take_date)}</Table.Cell> <Table.Cell>
<Table.Cell>{formatDate(loan.returned_date)}</Table.Cell> {loan.take_date ? (
formatDate(loan.take_date)
) : (
<Button
size="xs"
colorPalette="teal"
onClick={() => handleTakeAction(loan.loan_code)}
>
{t("take")}
</Button>
)}
</Table.Cell>
<Table.Cell>
{loan.returned_date ? (
formatDate(loan.returned_date)
) : (
<Button
size="xs"
colorPalette="blue"
onClick={() => handleReturnAction(loan.loan_code)}
disabled={!loan.take_date}
>
{t("return")}
</Button>
)}
</Table.Cell>
<Table.Cell>{loan.note}</Table.Cell> <Table.Cell>{loan.note}</Table.Cell>
<Table.Cell> <Table.Cell>
<Dialog.Root role="alertdialog"> <Dialog.Root role="alertdialog">

View File

@@ -81,5 +81,10 @@
"contactPage_messageLabel": "Nachricht", "contactPage_messageLabel": "Nachricht",
"contactPage_messagePlaceholder": "Geben Sie hier Ihre Nachricht ein...", "contactPage_messagePlaceholder": "Geben Sie hier Ihre Nachricht ein...",
"contactPage_messageErrorText": "Dieses Feld darf nicht leer sein.", "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."
} }

View File

@@ -16,7 +16,7 @@ export const createLoanInDatabase = async (
startDate, startDate,
endDate, endDate,
note, note,
itemIds itemIds,
) => { ) => {
if (!username) if (!username)
return { success: false, code: "BAD_REQUEST", message: "Missing 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 // Ensure all items exist and collect names + lockers
const [itemsRows] = await conn.query( const [itemsRows] = await conn.query(
"SELECT id, item_name, safe_nr FROM items WHERE id IN (?)", "SELECT id, item_name, safe_nr FROM items WHERE id IN (?)",
[itemIds] [itemIds],
); );
if (!itemsRows || itemsRows.length !== itemIds.length) { if (!itemsRows || itemsRows.length !== itemIds.length) {
await conn.rollback(); await conn.rollback();
@@ -65,7 +65,7 @@ export const createLoanInDatabase = async (
const itemNames = itemIds const itemNames = itemIds
.map( .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); .filter(Boolean);
@@ -80,9 +80,9 @@ export const createLoanInDatabase = async (
sn !== undefined && sn !== undefined &&
Number.isInteger(Number(sn)) && Number.isInteger(Number(sn)) &&
Number(sn) >= 0 && 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 l.start_date < ?
AND COALESCE(l.returned_date, l.end_date) > ? AND COALESCE(l.returned_date, l.end_date) > ?
`, `,
[itemIds, end, start] [itemIds, end, start],
); );
if (confRows?.[0]?.conflicts > 0) { if (confRows?.[0]?.conflicts > 0) {
await conn.rollback(); await conn.rollback();
@@ -115,7 +115,7 @@ export const createLoanInDatabase = async (
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
const [exists] = await conn.query( const [exists] = await conn.query(
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1", "SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
[candidate] [candidate],
); );
if (exists.length === 0) { if (exists.length === 0) {
loanCode = candidate; loanCode = candidate;
@@ -146,7 +146,7 @@ export const createLoanInDatabase = async (
JSON.stringify(itemIds.map((n) => Number(n))), JSON.stringify(itemIds.map((n) => Number(n))),
JSON.stringify(itemNames), JSON.stringify(itemNames),
note, note,
] ],
); );
await conn.commit(); await conn.commit();
@@ -189,7 +189,7 @@ export const getLoanInfoWithID = async (loanId) => {
export const getLoansFromDatabase = async (username) => { export const getLoansFromDatabase = async (username) => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT * FROM loans WHERE username = ? AND deleted = 0;", "SELECT * FROM loans WHERE username = ? AND deleted = 0;",
[username] [username],
); );
if (result.length > 0) { if (result.length > 0) {
return { success: true, status: true, data: result }; return { success: true, status: true, data: result };
@@ -202,7 +202,7 @@ export const getLoansFromDatabase = async (username) => {
export const getBorrowableItemsFromDatabase = async ( export const getBorrowableItemsFromDatabase = async (
startDate, startDate,
endDate, endDate,
role = 0 role = 0,
) => { ) => {
// Overlap if: loan.start < end AND effective_end > start // Overlap if: loan.start < end AND effective_end > start
// effective_end is returned_date if set, otherwise end_date // effective_end is returned_date if set, otherwise end_date
@@ -236,7 +236,7 @@ export const getBorrowableItemsFromDatabase = async (
export const SETdeleteLoanFromDatabase = async (loanId) => { export const SETdeleteLoanFromDatabase = async (loanId) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE loans SET deleted = 1 WHERE id = ?;", "UPDATE loans SET deleted = 1 WHERE id = ?;",
[loanId] [loanId],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { success: true }; return { success: true };
@@ -260,3 +260,69 @@ export const getItems = async () => {
} }
return { success: false }; 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 };
};

View File

@@ -13,6 +13,8 @@ import {
getALLLoans, getALLLoans,
getItems, getItems,
SETdeleteLoanFromDatabase, SETdeleteLoanFromDatabase,
setReturnDate,
setTakeDate,
} from "./database/loansMgmt.database.js"; } from "./database/loansMgmt.database.js";
import { sendMailLoan } from "./services/mailer.js"; import { sendMailLoan } from "./services/mailer.js";
@@ -48,7 +50,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
start, start,
end, end,
note, note,
itemIds itemIds,
); );
if (result.success) { if (result.success) {
@@ -59,7 +61,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
mailInfo.data.loaned_items_name, mailInfo.data.loaned_items_name,
mailInfo.data.start_date, mailInfo.data.start_date,
mailInfo.data.end_date, mailInfo.data.end_date,
mailInfo.data.created_at mailInfo.data.created_at,
); );
return res.status(201).json({ return res.status(201).json({
message: "Loan created successfully", 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) => { router.get("/all-items", authenticate, async (req, res) => {
const result = await getItems(); const result = await getItems();
if (result.success) { if (result.success) {
@@ -135,7 +157,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
const result = await getBorrowableItemsFromDatabase( const result = await getBorrowableItemsFromDatabase(
startDate, startDate,
endDate, endDate,
req.user.role req.user.role,
); );
if (result.success) { if (result.success) {
// return the array directly for consistency with /items // return the array directly for consistency with /items