import fix
This commit is contained in:
@@ -329,8 +329,29 @@ function parsePositiveInteger(value, fallback = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseNonNegativeInteger(value, fallback = 0) {
|
function parseNonNegativeInteger(value, fallback = 0) {
|
||||||
const parsed = Number(value);
|
const parsed = parseAssetImportNumericValue(value, Number.NaN);
|
||||||
if (Number.isInteger(parsed) && parsed >= 0) {
|
if (Number.isFinite(parsed) && parsed >= 0) {
|
||||||
|
return Math.floor(parsed);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOptionalNonNegativeInteger(value) {
|
||||||
|
if (value === undefined || value === null || String(value).trim() === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseAssetImportNumericValue(value, Number.NaN);
|
||||||
|
if (Number.isFinite(parsed) && parsed >= 0) {
|
||||||
|
return Math.floor(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNonNegativeIntegerOrFallback(value, fallback = 0) {
|
||||||
|
const parsed = parseOptionalNonNegativeInteger(value);
|
||||||
|
if (parsed !== null) {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -406,8 +427,11 @@ function normalizeAssetStatus(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAssetPayload(payload = {}) {
|
function normalizeAssetPayload(payload = {}) {
|
||||||
const quantity = parseNonNegativeInteger(payload.quantity, 0);
|
const quantity = parseNonNegativeIntegerOrFallback(payload.quantity, 0);
|
||||||
const endingBalance = parseNonNegativeInteger(payload.endingBalance, quantity);
|
const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0);
|
||||||
|
const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0);
|
||||||
|
const providedEndingBalance = parseOptionalNonNegativeInteger(payload.endingBalance);
|
||||||
|
const endingBalance = providedEndingBalance !== null ? providedEndingBalance : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetCode: String(payload.assetCode || '').trim(),
|
assetCode: String(payload.assetCode || '').trim(),
|
||||||
@@ -418,8 +442,8 @@ function normalizeAssetPayload(payload = {}) {
|
|||||||
unit: String(payload.unit || '').trim() || null,
|
unit: String(payload.unit || '').trim() || null,
|
||||||
department: String(payload.department || '').trim() || null,
|
department: String(payload.department || '').trim() || null,
|
||||||
project: String(payload.project || '').trim() || null,
|
project: String(payload.project || '').trim() || null,
|
||||||
importInPeriod: parseNonNegativeInteger(payload.importInPeriod, 0),
|
importInPeriod,
|
||||||
exportInPeriod: parseNonNegativeInteger(payload.exportInPeriod, 0),
|
exportInPeriod,
|
||||||
endingBalance,
|
endingBalance,
|
||||||
location: String(payload.location || '').trim() || null,
|
location: String(payload.location || '').trim() || null,
|
||||||
custodian: String(payload.custodian || '').trim() || null,
|
custodian: String(payload.custodian || '').trim() || null,
|
||||||
@@ -648,6 +672,28 @@ function isHeaderLikeAssetImportRow(row = {}) {
|
|||||||
return headerLikeCount >= 2;
|
return headerLikeCount >= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMeaningfulImportedAssetRow(row = {}) {
|
||||||
|
const assetName = String(row.assetName || '').trim();
|
||||||
|
if (!assetName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
row.assetCode,
|
||||||
|
row.assetName,
|
||||||
|
row.model,
|
||||||
|
row.unit,
|
||||||
|
row.location,
|
||||||
|
row.department,
|
||||||
|
row.project,
|
||||||
|
row.notes,
|
||||||
|
row.quantity,
|
||||||
|
row.importInPeriod,
|
||||||
|
row.exportInPeriod,
|
||||||
|
row.endingBalance
|
||||||
|
].some(value => String(value ?? '').trim() !== '');
|
||||||
|
}
|
||||||
|
|
||||||
const ASSET_IMPORT_ALIASES = {
|
const ASSET_IMPORT_ALIASES = {
|
||||||
stt: ['STT', 'So thu tu'],
|
stt: ['STT', 'So thu tu'],
|
||||||
assetCode: ['Asset Code', 'Ma tai san', 'Ma TS', 'Ma TSCD', 'Ma vat tu', 'Ma VT', 'Ma linh kien', 'Code', 'SKU', 'Part Number', 'PN', 'So the', 'So hieu', 'Ma tai san/CCDC'],
|
assetCode: ['Asset Code', 'Ma tai san', 'Ma TS', 'Ma TSCD', 'Ma vat tu', 'Ma VT', 'Ma linh kien', 'Code', 'SKU', 'Part Number', 'PN', 'So the', 'So hieu', 'Ma tai san/CCDC'],
|
||||||
@@ -815,6 +861,7 @@ function parseAssetImportRowsByHeaderMap(matrixRows) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mapped = {
|
const mapped = {
|
||||||
|
sourceStt: parseAssetImportSttNumber(pick(row, bestFieldMap.stt)),
|
||||||
assetCode: String(pick(row, bestFieldMap.assetCode)).trim(),
|
assetCode: String(pick(row, bestFieldMap.assetCode)).trim(),
|
||||||
assetName: String(pick(row, bestFieldMap.assetName)).trim(),
|
assetName: String(pick(row, bestFieldMap.assetName)).trim(),
|
||||||
model: String(pick(row, bestFieldMap.model)).trim(),
|
model: String(pick(row, bestFieldMap.model)).trim(),
|
||||||
@@ -844,7 +891,7 @@ function parseAssetImportRowsByHeaderMap(matrixRows) {
|
|||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter(row => !isHeaderLikeAssetImportRow(row))
|
.filter(row => !isHeaderLikeAssetImportRow(row))
|
||||||
.filter(row => row.assetCode && row.assetName);
|
.filter(row => isMeaningfulImportedAssetRow(row));
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
@@ -880,12 +927,46 @@ function inferAssetImportColumnIndex(headerRow, aliases = []) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAssetImportNumericValue(value, fallback = 1) {
|
function parseAssetImportNumericValue(value, fallback = 0) {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = String(value).trim().replace(/,/g, '');
|
const raw = String(value).trim();
|
||||||
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compact = raw.replace(/\s+/g, '');
|
||||||
|
let normalized = compact;
|
||||||
|
|
||||||
|
const hasDot = compact.includes('.');
|
||||||
|
const hasComma = compact.includes(',');
|
||||||
|
|
||||||
|
// vi-VN style: 1.234,56 or 1.234
|
||||||
|
if (hasDot && hasComma) {
|
||||||
|
if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(compact)) {
|
||||||
|
normalized = compact.replace(/\./g, '').replace(',', '.');
|
||||||
|
} else if (/^-?\d{1,3}(,\d{3})+(\.\d+)?$/.test(compact)) {
|
||||||
|
// en-US style: 1,234.56
|
||||||
|
normalized = compact.replace(/,/g, '');
|
||||||
|
} else {
|
||||||
|
normalized = compact.replace(/,/g, '');
|
||||||
|
}
|
||||||
|
} else if (hasDot) {
|
||||||
|
if (/^-?\d{1,3}(\.\d{3})+$/.test(compact)) {
|
||||||
|
normalized = compact.replace(/\./g, '');
|
||||||
|
}
|
||||||
|
} else if (hasComma) {
|
||||||
|
if (/^-?\d{1,3}(,\d{3})+$/.test(compact)) {
|
||||||
|
normalized = compact.replace(/,/g, '');
|
||||||
|
} else if (/^-?\d+,\d+$/.test(compact)) {
|
||||||
|
normalized = compact.replace(',', '.');
|
||||||
|
} else {
|
||||||
|
normalized = compact.replace(/,/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
@@ -929,7 +1010,9 @@ function generateImportAssetCodeFromRow(mapped, rowNumber = 0) {
|
|||||||
const fromSerial = sanitizeAssetCodeToken(mapped.serialNumber);
|
const fromSerial = sanitizeAssetCodeToken(mapped.serialNumber);
|
||||||
const fromName = sanitizeAssetCodeToken(mapped.assetName);
|
const fromName = sanitizeAssetCodeToken(mapped.assetName);
|
||||||
const base = fromModel || fromSerial || fromName || 'ASSET';
|
const base = fromModel || fromSerial || fromName || 'ASSET';
|
||||||
const suffix = String(rowNumber || 0).padStart(4, '0');
|
const sttNumber = parseAssetImportSttNumber(mapped?.sourceStt);
|
||||||
|
const suffixSeed = sttNumber || rowNumber || 0;
|
||||||
|
const suffix = String(suffixSeed).padStart(4, '0');
|
||||||
return `IMP-${base}-${suffix}`;
|
return `IMP-${base}-${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1058,6 +1141,7 @@ function mapAssetImportMatrixRowsByIndex(matrixRows, headerRowIndex) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mapped = {
|
const mapped = {
|
||||||
|
sourceStt: sttValue,
|
||||||
assetCode: String(pick(row, indexMap.assetCode)).trim(),
|
assetCode: String(pick(row, indexMap.assetCode)).trim(),
|
||||||
assetName: String(pick(row, indexMap.assetName)).trim(),
|
assetName: String(pick(row, indexMap.assetName)).trim(),
|
||||||
model: String(pick(row, indexMap.model)).trim(),
|
model: String(pick(row, indexMap.model)).trim(),
|
||||||
@@ -1081,7 +1165,7 @@ function mapAssetImportMatrixRowsByIndex(matrixRows, headerRowIndex) {
|
|||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter(row => !isHeaderLikeAssetImportRow(row))
|
.filter(row => !isHeaderLikeAssetImportRow(row))
|
||||||
.filter(row => row.assetCode && row.assetName);
|
.filter(row => isMeaningfulImportedAssetRow(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAssetImportRowsFromMatrix(matrixRows) {
|
function parseAssetImportRowsFromMatrix(matrixRows) {
|
||||||
@@ -1149,8 +1233,10 @@ function parseAssetImportRowsFromMatrix(matrixRows) {
|
|||||||
|
|
||||||
return sttRows
|
return sttRows
|
||||||
.map((row, idx) => {
|
.map((row, idx) => {
|
||||||
|
const sttValue = parseAssetImportSttNumber(row[detectedSttCol]);
|
||||||
const endingBalance = parseAssetImportNumericValue(row[detectedSttCol + 9] ?? '', 0);
|
const endingBalance = parseAssetImportNumericValue(row[detectedSttCol + 9] ?? '', 0);
|
||||||
const mapped = {
|
const mapped = {
|
||||||
|
sourceStt: sttValue,
|
||||||
assetCode: String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(),
|
assetCode: String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(),
|
||||||
assetName: String(row[detectedSttCol + 3] ?? '').trim() || String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(),
|
assetName: String(row[detectedSttCol + 3] ?? '').trim() || String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(),
|
||||||
model: String(row[detectedSttCol + 4] ?? '').trim(),
|
model: String(row[detectedSttCol + 4] ?? '').trim(),
|
||||||
@@ -1173,7 +1259,7 @@ function parseAssetImportRowsFromMatrix(matrixRows) {
|
|||||||
return finalizeImportedAssetPayload(mapped, idx + 2);
|
return finalizeImportedAssetPayload(mapped, idx + 2);
|
||||||
})
|
})
|
||||||
.filter(row => !isHeaderLikeAssetImportRow(row))
|
.filter(row => !isHeaderLikeAssetImportRow(row))
|
||||||
.filter(row => row.assetCode && row.assetName);
|
.filter(row => isMeaningfulImportedAssetRow(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectLikelySttColumn(matrixRows) {
|
function detectLikelySttColumn(matrixRows) {
|
||||||
@@ -1248,8 +1334,10 @@ function parseAssetImportRowsLoose(matrixRows) {
|
|||||||
|
|
||||||
return dataRows
|
return dataRows
|
||||||
.map((row, idx) => {
|
.map((row, idx) => {
|
||||||
|
const sttValue = parseAssetImportSttNumber(row[sttCol]);
|
||||||
const endingBalance = parseAssetImportNumericValue(row[sttCol + 9] ?? '', 0);
|
const endingBalance = parseAssetImportNumericValue(row[sttCol + 9] ?? '', 0);
|
||||||
const mapped = {
|
const mapped = {
|
||||||
|
sourceStt: sttValue,
|
||||||
assetCode: String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(),
|
assetCode: String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(),
|
||||||
assetName: String(row[sttCol + 3] ?? '').trim() || String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(),
|
assetName: String(row[sttCol + 3] ?? '').trim() || String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(),
|
||||||
model: String(row[sttCol + 4] ?? '').trim(),
|
model: String(row[sttCol + 4] ?? '').trim(),
|
||||||
@@ -1272,7 +1360,7 @@ function parseAssetImportRowsLoose(matrixRows) {
|
|||||||
return finalizeImportedAssetPayload(mapped, idx + 2);
|
return finalizeImportedAssetPayload(mapped, idx + 2);
|
||||||
})
|
})
|
||||||
.filter(row => !isHeaderLikeAssetImportRow(row))
|
.filter(row => !isHeaderLikeAssetImportRow(row))
|
||||||
.filter(row => row.assetCode && row.assetName);
|
.filter(row => isMeaningfulImportedAssetRow(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAssetImportRows(matrixRows) {
|
function parseAssetImportRows(matrixRows) {
|
||||||
@@ -1295,11 +1383,53 @@ function countNonEmptyMatrixRows(matrixRows) {
|
|||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureUniqueImportAssetCode(transaction, initialCode, maxAttempts = 12) {
|
||||||
|
let candidate = String(initialCode || '').trim();
|
||||||
|
if (!candidate) {
|
||||||
|
candidate = generateManualAssetCodeFromPayload({});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||||
|
const existed = await new sql.Request(transaction)
|
||||||
|
.input('assetCode', sql.NVarChar, candidate)
|
||||||
|
.query(`
|
||||||
|
SELECT TOP 1 AssetId
|
||||||
|
FROM AssetInventory
|
||||||
|
WHERE AssetCode = @assetCode
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (existed.recordset.length === 0) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = String(attempt + 1).padStart(2, '0');
|
||||||
|
candidate = `${String(initialCode || 'IMP-ASSET').slice(0, 90)}-${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateManualAssetCodeFromPayload({ assetName: initialCode || 'IMPORT' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreAssetImportSheetName(sheetName = '') {
|
||||||
|
const token = normalizeImportToken(sheetName);
|
||||||
|
if (!token) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
if (token.includes('xuatnhapton')) score += 120;
|
||||||
|
if (token.includes('kho')) score += 60;
|
||||||
|
if (token.includes('robotics')) score += 40;
|
||||||
|
if (token.includes('inventory')) score += 40;
|
||||||
|
if (token.includes('asset')) score += 30;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
function parseAssetImportRowsFromWorkbook(workbook) {
|
function parseAssetImportRowsFromWorkbook(workbook) {
|
||||||
const sheetNames = Array.isArray(workbook?.SheetNames) ? workbook.SheetNames : [];
|
const sheetNames = Array.isArray(workbook?.SheetNames) ? workbook.SheetNames : [];
|
||||||
let bestRows = [];
|
let bestRows = [];
|
||||||
let bestSheetName = '';
|
let bestSheetName = '';
|
||||||
let bestNonEmptyRows = 0;
|
let bestNonEmptyRows = 0;
|
||||||
|
let bestSheetPriority = 0;
|
||||||
const diagnostics = [];
|
const diagnostics = [];
|
||||||
|
|
||||||
for (const sheetName of sheetNames) {
|
for (const sheetName of sheetNames) {
|
||||||
@@ -1316,16 +1446,23 @@ function parseAssetImportRowsFromWorkbook(workbook) {
|
|||||||
|
|
||||||
const parsedRows = parseAssetImportRows(matrixRows);
|
const parsedRows = parseAssetImportRows(matrixRows);
|
||||||
const nonEmptyRows = countNonEmptyMatrixRows(matrixRows);
|
const nonEmptyRows = countNonEmptyMatrixRows(matrixRows);
|
||||||
|
const sheetPriority = scoreAssetImportSheetName(sheetName);
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
sheetName,
|
sheetName,
|
||||||
parsedRows: parsedRows.length,
|
parsedRows: parsedRows.length,
|
||||||
nonEmptyRows
|
nonEmptyRows,
|
||||||
|
sheetPriority
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parsedRows.length > bestRows.length || (parsedRows.length === bestRows.length && nonEmptyRows > bestNonEmptyRows)) {
|
if (
|
||||||
|
(sheetPriority > bestSheetPriority && parsedRows.length > 0)
|
||||||
|
|| (sheetPriority === bestSheetPriority && parsedRows.length > bestRows.length)
|
||||||
|
|| (sheetPriority === bestSheetPriority && parsedRows.length === bestRows.length && nonEmptyRows > bestNonEmptyRows)
|
||||||
|
) {
|
||||||
bestRows = parsedRows;
|
bestRows = parsedRows;
|
||||||
bestSheetName = sheetName;
|
bestSheetName = sheetName;
|
||||||
bestNonEmptyRows = nonEmptyRows;
|
bestNonEmptyRows = nonEmptyRows;
|
||||||
|
bestSheetPriority = sheetPriority;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4006,16 +4143,18 @@ 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);
|
||||||
if (!normalized.assetCode && normalized.assetName) {
|
const hasOriginalAssetCode = String(normalized.assetCode || '').trim() !== '';
|
||||||
|
if (!hasOriginalAssetCode && normalized.assetName) {
|
||||||
normalized.assetCode = generateImportAssetCodeFromRow(normalized, rowIndex + 1);
|
normalized.assetCode = generateImportAssetCodeFromRow(normalized, rowIndex + 1);
|
||||||
}
|
}
|
||||||
|
normalized.__hasOriginalAssetCode = hasOriginalAssetCode;
|
||||||
return normalized;
|
return normalized;
|
||||||
})
|
})
|
||||||
.filter(row => !isHeaderLikeAssetImportRow(row))
|
.filter(row => !isHeaderLikeAssetImportRow(row))
|
||||||
.filter(row => row.assetCode && row.assetName);
|
.filter(row => isMeaningfulImportedAssetRow(row));
|
||||||
|
|
||||||
if (!normalizedRows.length) {
|
if (!normalizedRows.length) {
|
||||||
return res.status(400).json({ success: false, message: 'No valid rows found. Asset code and name are required.' });
|
return res.status(400).json({ success: false, message: 'No valid rows found. Asset name or model is required.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction = new sql.Transaction(pool);
|
const transaction = new sql.Transaction(pool);
|
||||||
@@ -4026,6 +4165,48 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
await transaction.begin();
|
await transaction.begin();
|
||||||
|
|
||||||
for (const row of normalizedRows) {
|
for (const row of normalizedRows) {
|
||||||
|
if (!row.__hasOriginalAssetCode) {
|
||||||
|
const uniqueImportCode = await ensureUniqueImportAssetCode(transaction, row.assetCode);
|
||||||
|
row.assetCode = uniqueImportCode;
|
||||||
|
await new sql.Request(transaction)
|
||||||
|
.input('assetCode', sql.NVarChar, row.assetCode)
|
||||||
|
.input('assetName', sql.NVarChar, row.assetName)
|
||||||
|
.input('model', sql.NVarChar, row.model)
|
||||||
|
.input('serialNumber', sql.NVarChar, row.serialNumber)
|
||||||
|
.input('quantity', sql.Int, row.quantity)
|
||||||
|
.input('importInPeriod', sql.Int, row.importInPeriod)
|
||||||
|
.input('exportInPeriod', sql.Int, row.exportInPeriod)
|
||||||
|
.input('endingBalance', sql.Int, row.endingBalance)
|
||||||
|
.input('unit', sql.NVarChar, row.unit)
|
||||||
|
.input('department', sql.NVarChar, row.department)
|
||||||
|
.input('project', sql.NVarChar, row.project)
|
||||||
|
.input('location', sql.NVarChar, row.location)
|
||||||
|
.input('custodian', sql.NVarChar, row.custodian)
|
||||||
|
.input('borrower', sql.NVarChar, row.borrower)
|
||||||
|
.input('exportedBy', sql.NVarChar, exportedBy)
|
||||||
|
.input('purchaseDate', sql.Date, row.purchaseDate)
|
||||||
|
.input('purchasePrice', sql.Decimal(18, 2), row.purchasePrice)
|
||||||
|
.input('status', sql.NVarChar, row.status)
|
||||||
|
.input('notes', sql.NVarChar, row.notes)
|
||||||
|
.input('createdBy', sql.Int, createdBy)
|
||||||
|
.query(`
|
||||||
|
INSERT INTO AssetInventory (
|
||||||
|
AssetCode, AssetName, Model, SerialNumber,
|
||||||
|
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||||
|
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||||
|
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@assetCode, @assetName, @model, @serialNumber,
|
||||||
|
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
|
||||||
|
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
||||||
|
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
inserted += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const mergeResult = await new sql.Request(transaction)
|
const mergeResult = await new sql.Request(transaction)
|
||||||
.input('assetCode', sql.NVarChar, row.assetCode)
|
.input('assetCode', sql.NVarChar, row.assetCode)
|
||||||
.input('assetName', sql.NVarChar, row.assetName)
|
.input('assetName', sql.NVarChar, row.assetName)
|
||||||
@@ -4089,11 +4270,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const mergeAction = String(mergeResult.recordset?.[0]?.MergeAction || '').toUpperCase();
|
const mergeAction = String(mergeResult.recordset?.[0]?.MergeAction || '').toUpperCase();
|
||||||
if (mergeAction === 'INSERT') {
|
if (mergeAction === 'INSERT') inserted += 1;
|
||||||
inserted += 1;
|
if (mergeAction === 'UPDATE') updated += 1;
|
||||||
} else if (mergeAction === 'UPDATE') {
|
|
||||||
updated += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|||||||
@@ -1637,6 +1637,19 @@ class AccountManager {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseOptionalNonNegativeInteger(value) {
|
||||||
|
if (value === null || value === undefined || String(value).trim() === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(String(value).replace(/,/g, '').trim(), 10);
|
||||||
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
escapeHtml(value) {
|
escapeHtml(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -1850,17 +1863,26 @@ class AccountManager {
|
|||||||
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
|
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
|
||||||
? this.parseBorrowerEntries(borrowerEntriesOverride)
|
? this.parseBorrowerEntries(borrowerEntriesOverride)
|
||||||
: this.parseBorrowerEntries(asset?.Borrower ?? asset?.borrower);
|
: this.parseBorrowerEntries(asset?.Borrower ?? asset?.borrower);
|
||||||
const exportInPeriod = borrowerEntries.reduce((sum, entry) => (
|
const borrowerExportInPeriod = borrowerEntries.reduce((sum, entry) => (
|
||||||
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
|
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
|
||||||
), 0);
|
), 0);
|
||||||
const endingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
const storedExportInPeriod = this.parseOptionalNonNegativeInteger(asset?.ExportInPeriod ?? asset?.exportInPeriod);
|
||||||
|
const exportInPeriod = storedExportInPeriod !== null
|
||||||
|
? storedExportInPeriod
|
||||||
|
: borrowerExportInPeriod;
|
||||||
|
const computedEndingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
||||||
|
const storedEndingBalance = this.parseOptionalNonNegativeInteger(asset?.EndingBalance ?? asset?.endingBalance);
|
||||||
|
const endingBalance = storedEndingBalance !== null
|
||||||
|
? storedEndingBalance
|
||||||
|
: computedEndingBalance;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quantity,
|
quantity,
|
||||||
importInPeriod,
|
importInPeriod,
|
||||||
exportInPeriod,
|
exportInPeriod,
|
||||||
endingBalance,
|
endingBalance,
|
||||||
borrowerEntries
|
borrowerEntries,
|
||||||
|
borrowerExportInPeriod
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1890,19 +1912,16 @@ class AccountManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = this.buildAssetQuantityMetrics(
|
const quantity = this.parseNonNegativeInteger(quantityInput.value, 0);
|
||||||
{
|
const importInPeriod = this.parseNonNegativeInteger(importInput.value, 0);
|
||||||
Quantity: quantityInput.value,
|
const exportInPeriod = this.parseNonNegativeInteger(exportInput?.value ?? 0, 0);
|
||||||
ImportInPeriod: importInput.value
|
const endingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
||||||
},
|
|
||||||
this.editingAssetBorrowerEntries
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exportInput) {
|
if (exportInput) {
|
||||||
exportInput.value = String(metrics.exportInPeriod);
|
exportInput.value = String(exportInPeriod);
|
||||||
}
|
}
|
||||||
if (endingInput) {
|
if (endingInput) {
|
||||||
endingInput.value = String(metrics.endingBalance);
|
endingInput.value = String(endingBalance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3944,18 +3963,14 @@ class AccountManager {
|
|||||||
collectAssetFormPayload() {
|
collectAssetFormPayload() {
|
||||||
const quantity = this.parseNonNegativeInteger(document.getElementById('assetQuantityInput')?.value ?? 0, 0);
|
const quantity = this.parseNonNegativeInteger(document.getElementById('assetQuantityInput')?.value ?? 0, 0);
|
||||||
const importInPeriod = this.parseNonNegativeInteger(document.getElementById('assetImportInPeriodInput')?.value ?? 0, 0);
|
const importInPeriod = this.parseNonNegativeInteger(document.getElementById('assetImportInPeriodInput')?.value ?? 0, 0);
|
||||||
|
const exportInPeriod = this.parseNonNegativeInteger(document.getElementById('assetExportInPeriodInput')?.value ?? 0, 0);
|
||||||
|
const endingBalanceInput = this.parseOptionalNonNegativeInteger(document.getElementById('assetEndingBalanceInput')?.value ?? '');
|
||||||
const borrowerEntries = Array.isArray(this.editingAssetBorrowerEntries)
|
const borrowerEntries = Array.isArray(this.editingAssetBorrowerEntries)
|
||||||
? this.editingAssetBorrowerEntries
|
? this.editingAssetBorrowerEntries
|
||||||
: [];
|
: [];
|
||||||
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
|
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
|
||||||
const metrics = this.buildAssetQuantityMetrics(
|
const computedEndingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
||||||
{
|
const endingBalance = endingBalanceInput !== null ? endingBalanceInput : computedEndingBalance;
|
||||||
Quantity: quantity,
|
|
||||||
ImportInPeriod: importInPeriod,
|
|
||||||
Borrower: borrower
|
|
||||||
},
|
|
||||||
borrowerEntries
|
|
||||||
);
|
|
||||||
|
|
||||||
const purchasePrice = String(document.getElementById('assetPriceInput')?.value ?? '').trim();
|
const purchasePrice = String(document.getElementById('assetPriceInput')?.value ?? '').trim();
|
||||||
|
|
||||||
@@ -3967,8 +3982,8 @@ class AccountManager {
|
|||||||
serialNumber: document.getElementById('assetSerialInput')?.value?.trim() || '',
|
serialNumber: document.getElementById('assetSerialInput')?.value?.trim() || '',
|
||||||
quantity,
|
quantity,
|
||||||
importInPeriod,
|
importInPeriod,
|
||||||
exportInPeriod: metrics.exportInPeriod,
|
exportInPeriod,
|
||||||
endingBalance: metrics.endingBalance,
|
endingBalance,
|
||||||
unit: document.getElementById('assetUnitInput')?.value?.trim() || '',
|
unit: document.getElementById('assetUnitInput')?.value?.trim() || '',
|
||||||
department: document.getElementById('assetDepartmentInput')?.value?.trim() || '',
|
department: document.getElementById('assetDepartmentInput')?.value?.trim() || '',
|
||||||
project: document.getElementById('assetProjectInput')?.value?.trim() || '',
|
project: document.getElementById('assetProjectInput')?.value?.trim() || '',
|
||||||
@@ -3986,18 +4001,32 @@ class AccountManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quantity = this.parseNonNegativeInteger(asset?.Quantity, 0);
|
||||||
|
const importInPeriod = this.parseNonNegativeInteger(asset?.ImportInPeriod, 0);
|
||||||
|
const baseExportInPeriod = this.parseNonNegativeInteger(asset?.ExportInPeriod, 0);
|
||||||
|
const baseEndingBalance = this.parseOptionalNonNegativeInteger(asset?.EndingBalance);
|
||||||
|
const resolvedBaseEndingBalance = baseEndingBalance !== null
|
||||||
|
? baseEndingBalance
|
||||||
|
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
|
||||||
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
|
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
|
||||||
? borrowerEntriesOverride
|
? borrowerEntriesOverride
|
||||||
: this.parseBorrowerEntries(asset?.Borrower);
|
: this.parseBorrowerEntries(asset?.Borrower);
|
||||||
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
|
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
|
||||||
const metrics = this.buildAssetQuantityMetrics(
|
|
||||||
{
|
let exportInPeriod = baseExportInPeriod;
|
||||||
Quantity: asset?.Quantity,
|
let endingBalance = resolvedBaseEndingBalance;
|
||||||
ImportInPeriod: asset?.ImportInPeriod,
|
if (Array.isArray(borrowerEntriesOverride)) {
|
||||||
Borrower: borrower
|
const existingBorrowerEntries = this.parseBorrowerEntries(asset?.Borrower);
|
||||||
},
|
const previousBorrowerExport = existingBorrowerEntries.reduce((sum, entry) => (
|
||||||
borrowerEntries
|
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
|
||||||
);
|
), 0);
|
||||||
|
const nextBorrowerExport = this.parseBorrowerEntries(borrowerEntriesOverride).reduce((sum, entry) => (
|
||||||
|
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
|
||||||
|
), 0);
|
||||||
|
const exportDelta = nextBorrowerExport - previousBorrowerExport;
|
||||||
|
exportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
|
||||||
|
endingBalance = Math.max(resolvedBaseEndingBalance - exportDelta, 0);
|
||||||
|
}
|
||||||
|
|
||||||
const rawPrice = asset?.PurchasePrice;
|
const rawPrice = asset?.PurchasePrice;
|
||||||
const normalizedPrice = rawPrice === undefined || rawPrice === null || String(rawPrice).trim() === ''
|
const normalizedPrice = rawPrice === undefined || rawPrice === null || String(rawPrice).trim() === ''
|
||||||
@@ -4010,10 +4039,10 @@ class AccountManager {
|
|||||||
status: String(asset?.Status || 'in_use'),
|
status: String(asset?.Status || 'in_use'),
|
||||||
model: String(asset?.Model || '').trim(),
|
model: String(asset?.Model || '').trim(),
|
||||||
serialNumber: String(asset?.SerialNumber || '').trim(),
|
serialNumber: String(asset?.SerialNumber || '').trim(),
|
||||||
quantity: metrics.quantity,
|
quantity,
|
||||||
importInPeriod: metrics.importInPeriod,
|
importInPeriod,
|
||||||
exportInPeriod: metrics.exportInPeriod,
|
exportInPeriod,
|
||||||
endingBalance: metrics.endingBalance,
|
endingBalance,
|
||||||
unit: String(asset?.Unit || '').trim(),
|
unit: String(asset?.Unit || '').trim(),
|
||||||
department: String(asset?.Department || '').trim(),
|
department: String(asset?.Department || '').trim(),
|
||||||
project: String(asset?.Project || '').trim(),
|
project: String(asset?.Project || '').trim(),
|
||||||
|
|||||||
BIN
tmp-import-source.xls
Normal file
BIN
tmp-import-source.xls
Normal file
Binary file not shown.
BIN
tmp-user-import.xls
Normal file
BIN
tmp-user-import.xls
Normal file
Binary file not shown.
Reference in New Issue
Block a user