trạng thái

This commit is contained in:
2026-05-06 16:36:12 +07:00
parent d88aa39bd6
commit 9f14491562
4 changed files with 389 additions and 43 deletions

View File

@@ -56,6 +56,7 @@ class AccountManager {
this.initPromise = this.init();
this.pendingAccountAppId = undefined;
this.editingAssetBorrowerEntries = [];
this.editingAssetStockSnapshot = null;
this.pendingBorrowAssetId = undefined;
this.editingAssetDepartmentId = undefined;
this.pendingDeleteAssetDepartmentId = undefined;
@@ -1326,6 +1327,8 @@ class AccountManager {
asset.ImportInPeriod,
asset.ExportInPeriod,
asset.EndingBalance,
asset.NewQuantity,
asset.UsedQuantity,
asset.Department,
asset.Project,
asset.Location,
@@ -1700,15 +1703,12 @@ class AccountManager {
getAssetStatusMeta(status) {
const normalized = String(status || '').toLowerCase();
if (normalized === 'exported') {
return { label: 'Đã xuất', className: 'bg-rose-100 text-rose-700' };
}
if (normalized === 'in_stock') {
return { label: 'Trong kho', className: 'bg-emerald-100 text-emerald-700' };
}
if (normalized === 'maintenance') {
return { label: 'Bảo trì', className: 'bg-amber-100 text-amber-700' };
}
if (normalized === 'disposed') {
return { label: 'Thanh lý', className: 'bg-rose-100 text-rose-700' };
}
return { label: 'Đang sử dụng', className: 'bg-blue-100 text-blue-700' };
}
@@ -1977,16 +1977,21 @@ class AccountManager {
buildAssetQuantityMetrics(asset, borrowerEntriesOverride = null) {
const quantity = this.parseNonNegativeInteger(asset?.Quantity ?? asset?.quantity, 0);
const importInPeriod = this.parseNonNegativeInteger(asset?.ImportInPeriod ?? asset?.importInPeriod, 0);
const storedExportInPeriod = this.parseOptionalNonNegativeInteger(asset?.ExportInPeriod ?? asset?.exportInPeriod);
const storedEndingBalance = this.parseOptionalNonNegativeInteger(asset?.EndingBalance ?? asset?.endingBalance);
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
? this.parseBorrowerEntries(borrowerEntriesOverride)
: this.parseBorrowerEntries(asset?.Borrower ?? asset?.borrower);
const borrowerExportInPeriod = borrowerEntries.reduce((sum, entry) => (
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
), 0);
// Borrower entries are the source of truth for exported quantity.
// This keeps UI consistent even when legacy rows have stale stored balances.
const exportInPeriod = borrowerExportInPeriod;
const endingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
// Prefer stored stock numbers from DB/file to avoid overriding imported balances.
const exportInPeriod = storedExportInPeriod !== null
? storedExportInPeriod
: borrowerExportInPeriod;
const endingBalance = storedEndingBalance !== null
? storedEndingBalance
: Math.max(quantity + importInPeriod - exportInPeriod, 0);
return {
quantity,
@@ -1998,18 +2003,67 @@ class AccountManager {
};
}
computeAssetStatusCode(endingBalance, borrowingQuantity) {
const ending = this.parseNonNegativeInteger(endingBalance, 0);
const borrowing = this.parseNonNegativeInteger(borrowingQuantity, 0);
if (ending <= 0) {
return 'exported';
}
if (borrowing > 0) {
return 'in_use';
}
return 'in_stock';
}
normalizeAssetStockSplit(endingBalance, newQuantityValue, usedQuantityValue) {
const ending = this.parseNonNegativeInteger(endingBalance, 0);
let newQuantity = this.parseNonNegativeInteger(newQuantityValue, ending);
let usedQuantity = this.parseNonNegativeInteger(usedQuantityValue, 0);
const total = newQuantity + usedQuantity;
if (total < ending) {
newQuantity += (ending - total);
} else if (total > ending) {
let overflow = total - ending;
const takeFromNew = Math.min(newQuantity, overflow);
newQuantity -= takeFromNew;
overflow -= takeFromNew;
if (overflow > 0) {
usedQuantity = Math.max(usedQuantity - overflow, 0);
}
}
return {
newQuantity: Math.max(newQuantity, 0),
usedQuantity: Math.max(usedQuantity, 0)
};
}
normalizeAssetComputedFields(asset) {
if (!asset || typeof asset !== 'object') {
return asset;
}
const metrics = this.buildAssetQuantityMetrics(asset);
const stockSplit = this.normalizeAssetStockSplit(
metrics.endingBalance,
asset?.NewQuantity ?? asset?.newQuantity,
asset?.UsedQuantity ?? asset?.usedQuantity
);
const status = this.computeAssetStatusCode(metrics.endingBalance, metrics.exportInPeriod);
return {
...asset,
Quantity: metrics.quantity,
ImportInPeriod: metrics.importInPeriod,
ExportInPeriod: metrics.exportInPeriod,
EndingBalance: metrics.endingBalance,
NewQuantity: stockSplit.newQuantity,
UsedQuantity: stockSplit.usedQuantity,
Status: status,
Borrower: this.formatBorrowerEntries(metrics.borrowerEntries, '; ') || null
};
}
@@ -2019,6 +2073,7 @@ class AccountManager {
const importInput = document.getElementById('assetImportInPeriodInput');
const exportInput = document.getElementById('assetExportInPeriodInput');
const endingInput = document.getElementById('assetEndingBalanceInput');
const statusInput = document.getElementById('assetStatusInput');
if (!quantityInput || !importInput) {
return;
@@ -2035,6 +2090,12 @@ class AccountManager {
if (endingInput) {
endingInput.value = String(endingBalance);
}
if (statusInput) {
const statusCode = this.computeAssetStatusCode(endingBalance, exportInPeriod);
const statusMeta = this.getAssetStatusMeta(statusCode);
statusInput.value = statusMeta.label;
statusInput.dataset.statusCode = statusCode;
}
}
setupAssetStockListeners() {
@@ -3510,8 +3571,7 @@ class AccountManager {
<option value="">Tất cả</option>
<option value="in_use">Đang sử dụng</option>
<option value="in_stock">Trong kho</option>
<option value="maintenance">Bảo trì</option>
<option value="disposed">Thanh lý</option>
<option value="exported">Đã xuất</option>
</select>
</div>
<div class="flex items-center gap-1.5 flex-1">
@@ -3527,7 +3587,7 @@ class AccountManager {
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
${pageInfo.data.length > 0 ? `
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
<table class="w-full text-left border-collapse" style="min-width: 2400px; border-collapse: separate; border-spacing: 0;">
<table class="w-full text-left border-collapse" style="min-width: 2720px; border-collapse: separate; border-spacing: 0;">
<thead class="sticky top-0 z-50 bg-slate-50 border-b border-slate-200">
<tr>
<th class="px-4 py-2.5 text-center">
@@ -3547,6 +3607,9 @@ class AccountManager {
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Dự án</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người phụ trách</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500" style="width: 132px; min-width: 132px; white-space: nowrap;">Trạng thái</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL hàng mới</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL đã qua sử dụng</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL đang mượn</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Vị trí</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày mua</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500" style="width: 240px; min-width: 240px;">Người mượn</th>
@@ -3583,6 +3646,9 @@ class AccountManager {
<td class="px-4 py-3 text-sm" style="width: 132px; min-width: 132px; white-space: nowrap;">
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}" style="display: inline-block; white-space: nowrap; word-break: keep-all; overflow-wrap: normal;">${statusMeta.label}</span>
</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.NewQuantity ?? 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.UsedQuantity ?? 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.ExportInPeriod ?? 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.Location || '-'}</td>
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(asset.PurchaseDate)}</td>
<td class="px-4 py-3 text-sm text-slate-600 whitespace-normal" style="width: 240px; min-width: 240px;">${this.formatBorrowerTableHtml(asset.Borrower)}</td>
@@ -3667,6 +3733,9 @@ class AccountManager {
<td class="px-4 py-3 text-sm" style="width: 132px; min-width: 132px; white-space: nowrap;">
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}" style="display: inline-block; white-space: nowrap; word-break: keep-all; overflow-wrap: normal;">${statusMeta.label}</span>
</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.NewQuantity ?? 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.UsedQuantity ?? 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.ExportInPeriod ?? 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${asset.Location || '-'}</td>
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(asset.PurchaseDate)}</td>
<td class="px-4 py-3 text-sm text-slate-600 whitespace-normal" style="width: 240px; min-width: 240px;">${this.formatBorrowerTableHtml(asset.Borrower)}</td>
@@ -3895,6 +3964,9 @@ class AccountManager {
['Nhập trong kỳ', asset?.ImportInPeriod ?? 0],
['Xuất trong kỳ', asset?.ExportInPeriod ?? 0],
['Tồn cuối kỳ', asset?.EndingBalance ?? 0],
['SL hàng mới', asset?.NewQuantity ?? 0],
['SL đã qua sử dụng', asset?.UsedQuantity ?? 0],
['SL đang mượn', asset?.ExportInPeriod ?? 0],
['Phòng ban', asset?.Department],
['Dự án', asset?.Project],
['Vị trí', asset?.Location],
@@ -3922,16 +3994,36 @@ class AccountManager {
populateAssetForm(asset) {
const sourceAsset = asset || {};
const borrowerEntries = this.parseBorrowerEntries(sourceAsset?.Borrower);
const metrics = this.buildAssetQuantityMetrics(sourceAsset, borrowerEntries);
const stockSplit = this.normalizeAssetStockSplit(
metrics.endingBalance,
sourceAsset?.NewQuantity ?? metrics.endingBalance,
sourceAsset?.UsedQuantity ?? 0
);
const statusCode = sourceAsset?.AssetId
? this.computeAssetStatusCode(metrics.endingBalance, metrics.exportInPeriod)
: 'in_stock';
const statusMeta = this.getAssetStatusMeta(statusCode);
this.editingAssetBorrowerEntries = borrowerEntries;
this.editingAssetStockSnapshot = {
endingBalance: metrics.endingBalance,
newQuantity: stockSplit.newQuantity,
usedQuantity: stockSplit.usedQuantity
};
this.clearAssetFormValidation();
document.getElementById('assetCodeInput').value = sourceAsset?.AssetCode || '';
document.getElementById('assetNameInput').value = sourceAsset?.AssetName || '';
document.getElementById('assetStatusInput').value = String(sourceAsset?.Status || 'in_use').toLowerCase();
const statusInput = document.getElementById('assetStatusInput');
if (statusInput) {
statusInput.value = statusMeta.label;
statusInput.dataset.statusCode = statusCode;
}
document.getElementById('assetModelInput').value = sourceAsset?.Model || '';
document.getElementById('assetSerialInput').value = sourceAsset?.SerialNumber || '';
document.getElementById('assetQuantityInput').value = this.parseNonNegativeInteger(sourceAsset?.Quantity, 0);
document.getElementById('assetImportInPeriodInput').value = this.parseNonNegativeInteger(sourceAsset?.ImportInPeriod, 0);
document.getElementById('assetQuantityInput').value = metrics.quantity;
document.getElementById('assetImportInPeriodInput').value = metrics.importInPeriod;
document.getElementById('assetUnitInput').value = sourceAsset?.Unit || '';
this.refreshAssetDepartmentOptions(sourceAsset?.Department || '');
this.refreshAssetProjectOptions(sourceAsset?.Project || '');
@@ -3962,6 +4054,7 @@ class AccountManager {
}
if (this.editingAssetId === undefined) {
this.editingAssetStockSnapshot = null;
this.populateAssetForm(null);
}
@@ -4087,19 +4180,36 @@ class AccountManager {
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
const computedEndingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
const endingBalance = endingBalanceInput !== null ? endingBalanceInput : computedEndingBalance;
const status = this.computeAssetStatusCode(endingBalance, exportInPeriod);
const previousSnapshot = this.editingAssetStockSnapshot;
let nextNewQuantity = endingBalance;
let nextUsedQuantity = 0;
if (previousSnapshot) {
const previousEnding = this.parseNonNegativeInteger(previousSnapshot.endingBalance, 0);
const previousNew = this.parseNonNegativeInteger(previousSnapshot.newQuantity, previousEnding);
const previousUsed = this.parseNonNegativeInteger(previousSnapshot.usedQuantity, 0);
const deltaEnding = endingBalance - previousEnding;
const tentativeNew = Math.max(previousNew + deltaEnding, 0);
const normalized = this.normalizeAssetStockSplit(endingBalance, tentativeNew, previousUsed);
nextNewQuantity = normalized.newQuantity;
nextUsedQuantity = normalized.usedQuantity;
}
const purchasePrice = String(document.getElementById('assetPriceInput')?.value ?? '').trim();
return {
assetCode: document.getElementById('assetCodeInput')?.value?.trim() || '',
assetName: document.getElementById('assetNameInput')?.value?.trim() || '',
status: document.getElementById('assetStatusInput')?.value || 'in_use',
status,
model: document.getElementById('assetModelInput')?.value?.trim() || '',
serialNumber: document.getElementById('assetSerialInput')?.value?.trim() || '',
quantity,
importInPeriod,
exportInPeriod,
endingBalance,
newQuantity: nextNewQuantity,
usedQuantity: nextUsedQuantity,
unit: document.getElementById('assetUnitInput')?.value?.trim() || '',
department: document.getElementById('assetDepartmentInput')?.value?.trim() || '',
project: document.getElementById('assetProjectInput')?.value?.trim() || '',
@@ -4124,6 +4234,13 @@ class AccountManager {
const resolvedBaseEndingBalance = baseEndingBalance !== null
? baseEndingBalance
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
const baseNewQuantity = this.parseOptionalNonNegativeInteger(asset?.NewQuantity);
const baseUsedQuantity = this.parseOptionalNonNegativeInteger(asset?.UsedQuantity);
const resolvedStockSplit = this.normalizeAssetStockSplit(
resolvedBaseEndingBalance,
baseNewQuantity !== null ? baseNewQuantity : resolvedBaseEndingBalance,
baseUsedQuantity !== null ? baseUsedQuantity : 0
);
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
? borrowerEntriesOverride
: this.parseBorrowerEntries(asset?.Borrower);
@@ -4131,6 +4248,8 @@ class AccountManager {
let exportInPeriod = baseExportInPeriod;
let endingBalance = resolvedBaseEndingBalance;
let newQuantity = resolvedStockSplit.newQuantity;
let usedQuantity = resolvedStockSplit.usedQuantity;
if (Array.isArray(borrowerEntriesOverride)) {
const existingBorrowerEntries = this.parseBorrowerEntries(asset?.Borrower);
const previousBorrowerExport = existingBorrowerEntries.reduce((sum, entry) => (
@@ -4142,6 +4261,13 @@ class AccountManager {
const exportDelta = nextBorrowerExport - previousBorrowerExport;
exportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
endingBalance = Math.max(resolvedBaseEndingBalance - exportDelta, 0);
const borrowFromNew = Math.min(newQuantity, Math.max(exportDelta, 0));
const borrowFromUsed = Math.max(Math.max(exportDelta, 0) - borrowFromNew, 0);
newQuantity = Math.max(newQuantity - borrowFromNew, 0);
usedQuantity = Math.max(usedQuantity - borrowFromUsed, 0);
const normalizedSplit = this.normalizeAssetStockSplit(endingBalance, newQuantity, usedQuantity);
newQuantity = normalizedSplit.newQuantity;
usedQuantity = normalizedSplit.usedQuantity;
}
const rawPrice = asset?.PurchasePrice;
@@ -4152,13 +4278,15 @@ class AccountManager {
return {
assetCode: String(asset?.AssetCode || '').trim(),
assetName: String(asset?.AssetName || '').trim(),
status: String(asset?.Status || 'in_use'),
status: this.computeAssetStatusCode(endingBalance, exportInPeriod),
model: String(asset?.Model || '').trim(),
serialNumber: String(asset?.SerialNumber || '').trim(),
quantity,
importInPeriod,
exportInPeriod,
endingBalance,
newQuantity,
usedQuantity,
unit: String(asset?.Unit || '').trim(),
department: String(asset?.Department || '').trim(),
project: String(fieldOverrides?.project ?? asset?.Project ?? '').trim(),
@@ -4398,6 +4526,7 @@ class AccountManager {
}
this.editingAssetId = undefined;
this.editingAssetStockSnapshot = null;
this.notifySuccess(isEdit ? 'Cập nhật tài sản thành công' : 'Thêm tài sản thành công');
this.closeModals();
await this.refreshAssetsUI();

View File

@@ -222,12 +222,7 @@
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Trạng thái</label>
<select id="assetStatusInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="in_use">Đang sử dụng</option>
<option value="in_stock">Trong kho</option>
<option value="maintenance">Bảo trì</option>
<option value="disposed">Thanh lý</option>
</select>
<input type="text" id="assetStatusInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly value="Trong kho">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Model</label>