case hỏng

This commit is contained in:
2026-05-15 10:51:39 +07:00
parent 12c6380ceb
commit 41e523ff35
4 changed files with 815 additions and 4 deletions

View File

@@ -53,6 +53,7 @@ class AccountManager {
this.assetProjects = [];
this.assetProjectSearchTerm = '';
this.assetExportHistories = [];
this.assetDamageHistories = [];
this.selectedAssetIds = new Set();
this.mobileBreakpoint = 900;
this.boundResizeHandler = null;
@@ -71,6 +72,7 @@ class AccountManager {
this.assetBorrowAutoRefreshTimer = undefined;
this.pendingAssetRequestDeleteConfirmResolver = undefined;
this.pendingBulkAssetDeleteConfirmResolver = undefined;
this.pendingAssetDamageId = undefined;
}
configureNotifications() {
@@ -1072,6 +1074,114 @@ class AccountManager {
this.renderAssetExportHistoryModal();
}
normalizeAssetDamageType(value) {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'disposed' || normalized === 'thanh_ly' || normalized === 'thanh ly'
? 'disposed'
: 'damaged';
}
getAssetDamageTypeMeta(value) {
const type = this.normalizeAssetDamageType(value);
if (type === 'disposed') {
return {
value: 'disposed',
label: 'Thanh lý',
className: 'bg-slate-100 text-slate-700 border border-slate-200'
};
}
return {
value: 'damaged',
label: 'Hỏng',
className: 'bg-red-100 text-red-700 border border-red-200'
};
}
async fetchAssetDamageHistories(limit = 300) {
try {
const safeLimit = Number.isFinite(Number(limit)) ? Math.max(1, Math.min(Number(limit), 2000)) : 300;
const res = await fetch(`${this.apiBase}/asset-damage-disposal-history?limit=${safeLimit}`, {
headers: this.getAuthHeaders(false)
});
const data = await res.json();
if (data.success) {
this.assetDamageHistories = Array.isArray(data.data) ? data.data : [];
} else {
console.error('Load asset damage/disposal history failed:', data.message);
}
} catch (err) {
console.error('Fetch asset damage/disposal history error:', err);
}
}
buildAssetDamageHistoryRowsHtml(rows = []) {
if (!Array.isArray(rows) || rows.length === 0) {
return `
<tr>
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu tài sản hỏng/thanh lý.</td>
</tr>
`;
}
return rows.map(item => {
const typeMeta = this.getAssetDamageTypeMeta(item?.ActionType);
const assetLabel = [String(item?.AssetCode || '').trim(), String(item?.AssetName || '').trim()]
.filter(Boolean)
.join(' - ') || '-';
const unit = String(item?.Unit || '').trim();
const quantityLabel = `${Number(item?.ActionQuantity) || 0}${unit ? ` ${unit}` : ''}`;
return `
<tr class="hover:bg-slate-50/80 transition-colors">
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateTime(item?.ActionDate || item?.CreatedDate)}</td>
<td class="px-4 py-3 text-sm">
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-semibold ${typeMeta.className}">${typeMeta.label}</span>
</td>
<td class="px-4 py-3 text-sm text-slate-700">${this.escapeHtml(assetLabel)}</td>
<td class="px-4 py-3 text-sm text-slate-700 font-semibold">${this.escapeHtml(quantityLabel)}</td>
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousQuantity) || 0} -> ${Number(item?.NextQuantity) || 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousEndingBalance) || 0} -> ${Number(item?.NextEndingBalance) || 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousNewQuantity) || 0} -> ${Number(item?.NextNewQuantity) || 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousUsedQuantity) || 0} -> ${Number(item?.NextUsedQuantity) || 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(item?.CreatedByName || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600 whitespace-pre-line">${this.escapeHtml(item?.ActionNote || '-')}</td>
</tr>
`;
}).join('');
}
renderAssetDamageHistoryModal() {
const tbody = document.getElementById('assetDamageHistoryTableBody');
if (!tbody) {
return;
}
tbody.innerHTML = this.buildAssetDamageHistoryRowsHtml(this.assetDamageHistories);
}
async openAssetDamageHistoryModal() {
if (!this.ensureAssetManagePermission('xem danh sach tai san hong/thanh ly')) {
return;
}
const modal = document.getElementById('assetDamageHistoryModal');
const tbody = document.getElementById('assetDamageHistoryTableBody');
if (!modal || !tbody) {
this.notifyFailure('Không tìm thấy bảng tài sản hỏng/thanh lý.');
return;
}
tbody.innerHTML = `
<tr>
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Đang tải dữ liệu tài sản hỏng/thanh lý...</td>
</tr>
`;
modal.classList.add('open');
await this.fetchAssetDamageHistories();
this.renderAssetDamageHistoryModal();
}
async fetchRoles() {
try {
const res = await fetch(`${this.apiBase}/roles`);
@@ -1244,6 +1354,14 @@ class AccountManager {
}
}
const assetDamageForm = document.getElementById('assetDamageForm');
if (assetDamageForm) {
if (!assetDamageForm.dataset.boundSubmit) {
assetDamageForm.addEventListener('submit', (e) => this.handleAssetDamageSubmit(e));
assetDamageForm.dataset.boundSubmit = 'true';
}
}
const assetBorrowRequestForm = document.getElementById('assetBorrowRequestForm');
if (assetBorrowRequestForm) {
if (!assetBorrowRequestForm.dataset.boundSubmit) {
@@ -4278,14 +4396,22 @@ class AccountManager {
<span class="material-symbols-outlined text-base">history</span>
Lịch sử xuất
</button>
<button id="openAssetDamageHistoryBtn" class="border border-red-200 text-red-700 px-3 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-red-50' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">inventory_2</span>
DS hỏng/TL
</button>
<button id="addAssetBtn" class="bg-primary text-on-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-primary-dim' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">add_box</span>
Thêm tài sản
</button>
<button id="borrowAssetBtn" class="border border-primary text-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-primary/5' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
<button id="borrowAssetBtn" class="border border-primary text-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${(canManageAssets && selectedCount === 1) ? 'hover:bg-primary/5' : 'opacity-50 cursor-not-allowed'}" ${(canManageAssets && selectedCount === 1) ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">handshake</span>
Xuất tài sản
</button>
<button id="damageAssetBtn" class="border border-red-300 text-red-700 px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${(canManageAssets && selectedCount === 1) ? 'hover:bg-red-50' : 'opacity-50 cursor-not-allowed'}" ${(canManageAssets && selectedCount === 1) ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">broken_image</span>
Hỏng/Thanh lý
</button>
<input id="assetImportInput" type="file" accept=".xlsx,.xls" class="hidden" />
</div>
</div>
@@ -4609,6 +4735,16 @@ class AccountManager {
borrowAssetBtn.disabled = disabled;
borrowAssetBtn.classList.toggle('opacity-50', disabled);
borrowAssetBtn.classList.toggle('cursor-not-allowed', disabled);
borrowAssetBtn.classList.toggle('hover:bg-primary/5', !disabled);
}
const damageAssetBtn = document.getElementById('damageAssetBtn');
if (damageAssetBtn) {
const disabled = !canManageAssets || selectedCount !== 1;
damageAssetBtn.disabled = disabled;
damageAssetBtn.classList.toggle('opacity-50', disabled);
damageAssetBtn.classList.toggle('cursor-not-allowed', disabled);
damageAssetBtn.classList.toggle('hover:bg-red-50', !disabled);
}
}
@@ -5051,6 +5187,161 @@ class AccountManager {
return asset;
}
getSingleSelectedAssetForDamage(showWarning = true) {
const selectedIds = [...this.selectedAssetIds];
if (!selectedIds.length) {
if (showWarning) {
this.notifyWarning('Vui lòng chọn 1 tài sản để ghi nhận hỏng/thanh lý.');
}
return null;
}
if (selectedIds.length > 1) {
if (showWarning) {
this.notifyWarning('Chỉ chọn đúng 1 tài sản cho mỗi lần ghi nhận hỏng/thanh lý.');
}
return null;
}
const assetId = Number(selectedIds[0]);
const asset = this.assets.find(item => Number(item?.AssetId) === assetId) || null;
if (!asset && showWarning) {
this.notifyFailure('Không tìm thấy tài sản đã chọn.');
}
return asset;
}
openAssetDamageModal() {
if (!this.ensureAssetManagePermission('ghi nhan tai san hong/thanh ly')) {
return;
}
const asset = this.getSingleSelectedAssetForDamage(true);
if (!asset) {
return;
}
const metrics = this.buildAssetQuantityMetrics(asset);
if (metrics.endingBalance <= 0) {
this.notifyWarning('Tài sản đã hết tồn cuối kỳ, không thể ghi nhận hỏng/thanh lý thêm.');
return;
}
this.pendingAssetDamageId = Number(asset.AssetId);
const modal = document.getElementById('assetDamageModal');
const assetIdInput = document.getElementById('assetDamageAssetIdInput');
const assetNameInput = document.getElementById('assetDamageAssetNameInput');
const typeInput = document.getElementById('assetDamageTypeInput');
const quantityInput = document.getElementById('assetDamageQuantityInput');
const currentQuantityInput = document.getElementById('assetDamageCurrentQuantityInput');
const currentEndingInput = document.getElementById('assetDamageCurrentEndingInput');
const currentNewInput = document.getElementById('assetDamageCurrentNewInput');
const currentUsedInput = document.getElementById('assetDamageCurrentUsedInput');
const actorInput = document.getElementById('assetDamageActorInput');
const noteInput = document.getElementById('assetDamageNoteInput');
if (!modal || !assetNameInput || !quantityInput || !currentQuantityInput || !currentEndingInput) {
this.notifyFailure('Không tìm thấy biểu mẫu hỏng/thanh lý tài sản.');
return;
}
const stockSplit = this.normalizeAssetStockSplit(
metrics.endingBalance,
asset?.NewQuantity ?? metrics.endingBalance,
asset?.UsedQuantity ?? 0
);
if (assetIdInput) {
assetIdInput.value = String(asset.AssetId || '');
}
assetNameInput.value = `${asset.AssetCode || ''} - ${asset.AssetName || ''}`.trim();
currentQuantityInput.value = String(metrics.quantity);
currentEndingInput.value = String(metrics.endingBalance);
if (currentNewInput) currentNewInput.value = String(stockSplit.newQuantity);
if (currentUsedInput) currentUsedInput.value = String(stockSplit.usedQuantity);
if (typeInput) typeInput.value = 'damaged';
quantityInput.value = '1';
quantityInput.min = '1';
quantityInput.max = String(metrics.endingBalance);
if (actorInput) actorInput.value = this.getCurrentUserDisplayName();
if (noteInput) noteInput.value = '';
modal.classList.add('open');
}
async handleAssetDamageSubmit(e) {
e.preventDefault();
if (!this.ensureAssetManagePermission('ghi nhan tai san hong/thanh ly')) {
return;
}
const assetIdInput = document.getElementById('assetDamageAssetIdInput');
const typeInput = document.getElementById('assetDamageTypeInput');
const quantityInput = document.getElementById('assetDamageQuantityInput');
const noteInput = document.getElementById('assetDamageNoteInput');
const selectedAssetId = Number(assetIdInput?.value || this.pendingAssetDamageId);
if (!Number.isFinite(selectedAssetId) || selectedAssetId <= 0) {
this.notifyFailure('Không xác định được tài sản cần ghi nhận.');
return;
}
const asset = this.assets.find(item => Number(item?.AssetId) === selectedAssetId);
if (!asset) {
this.notifyFailure('Không tìm thấy tài sản cần ghi nhận.');
return;
}
const actionType = this.normalizeAssetDamageType(typeInput?.value || 'damaged');
const actionMeta = this.getAssetDamageTypeMeta(actionType);
const actionQuantity = this.parseNonNegativeInteger(quantityInput?.value ?? 0, 0);
const currentMetrics = this.buildAssetQuantityMetrics(asset);
if (actionQuantity <= 0) {
this.notifyWarning('Số lượng phải lớn hơn 0.');
return;
}
if (actionQuantity > currentMetrics.endingBalance) {
this.notifyWarning(`Số lượng ${actionMeta.label.toLowerCase()} (${actionQuantity}) vượt quá tồn cuối kỳ (${currentMetrics.endingBalance}).`);
return;
}
try {
const response = await fetch(`${this.apiBase}/assets/${selectedAssetId}/damage-disposal`, {
method: 'POST',
headers: this.getAuthHeaders(true),
body: JSON.stringify({
actionType,
quantity: actionQuantity,
note: String(noteInput?.value || '').trim()
})
});
const data = await response.json();
if (!response.ok || !data.success) {
this.notifyFailure(data.message || 'Ghi nhận hỏng/thanh lý thất bại');
return;
}
this.pendingAssetDamageId = undefined;
document.getElementById('assetDamageModal')?.classList.remove('open');
this.notifySuccess(data.message || `Đã ghi nhận tài sản ${actionMeta.label.toLowerCase()}`);
await this.refreshAssetsUI();
const historyModal = document.getElementById('assetDamageHistoryModal');
if (historyModal?.classList.contains('open')) {
await this.fetchAssetDamageHistories();
this.renderAssetDamageHistoryModal();
}
} catch (err) {
console.error(err);
this.notifyFailure('Ghi nhận hỏng/thanh lý thất bại');
}
}
async openBorrowAssetModal() {
if (!this.ensureAssetManagePermission('xuat tai san')) {
return;
@@ -6616,10 +6907,17 @@ class AccountManager {
borrowAssetBtn.dataset.boundClick = 'true';
}
const damageAssetBtn = document.getElementById('damageAssetBtn');
if (damageAssetBtn && !damageAssetBtn.dataset.boundClick) {
damageAssetBtn.addEventListener('click', () => this.openAssetDamageModal());
damageAssetBtn.dataset.boundClick = 'true';
}
const importAssetBtn = document.getElementById('importAssetBtn');
const assetImportInput = document.getElementById('assetImportInput');
const exportAssetBtn = document.getElementById('exportAssetBtn');
const openAssetExportHistoryBtn = document.getElementById('openAssetExportHistoryBtn');
const openAssetDamageHistoryBtn = document.getElementById('openAssetDamageHistoryBtn');
if (importAssetBtn && assetImportInput && !importAssetBtn.dataset.boundClick) {
importAssetBtn.addEventListener('click', () => {
@@ -6645,6 +6943,11 @@ class AccountManager {
openAssetExportHistoryBtn.addEventListener('click', () => this.openAssetExportHistoryModal());
openAssetExportHistoryBtn.dataset.boundClick = 'true';
}
if (openAssetDamageHistoryBtn && !openAssetDamageHistoryBtn.dataset.boundClick) {
openAssetDamageHistoryBtn.addEventListener('click', () => this.openAssetDamageHistoryModal());
openAssetDamageHistoryBtn.dataset.boundClick = 'true';
}
}
setupFilters() {
@@ -7856,6 +8159,13 @@ function closeBorrowAssetModal() {
}
}
function closeAssetDamageModal() {
const modal = document.getElementById('assetDamageModal');
if (modal) {
modal.classList.remove('open');
}
}
function closeAssetExportHistoryModal() {
const modal = document.getElementById('assetExportHistoryModal');
if (modal) {
@@ -7863,6 +8173,13 @@ function closeAssetExportHistoryModal() {
}
}
function closeAssetDamageHistoryModal() {
const modal = document.getElementById('assetDamageHistoryModal');
if (modal) {
modal.classList.remove('open');
}
}
function closeAssetBorrowRequestModal() {
const modal = document.getElementById('assetBorrowRequestModal');
const dropdown = document.getElementById('assetBorrowProductDropdown');