add dự án

This commit is contained in:
2026-04-25 21:34:17 +07:00
parent 8bd67200ce
commit 197186eac8
5 changed files with 773 additions and 7 deletions

View File

@@ -263,6 +263,10 @@ function normalizeDepartmentName(value) {
return String(value || '').trim();
}
function normalizeProjectName(value) {
return String(value || '').trim();
}
async function syncAssetDepartmentsFromInventory() {
if (!pool) {
return;
@@ -1547,6 +1551,17 @@ async function createTables() {
)
END`,
// Asset Projects Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetProjects')
BEGIN
CREATE TABLE AssetProjects (
ProjectId INT PRIMARY KEY IDENTITY(1,1),
ProjectName NVARCHAR(150) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
)
END`,
// Asset Borrow Requests Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
BEGIN
@@ -1614,6 +1629,13 @@ async function createTables() {
console.error('AssetDepartments index creation error:', err.message);
}
// Ensure AssetProjects indexes exist
try {
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetProjects_ProjectName') CREATE UNIQUE INDEX UX_AssetProjects_ProjectName ON AssetProjects(ProjectName);`);
} catch (err) {
console.error('AssetProjects index creation error:', err.message);
}
// Ensure AssetBorrowRequests indexes exist
try {
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_AssetId') CREATE INDEX IX_AssetBorrowRequests_AssetId ON AssetBorrowRequests(AssetId);`);
@@ -3058,6 +3080,216 @@ app.delete('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) =
}
});
// ==========================================
// API ROUTES - Asset Projects
// ==========================================
app.get('/api/asset-projects', async (req, res) => {
try {
const result = await pool.request().query(`
SELECT
p.ProjectId,
p.ProjectName,
p.CreatedDate,
p.UpdatedDate,
COUNT(ai.AssetId) AS AssetCount
FROM AssetProjects p
LEFT JOIN AssetInventory ai
ON LOWER(LTRIM(RTRIM(ai.Project))) = LOWER(LTRIM(RTRIM(p.ProjectName)))
GROUP BY p.ProjectId, p.ProjectName, p.CreatedDate, p.UpdatedDate
ORDER BY p.ProjectName ASC
`);
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
app.post('/api/asset-projects', requireAssetOrAdmin, async (req, res) => {
try {
const projectName = normalizeProjectName(req.body?.projectName);
if (!projectName) {
return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' });
}
const existed = await pool.request()
.input('projectName', sql.NVarChar, projectName)
.query(`
SELECT TOP 1 ProjectId
FROM AssetProjects
WHERE LOWER(LTRIM(RTRIM(ProjectName))) = LOWER(@projectName)
`);
if (existed.recordset.length > 0) {
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
}
const inserted = await pool.request()
.input('projectName', sql.NVarChar, projectName)
.query(`
INSERT INTO AssetProjects (ProjectName)
VALUES (@projectName);
SELECT SCOPE_IDENTITY() AS ProjectId;
`);
res.json({
success: true,
message: 'Da them du an',
projectId: inserted.recordset[0]?.ProjectId
});
} catch (err) {
if (String(err.message || '').includes('UX_AssetProjects_ProjectName')) {
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
}
res.status(500).json({ success: false, message: err.message });
}
});
app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
try {
const projectId = Number(req.params.id);
if (!Number.isInteger(projectId) || projectId <= 0) {
return res.status(400).json({ success: false, message: 'Ma du an khong hop le' });
}
const projectName = normalizeProjectName(req.body?.projectName);
if (!projectName) {
return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' });
}
const currentResult = await pool.request()
.input('projectId', sql.Int, projectId)
.query(`
SELECT ProjectId, ProjectName
FROM AssetProjects
WHERE ProjectId = @projectId
`);
if (currentResult.recordset.length === 0) {
return res.status(404).json({ success: false, message: 'Khong tim thay du an' });
}
const currentProject = currentResult.recordset[0];
const currentName = String(currentProject.ProjectName || '').trim();
if (currentName.toLowerCase() === projectName.toLowerCase()) {
return res.json({ success: true, message: 'Da cap nhat du an' });
}
const duplicated = await pool.request()
.input('projectName', sql.NVarChar, projectName)
.input('projectId', sql.Int, projectId)
.query(`
SELECT TOP 1 ProjectId
FROM AssetProjects
WHERE ProjectId <> @projectId
AND LOWER(LTRIM(RTRIM(ProjectName))) = LOWER(@projectName)
`);
if (duplicated.recordset.length > 0) {
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
}
const transaction = new sql.Transaction(pool);
await transaction.begin();
try {
await new sql.Request(transaction)
.input('projectId', sql.Int, projectId)
.input('projectName', sql.NVarChar, projectName)
.query(`
UPDATE AssetProjects
SET ProjectName = @projectName,
UpdatedDate = GETDATE()
WHERE ProjectId = @projectId
`);
await new sql.Request(transaction)
.input('oldProjectName', sql.NVarChar, currentName)
.input('newProjectName', sql.NVarChar, projectName)
.query(`
UPDATE AssetInventory
SET Project = @newProjectName,
UpdatedDate = GETDATE()
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@oldProjectName)
`);
await transaction.commit();
res.json({ success: true, message: 'Da cap nhat du an' });
} catch (transactionErr) {
try {
await transaction.rollback();
} catch (rollbackErr) {
// Ignore rollback errors if transaction already ended.
}
throw transactionErr;
}
} catch (err) {
if (String(err.message || '').includes('UX_AssetProjects_ProjectName')) {
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
}
res.status(500).json({ success: false, message: err.message });
}
});
app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
try {
const projectId = Number(req.params.id);
if (!Number.isInteger(projectId) || projectId <= 0) {
return res.status(400).json({ success: false, message: 'Ma du an khong hop le' });
}
const currentResult = await pool.request()
.input('projectId', sql.Int, projectId)
.query(`
SELECT ProjectId, ProjectName
FROM AssetProjects
WHERE ProjectId = @projectId
`);
if (currentResult.recordset.length === 0) {
return res.status(404).json({ success: false, message: 'Khong tim thay du an' });
}
const projectName = String(currentResult.recordset[0].ProjectName || '').trim();
const transaction = new sql.Transaction(pool);
await transaction.begin();
try {
await new sql.Request(transaction)
.input('projectName', sql.NVarChar, projectName)
.query(`
UPDATE AssetInventory
SET Project = NULL,
UpdatedDate = GETDATE()
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@projectName)
`);
await new sql.Request(transaction)
.input('projectId', sql.Int, projectId)
.query(`
DELETE FROM AssetProjects
WHERE ProjectId = @projectId
`);
await transaction.commit();
res.json({ success: true, message: 'Da xoa du an' });
} catch (transactionErr) {
try {
await transaction.rollback();
} catch (rollbackErr) {
// Ignore rollback errors if transaction already ended.
}
throw transactionErr;
}
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ==========================================
// API ROUTES - Asset Inventory
// ==========================================