import final
This commit is contained in:
@@ -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() {
|
async function ensurePasswordResetColumns() {
|
||||||
if (!pool) {
|
if (!pool) {
|
||||||
return;
|
return;
|
||||||
@@ -470,6 +493,9 @@ function normalizeAssetStockBuckets(endingBalance, proposedNewQuantity, proposed
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAssetPayload(payload = {}) {
|
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 quantity = parseNonNegativeIntegerOrFallback(payload.quantity, 0);
|
||||||
const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0);
|
const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0);
|
||||||
const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0);
|
const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0);
|
||||||
@@ -487,9 +513,9 @@ function normalizeAssetPayload(payload = {}) {
|
|||||||
const status = resolveAssetStatusFromStock(endingBalance, exportInPeriod);
|
const status = resolveAssetStatusFromStock(endingBalance, exportInPeriod);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetCode: String(payload.assetCode || '').trim(),
|
assetCode,
|
||||||
assetName: String(payload.assetName || '').trim(),
|
assetName: assetName || model || assetCode || null,
|
||||||
model: String(payload.model || '').trim() || null,
|
model: model || null,
|
||||||
serialNumber: String(payload.serialNumber || '').trim() || null,
|
serialNumber: String(payload.serialNumber || '').trim() || null,
|
||||||
quantity,
|
quantity,
|
||||||
unit: String(payload.unit || '').trim() || null,
|
unit: String(payload.unit || '').trim() || null,
|
||||||
@@ -728,11 +754,6 @@ function isHeaderLikeAssetImportRow(row = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isMeaningfulImportedAssetRow(row = {}) {
|
function isMeaningfulImportedAssetRow(row = {}) {
|
||||||
const assetName = String(row.assetName || '').trim();
|
|
||||||
if (!assetName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
row.assetCode,
|
row.assetCode,
|
||||||
row.assetName,
|
row.assetName,
|
||||||
@@ -2002,6 +2023,13 @@ async function createTables() {
|
|||||||
console.error('AssetDepartments sync error:', err.message);
|
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
|
// Insert initial admin user
|
||||||
try {
|
try {
|
||||||
const adminPasswordHash = await hashPassword('admin');
|
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) => {
|
app.get('/api/asset-projects', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
await syncAssetProjectsFromInventory();
|
||||||
|
|
||||||
const result = await pool.request().query(`
|
const result = await pool.request().query(`
|
||||||
SELECT
|
SELECT
|
||||||
p.ProjectId,
|
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' });
|
return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncAssetProjectsFromInventory();
|
||||||
|
|
||||||
const existed = await pool.request()
|
const existed = await pool.request()
|
||||||
.input('projectName', sql.NVarChar, projectName)
|
.input('projectName', sql.NVarChar, projectName)
|
||||||
.query(`
|
.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' });
|
return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncAssetProjectsFromInventory();
|
||||||
|
|
||||||
const currentResult = await pool.request()
|
const currentResult = await pool.request()
|
||||||
.input('projectId', sql.Int, projectId)
|
.input('projectId', sql.Int, projectId)
|
||||||
.query(`
|
.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' });
|
return res.status(400).json({ success: false, message: 'Ma du an khong hop le' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncAssetProjectsFromInventory();
|
||||||
|
|
||||||
const currentResult = await pool.request()
|
const currentResult = await pool.request()
|
||||||
.input('projectId', sql.Int, projectId)
|
.input('projectId', sql.Int, projectId)
|
||||||
.query(`
|
.query(`
|
||||||
@@ -4422,10 +4458,6 @@ app.post('/api/assets', requireAssetOrAdmin, async (req, res) => {
|
|||||||
const createdBy = getUserIdFromRequest(req);
|
const createdBy = getUserIdFromRequest(req);
|
||||||
const exportedBy = await getUserDisplayNameById(createdBy);
|
const exportedBy = await getUserDisplayNameById(createdBy);
|
||||||
|
|
||||||
if (!payload.assetName) {
|
|
||||||
return res.status(400).json({ success: false, message: 'Asset name is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payload.model) {
|
if (!payload.model) {
|
||||||
return res.status(400).json({ success: false, message: 'Model is required' });
|
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 updatedBy = getUserIdFromRequest(req);
|
||||||
const exportedBy = await getUserDisplayNameById(updatedBy);
|
const exportedBy = await getUserDisplayNameById(updatedBy);
|
||||||
|
|
||||||
if (!payload.assetCode || !payload.assetName) {
|
if (!payload.assetCode) {
|
||||||
return res.status(400).json({ success: false, message: 'Asset code and asset name are required' });
|
return res.status(400).json({ success: false, message: 'Asset code is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.model) {
|
if (!payload.model) {
|
||||||
@@ -4616,8 +4648,6 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
const normalizedRows = incomingRows
|
const normalizedRows = incomingRows
|
||||||
.map((row, rowIndex) => {
|
.map((row, rowIndex) => {
|
||||||
const normalized = normalizeAssetPayload(row);
|
const normalized = normalizeAssetPayload(row);
|
||||||
const rowSourceStt = parseAssetImportSttNumber(row?.sourceStt);
|
|
||||||
normalized.__importRowLabel = rowSourceStt !== null ? `STT ${rowSourceStt}` : `dong ${rowIndex + 1}`;
|
|
||||||
const hasOriginalAssetCode = String(normalized.assetCode || '').trim() !== '';
|
const hasOriginalAssetCode = String(normalized.assetCode || '').trim() !== '';
|
||||||
if (!hasOriginalAssetCode && normalized.assetName) {
|
if (!hasOriginalAssetCode && normalized.assetName) {
|
||||||
normalized.assetCode = generateImportAssetCodeFromRow(normalized, rowIndex + 1);
|
normalized.assetCode = generateImportAssetCodeFromRow(normalized, rowIndex + 1);
|
||||||
@@ -4629,20 +4659,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
.filter(row => isMeaningfulImportedAssetRow(row));
|
.filter(row => isMeaningfulImportedAssetRow(row));
|
||||||
|
|
||||||
if (!normalizedRows.length) {
|
if (!normalizedRows.length) {
|
||||||
return res.status(400).json({ success: false, message: 'No valid rows found. MODEL is required.' });
|
return res.status(400).json({ success: false, message: 'No valid rows found in import data.' });
|
||||||
}
|
|
||||||
|
|
||||||
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}`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction = new sql.Transaction(pool);
|
const transaction = new sql.Transaction(pool);
|
||||||
@@ -4770,6 +4787,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
await syncAssetDepartmentsFromInventory();
|
await syncAssetDepartmentsFromInventory();
|
||||||
|
await syncAssetProjectsFromInventory();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -4823,13 +4823,6 @@ class AccountManager {
|
|||||||
const payload = this.collectAssetFormPayload();
|
const payload = this.collectAssetFormPayload();
|
||||||
this.clearAssetFormValidation();
|
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) {
|
if (!payload.model) {
|
||||||
this.setAssetFieldValidationError('assetModelInput', 'assetModelError', 'Vui lòng nhập 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.');
|
this.notifyWarning('Vui lòng nhập đầy đủ các trường bắt buộc.');
|
||||||
@@ -4837,6 +4830,10 @@ class AccountManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!payload.assetName) {
|
||||||
|
payload.assetName = payload.model;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit && !payload.assetCode) {
|
if (isEdit && !payload.assetCode) {
|
||||||
this.setAssetFieldValidationError('assetCodeInput', 'assetCodeError', 'Mã tài sản là bắt buộc khi cập nhật.');
|
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.');
|
this.notifyWarning('Vui lòng nhập đầy đủ các trường bắt buộc.');
|
||||||
|
|||||||
@@ -216,8 +216,8 @@
|
|||||||
<p id="assetCodeError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
|
<p id="assetCodeError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên tài sản <span class="text-red-600">*</span></label>
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên tài sản</label>
|
||||||
<input type="text" id="assetNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Laptop Dell Latitude 5440">
|
<input type="text" id="assetNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Laptop Dell Latitude 5440">
|
||||||
<p id="assetNameError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
|
<p id="assetNameError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -669,3 +669,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user