xuất tài sản
This commit is contained in:
@@ -1724,6 +1724,27 @@ async function createTables() {
|
||||
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
)
|
||||
END`,
|
||||
|
||||
// Asset Export History Table
|
||||
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetExportHistory')
|
||||
BEGIN
|
||||
CREATE TABLE AssetExportHistory (
|
||||
ExportHistoryId INT PRIMARY KEY IDENTITY(1,1),
|
||||
AssetId INT NOT NULL,
|
||||
AssetCode NVARCHAR(100) NOT NULL,
|
||||
AssetName NVARCHAR(255) NOT NULL,
|
||||
ExportQuantity INT NOT NULL DEFAULT 1,
|
||||
ProjectName NVARCHAR(150) NULL,
|
||||
CustodianName NVARCHAR(100) NOT NULL,
|
||||
ExportedByName NVARCHAR(100) NOT NULL,
|
||||
ExportNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
ExportedDate 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
|
||||
)
|
||||
END`,
|
||||
|
||||
// AuditLog Table
|
||||
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
||||
@@ -1783,6 +1804,14 @@ async function createTables() {
|
||||
console.error('AssetBorrowRequests index creation error:', err.message);
|
||||
}
|
||||
|
||||
// Ensure AssetExportHistory indexes exist
|
||||
try {
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetExportHistory_AssetId') CREATE INDEX IX_AssetExportHistory_AssetId ON AssetExportHistory(AssetId);`);
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetExportHistory_ExportedDate') CREATE INDEX IX_AssetExportHistory_ExportedDate ON AssetExportHistory(ExportedDate DESC);`);
|
||||
} catch (err) {
|
||||
console.error('AssetExportHistory index creation error:', err.message);
|
||||
}
|
||||
|
||||
// Ensure new columns exist on Applications for migrations
|
||||
try {
|
||||
await pool.request().query(`IF EXISTS (
|
||||
@@ -1820,6 +1849,34 @@ async function createTables() {
|
||||
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
|
||||
`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','UpdatedDate') IS NULL ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','AssetCode') IS NULL ALTER TABLE AssetExportHistory ADD AssetCode NVARCHAR(100) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','AssetName') IS NULL ALTER TABLE AssetExportHistory ADD AssetName NVARCHAR(255) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportQuantity') IS NULL ALTER TABLE AssetExportHistory ADD ExportQuantity INT NOT NULL CONSTRAINT DF_AssetExportHistory_ExportQuantity DEFAULT(1);`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ProjectName') IS NULL ALTER TABLE AssetExportHistory ADD ProjectName NVARCHAR(150) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CustodianName') IS NULL ALTER TABLE AssetExportHistory ADD CustodianName NVARCHAR(100) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedByName') IS NULL ALTER TABLE AssetExportHistory ADD ExportedByName NVARCHAR(100) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportNote') IS NULL ALTER TABLE AssetExportHistory ADD ExportNote NVARCHAR(1000) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedBy') IS NULL ALTER TABLE AssetExportHistory ADD CreatedBy INT NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedDate') IS NULL ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(GETDATE());`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedDate') IS NULL ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(GETDATE());`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','UpdatedDate') IS NULL ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(GETDATE());`);
|
||||
await pool.request().query(`
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.foreign_key_columns fkc
|
||||
INNER JOIN sys.columns c
|
||||
ON c.object_id = fkc.parent_object_id
|
||||
AND c.column_id = fkc.parent_column_id
|
||||
WHERE fkc.parent_object_id = OBJECT_ID('dbo.AssetExportHistory')
|
||||
AND c.name = 'CreatedBy'
|
||||
)
|
||||
AND COL_LENGTH('dbo.AssetExportHistory', 'CreatedBy') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE AssetExportHistory
|
||||
ADD CONSTRAINT FK_AssetExportHistory_CreatedBy
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
|
||||
END
|
||||
`);
|
||||
await pool.request().query(`UPDATE AssetBorrowRequests SET RequestType = ISNULL(NULLIF(LTRIM(RTRIM(RequestType)), ''), 'borrow');`);
|
||||
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));`);
|
||||
@@ -3711,10 +3768,19 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
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('exportedBy', sql.NVarChar, processorName || null)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Borrower = @borrower,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
ExportedBy = @exportedBy,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
@@ -3735,13 +3801,22 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
}
|
||||
|
||||
const borrowerSummary = decreased.summary || null;
|
||||
const remainingBorrowed = decreased.entries.reduce((sum, entry) => (
|
||||
sum + parseNonNegativeInteger(entry?.quantity, 0)
|
||||
), 0);
|
||||
const quantity = parseNonNegativeInteger(targetRequest.Quantity, 0);
|
||||
const importInPeriod = parseNonNegativeInteger(targetRequest.ImportInPeriod, 0);
|
||||
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('exportedBy', sql.NVarChar, processorName || null)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Borrower = @borrower,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
@@ -3957,6 +4032,215 @@ app.get('/api/assets/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/asset-export-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)
|
||||
ExportHistoryId,
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
ExportQuantity,
|
||||
ProjectName,
|
||||
CustodianName,
|
||||
ExportedByName,
|
||||
ExportNote,
|
||||
CreatedBy,
|
||||
ExportedDate,
|
||||
CreatedDate,
|
||||
UpdatedDate
|
||||
FROM AssetExportHistory
|
||||
ORDER BY ExportedDate DESC, ExportHistoryId 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/export', requireAssetOrAdmin, async (req, res) => {
|
||||
let transaction;
|
||||
try {
|
||||
const assetId = Number(req.params.id);
|
||||
const exportQuantity = parseNonNegativeInteger(req.body?.quantity, 0);
|
||||
const borrowerName = String(req.body?.borrowerName || req.body?.custodianName || '').trim();
|
||||
const projectName = String(req.body?.projectName || '').trim() || null;
|
||||
const exportNote = String(req.body?.note || '').trim() || null;
|
||||
const createdBy = getUserIdFromRequest(req);
|
||||
const exportedByName = await getUserDisplayNameById(createdBy) || String(req.headers['x-user-role'] || '').trim() || 'Unknown';
|
||||
const exportedDate = new Date();
|
||||
|
||||
if (!Number.isInteger(assetId) || assetId <= 0) {
|
||||
return res.status(400).json({ success: false, message: 'Asset id is invalid' });
|
||||
}
|
||||
if (exportQuantity <= 0) {
|
||||
return res.status(400).json({ success: false, message: 'So luong xuat phai lon hon 0' });
|
||||
}
|
||||
if (!borrowerName) {
|
||||
return res.status(400).json({ success: false, message: 'Nguoi muon la bat buoc' });
|
||||
}
|
||||
if (!projectName) {
|
||||
return res.status(400).json({ success: false, message: 'Du an xuat la bat buoc' });
|
||||
}
|
||||
|
||||
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,
|
||||
Custodian,
|
||||
Borrower
|
||||
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 quantity = parseNonNegativeInteger(asset.Quantity, 0);
|
||||
const importInPeriod = parseNonNegativeInteger(asset.ImportInPeriod, 0);
|
||||
const existingBorrowerEntries = parseBorrowerEntries(asset.Borrower);
|
||||
const previousBorrowerExport = existingBorrowerEntries.reduce((sum, entry) => (
|
||||
sum + parseNonNegativeInteger(entry?.quantity, 0)
|
||||
), 0);
|
||||
|
||||
const storedExportInPeriod = parseOptionalNonNegativeInteger(asset.ExportInPeriod);
|
||||
const baseExportInPeriod = storedExportInPeriod !== null ? storedExportInPeriod : previousBorrowerExport;
|
||||
const storedEndingBalance = parseOptionalNonNegativeInteger(asset.EndingBalance);
|
||||
const baseEndingBalance = storedEndingBalance !== null
|
||||
? storedEndingBalance
|
||||
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
|
||||
|
||||
if (baseEndingBalance <= 0) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ success: false, message: 'Tai san da het ton cuoi ky, khong the xuat them' });
|
||||
}
|
||||
if (exportQuantity > baseEndingBalance) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `So luong xuat (${exportQuantity}) vuot qua ton cuoi ky (${baseEndingBalance})`
|
||||
});
|
||||
}
|
||||
|
||||
const borrowerSummary = mergeBorrowerEntries(asset.Borrower, borrowerName, exportQuantity);
|
||||
const nextBorrowerEntries = parseBorrowerEntries(borrowerSummary);
|
||||
const nextBorrowerExport = nextBorrowerEntries.reduce((sum, entry) => (
|
||||
sum + parseNonNegativeInteger(entry?.quantity, 0)
|
||||
), 0);
|
||||
const exportDelta = nextBorrowerExport - previousBorrowerExport;
|
||||
const nextExportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
|
||||
const nextEndingBalance = Math.max(baseEndingBalance - exportDelta, 0);
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
.input('project', sql.NVarChar, projectName)
|
||||
.input('borrower', sql.NVarChar, borrowerSummary)
|
||||
.input('exportInPeriod', sql.Int, nextExportInPeriod)
|
||||
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||
.input('exportedBy', sql.NVarChar, exportedByName)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Project = @project,
|
||||
Borrower = @borrower,
|
||||
ExportInPeriod = @exportInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
ExportedBy = @exportedBy,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
const historyCustodianName = String(asset.Custodian || '').trim() || '-';
|
||||
|
||||
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('exportQuantity', sql.Int, exportQuantity)
|
||||
.input('projectName', sql.NVarChar, projectName)
|
||||
.input('custodianName', sql.NVarChar, historyCustodianName)
|
||||
.input('exportedByName', sql.NVarChar, exportedByName)
|
||||
.input('exportNote', sql.NVarChar, exportNote)
|
||||
.input('createdBy', sql.Int, createdBy)
|
||||
.input('exportedDate', sql.DateTime, exportedDate)
|
||||
.query(`
|
||||
INSERT INTO AssetExportHistory (
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
ExportQuantity,
|
||||
ProjectName,
|
||||
CustodianName,
|
||||
ExportedByName,
|
||||
ExportNote,
|
||||
CreatedBy,
|
||||
ExportedDate
|
||||
)
|
||||
OUTPUT
|
||||
INSERTED.ExportHistoryId,
|
||||
INSERTED.AssetId,
|
||||
INSERTED.AssetCode,
|
||||
INSERTED.AssetName,
|
||||
INSERTED.ExportQuantity,
|
||||
INSERTED.ProjectName,
|
||||
INSERTED.CustodianName,
|
||||
INSERTED.ExportedByName,
|
||||
INSERTED.ExportNote,
|
||||
INSERTED.CreatedBy,
|
||||
INSERTED.ExportedDate,
|
||||
INSERTED.CreatedDate,
|
||||
INSERTED.UpdatedDate
|
||||
VALUES (
|
||||
@assetId,
|
||||
@assetCode,
|
||||
@assetName,
|
||||
@exportQuantity,
|
||||
@projectName,
|
||||
@custodianName,
|
||||
@exportedByName,
|
||||
@exportNote,
|
||||
@createdBy,
|
||||
@exportedDate
|
||||
)
|
||||
`);
|
||||
|
||||
await transaction.commit();
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Xuat tai san thanh cong',
|
||||
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', requireAssetOrAdmin, async (req, res) => {
|
||||
try {
|
||||
const payload = normalizeAssetPayload(req.body);
|
||||
|
||||
Reference in New Issue
Block a user