diff --git a/backend/server.js b/backend/server.js index 518b3cb..45f6d25 100644 --- a/backend/server.js +++ b/backend/server.js @@ -290,6 +290,29 @@ async function syncAssetDepartmentsFromInventory() { `); } +async function syncAssetProjectsFromInventory() { + if (!pool) { + return; + } + + await pool.request().query(` + WITH SourceProjects AS ( + SELECT DISTINCT LTRIM(RTRIM(Project)) AS ProjectName + FROM AssetInventory + WHERE Project IS NOT NULL + AND LTRIM(RTRIM(Project)) <> '' + ) + INSERT INTO AssetProjects (ProjectName) + SELECT source.ProjectName + FROM SourceProjects source + WHERE NOT EXISTS ( + SELECT 1 + FROM AssetProjects target + WHERE LOWER(LTRIM(RTRIM(target.ProjectName))) = LOWER(source.ProjectName) + ); + `); +} + async function ensurePasswordResetColumns() { if (!pool) { return; @@ -470,6 +493,9 @@ function normalizeAssetStockBuckets(endingBalance, proposedNewQuantity, proposed } function normalizeAssetPayload(payload = {}) { + const assetName = String(payload.assetName || '').trim(); + const model = String(payload.model || '').trim(); + const assetCode = String(payload.assetCode || '').trim(); const quantity = parseNonNegativeIntegerOrFallback(payload.quantity, 0); const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0); const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0); @@ -487,9 +513,9 @@ function normalizeAssetPayload(payload = {}) { const status = resolveAssetStatusFromStock(endingBalance, exportInPeriod); return { - assetCode: String(payload.assetCode || '').trim(), - assetName: String(payload.assetName || '').trim(), - model: String(payload.model || '').trim() || null, + assetCode, + assetName: assetName || model || assetCode || null, + model: model || null, serialNumber: String(payload.serialNumber || '').trim() || null, quantity, unit: String(payload.unit || '').trim() || null, @@ -728,11 +754,6 @@ function isHeaderLikeAssetImportRow(row = {}) { } function isMeaningfulImportedAssetRow(row = {}) { - const assetName = String(row.assetName || '').trim(); - if (!assetName) { - return false; - } - return [ row.assetCode, row.assetName, @@ -2002,6 +2023,13 @@ async function createTables() { console.error('AssetDepartments sync error:', err.message); } + // Sync legacy projects from AssetInventory to AssetProjects + try { + await syncAssetProjectsFromInventory(); + } catch (err) { + console.error('AssetProjects sync error:', err.message); + } + // Insert initial admin user try { const adminPasswordHash = await hashPassword('admin'); @@ -3367,6 +3395,8 @@ app.delete('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) = app.get('/api/asset-projects', async (req, res) => { try { + await syncAssetProjectsFromInventory(); + const result = await pool.request().query(` SELECT p.ProjectId, @@ -3394,6 +3424,8 @@ app.post('/api/asset-projects', requireAssetOrAdmin, async (req, res) => { return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' }); } + await syncAssetProjectsFromInventory(); + const existed = await pool.request() .input('projectName', sql.NVarChar, projectName) .query(` @@ -3440,6 +3472,8 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => { return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' }); } + await syncAssetProjectsFromInventory(); + const currentResult = await pool.request() .input('projectId', sql.Int, projectId) .query(` @@ -3523,6 +3557,8 @@ app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => { return res.status(400).json({ success: false, message: 'Ma du an khong hop le' }); } + await syncAssetProjectsFromInventory(); + const currentResult = await pool.request() .input('projectId', sql.Int, projectId) .query(` @@ -4422,10 +4458,6 @@ app.post('/api/assets', requireAssetOrAdmin, async (req, res) => { const createdBy = getUserIdFromRequest(req); const exportedBy = await getUserDisplayNameById(createdBy); - if (!payload.assetName) { - return res.status(400).json({ success: false, message: 'Asset name is required' }); - } - if (!payload.model) { return res.status(400).json({ success: false, message: 'Model is required' }); } @@ -4490,8 +4522,8 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => { const updatedBy = getUserIdFromRequest(req); const exportedBy = await getUserDisplayNameById(updatedBy); - if (!payload.assetCode || !payload.assetName) { - return res.status(400).json({ success: false, message: 'Asset code and asset name are required' }); + if (!payload.assetCode) { + return res.status(400).json({ success: false, message: 'Asset code is required' }); } if (!payload.model) { @@ -4616,8 +4648,6 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async const normalizedRows = incomingRows .map((row, rowIndex) => { const normalized = normalizeAssetPayload(row); - const rowSourceStt = parseAssetImportSttNumber(row?.sourceStt); - normalized.__importRowLabel = rowSourceStt !== null ? `STT ${rowSourceStt}` : `dong ${rowIndex + 1}`; const hasOriginalAssetCode = String(normalized.assetCode || '').trim() !== ''; if (!hasOriginalAssetCode && normalized.assetName) { normalized.assetCode = generateImportAssetCodeFromRow(normalized, rowIndex + 1); @@ -4629,20 +4659,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async .filter(row => isMeaningfulImportedAssetRow(row)); if (!normalizedRows.length) { - return res.status(400).json({ success: false, message: 'No valid rows found. MODEL is required.' }); - } - - const missingModelRows = normalizedRows - .filter(row => !String(row.model || '').trim()) - .map(row => row.__importRowLabel) - .filter(Boolean); - if (missingModelRows.length) { - const limitedRows = missingModelRows.slice(0, 20); - const suffix = missingModelRows.length > limitedRows.length ? ', ...' : ''; - return res.status(400).json({ - success: false, - message: `Cot MODEL la bat buoc. Thieu du lieu tai: ${limitedRows.join(', ')}${suffix}` - }); + return res.status(400).json({ success: false, message: 'No valid rows found in import data.' }); } const transaction = new sql.Transaction(pool); @@ -4770,6 +4787,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async await transaction.commit(); await syncAssetDepartmentsFromInventory(); + await syncAssetProjectsFromInventory(); res.json({ success: true, diff --git a/public/js/app.js b/public/js/app.js index 80ec111..853ecb9 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -4823,13 +4823,6 @@ class AccountManager { const payload = this.collectAssetFormPayload(); this.clearAssetFormValidation(); - if (!payload.assetName) { - this.setAssetFieldValidationError('assetNameInput', 'assetNameError', 'Vui lòng nhập tên tài sản.'); - this.notifyWarning('Vui lòng nhập đầy đủ các trường bắt buộc.'); - document.getElementById('assetNameInput')?.focus(); - return; - } - if (!payload.model) { this.setAssetFieldValidationError('assetModelInput', 'assetModelError', 'Vui lòng nhập model.'); this.notifyWarning('Vui lòng nhập đầy đủ các trường bắt buộc.'); @@ -4837,6 +4830,10 @@ class AccountManager { return; } + if (!payload.assetName) { + payload.assetName = payload.model; + } + if (isEdit && !payload.assetCode) { this.setAssetFieldValidationError('assetCodeInput', 'assetCodeError', 'Mã tài sản là bắt buộc khi cập nhật.'); this.notifyWarning('Vui lòng nhập đầy đủ các trường bắt buộc.'); diff --git a/public/modals.html b/public/modals.html index 45e233b..609c1b7 100644 --- a/public/modals.html +++ b/public/modals.html @@ -216,8 +216,8 @@