trạng thái
This commit is contained in:
@@ -407,6 +407,10 @@ function parseNullableDate(value) {
|
||||
function normalizeAssetStatus(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
|
||||
if (['exported', 'da xuat'].includes(normalized)) {
|
||||
return 'exported';
|
||||
}
|
||||
|
||||
if (['in_use', 'in use', 'dang su dung', 'active'].includes(normalized)) {
|
||||
return 'in_use';
|
||||
}
|
||||
@@ -426,12 +430,61 @@ function normalizeAssetStatus(value) {
|
||||
return 'in_use';
|
||||
}
|
||||
|
||||
function resolveAssetStatusFromStock(endingBalance, borrowingQuantity) {
|
||||
const ending = parseNonNegativeIntegerOrFallback(endingBalance, 0);
|
||||
const borrowing = parseNonNegativeIntegerOrFallback(borrowingQuantity, 0);
|
||||
|
||||
if (ending <= 0) {
|
||||
return 'exported';
|
||||
}
|
||||
|
||||
if (borrowing > 0) {
|
||||
return 'in_use';
|
||||
}
|
||||
|
||||
return 'in_stock';
|
||||
}
|
||||
|
||||
function normalizeAssetStockBuckets(endingBalance, proposedNewQuantity, proposedUsedQuantity) {
|
||||
const ending = parseNonNegativeIntegerOrFallback(endingBalance, 0);
|
||||
let newQuantity = parseNonNegativeIntegerOrFallback(proposedNewQuantity, ending);
|
||||
let usedQuantity = parseNonNegativeIntegerOrFallback(proposedUsedQuantity, 0);
|
||||
|
||||
const currentTotal = newQuantity + usedQuantity;
|
||||
if (currentTotal < ending) {
|
||||
newQuantity += (ending - currentTotal);
|
||||
} else if (currentTotal > ending) {
|
||||
let overflow = currentTotal - ending;
|
||||
const reduceFromNew = Math.min(newQuantity, overflow);
|
||||
newQuantity -= reduceFromNew;
|
||||
overflow -= reduceFromNew;
|
||||
if (overflow > 0) {
|
||||
usedQuantity = Math.max(usedQuantity - overflow, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
newQuantity: Math.max(newQuantity, 0),
|
||||
usedQuantity: Math.max(usedQuantity, 0)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAssetPayload(payload = {}) {
|
||||
const quantity = parseNonNegativeIntegerOrFallback(payload.quantity, 0);
|
||||
const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0);
|
||||
const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0);
|
||||
const providedEndingBalance = parseOptionalNonNegativeInteger(payload.endingBalance);
|
||||
const endingBalance = providedEndingBalance !== null ? providedEndingBalance : 0;
|
||||
const endingBalance = providedEndingBalance !== null
|
||||
? providedEndingBalance
|
||||
: Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
||||
const providedNewQuantity = parseOptionalNonNegativeInteger(payload.newQuantity);
|
||||
const providedUsedQuantity = parseOptionalNonNegativeInteger(payload.usedQuantity);
|
||||
const stockBuckets = normalizeAssetStockBuckets(
|
||||
endingBalance,
|
||||
providedNewQuantity !== null ? providedNewQuantity : endingBalance,
|
||||
providedUsedQuantity !== null ? providedUsedQuantity : 0
|
||||
);
|
||||
const status = resolveAssetStatusFromStock(endingBalance, exportInPeriod);
|
||||
|
||||
return {
|
||||
assetCode: String(payload.assetCode || '').trim(),
|
||||
@@ -445,12 +498,14 @@ function normalizeAssetPayload(payload = {}) {
|
||||
importInPeriod,
|
||||
exportInPeriod,
|
||||
endingBalance,
|
||||
newQuantity: stockBuckets.newQuantity,
|
||||
usedQuantity: stockBuckets.usedQuantity,
|
||||
location: String(payload.location || '').trim() || null,
|
||||
custodian: String(payload.custodian || '').trim() || null,
|
||||
borrower: String(payload.borrower || '').trim() || null,
|
||||
purchaseDate: parseNullableDate(payload.purchaseDate),
|
||||
purchasePrice: parseNullableDecimal(payload.purchasePrice),
|
||||
status: normalizeAssetStatus(payload.status),
|
||||
status,
|
||||
notes: String(payload.notes || '').trim() || null
|
||||
};
|
||||
}
|
||||
@@ -1659,6 +1714,8 @@ async function createTables() {
|
||||
ImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
ExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
EndingBalance INT NOT NULL DEFAULT 0,
|
||||
NewQuantity INT NOT NULL DEFAULT 0,
|
||||
UsedQuantity INT NOT NULL DEFAULT 0,
|
||||
Unit NVARCHAR(50),
|
||||
Department NVARCHAR(100),
|
||||
Project NVARCHAR(150),
|
||||
@@ -1826,6 +1883,8 @@ async function createTables() {
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ImportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ImportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ImportInPeriod DEFAULT(0);`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ExportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ExportInPeriod DEFAULT(0);`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','EndingBalance') IS NULL ALTER TABLE AssetInventory ADD EndingBalance INT NOT NULL CONSTRAINT DF_AssetInventory_EndingBalance DEFAULT(0);`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','NewQuantity') IS NULL ALTER TABLE AssetInventory ADD NewQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_NewQuantity DEFAULT(0);`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','UsedQuantity') IS NULL ALTER TABLE AssetInventory ADD UsedQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_UsedQuantity DEFAULT(0);`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Project') IS NULL ALTER TABLE AssetInventory ADD Project NVARCHAR(150) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Borrower') IS NULL ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportedBy') IS NULL ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;`);
|
||||
@@ -1881,6 +1940,34 @@ async function createTables() {
|
||||
await pool.request().query(`UPDATE AssetBorrowRequests SET RequestStatus = ISNULL(NULLIF(LTRIM(RTRIM(RequestStatus)), ''), 'approved');`);
|
||||
await pool.request().query(`UPDATE AssetInventory SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));`);
|
||||
await pool.request().query(`UPDATE AssetInventory SET Quantity = ISNULL(NULLIF(Quantity, 0), EndingBalance);`);
|
||||
await pool.request().query(`UPDATE AssetInventory SET UsedQuantity = CASE WHEN UsedQuantity < 0 THEN 0 ELSE ISNULL(UsedQuantity, 0) END;`);
|
||||
await pool.request().query(`
|
||||
UPDATE AssetInventory
|
||||
SET NewQuantity = CASE
|
||||
WHEN ISNULL(NewQuantity, 0) < 0 THEN 0
|
||||
ELSE ISNULL(NewQuantity, 0)
|
||||
END;
|
||||
`);
|
||||
await pool.request().query(`
|
||||
UPDATE AssetInventory
|
||||
SET NewQuantity = CASE
|
||||
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) < ISNULL(EndingBalance, 0)
|
||||
THEN ISNULL(NewQuantity, 0) + (ISNULL(EndingBalance, 0) - (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)))
|
||||
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||
THEN CASE
|
||||
WHEN ISNULL(NewQuantity, 0) >= ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||
THEN ISNULL(NewQuantity, 0) - ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||
ELSE 0
|
||||
END
|
||||
ELSE ISNULL(NewQuantity, 0)
|
||||
END,
|
||||
UsedQuantity = CASE
|
||||
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||
AND ISNULL(NewQuantity, 0) < ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||
THEN ISNULL(EndingBalance, 0)
|
||||
ELSE ISNULL(UsedQuantity, 0)
|
||||
END;
|
||||
`);
|
||||
await pool.request().query(`
|
||||
UPDATE ai
|
||||
SET ai.ExportedBy = COALESCE(NULLIF(LTRIM(RTRIM(u.FullName)), ''), NULLIF(LTRIM(RTRIM(u.Username)), ''))
|
||||
@@ -3708,6 +3795,11 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
ai.AssetName,
|
||||
ai.Quantity,
|
||||
ai.ImportInPeriod,
|
||||
ai.ExportInPeriod,
|
||||
ai.EndingBalance,
|
||||
ai.NewQuantity,
|
||||
ai.UsedQuantity,
|
||||
ai.Status,
|
||||
ai.Borrower,
|
||||
ai.Unit AS AssetUnit
|
||||
FROM AssetBorrowRequests br
|
||||
@@ -3736,12 +3828,21 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
const currentBorrowed = parseBorrowerEntries(targetRequest.Borrower).reduce((sum, entry) => (
|
||||
sum + parseNonNegativeInteger(entry?.quantity, 0)
|
||||
), 0);
|
||||
const endingBalance = Math.max(
|
||||
const derivedEndingBalance = Math.max(
|
||||
parseNonNegativeInteger(targetRequest.Quantity, 0)
|
||||
+ parseNonNegativeInteger(targetRequest.ImportInPeriod, 0)
|
||||
- currentBorrowed,
|
||||
0
|
||||
);
|
||||
const baseEndingBalance = parseOptionalNonNegativeInteger(targetRequest.EndingBalance);
|
||||
const endingBalance = baseEndingBalance !== null ? baseEndingBalance : derivedEndingBalance;
|
||||
const baseNewQuantity = parseOptionalNonNegativeInteger(targetRequest.NewQuantity);
|
||||
const baseUsedQuantity = parseOptionalNonNegativeInteger(targetRequest.UsedQuantity);
|
||||
const stockBuckets = normalizeAssetStockBuckets(
|
||||
endingBalance,
|
||||
baseNewQuantity !== null ? baseNewQuantity : endingBalance,
|
||||
baseUsedQuantity !== null ? baseUsedQuantity : 0
|
||||
);
|
||||
|
||||
if (requestQuantity > endingBalance) {
|
||||
await transaction.rollback();
|
||||
@@ -3765,22 +3866,31 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
});
|
||||
}
|
||||
|
||||
const borrowFromNew = Math.min(stockBuckets.newQuantity, requestQuantity);
|
||||
const borrowFromUsed = Math.max(requestQuantity - borrowFromNew, 0);
|
||||
const nextEndingBalance = Math.max(endingBalance - requestQuantity, 0);
|
||||
const nextNewQuantity = Math.max(stockBuckets.newQuantity - borrowFromNew, 0);
|
||||
const nextUsedQuantity = Math.max(stockBuckets.usedQuantity - borrowFromUsed, 0);
|
||||
const nextBorrowingQuantity = currentBorrowed + requestQuantity;
|
||||
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextBorrowingQuantity);
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, targetRequest.AssetId)
|
||||
.input('borrower', sql.NVarChar, mergedBorrowerSummary)
|
||||
.input('exportInPeriod', sql.Int, currentBorrowed + requestQuantity)
|
||||
.input('endingBalance', sql.Int, Math.max(
|
||||
parseNonNegativeInteger(targetRequest.Quantity, 0)
|
||||
+ parseNonNegativeInteger(targetRequest.ImportInPeriod, 0)
|
||||
- (currentBorrowed + requestQuantity),
|
||||
0
|
||||
))
|
||||
.input('exportInPeriod', sql.Int, nextBorrowingQuantity)
|
||||
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||
.input('newQuantity', sql.Int, nextNewQuantity)
|
||||
.input('usedQuantity', sql.Int, nextUsedQuantity)
|
||||
.input('status', sql.NVarChar, nextStatus)
|
||||
.input('exportedBy', sql.NVarChar, processorName || null)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Borrower = @borrower,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
ExportedBy = @exportedBy,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
@@ -3806,17 +3916,40 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
), 0);
|
||||
const quantity = parseNonNegativeInteger(targetRequest.Quantity, 0);
|
||||
const importInPeriod = parseNonNegativeInteger(targetRequest.ImportInPeriod, 0);
|
||||
const derivedEndingBalance = Math.max(quantity + importInPeriod - parseNonNegativeInteger(targetRequest.ExportInPeriod, 0), 0);
|
||||
const baseEndingBalance = parseOptionalNonNegativeInteger(targetRequest.EndingBalance);
|
||||
const currentEndingBalance = baseEndingBalance !== null ? baseEndingBalance : derivedEndingBalance;
|
||||
const baseNewQuantity = parseOptionalNonNegativeInteger(targetRequest.NewQuantity);
|
||||
const baseUsedQuantity = parseOptionalNonNegativeInteger(targetRequest.UsedQuantity);
|
||||
const stockBuckets = normalizeAssetStockBuckets(
|
||||
currentEndingBalance,
|
||||
baseNewQuantity !== null ? baseNewQuantity : currentEndingBalance,
|
||||
baseUsedQuantity !== null ? baseUsedQuantity : 0
|
||||
);
|
||||
const nextEndingBalance = Math.max(quantity + importInPeriod - remainingBorrowed, 0);
|
||||
const nextBuckets = normalizeAssetStockBuckets(
|
||||
nextEndingBalance,
|
||||
stockBuckets.newQuantity,
|
||||
stockBuckets.usedQuantity + requestQuantity
|
||||
);
|
||||
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, remainingBorrowed);
|
||||
await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, targetRequest.AssetId)
|
||||
.input('borrower', sql.NVarChar, borrowerSummary)
|
||||
.input('exportInPeriod', sql.Int, remainingBorrowed)
|
||||
.input('endingBalance', sql.Int, Math.max(quantity + importInPeriod - remainingBorrowed, 0))
|
||||
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||
.input('newQuantity', sql.Int, nextBuckets.newQuantity)
|
||||
.input('usedQuantity', sql.Int, nextBuckets.usedQuantity)
|
||||
.input('status', sql.NVarChar, nextStatus)
|
||||
.input('exportedBy', sql.NVarChar, processorName || null)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Borrower = @borrower,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
@@ -3931,8 +4064,15 @@ app.get('/api/assets', async (req, res) => {
|
||||
const result = await pool.request().query(`
|
||||
SELECT AssetId, AssetCode, AssetName, Model, SerialNumber,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||
NewQuantity, UsedQuantity,
|
||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy, CreatedDate, UpdatedDate
|
||||
PurchaseDate, PurchasePrice,
|
||||
CASE
|
||||
WHEN ISNULL(EndingBalance, 0) <= 0 THEN 'exported'
|
||||
WHEN ISNULL(ExportInPeriod, 0) > 0 THEN 'in_use'
|
||||
ELSE 'in_stock'
|
||||
END AS Status,
|
||||
Notes, CreatedBy, CreatedDate, UpdatedDate
|
||||
FROM AssetInventory
|
||||
ORDER BY UpdatedDate DESC, AssetName ASC
|
||||
`);
|
||||
@@ -4016,8 +4156,15 @@ app.get('/api/assets/:id', async (req, res) => {
|
||||
.query(`
|
||||
SELECT AssetId, AssetCode, AssetName, Model, SerialNumber,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||
NewQuantity, UsedQuantity,
|
||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy, CreatedDate, UpdatedDate
|
||||
PurchaseDate, PurchasePrice,
|
||||
CASE
|
||||
WHEN ISNULL(EndingBalance, 0) <= 0 THEN 'exported'
|
||||
WHEN ISNULL(ExportInPeriod, 0) > 0 THEN 'in_use'
|
||||
ELSE 'in_stock'
|
||||
END AS Status,
|
||||
Notes, CreatedBy, CreatedDate, UpdatedDate
|
||||
FROM AssetInventory
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
@@ -4104,6 +4251,8 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
ImportInPeriod,
|
||||
ExportInPeriod,
|
||||
EndingBalance,
|
||||
NewQuantity,
|
||||
UsedQuantity,
|
||||
Custodian,
|
||||
Borrower
|
||||
FROM AssetInventory WITH (UPDLOCK, ROWLOCK)
|
||||
@@ -4129,6 +4278,13 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
const baseEndingBalance = storedEndingBalance !== null
|
||||
? storedEndingBalance
|
||||
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
|
||||
const baseNewQuantity = parseOptionalNonNegativeInteger(asset.NewQuantity);
|
||||
const baseUsedQuantity = parseOptionalNonNegativeInteger(asset.UsedQuantity);
|
||||
const stockBuckets = normalizeAssetStockBuckets(
|
||||
baseEndingBalance,
|
||||
baseNewQuantity !== null ? baseNewQuantity : baseEndingBalance,
|
||||
baseUsedQuantity !== null ? baseUsedQuantity : 0
|
||||
);
|
||||
|
||||
if (baseEndingBalance <= 0) {
|
||||
await transaction.rollback();
|
||||
@@ -4150,6 +4306,11 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
const exportDelta = nextBorrowerExport - previousBorrowerExport;
|
||||
const nextExportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
|
||||
const nextEndingBalance = Math.max(baseEndingBalance - exportDelta, 0);
|
||||
const borrowFromNew = Math.min(stockBuckets.newQuantity, exportDelta);
|
||||
const borrowFromUsed = Math.max(exportDelta - borrowFromNew, 0);
|
||||
const nextNewQuantity = Math.max(stockBuckets.newQuantity - borrowFromNew, 0);
|
||||
const nextUsedQuantity = Math.max(stockBuckets.usedQuantity - borrowFromUsed, 0);
|
||||
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextExportInPeriod);
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
@@ -4157,6 +4318,9 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
.input('borrower', sql.NVarChar, borrowerSummary)
|
||||
.input('exportInPeriod', sql.Int, nextExportInPeriod)
|
||||
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||
.input('newQuantity', sql.Int, nextNewQuantity)
|
||||
.input('usedQuantity', sql.Int, nextUsedQuantity)
|
||||
.input('status', sql.NVarChar, nextStatus)
|
||||
.input('exportedBy', sql.NVarChar, exportedByName)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
@@ -4164,6 +4328,9 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
Borrower = @borrower,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
ExportedBy = @exportedBy,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
@@ -4266,6 +4433,8 @@ app.post('/api/assets', requireAssetOrAdmin, async (req, res) => {
|
||||
.input('importInPeriod', sql.Int, payload.importInPeriod)
|
||||
.input('exportInPeriod', sql.Int, payload.exportInPeriod)
|
||||
.input('endingBalance', sql.Int, payload.endingBalance)
|
||||
.input('newQuantity', sql.Int, payload.newQuantity)
|
||||
.input('usedQuantity', sql.Int, payload.usedQuantity)
|
||||
.input('unit', sql.NVarChar, payload.unit)
|
||||
.input('department', sql.NVarChar, payload.department)
|
||||
.input('project', sql.NVarChar, payload.project)
|
||||
@@ -4281,12 +4450,12 @@ app.post('/api/assets', requireAssetOrAdmin, async (req, res) => {
|
||||
.query(`
|
||||
INSERT INTO AssetInventory (
|
||||
AssetCode, AssetName, Model, SerialNumber,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, NewQuantity, UsedQuantity,
|
||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
||||
) VALUES (
|
||||
@assetCode, @assetName, @model, @serialNumber,
|
||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
|
||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance, @newQuantity, @usedQuantity,
|
||||
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
||||
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
||||
);
|
||||
@@ -4325,6 +4494,8 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
.input('importInPeriod', sql.Int, payload.importInPeriod)
|
||||
.input('exportInPeriod', sql.Int, payload.exportInPeriod)
|
||||
.input('endingBalance', sql.Int, payload.endingBalance)
|
||||
.input('newQuantity', sql.Int, payload.newQuantity)
|
||||
.input('usedQuantity', sql.Int, payload.usedQuantity)
|
||||
.input('unit', sql.NVarChar, payload.unit)
|
||||
.input('department', sql.NVarChar, payload.department)
|
||||
.input('project', sql.NVarChar, payload.project)
|
||||
@@ -4346,6 +4517,8 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
ImportInPeriod = @importInPeriod,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Unit = @unit,
|
||||
Department = @department,
|
||||
Project = @project,
|
||||
@@ -4461,6 +4634,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
||||
.input('importInPeriod', sql.Int, row.importInPeriod)
|
||||
.input('exportInPeriod', sql.Int, row.exportInPeriod)
|
||||
.input('endingBalance', sql.Int, row.endingBalance)
|
||||
.input('newQuantity', sql.Int, row.newQuantity)
|
||||
.input('usedQuantity', sql.Int, row.usedQuantity)
|
||||
.input('unit', sql.NVarChar, row.unit)
|
||||
.input('department', sql.NVarChar, row.department)
|
||||
.input('project', sql.NVarChar, row.project)
|
||||
@@ -4476,13 +4651,13 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
||||
.query(`
|
||||
INSERT INTO AssetInventory (
|
||||
AssetCode, AssetName, Model, SerialNumber,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, NewQuantity, UsedQuantity,
|
||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
||||
)
|
||||
VALUES (
|
||||
@assetCode, @assetName, @model, @serialNumber,
|
||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
|
||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance, @newQuantity, @usedQuantity,
|
||||
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
||||
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
||||
);
|
||||
@@ -4500,6 +4675,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
||||
.input('importInPeriod', sql.Int, row.importInPeriod)
|
||||
.input('exportInPeriod', sql.Int, row.exportInPeriod)
|
||||
.input('endingBalance', sql.Int, row.endingBalance)
|
||||
.input('newQuantity', sql.Int, row.newQuantity)
|
||||
.input('usedQuantity', sql.Int, row.usedQuantity)
|
||||
.input('unit', sql.NVarChar, row.unit)
|
||||
.input('department', sql.NVarChar, row.department)
|
||||
.input('project', sql.NVarChar, row.project)
|
||||
@@ -4525,6 +4702,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
||||
ImportInPeriod = @importInPeriod,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Unit = @unit,
|
||||
Department = @department,
|
||||
Project = @project,
|
||||
@@ -4540,13 +4719,13 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (
|
||||
AssetCode, AssetName, Model, SerialNumber,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, NewQuantity, UsedQuantity,
|
||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
||||
)
|
||||
VALUES (
|
||||
@assetCode, @assetName, @model, @serialNumber,
|
||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
|
||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance, @newQuantity, @usedQuantity,
|
||||
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
||||
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
||||
)
|
||||
|
||||
@@ -95,6 +95,8 @@ BEGIN
|
||||
ImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
ExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
EndingBalance INT NOT NULL DEFAULT 0,
|
||||
NewQuantity INT NOT NULL DEFAULT 0,
|
||||
UsedQuantity INT NOT NULL DEFAULT 0,
|
||||
Unit NVARCHAR(50),
|
||||
Department NVARCHAR(100),
|
||||
Project NVARCHAR(150),
|
||||
@@ -124,6 +126,47 @@ BEGIN
|
||||
ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;
|
||||
END
|
||||
|
||||
IF COL_LENGTH('dbo.AssetInventory', 'NewQuantity') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE AssetInventory ADD NewQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_NewQuantity DEFAULT(0);
|
||||
END
|
||||
|
||||
IF COL_LENGTH('dbo.AssetInventory', 'UsedQuantity') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE AssetInventory ADD UsedQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_UsedQuantity DEFAULT(0);
|
||||
END
|
||||
|
||||
UPDATE AssetInventory
|
||||
SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));
|
||||
|
||||
UPDATE AssetInventory
|
||||
SET UsedQuantity = CASE WHEN ISNULL(UsedQuantity, 0) < 0 THEN 0 ELSE ISNULL(UsedQuantity, 0) END;
|
||||
|
||||
UPDATE AssetInventory
|
||||
SET NewQuantity = CASE
|
||||
WHEN ISNULL(NewQuantity, 0) < 0 THEN 0
|
||||
ELSE ISNULL(NewQuantity, 0)
|
||||
END;
|
||||
|
||||
UPDATE AssetInventory
|
||||
SET NewQuantity = CASE
|
||||
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) < ISNULL(EndingBalance, 0)
|
||||
THEN ISNULL(NewQuantity, 0) + (ISNULL(EndingBalance, 0) - (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)))
|
||||
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||
THEN CASE
|
||||
WHEN ISNULL(NewQuantity, 0) >= ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||
THEN ISNULL(NewQuantity, 0) - ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||
ELSE 0
|
||||
END
|
||||
ELSE ISNULL(NewQuantity, 0)
|
||||
END,
|
||||
UsedQuantity = CASE
|
||||
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||
AND ISNULL(NewQuantity, 0) < ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||
THEN ISNULL(EndingBalance, 0)
|
||||
ELSE ISNULL(UsedQuantity, 0)
|
||||
END;
|
||||
|
||||
-- ===========================================
|
||||
-- 5. CREATE ASSET DEPARTMENTS TABLE
|
||||
-- ===========================================
|
||||
|
||||
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