refactor: update API endpoints and enhance loan management features

This commit is contained in:
2025-11-17 22:49:54 +01:00
parent 88a2c74e88
commit 084a0fa2e2
13 changed files with 209 additions and 20 deletions

View File

@@ -49,9 +49,9 @@ export const createLoanInDatabase = async (
try {
await conn.beginTransaction();
// Ensure all items exist and collect names
// Ensure all items exist and collect names + lockers
const [itemsRows] = await conn.query(
"SELECT id, item_name FROM items WHERE id IN (?)",
"SELECT id, item_name, safe_nr FROM items WHERE id IN (?)",
[itemIds]
);
if (!itemsRows || itemsRows.length !== itemIds.length) {
@@ -62,12 +62,22 @@ export const createLoanInDatabase = async (
message: "One or more items not found",
};
}
const itemNames = itemIds
.map(
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name
)
.filter(Boolean);
// Build lockers array (unique, only 2-digit strings)
const lockers = [
...new Set(
itemsRows
.map((r) => r.safe_nr)
.filter((sn) => typeof sn === "string" && /^\d{2}$/.test(sn))
),
];
// Check availability (no overlap with existing loans)
const [confRows] = await conn.query(
`
@@ -113,18 +123,18 @@ export const createLoanInDatabase = async (
};
}
// Insert loan
// Insert loan (now includes lockers)
const [insertRes] = await conn.query(
`
INSERT INTO loans (username, loan_code, start_date, end_date, loaned_items_id, loaned_items_name, note)
VALUES (?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?)
INSERT INTO loans (username, loan_code, start_date, end_date, lockers, loaned_items_id, loaned_items_name, note)
VALUES (?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), CAST(? AS JSON), ?)
`,
[
username,
loanCode,
// Use DATETIME/TIMESTAMP friendly format
new Date(start).toISOString().slice(0, 19).replace("T", " "),
new Date(end).toISOString().slice(0, 19).replace("T", " "),
JSON.stringify(lockers),
JSON.stringify(itemIds.map((n) => Number(n))),
JSON.stringify(itemNames),
note,
@@ -142,6 +152,7 @@ export const createLoanInDatabase = async (
end_date: end,
items: itemIds,
item_names: itemNames,
lockers,
},
};
} catch (err) {
@@ -172,6 +183,70 @@ export const getLoansFromDatabase = async (username) => {
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
[username]
);
if (result.length > 0) {
return { success: true, status: true, data: result };
} else if (result.length === 0) {
return { success: true, status: true, data: [] };
}
return { success: false };
};
export const getBorrowableItemsFromDatabase = async (
startDate,
endDate,
role = 0
) => {
// Overlap if: loan.start < end AND effective_end > start
// effective_end is returned_date if set, otherwise end_date
const hasRoleFilter = Number(role) > 0;
const sql = `
SELECT i.*
FROM items i
WHERE ${hasRoleFilter ? "i.can_borrow_role >= ? AND " : ""}NOT EXISTS (
SELECT 1
FROM loans l
JOIN JSON_TABLE(l.loaned_items_id, '$[*]' COLUMNS (item_id INT PATH '$')) jt
WHERE jt.item_id = i.id
AND l.deleted = 0
AND l.start_date < ?
AND COALESCE(l.returned_date, l.end_date) > ?
);
`;
const params = hasRoleFilter
? [role, endDate, startDate]
: [endDate, startDate];
const [rows] = await pool.query(sql, params);
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
export const SETdeleteLoanFromDatabase = async (loanId) => {
const [result] = await pool.query(
"UPDATE loans SET deleted = 1 WHERE id = ?;",
[loanId]
);
if (result.affectedRows > 0) {
return { success: true };
} else {
return { success: false };
}
};
export const getALLLoans = async () => {
const [result] = await pool.query("SELECT * FROM loans WHERE deleted = 0;");
if (result.length > 0) {
return { success: true, data: result };
}
return { success: false };
};
export const getItems = async () => {
const [result] = await pool.query("SELECT * FROM items;");
if (result.length > 0) {
return { success: true, data: result };
}

View File

@@ -20,6 +20,22 @@ export const loginFunc = async (username, password) => {
return { success: false };
};
export const getItems = async () => {
const [rows] = await pool.query("SELECT * FROM items;");
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
export const getALLLoans = async () => {
const [rows] = await pool.query("SELECT * FROM loans;");
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
export const changePassword = async (username, oldPassword, newPassword) => {
// get user current password
const [user] = await pool.query(

View File

@@ -9,6 +9,10 @@ import {
createLoanInDatabase,
getLoanInfoWithID,
getLoansFromDatabase,
getBorrowableItemsFromDatabase,
getALLLoans,
getItems,
SETdeleteLoanFromDatabase,
} from "./database/loansMgmt.database.js";
import { sendMailLoan } from "./services/mailer.js";
@@ -85,9 +89,62 @@ router.get("/loans", authenticate, async (req, res) => {
const result = await getLoansFromDatabase(req.user.username);
if (result.success) {
res.status(200).json(result.data);
} else if (result.status) {
res.status(200).json([]);
} else {
res.status(500).json({ message: "Failed to fetch loans" });
}
});
router.get("/all-items", authenticate, async (req, res) => {
const result = await getItems();
if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ message: "Failed to fetch items" });
}
});
router.delete("/delete-loan/:id", authenticate, async (req, res) => {
const loanId = req.params.id;
const result = await SETdeleteLoanFromDatabase(loanId);
if (result.success) {
res.status(200).json({ message: "Loan deleted successfully" });
} else {
res.status(500).json({ message: "Failed to delete loan" });
}
});
router.get("/all-loans", authenticate, async (req, res) => {
const result = await getALLLoans();
if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ message: "Failed to fetch loans" });
}
});
router.post("/borrowable-items", authenticate, async (req, res) => {
const { startDate, endDate } = req.body || {};
if (!startDate || !endDate) {
return res
.status(400)
.json({ message: "startDate and endDate are required" });
}
const result = await getBorrowableItemsFromDatabase(
startDate,
endDate,
req.user.role
);
if (result.success) {
// return the array directly for consistency with /items
return res.status(200).json(result.data);
} else {
return res
.status(500)
.json({ message: "Failed to fetch borrowable items" });
}
});
export default router;

View File

@@ -1,3 +1,7 @@
import nodemailer from "nodemailer";
import dotenv from "dotenv";
dotenv.config();
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
const itemsList =
@@ -112,7 +116,7 @@ function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
].join("\n");
}
function sendMailLoan(user, items, startDate, endDate, createdDate) {
export function sendMailLoan(user, items, startDate, endDate, createdDate) {
const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,