This commit is contained in:
2026-05-06 16:56:58 +07:00
parent 9f14491562
commit 395b1f6e85
3 changed files with 101 additions and 3 deletions

View File

@@ -4254,7 +4254,8 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
NewQuantity, NewQuantity,
UsedQuantity, UsedQuantity,
Custodian, Custodian,
Borrower Borrower,
Notes
FROM AssetInventory WITH (UPDLOCK, ROWLOCK) FROM AssetInventory WITH (UPDLOCK, ROWLOCK)
WHERE AssetId = @assetId WHERE AssetId = @assetId
`); `);
@@ -4311,6 +4312,11 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
const nextNewQuantity = Math.max(stockBuckets.newQuantity - borrowFromNew, 0); const nextNewQuantity = Math.max(stockBuckets.newQuantity - borrowFromNew, 0);
const nextUsedQuantity = Math.max(stockBuckets.usedQuantity - borrowFromUsed, 0); const nextUsedQuantity = Math.max(stockBuckets.usedQuantity - borrowFromUsed, 0);
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextExportInPeriod); const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextExportInPeriod);
const existingAssetNotes = String(asset.Notes || '').trim();
const cleanExportNote = String(exportNote || '').trim();
const nextAssetNotes = cleanExportNote
? (existingAssetNotes ? `${existingAssetNotes}\n${cleanExportNote}` : cleanExportNote)
: (existingAssetNotes || null);
await new sql.Request(transaction) await new sql.Request(transaction)
.input('assetId', sql.Int, assetId) .input('assetId', sql.Int, assetId)
@@ -4322,6 +4328,7 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
.input('usedQuantity', sql.Int, nextUsedQuantity) .input('usedQuantity', sql.Int, nextUsedQuantity)
.input('status', sql.NVarChar, nextStatus) .input('status', sql.NVarChar, nextStatus)
.input('exportedBy', sql.NVarChar, exportedByName) .input('exportedBy', sql.NVarChar, exportedByName)
.input('notes', sql.NVarChar, nextAssetNotes)
.query(` .query(`
UPDATE AssetInventory UPDATE AssetInventory
SET Project = @project, SET Project = @project,
@@ -4332,6 +4339,7 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
UsedQuantity = @usedQuantity, UsedQuantity = @usedQuantity,
Status = @status, Status = @status,
ExportedBy = @exportedBy, ExportedBy = @exportedBy,
Notes = @notes,
UpdatedDate = GETDATE() UpdatedDate = GETDATE()
WHERE AssetId = @assetId WHERE AssetId = @assetId
`); `);

View File

@@ -66,6 +66,7 @@ class AccountManager {
this.pendingAssetRequestRejectId = undefined; this.pendingAssetRequestRejectId = undefined;
this.assetBorrowAutoRefreshTimer = undefined; this.assetBorrowAutoRefreshTimer = undefined;
this.pendingAssetRequestDeleteConfirmResolver = undefined; this.pendingAssetRequestDeleteConfirmResolver = undefined;
this.pendingBulkAssetDeleteConfirmResolver = undefined;
} }
configureNotifications() { configureNotifications() {
@@ -1228,6 +1229,21 @@ class AccountManager {
btn.dataset.boundClick = 'true'; btn.dataset.boundClick = 'true';
}); });
const confirmBulkAssetDeleteBtn = document.getElementById('confirmBulkAssetDeleteBtn');
if (confirmBulkAssetDeleteBtn && confirmBulkAssetDeleteBtn.dataset.boundClick !== 'true') {
confirmBulkAssetDeleteBtn.addEventListener('click', () => this.resolveBulkAssetDeleteConfirm(true));
confirmBulkAssetDeleteBtn.dataset.boundClick = 'true';
}
document.querySelectorAll('.cancel-bulk-asset-delete-confirm').forEach(btn => {
if (btn.dataset.boundClick === 'true') {
return;
}
btn.addEventListener('click', () => this.resolveBulkAssetDeleteConfirm(false));
btn.dataset.boundClick = 'true';
});
this.setupAssetBorrowRequestModalListeners(); this.setupAssetBorrowRequestModalListeners();
const assetDepartmentForm = document.getElementById('assetDepartmentForm'); const assetDepartmentForm = document.getElementById('assetDepartmentForm');
@@ -3358,6 +3374,42 @@ class AccountManager {
} }
} }
resolveBulkAssetDeleteConfirm(confirmed) {
const modal = document.getElementById('bulkDeleteAssetsConfirmModal');
if (modal) {
modal.classList.remove('open');
}
const resolver = this.pendingBulkAssetDeleteConfirmResolver;
this.pendingBulkAssetDeleteConfirmResolver = undefined;
if (typeof resolver === 'function') {
resolver(Boolean(confirmed));
}
}
async confirmBulkAssetDelete(selectedCount) {
const modal = document.getElementById('bulkDeleteAssetsConfirmModal');
const messageNode = document.getElementById('bulkDeleteAssetsConfirmMessage');
const countNode = document.getElementById('bulkDeleteAssetsConfirmCount');
if (!modal || !messageNode || !countNode) {
return window.confirm(`Bạn có chắc muốn xóa ${selectedCount} tài sản đã chọn?`);
}
countNode.textContent = String(selectedCount);
messageNode.textContent = `Bạn có chắc muốn xóa ${selectedCount} tài sản đã chọn?`;
if (this.pendingBulkAssetDeleteConfirmResolver) {
this.resolveBulkAssetDeleteConfirm(false);
}
modal.classList.add('open');
return new Promise(resolve => {
this.pendingBulkAssetDeleteConfirmResolver = resolve;
});
}
async confirmAssetRequestDelete(message, confirmButtonText = 'Xóa đơn') { async confirmAssetRequestDelete(message, confirmButtonText = 'Xóa đơn') {
const modal = document.getElementById('assetRequestDeleteConfirmModal'); const modal = document.getElementById('assetRequestDeleteConfirmModal');
const messageNode = document.getElementById('assetRequestDeleteConfirmMessage'); const messageNode = document.getElementById('assetRequestDeleteConfirmMessage');
@@ -3580,7 +3632,7 @@ class AccountManager {
</div> </div>
<button id="bulkDeleteAssetsBtn" class="border border-red-200 text-red-600 px-3 py-1.5 rounded-md text-[11px] font-bold flex items-center gap-1.5 transition-colors ${(selectedCount === 0 || !canManageAssets) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-red-50'}" ${(selectedCount === 0 || !canManageAssets) ? 'disabled' : ''}> <button id="bulkDeleteAssetsBtn" class="border border-red-200 text-red-600 px-3 py-1.5 rounded-md text-[11px] font-bold flex items-center gap-1.5 transition-colors ${(selectedCount === 0 || !canManageAssets) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-red-50'}" ${(selectedCount === 0 || !canManageAssets) ? 'disabled' : ''}>
<span class="material-symbols-outlined text-base">delete_sweep</span> <span class="material-symbols-outlined text-base">delete_sweep</span>
Xóa dã chọn (<span id="selectedAssetCount">${selectedCount}</span>) Xóa đã chọn (<span id="selectedAssetCount">${selectedCount}</span>)
</button> </button>
</div> </div>
@@ -3897,7 +3949,7 @@ class AccountManager {
return; return;
} }
const confirmed = window.confirm(`Bạn có chắc mudn xóa ${selectedIds.length} tài sản dã chọn?`); const confirmed = await this.confirmBulkAssetDelete(selectedIds.length);
if (!confirmed) { if (!confirmed) {
return; return;
} }
@@ -6148,6 +6200,9 @@ class AccountManager {
if (this.pendingAssetRequestDeleteConfirmResolver) { if (this.pendingAssetRequestDeleteConfirmResolver) {
this.resolveAssetRequestDeleteConfirm(false); this.resolveAssetRequestDeleteConfirm(false);
} }
if (this.pendingBulkAssetDeleteConfirmResolver) {
this.resolveBulkAssetDeleteConfirm(false);
}
document.querySelectorAll('.modal-backdrop').forEach(modal => { document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('open'); modal.classList.remove('open');
}); });
@@ -7058,6 +7113,9 @@ function closeAllModals() {
if (app?.pendingAssetRequestDeleteConfirmResolver) { if (app?.pendingAssetRequestDeleteConfirmResolver) {
app.resolveAssetRequestDeleteConfirm(false); app.resolveAssetRequestDeleteConfirm(false);
} }
if (app?.pendingBulkAssetDeleteConfirmResolver) {
app.resolveBulkAssetDeleteConfirm(false);
}
document.querySelectorAll('.modal-backdrop').forEach(modal => { document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('open'); modal.classList.remove('open');
}); });
@@ -7099,6 +7157,18 @@ function closeDeleteAssetModal() {
document.getElementById('deleteAssetModal').classList.remove('open'); document.getElementById('deleteAssetModal').classList.remove('open');
} }
function closeBulkDeleteAssetsConfirmModal() {
if (app?.pendingBulkAssetDeleteConfirmResolver) {
app.resolveBulkAssetDeleteConfirm(false);
return;
}
const modal = document.getElementById('bulkDeleteAssetsConfirmModal');
if (modal) {
modal.classList.remove('open');
}
}
function closeBorrowAssetModal() { function closeBorrowAssetModal() {
const modal = document.getElementById('borrowAssetModal'); const modal = document.getElementById('borrowAssetModal');
if (modal) { if (modal) {

View File

@@ -571,6 +571,26 @@
</div> </div>
</div> </div>
<!-- Bulk Delete Assets Confirm Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="bulkDeleteAssetsConfirmModal" style="z-index: 130;">
<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óa tài sản đã chọn</h3>
</div>
<div class="p-6">
<p id="bulkDeleteAssetsConfirmMessage" class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa các tài sản đã chọn?</p>
<div class="mb-4 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700">
Số lượng: <strong id="bulkDeleteAssetsConfirmCount">0</strong> tài sản
</div>
<div class="flex gap-3">
<button type="button" class="cancel-bulk-asset-delete-confirm flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeBulkDeleteAssetsConfirmModal()">Hủy</button>
<button type="button" id="confirmBulkAssetDeleteBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xóa</button>
</div>
</div>
</div>
</div>
<!-- Add/Edit Asset Department Modal --> <!-- Add/Edit Asset Department Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDepartmentModal"> <div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDepartmentModal">
<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="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">