refactor: update API endpoints and enhance loan management features
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from "express";
|
||||
import cors from "cors";
|
||||
import env from "dotenv";
|
||||
import info from "./info.json" assert { type: "json" };
|
||||
import { authenticate } from "./services/authentication.js";
|
||||
|
||||
// frontend routes
|
||||
import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
|
||||
@@ -46,6 +47,10 @@ app.listen(port, () => {
|
||||
console.log(`Server is running on port: ${port}`);
|
||||
});
|
||||
|
||||
app.get("/verify", authenticate, async (req, res) => {
|
||||
res.status(200).json({ message: "Token is valid", user: req.user });
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.send(info);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user