trạng thái
This commit is contained in:
165
public/js/app.js
165
public/js/app.js
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user