case hỏng
This commit is contained in:
@@ -692,6 +692,19 @@ function normalizeImportToken(value) {
|
||||
.replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
function normalizeAssetDamageType(value) {
|
||||
const normalized = normalizeImportToken(value);
|
||||
if (['disposed', 'dispose', 'disposal', 'thanhly', 'liquidated', 'liquidation'].includes(normalized)) {
|
||||
return 'disposed';
|
||||
}
|
||||
|
||||
return 'damaged';
|
||||
}
|
||||
|
||||
function getAssetDamageTypeLabel(value) {
|
||||
return normalizeAssetDamageType(value) === 'disposed' ? 'Thanh lý' : 'Hỏng';
|
||||
}
|
||||
|
||||
function isHeaderLikeAssetImportRow(row = {}) {
|
||||
const headerTokens = new Set([
|
||||
'stt',
|
||||
@@ -1841,6 +1854,41 @@ async function createTables() {
|
||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
|
||||
)
|
||||
END`,
|
||||
|
||||
// Asset Damage/Disposal History Table
|
||||
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDamageDisposalHistory')
|
||||
BEGIN
|
||||
CREATE TABLE AssetDamageDisposalHistory (
|
||||
DamageHistoryId INT PRIMARY KEY IDENTITY(1,1),
|
||||
AssetId INT NOT NULL,
|
||||
AssetCode NVARCHAR(100) NOT NULL,
|
||||
AssetName NVARCHAR(255) NOT NULL,
|
||||
ActionType NVARCHAR(20) NOT NULL,
|
||||
ActionLabel NVARCHAR(50) NOT NULL,
|
||||
ActionQuantity INT NOT NULL DEFAULT 1,
|
||||
Unit NVARCHAR(50) NULL,
|
||||
PreviousQuantity INT NOT NULL DEFAULT 0,
|
||||
NextQuantity INT NOT NULL DEFAULT 0,
|
||||
PreviousImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
NextImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
PreviousExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
NextExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
PreviousEndingBalance INT NOT NULL DEFAULT 0,
|
||||
NextEndingBalance INT NOT NULL DEFAULT 0,
|
||||
PreviousNewQuantity INT NOT NULL DEFAULT 0,
|
||||
NextNewQuantity INT NOT NULL DEFAULT 0,
|
||||
PreviousUsedQuantity INT NOT NULL DEFAULT 0,
|
||||
NextUsedQuantity INT NOT NULL DEFAULT 0,
|
||||
ActionNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
CreatedByName NVARCHAR(100) NULL,
|
||||
ActionDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
)
|
||||
END`,
|
||||
|
||||
// AuditLog Table
|
||||
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
||||
@@ -1911,6 +1959,15 @@ async function createTables() {
|
||||
console.error('AssetExportHistory index creation error:', err.message);
|
||||
}
|
||||
|
||||
// Ensure AssetDamageDisposalHistory indexes exist
|
||||
try {
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_AssetId') CREATE INDEX IX_AssetDamageDisposalHistory_AssetId ON AssetDamageDisposalHistory(AssetId);`);
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionDate') CREATE INDEX IX_AssetDamageDisposalHistory_ActionDate ON AssetDamageDisposalHistory(ActionDate DESC);`);
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionType') CREATE INDEX IX_AssetDamageDisposalHistory_ActionType ON AssetDamageDisposalHistory(ActionType);`);
|
||||
} catch (err) {
|
||||
console.error('AssetDamageDisposalHistory index creation error:', err.message);
|
||||
}
|
||||
|
||||
// Ensure new columns exist on Applications for migrations
|
||||
try {
|
||||
await pool.request().query(`IF EXISTS (
|
||||
@@ -4979,6 +5036,291 @@ app.get('/api/asset-export-history', requireAssetOrAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/asset-damage-disposal-history', requireAssetOrAdmin, async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parsePositiveInteger(req.query.limit, 300), 2000);
|
||||
const result = await pool.request()
|
||||
.input('limit', sql.Int, limit)
|
||||
.query(`
|
||||
SELECT TOP (@limit)
|
||||
DamageHistoryId,
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
ActionType,
|
||||
ActionLabel,
|
||||
ActionQuantity,
|
||||
Unit,
|
||||
PreviousQuantity,
|
||||
NextQuantity,
|
||||
PreviousImportInPeriod,
|
||||
NextImportInPeriod,
|
||||
PreviousExportInPeriod,
|
||||
NextExportInPeriod,
|
||||
PreviousEndingBalance,
|
||||
NextEndingBalance,
|
||||
PreviousNewQuantity,
|
||||
NextNewQuantity,
|
||||
PreviousUsedQuantity,
|
||||
NextUsedQuantity,
|
||||
ActionNote,
|
||||
CreatedBy,
|
||||
CreatedByName,
|
||||
ActionDate,
|
||||
CreatedDate,
|
||||
UpdatedDate
|
||||
FROM AssetDamageDisposalHistory
|
||||
ORDER BY ActionDate DESC, DamageHistoryId DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: Array.isArray(result.recordset) ? result.recordset : []
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/assets/:id/damage-disposal', requireAssetOrAdmin, async (req, res) => {
|
||||
let transaction;
|
||||
|
||||
try {
|
||||
const assetId = Number(req.params.id);
|
||||
const actionType = normalizeAssetDamageType(req.body?.actionType || req.body?.reason);
|
||||
const actionLabel = getAssetDamageTypeLabel(actionType);
|
||||
const actionQuantity = parseNonNegativeInteger(req.body?.quantity, 0);
|
||||
const actionNote = String(req.body?.note || '').trim() || null;
|
||||
const createdBy = getUserIdFromRequest(req);
|
||||
const createdByName = await getUserDisplayNameById(createdBy);
|
||||
const actionDate = new Date();
|
||||
|
||||
if (!Number.isInteger(assetId) || assetId <= 0) {
|
||||
return res.status(400).json({ success: false, message: 'Asset id is invalid' });
|
||||
}
|
||||
|
||||
if (actionQuantity <= 0) {
|
||||
return res.status(400).json({ success: false, message: 'Số lượng phải lớn hơn 0' });
|
||||
}
|
||||
|
||||
transaction = new sql.Transaction(pool);
|
||||
await transaction.begin();
|
||||
|
||||
const assetResult = await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
.query(`
|
||||
SELECT TOP 1
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
Quantity,
|
||||
ImportInPeriod,
|
||||
ExportInPeriod,
|
||||
EndingBalance,
|
||||
NewQuantity,
|
||||
UsedQuantity,
|
||||
Unit
|
||||
FROM AssetInventory WITH (UPDLOCK, ROWLOCK)
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
const asset = assetResult.recordset?.[0];
|
||||
if (!asset) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({ success: false, message: 'Asset not found' });
|
||||
}
|
||||
|
||||
const previousQuantity = parseNonNegativeInteger(asset.Quantity, 0);
|
||||
const previousImportInPeriod = parseNonNegativeInteger(asset.ImportInPeriod, 0);
|
||||
const previousExportInPeriod = parseNonNegativeInteger(asset.ExportInPeriod, 0);
|
||||
const storedEndingBalance = parseOptionalNonNegativeInteger(asset.EndingBalance);
|
||||
const previousEndingBalance = storedEndingBalance !== null
|
||||
? storedEndingBalance
|
||||
: Math.max(previousQuantity + previousImportInPeriod - previousExportInPeriod, 0);
|
||||
const previousStockBuckets = normalizeAssetStockBuckets(
|
||||
previousEndingBalance,
|
||||
parseOptionalNonNegativeInteger(asset.NewQuantity) ?? previousEndingBalance,
|
||||
parseOptionalNonNegativeInteger(asset.UsedQuantity) ?? 0
|
||||
);
|
||||
|
||||
if (previousEndingBalance <= 0) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ success: false, message: 'Tài sản đã hết tồn cuối kỳ' });
|
||||
}
|
||||
|
||||
if (actionQuantity > previousEndingBalance) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Số lượng ${actionLabel.toLowerCase()} (${actionQuantity}) vượt quá tồn cuối kỳ (${previousEndingBalance})`
|
||||
});
|
||||
}
|
||||
|
||||
let remainingSourceReduction = actionQuantity;
|
||||
const reduceFromQuantity = Math.min(previousQuantity, remainingSourceReduction);
|
||||
const nextQuantity = Math.max(previousQuantity - reduceFromQuantity, 0);
|
||||
remainingSourceReduction -= reduceFromQuantity;
|
||||
const reduceFromImport = Math.min(previousImportInPeriod, remainingSourceReduction);
|
||||
const nextImportInPeriod = Math.max(previousImportInPeriod - reduceFromImport, 0);
|
||||
|
||||
const nextExportInPeriod = previousExportInPeriod;
|
||||
const nextEndingBalance = Math.max(nextQuantity + nextImportInPeriod - nextExportInPeriod, 0);
|
||||
|
||||
let nextUsedQuantity = previousStockBuckets.usedQuantity;
|
||||
let nextNewQuantity = previousStockBuckets.newQuantity;
|
||||
let remainingStockReduction = actionQuantity;
|
||||
const reduceFromUsed = Math.min(nextUsedQuantity, remainingStockReduction);
|
||||
nextUsedQuantity -= reduceFromUsed;
|
||||
remainingStockReduction -= reduceFromUsed;
|
||||
const reduceFromNew = Math.min(nextNewQuantity, remainingStockReduction);
|
||||
nextNewQuantity -= reduceFromNew;
|
||||
|
||||
const nextStockBuckets = normalizeAssetStockBuckets(nextEndingBalance, nextNewQuantity, nextUsedQuantity);
|
||||
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextExportInPeriod);
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
.input('quantity', sql.Int, nextQuantity)
|
||||
.input('importInPeriod', sql.Int, nextImportInPeriod)
|
||||
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||
.input('newQuantity', sql.Int, nextStockBuckets.newQuantity)
|
||||
.input('usedQuantity', sql.Int, nextStockBuckets.usedQuantity)
|
||||
.input('status', sql.NVarChar, nextStatus)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Quantity = @quantity,
|
||||
ImportInPeriod = @importInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
const historyResult = await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
.input('assetCode', sql.NVarChar, String(asset.AssetCode || '').trim())
|
||||
.input('assetName', sql.NVarChar, String(asset.AssetName || '').trim())
|
||||
.input('actionType', sql.NVarChar, actionType)
|
||||
.input('actionLabel', sql.NVarChar, actionLabel)
|
||||
.input('actionQuantity', sql.Int, actionQuantity)
|
||||
.input('unit', sql.NVarChar, String(asset.Unit || '').trim() || null)
|
||||
.input('previousQuantity', sql.Int, previousQuantity)
|
||||
.input('nextQuantity', sql.Int, nextQuantity)
|
||||
.input('previousImportInPeriod', sql.Int, previousImportInPeriod)
|
||||
.input('nextImportInPeriod', sql.Int, nextImportInPeriod)
|
||||
.input('previousExportInPeriod', sql.Int, previousExportInPeriod)
|
||||
.input('nextExportInPeriod', sql.Int, nextExportInPeriod)
|
||||
.input('previousEndingBalance', sql.Int, previousEndingBalance)
|
||||
.input('nextEndingBalance', sql.Int, nextEndingBalance)
|
||||
.input('previousNewQuantity', sql.Int, previousStockBuckets.newQuantity)
|
||||
.input('nextNewQuantity', sql.Int, nextStockBuckets.newQuantity)
|
||||
.input('previousUsedQuantity', sql.Int, previousStockBuckets.usedQuantity)
|
||||
.input('nextUsedQuantity', sql.Int, nextStockBuckets.usedQuantity)
|
||||
.input('actionNote', sql.NVarChar, actionNote)
|
||||
.input('createdBy', sql.Int, createdBy)
|
||||
.input('createdByName', sql.NVarChar, createdByName)
|
||||
.input('actionDate', sql.DateTime, actionDate)
|
||||
.query(`
|
||||
INSERT INTO AssetDamageDisposalHistory (
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
ActionType,
|
||||
ActionLabel,
|
||||
ActionQuantity,
|
||||
Unit,
|
||||
PreviousQuantity,
|
||||
NextQuantity,
|
||||
PreviousImportInPeriod,
|
||||
NextImportInPeriod,
|
||||
PreviousExportInPeriod,
|
||||
NextExportInPeriod,
|
||||
PreviousEndingBalance,
|
||||
NextEndingBalance,
|
||||
PreviousNewQuantity,
|
||||
NextNewQuantity,
|
||||
PreviousUsedQuantity,
|
||||
NextUsedQuantity,
|
||||
ActionNote,
|
||||
CreatedBy,
|
||||
CreatedByName,
|
||||
ActionDate
|
||||
)
|
||||
OUTPUT
|
||||
INSERTED.DamageHistoryId,
|
||||
INSERTED.AssetId,
|
||||
INSERTED.AssetCode,
|
||||
INSERTED.AssetName,
|
||||
INSERTED.ActionType,
|
||||
INSERTED.ActionLabel,
|
||||
INSERTED.ActionQuantity,
|
||||
INSERTED.Unit,
|
||||
INSERTED.PreviousQuantity,
|
||||
INSERTED.NextQuantity,
|
||||
INSERTED.PreviousImportInPeriod,
|
||||
INSERTED.NextImportInPeriod,
|
||||
INSERTED.PreviousExportInPeriod,
|
||||
INSERTED.NextExportInPeriod,
|
||||
INSERTED.PreviousEndingBalance,
|
||||
INSERTED.NextEndingBalance,
|
||||
INSERTED.PreviousNewQuantity,
|
||||
INSERTED.NextNewQuantity,
|
||||
INSERTED.PreviousUsedQuantity,
|
||||
INSERTED.NextUsedQuantity,
|
||||
INSERTED.ActionNote,
|
||||
INSERTED.CreatedBy,
|
||||
INSERTED.CreatedByName,
|
||||
INSERTED.ActionDate,
|
||||
INSERTED.CreatedDate,
|
||||
INSERTED.UpdatedDate
|
||||
VALUES (
|
||||
@assetId,
|
||||
@assetCode,
|
||||
@assetName,
|
||||
@actionType,
|
||||
@actionLabel,
|
||||
@actionQuantity,
|
||||
@unit,
|
||||
@previousQuantity,
|
||||
@nextQuantity,
|
||||
@previousImportInPeriod,
|
||||
@nextImportInPeriod,
|
||||
@previousExportInPeriod,
|
||||
@nextExportInPeriod,
|
||||
@previousEndingBalance,
|
||||
@nextEndingBalance,
|
||||
@previousNewQuantity,
|
||||
@nextNewQuantity,
|
||||
@previousUsedQuantity,
|
||||
@nextUsedQuantity,
|
||||
@actionNote,
|
||||
@createdBy,
|
||||
@createdByName,
|
||||
@actionDate
|
||||
)
|
||||
`);
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Đã ghi nhận tài sản ${actionLabel.toLowerCase()}`,
|
||||
data: historyResult.recordset?.[0] || null
|
||||
});
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
try {
|
||||
await transaction.rollback();
|
||||
} catch (_rollbackErr) {
|
||||
// Ignore rollback error, respond original error below.
|
||||
}
|
||||
}
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
let transaction;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user