fix mượn trả

This commit is contained in:
2026-04-25 09:24:35 +07:00
parent bc7a484a01
commit 4fb7f412bf
5 changed files with 197 additions and 26 deletions

View File

@@ -3230,42 +3230,67 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
}
});
app.delete('/api/asset-borrows/:id', requireAssetOrAdmin, async (req, res) => {
app.delete('/api/asset-borrows/:id', async (req, res) => {
try {
const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole);
const requesterId = getUserIdFromRequest(req);
const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset';
const borrowId = Number(req.params.id);
if (!Number.isInteger(borrowId) || borrowId <= 0) {
return res.status(400).json({ success: false, message: 'Mã đơn không hp l' });
return res.status(400).json({ success: false, message: 'Ma don khong hop le' });
}
if (!canManageRequests && (!Number.isInteger(requesterId) || requesterId <= 0)) {
return res.status(401).json({ success: false, message: 'Yeu cau xac thuc nguoi dung' });
}
const deleteResult = await pool.request()
.input('borrowId', sql.Int, borrowId)
.input('requesterId', sql.Int, requesterId || -1)
.query(`
DELETE FROM AssetBorrowRequests
OUTPUT DELETED.BorrowId
WHERE BorrowId = @borrowId
AND LOWER(LTRIM(RTRIM(ISNULL(RequestStatus, '')))) = 'pending'
${canManageRequests ? '' : 'AND CreatedBy = @requesterId'}
`);
if (Array.isArray(deleteResult.recordset) && deleteResult.recordset.length > 0) {
return res.json({ success: true, message: 'Đã xóa đơn ch' });
return res.json({ success: true, message: 'Da huy don cho' });
}
const existed = await pool.request()
.input('borrowId', sql.Int, borrowId)
.query(`
SELECT TOP 1 BorrowId, RequestStatus
SELECT TOP 1 BorrowId, RequestStatus, CreatedBy
FROM AssetBorrowRequests
WHERE BorrowId = @borrowId
`);
const row = existed.recordset?.[0];
if (!row) {
return res.status(404).json({ success: false, message: 'Không tìm thy đơn cn xóa' });
return res.status(404).json({ success: false, message: 'Khong tim thay don can xoa' });
}
if (!canManageRequests && Number(row.CreatedBy) !== requesterId) {
return res.status(403).json({
success: false,
message: 'Ban chi duoc huy don do chinh minh tao'
});
}
const currentStatus = normalizeAssetRequestStatus(row.RequestStatus);
if (currentStatus !== 'pending') {
return res.status(400).json({
success: false,
message: 'Chi duoc huy don o trang thai cho xu ly'
});
}
return res.status(400).json({
success: false,
message: 'Chỉ được xóa đơn ở trạng thái chờ xử lý'
message: 'Khong the huy don vao luc nay'
});
} catch (err) {
return res.status(500).json({ success: false, message: err.message });

File diff suppressed because one or more lines are too long

View File

@@ -59,6 +59,7 @@ class AccountManager {
this.assetBorrowRequestType = 'borrow';
this.pendingAssetRequestRejectId = undefined;
this.assetBorrowAutoRefreshTimer = undefined;
this.pendingAssetRequestDeleteConfirmResolver = undefined;
}
configureNotifications() {
@@ -1008,6 +1009,21 @@ class AccountManager {
}
}
const confirmAssetRequestDeleteBtn = document.getElementById('confirmAssetRequestDeleteBtn');
if (confirmAssetRequestDeleteBtn && confirmAssetRequestDeleteBtn.dataset.boundClick !== 'true') {
confirmAssetRequestDeleteBtn.addEventListener('click', () => this.resolveAssetRequestDeleteConfirm(true));
confirmAssetRequestDeleteBtn.dataset.boundClick = 'true';
}
document.querySelectorAll('.cancel-asset-request-delete-confirm').forEach(btn => {
if (btn.dataset.boundClick === 'true') {
return;
}
btn.addEventListener('click', () => this.resolveAssetRequestDeleteConfirm(false));
btn.dataset.boundClick = 'true';
});
this.setupAssetBorrowRequestModalListeners();
const assetDepartmentForm = document.getElementById('assetDepartmentForm');
@@ -2151,6 +2167,11 @@ class AccountManager {
const statusMeta = this.getAssetRequestStatusMeta(item.RequestStatus);
const note = String(item?.RequestNote || '').trim();
const rejectReason = String(item?.RejectReason || '').trim();
const canCancel = this.canCurrentUserCancelAssetRequest(item);
const requestId = Number(item?.BorrowId) || 0;
const cancelActionHtml = canCancel
? `<button class="asset-borrow-cancel-btn px-3 py-1.5 rounded-md bg-red-600 hover:bg-red-700 text-white text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Hủy đơn</button>`
: `<span class="text-xs text-slate-400">-</span>`;
return `
<tr class="hover:bg-slate-50/80 transition-colors">
@@ -2171,14 +2192,33 @@ class AccountManager {
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(item.BorrowDate)}</td>
<td class="px-4 py-3 text-sm text-slate-600 max-w-xs">${this.escapeHtml(note || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600 max-w-xs">${this.escapeHtml(rejectReason || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600">${cancelActionHtml}</td>
</tr>
`;
}
canCurrentUserCancelAssetRequest(item) {
const status = this.normalizeAssetRequestStatus(item?.RequestStatus);
if (status !== 'pending') {
return false;
}
if (this.canCurrentUserManageAssets()) {
return true;
}
const currentUserId = Number(this.getUserId());
const createdBy = Number(item?.CreatedBy);
return Number.isFinite(currentUserId)
&& currentUserId > 0
&& Number.isFinite(createdBy)
&& createdBy === currentUserId;
}
buildAssetBorrowEmptyRowHtml() {
return `
<tr>
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có đơn mượn/trả tài sản nào.</td>
<td colspan="11" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có đơn mượn/trả tài sản nào.</td>
</tr>
`;
}
@@ -2235,7 +2275,7 @@ class AccountManager {
>
<span class="material-symbols-outlined text-base">notifications_active</span>
Đơn chờ
<span id="pendingAssetBorrowsCountBadge" class="${pendingCount > 0 ? '' : 'hidden'} absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-full bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">${pendingCount > 99 ? '99+' : pendingCount}</span>
<span id="pendingAssetBorrowsCountBadge" class="${pendingCount > 0 ? '' : 'hidden'} absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-[999px] bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">${pendingCount > 99 ? '99+' : pendingCount}</span>
</button>
` : ''}
</div>
@@ -2268,6 +2308,7 @@ class AccountManager {
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Lý do</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Hành động</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 asset-borrows-table-body">
@@ -2339,6 +2380,30 @@ class AccountManager {
searchInput.dataset.boundInput = 'true';
}
const tableBody = document.querySelector('.asset-borrows-table-body');
if (tableBody && tableBody.dataset.boundActions !== 'true') {
tableBody.addEventListener('click', (event) => {
const cancelButton = event.target.closest('.asset-borrow-cancel-btn');
if (!cancelButton) {
return;
}
const requestId = Number(cancelButton.dataset.requestId);
if (!Number.isFinite(requestId) || requestId <= 0) {
return;
}
this.deletePendingAssetBorrowRequest(requestId, {
confirmMessage: `Bạn có chắc muốn hủy đơn #${requestId}?`,
confirmButtonText: 'Hủy đơn',
successMessage: 'Đã hủy đơn thành công',
failureMessage: 'Hủy đơn thất bại'
});
});
tableBody.dataset.boundActions = 'true';
}
this.setupAssetBorrowPagerListeners();
}
@@ -2489,6 +2554,7 @@ class AccountManager {
this.notifyFailure('Tạo đơn thất bại');
}
}
buildPendingAssetRequestCardHtml(item) {
const typeMeta = this.getAssetRequestTypeMeta(item?.RequestType);
const dateLabel = typeMeta.value === 'return' ? 'Ngày trả' : 'Ngày mượn';
@@ -2508,10 +2574,10 @@ buildPendingAssetRequestCardHtml(item) {
<div><span class="font-bold">Ghi chú:</span> ${this.escapeHtml(note || '-')}</div>
<div><span class="font-bold">${dateLabel}:</span> ${this.formatDateOnly(item?.BorrowDate)}</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 pt-1">
<button class="asset-request-approve-btn w-full px-3 py-1.5 rounded-md bg-primary hover:bg-primary-dim text-on-primary text-xs font-bold" data-request-id="${requestId}">Chấp nhận</button>
<button class="asset-request-reject-btn w-full px-3 py-1.5 rounded-md bg-red-600 hover:bg-red-700 text-white text-xs font-bold" data-request-id="${requestId}">Từ chối</button>
<button class="asset-request-delete-btn w-full px-3 py-1.5 rounded-md bg-slate-700 hover:bg-slate-800 text-white text-xs font-bold" data-request-id="${requestId}">Xóa đơn</button>
<div class="flex items-center gap-2 pt-1">
<button class="asset-request-approve-btn flex-1 min-w-0 px-3 py-1.5 rounded-md bg-primary hover:bg-primary-dim text-on-primary text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Chấp nhận</button>
<button class="asset-request-reject-btn flex-1 min-w-0 px-3 py-1.5 rounded-md bg-red-600 hover:bg-red-700 text-white text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Từ chối</button>
<button class="asset-request-delete-btn flex-1 min-w-0 px-3 py-1.5 rounded-md bg-slate-700 hover:bg-slate-800 text-white text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Xóa đơn</button>
</div>
</div>
`;
@@ -2639,6 +2705,43 @@ buildPendingAssetRequestCardHtml(item) {
reasonInput.focus();
}
resolveAssetRequestDeleteConfirm(confirmed) {
const modal = document.getElementById('assetRequestDeleteConfirmModal');
if (modal) {
modal.classList.remove('open');
}
const resolver = this.pendingAssetRequestDeleteConfirmResolver;
this.pendingAssetRequestDeleteConfirmResolver = undefined;
if (typeof resolver === 'function') {
resolver(Boolean(confirmed));
}
}
async confirmAssetRequestDelete(message, confirmButtonText = 'Xóa đơn') {
const modal = document.getElementById('assetRequestDeleteConfirmModal');
const messageNode = document.getElementById('assetRequestDeleteConfirmMessage');
const confirmButton = document.getElementById('confirmAssetRequestDeleteBtn');
if (!modal || !messageNode || !confirmButton) {
return window.confirm(message);
}
messageNode.textContent = String(message || 'Bạn có chắc muốn thực hiện thao tác này?');
confirmButton.textContent = String(confirmButtonText || 'Xóa đơn');
if (this.pendingAssetRequestDeleteConfirmResolver) {
this.resolveAssetRequestDeleteConfirm(false);
}
modal.style.zIndex = '140';
modal.classList.add('open');
return new Promise(resolve => {
this.pendingAssetRequestDeleteConfirmResolver = resolve;
});
}
async handleAssetRequestRejectSubmit(event) {
event.preventDefault();
@@ -2686,7 +2789,10 @@ buildPendingAssetRequestCardHtml(item) {
&& failureMessage.toLowerCase().includes('xóa đơn chờ');
if (canAutoSuggestDelete) {
const shouldDelete = window.confirm(`Đơn #${requestId} không còn hợp lệ. Bạn có muốn xóa đơn chờ này không?`);
const shouldDelete = await this.confirmAssetRequestDelete(
`Đơn #${requestId} không còn hợp lệ. Bạn có muốn xóa đơn chờ này không?`,
'Xóa đơn'
);
if (shouldDelete) {
await this.deletePendingAssetBorrowRequest(requestId);
}
@@ -2722,19 +2828,18 @@ buildPendingAssetRequestCardHtml(item) {
}
}
async deletePendingAssetBorrowRequest(requestId) {
if (!this.canCurrentUserManageAssets()) {
this.notifyWarning('Chỉ role Asset/Admin mới được xóa đơn chờ.');
return;
}
async deletePendingAssetBorrowRequest(requestId, options = {}) {
const targetId = Number(requestId);
if (!Number.isFinite(targetId) || targetId <= 0) {
this.notifyWarning('Không xác định được đơn cần xóa.');
return;
}
const confirmed = window.confirm(`Bạn có chắc muốn xóa đơn chờ #${targetId}?`);
const confirmMessage = String(options?.confirmMessage || `Bạn có chắc muốn xóa đơn chờ #${targetId}?`);
const confirmButtonText = String(options?.confirmButtonText || 'Xóa đơn');
const successMessage = String(options?.successMessage || 'Đã xóa đơn chờ');
const failureMessage = String(options?.failureMessage || 'Xóa đơn chờ thất bại');
const confirmed = await this.confirmAssetRequestDelete(confirmMessage, confirmButtonText);
if (!confirmed) {
return;
}
@@ -2747,11 +2852,16 @@ buildPendingAssetRequestCardHtml(item) {
const data = await response.json();
if (!response.ok || !data.success) {
this.notifyFailure(data.message || 'Xóa đơn chờ thất bại');
const resolvedFailureMessage = data.message || failureMessage;
if (response.status === 403) {
this.notifyWarning(resolvedFailureMessage);
} else {
this.notifyFailure(resolvedFailureMessage);
}
return;
}
this.notifySuccess(data.message || 'Đã xóa đơn chờ');
this.notifySuccess(data.message || successMessage);
await this.fetchAssetBorrows();
if (this.currentPage === 'asset-borrows') {
@@ -2766,7 +2876,7 @@ buildPendingAssetRequestCardHtml(item) {
this.updatePendingAssetRequestsBadge();
} catch (err) {
console.error(err);
this.notifyFailure('Xóa đơn chờ thất bại');
this.notifyFailure(failureMessage);
}
}
getAssetsContent() {
@@ -5279,6 +5389,9 @@ buildPendingAssetRequestCardHtml(item) {
}
closeModals() {
if (this.pendingAssetRequestDeleteConfirmResolver) {
this.resolveAssetRequestDeleteConfirm(false);
}
document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('open');
});
@@ -6186,6 +6299,9 @@ buildPendingAssetRequestCardHtml(item) {
// Global modal close functions
function closeAllModals() {
if (app?.pendingAssetRequestDeleteConfirmResolver) {
app.resolveAssetRequestDeleteConfirm(false);
}
document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('open');
});
@@ -6250,6 +6366,7 @@ function closeAssetPendingRequestsModal() {
if (modal) {
modal.classList.remove('open');
}
closeAssetRequestDeleteConfirmModal();
const rejectModal = document.getElementById('assetRequestRejectModal');
if (rejectModal) {
rejectModal.classList.remove('open');
@@ -6263,6 +6380,18 @@ function closeAssetRequestRejectModal() {
}
}
function closeAssetRequestDeleteConfirmModal() {
if (app?.pendingAssetRequestDeleteConfirmResolver) {
app.resolveAssetRequestDeleteConfirm(false);
return;
}
const modal = document.getElementById('assetRequestDeleteConfirmModal');
if (modal) {
modal.classList.remove('open');
}
}
function closeAssetDepartmentModal() {
const modal = document.getElementById('assetDepartmentModal');
if (modal) {

View File

@@ -454,6 +454,23 @@
</div>
</div>
<!-- Confirm Delete/Cancel Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[120] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestDeleteConfirmModal" style="z-index: 140;">
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
<h3 class="text-base font-extrabold text-red-700">Xác nhận thao tác</h3>
</div>
<div class="p-6">
<p id="assetRequestDeleteConfirmMessage" class="text-sm text-slate-600 mb-6">Bạn có chắc muốn thực hiện thao tác này?</p>
<div class="flex gap-3">
<button type="button" class="cancel-asset-request-delete-confirm flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg">Đóng</button>
<button type="button" id="confirmAssetRequestDeleteBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xóa đơn</button>
</div>
</div>
</div>
</div>
<!-- Reject Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestRejectModal" style="z-index: 130;">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">

View File

@@ -280,7 +280,7 @@
<button id="pendingAssetRequestsBtn" type="button" class="hidden relative flex items-center gap-2 px-3 py-2 rounded-lg border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-800 transition-colors" title="Đơn chờ xử lý">
<span class="material-symbols-outlined text-base">notifications_active</span>
<span class="text-xs font-bold">Đơn chờ</span>
<span id="pendingAssetRequestsBadge" class="hidden absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-full bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">0</span>
<span id="pendingAssetRequestsBadge" class="hidden absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-[999px] bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">0</span>
</button>
<button id="profileBtn" type="button" class="profile-btn flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Sửa hồ sơ">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>