diff --git a/.gitignore b/.gitignore index 3c3629e..29e62d3 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/backend/server.js b/backend/server.js index b69b70d..e7ef78b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -186,863 +186,6 @@ function getUserIdFromRequest(req) { return Number.isInteger(userId) && userId > 0 ? userId : null; } -async function getUserDisplayNameById(userId) { - if (!userId) { - return null; - } - - try { - const result = await pool.request() - .input('userId', sql.Int, userId) - .query(` - SELECT TOP 1 - NULLIF(LTRIM(RTRIM(FullName)), '') AS FullName, - NULLIF(LTRIM(RTRIM(Username)), '') AS Username - FROM Users - WHERE UserId = @userId - `); - - const user = result.recordset?.[0]; - return user?.FullName || user?.Username || null; - } catch (err) { - return null; - } -} - -function parsePositiveInteger(value, fallback = 1) { - const parsed = Number(value); - if (Number.isInteger(parsed) && parsed > 0) { - return parsed; - } - return fallback; -} - -function parseNonNegativeInteger(value, fallback = 0) { - const parsed = Number(value); - if (Number.isInteger(parsed) && parsed >= 0) { - return parsed; - } - return fallback; -} - -function parseNullableDecimal(value) { - if (value === undefined || value === null) { - return null; - } - - const normalized = String(value).trim().replace(/,/g, ''); - if (!normalized) { - return null; - } - - const parsed = Number(normalized); - return Number.isFinite(parsed) ? parsed : null; -} - -function parseNullableDate(value) { - if (value === undefined || value === null) { - return null; - } - - const raw = String(value).trim(); - if (!raw) { - return null; - } - - const directDate = new Date(raw); - if (!Number.isNaN(directDate.getTime())) { - return directDate; - } - - const localDateParts = raw.match(/^(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})$/); - if (localDateParts) { - const day = Number(localDateParts[1]); - const month = Number(localDateParts[2]); - let year = Number(localDateParts[3]); - if (year < 100) { - year += 2000; - } - - const localizedDate = new Date(year, month - 1, day); - if (!Number.isNaN(localizedDate.getTime())) { - return localizedDate; - } - } - - return null; -} - -function normalizeAssetStatus(value) { - const normalized = String(value || '').trim().toLowerCase(); - - if (['in_use', 'in use', 'dang su dung', 'đang sử dụng', 'active'].includes(normalized)) { - return 'in_use'; - } - - if (['maintenance', 'bao tri', 'bảo trì'].includes(normalized)) { - return 'maintenance'; - } - - if (['disposed', 'thanh ly', 'thanh lý', 'retired'].includes(normalized)) { - return 'disposed'; - } - - if (['in_stock', 'in stock', 'ton kho', 'tồn kho', 'warehouse'].includes(normalized)) { - return 'in_stock'; - } - - return 'in_use'; -} - -function normalizeAssetPayload(payload = {}) { - const quantity = parseNonNegativeInteger(payload.quantity, 0); - const endingBalance = parseNonNegativeInteger(payload.endingBalance, quantity); - - return { - assetCode: String(payload.assetCode || '').trim(), - assetName: String(payload.assetName || '').trim(), - model: String(payload.model || '').trim() || null, - serialNumber: String(payload.serialNumber || '').trim() || null, - quantity, - unit: String(payload.unit || '').trim() || null, - department: String(payload.department || '').trim() || null, - project: String(payload.project || '').trim() || null, - importInPeriod: parseNonNegativeInteger(payload.importInPeriod, 0), - exportInPeriod: parseNonNegativeInteger(payload.exportInPeriod, 0), - endingBalance, - location: String(payload.location || '').trim() || null, - custodian: String(payload.custodian || '').trim() || null, - borrower: String(payload.borrower || '').trim() || null, - purchaseDate: parseNullableDate(payload.purchaseDate), - purchasePrice: parseNullableDecimal(payload.purchasePrice), - status: normalizeAssetStatus(payload.status), - notes: String(payload.notes || '').trim() || null - }; -} - -function normalizeImportToken(value) { - return String(value || '') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .replace(/[đĐ]/g, 'd') - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); -} - -function isHeaderLikeAssetImportRow(row = {}) { - const headerTokens = new Set([ - 'stt', - 'ngayve', - 'mavattu', - 'mavt', - 'mataisan', - 'mats', - 'matscd', - 'tentaisan', - 'tenlinhkiensp', - 'model', - 'dvt', - 'donvi', - 'tondauky', - 'tondauki', - 'nhaptrongky', - 'nhaptrongki', - 'xuattrongky', - 'xuattrongki', - 'toncuoiky', - 'toncuoiki', - 'lidoxuat', - 'lydoxuat', - 'tinhtrang', - 'vitri', - 'duan', - 'assetcode', - 'assetname', - 'quantity', - 'importinperiod', - 'exportinperiod', - 'endingbalance', - 'unit', - 'location', - 'department', - 'project', - 'status', - 'notes' - ]); - - const fields = [ - row.assetCode, - row.assetName, - row.model, - row.unit, - row.status, - row.location, - row.department, - row.project, - row.importInPeriod, - row.exportInPeriod, - row.endingBalance, - row.notes - ]; - - const headerLikeCount = fields.reduce((count, value) => { - const token = normalizeImportToken(value); - return count + (token && headerTokens.has(token) ? 1 : 0); - }, 0); - - return headerLikeCount >= 2; -} - -const ASSET_IMPORT_ALIASES = { - 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'], - assetName: ['Asset Name', 'Ten tai san', 'Ten TS', 'Ten TSCD', 'Ten CCDC', 'Ten vat tu', 'Ten linh kien', 'Ten linh kien/sp', 'Ten linh kien sp', 'Ten sp', 'Name', 'Dien giai', 'Mo ta', 'Ten tai san/CCDC'], - model: ['Model', 'Dong may'], - serialNumber: ['Serial Number', 'Serial', 'So serial', 'So seri'], - quantity: ['Ton dau ky', 'Ton dau ki', 'Opening Balance', 'Quantity', 'So luong', 'SL'], - importInPeriod: ['Nhap trong ky', 'Nhap trong ki', 'Nhap ky', 'Nhap'], - exportInPeriod: ['Xuat trong ky', 'Xuat trong ki', 'Xuat ky', 'Xuat'], - endingBalance: ['Ton cuoi ky', 'Ton cuoi ki', 'Ton cuoi', 'Ending Balance'], - unit: ['Unit', 'Don vi', 'DVT'], - department: ['Department', 'Bo phan', 'Phong ban'], - project: ['Project', 'Du an', 'Cong trinh'], - location: ['Location', 'Vi tri', 'Noi dat'], - custodian: ['Custodian', 'Nguoi quan ly', 'Nguoi su dung'], - purchaseDate: ['Purchase Date', 'Ngay mua', 'Ngay nhap', 'Ngay ve'], - purchasePrice: ['Purchase Price', 'Gia mua', 'Don gia'], - status: ['Status', 'Trang thai', 'Tinh trang'], - notes: ['Notes', 'Ghi chu', 'Li do xuat', 'Ly do xuat'] -}; - -function inferAssetFieldFromHeaderToken(headerToken) { - const token = String(headerToken || ''); - if (!token) { - return null; - } - - if (token.includes('model')) return 'model'; - if (token.includes('serial') || token.includes('seri')) return 'serialNumber'; - if (token.includes('tondau')) return 'quantity'; - if (token.includes('nhaptrongky') || token.includes('nhaptrongki')) return 'importInPeriod'; - if (token.includes('xuattrongky') || token.includes('xuattrongki')) return 'exportInPeriod'; - if (token.includes('toncuoi')) return 'endingBalance'; - if (token.includes('donvi') || token.includes('dvt') || token === 'unit') return 'unit'; - if (token.includes('vitri') || token.includes('location')) return 'location'; - if (token.includes('tinhtrang') || token === 'status') return 'status'; - if (token.includes('duan') || token.includes('project')) return 'project'; - if (token.includes('phongban') || token.includes('bophan') || token.includes('department')) return 'department'; - if (token.includes('lydoxuat') || token.includes('lidoxuat') || token.includes('ghichu') || token === 'notes') return 'notes'; - if (token.includes('soluong') || token === 'sl' || token === 'quantity') return 'quantity'; - - const hasTen = token.includes('ten'); - const hasMa = token.includes('ma'); - const hasAssetLike = token.includes('linhkien') || token.includes('vattu') || token.includes('sanpham') || token.includes('taisan') || token.includes('sp'); - - if (hasTen && hasAssetLike) return 'assetName'; - if (hasMa && hasAssetLike) return 'assetCode'; - - return null; -} - -function resolveAssetImportFieldByHeader(headerCell) { - const token = normalizeImportToken(headerCell); - if (!token) { - return null; - } - - let bestField = null; - let bestScore = 0; - - for (const [field, aliases] of Object.entries(ASSET_IMPORT_ALIASES)) { - for (const alias of aliases) { - const aliasToken = normalizeImportToken(alias); - if (!aliasToken) { - continue; - } - - let score = 0; - if (token === aliasToken) { - score = 5; - } else if (token.includes(aliasToken) || aliasToken.includes(token)) { - score = 3; - } - - if (score > bestScore) { - bestScore = score; - bestField = field; - } - } - } - - if (bestField) { - return bestField; - } - - return inferAssetFieldFromHeaderToken(token); -} - -function buildAssetImportFieldMapFromHeaderRow(headerRow) { - const row = Array.isArray(headerRow) ? headerRow : []; - const fieldMap = {}; - - for (let index = 0; index < row.length; index += 1) { - const field = resolveAssetImportFieldByHeader(row[index]); - if (!field) { - continue; - } - - if (fieldMap[field] === undefined) { - fieldMap[field] = index; - } - } - - return fieldMap; -} - -function scoreAssetImportFieldMap(fieldMap = {}) { - let score = 0; - const keys = Object.keys(fieldMap); - score += keys.length; - if (fieldMap.assetName !== undefined) score += 5; - if (fieldMap.assetCode !== undefined) score += 4; - if (fieldMap.model !== undefined) score += 3; - if (fieldMap.endingBalance !== undefined) score += 2; - if (fieldMap.importInPeriod !== undefined) score += 1; - if (fieldMap.exportInPeriod !== undefined) score += 1; - if (fieldMap.quantity !== undefined) score += 2; - if (fieldMap.unit !== undefined) score += 1; - if (fieldMap.location !== undefined) score += 1; - if (fieldMap.project !== undefined) score += 1; - return score; -} - -function parseAssetImportRowsByHeaderMap(matrixRows) { - const rows = Array.isArray(matrixRows) ? matrixRows : []; - const maxScanRows = Math.min(rows.length, 300); - let bestHeaderRowIndex = -1; - let bestFieldMap = {}; - let bestScore = 0; - - for (let rowIndex = 0; rowIndex < maxScanRows; rowIndex += 1) { - const headerRow = Array.isArray(rows[rowIndex]) ? rows[rowIndex] : []; - if (!headerRow.some(cell => String(cell ?? '').trim() !== '')) { - continue; - } - - const candidateMap = buildAssetImportFieldMapFromHeaderRow(headerRow); - const score = scoreAssetImportFieldMap(candidateMap); - - if (score > bestScore) { - bestScore = score; - bestHeaderRowIndex = rowIndex; - bestFieldMap = candidateMap; - } - } - - if (bestHeaderRowIndex < 0 || bestScore < 2) { - return []; - } - - const pick = (row, index) => { - if (!Array.isArray(row) || index === undefined || index < 0) { - return ''; - } - return row[index] ?? ''; - }; - - const parsed = rows - .slice(bestHeaderRowIndex + 1) - .filter(row => Array.isArray(row) && row.some(cell => String(cell ?? '').trim() !== '')) - .map((row, rowOffset) => { - const endingBalance = parseAssetImportNumericValue( - pick(row, bestFieldMap.endingBalance), - 0 - ); - - const mapped = { - assetCode: String(pick(row, bestFieldMap.assetCode)).trim(), - assetName: String(pick(row, bestFieldMap.assetName)).trim(), - model: String(pick(row, bestFieldMap.model)).trim(), - serialNumber: String(pick(row, bestFieldMap.serialNumber)).trim(), - quantity: parseAssetImportNumericValue(pick(row, bestFieldMap.quantity), 0), - importInPeriod: parseAssetImportNumericValue(pick(row, bestFieldMap.importInPeriod), 0), - exportInPeriod: parseAssetImportNumericValue(pick(row, bestFieldMap.exportInPeriod), 0), - endingBalance, - unit: String(pick(row, bestFieldMap.unit)).trim(), - department: String(pick(row, bestFieldMap.department)).trim(), - project: String(pick(row, bestFieldMap.project)).trim(), - location: String(pick(row, bestFieldMap.location)).trim(), - custodian: String(pick(row, bestFieldMap.custodian)).trim(), - purchaseDate: pick(row, bestFieldMap.purchaseDate), - purchasePrice: pick(row, bestFieldMap.purchasePrice), - status: String(pick(row, bestFieldMap.status)).trim(), - notes: String(pick(row, bestFieldMap.notes)).trim() - }; - - const hasAnyCoreValue = [mapped.assetCode, mapped.assetName, mapped.model, mapped.location, mapped.notes] - .some(value => String(value || '').trim() !== ''); - if (!hasAnyCoreValue) { - return null; - } - - return finalizeImportedAssetPayload(mapped, bestHeaderRowIndex + rowOffset + 2); - }) - .filter(Boolean) - .filter(row => !isHeaderLikeAssetImportRow(row)) - .filter(row => row.assetCode && row.assetName); - - return parsed; -} - -function isAssetImportHeaderMatch(actualHeader, alias) { - const normalizedHeader = normalizeImportToken(actualHeader); - const normalizedAlias = normalizeImportToken(alias); - - if (!normalizedHeader || !normalizedAlias) { - return false; - } - - if (normalizedHeader === normalizedAlias) { - return true; - } - - if (normalizedAlias.length < 4 || normalizedHeader.length < 4) { - return false; - } - - return normalizedHeader.includes(normalizedAlias) || normalizedAlias.includes(normalizedHeader); -} - -function inferAssetImportColumnIndex(headerRow, aliases = []) { - const row = Array.isArray(headerRow) ? headerRow : []; - - for (let index = 0; index < row.length; index += 1) { - if (aliases.some(alias => isAssetImportHeaderMatch(row[index], alias))) { - return index; - } - } - - return -1; -} - -function parseAssetImportNumericValue(value, fallback = 1) { - if (value === undefined || value === null || value === '') { - return fallback; - } - - const normalized = String(value).trim().replace(/,/g, ''); - if (!normalized) { - return fallback; - } - - const parsed = Number(normalized); - return Number.isFinite(parsed) ? parsed : fallback; -} - -function parseAssetImportSttNumber(value) { - const raw = String(value ?? '') - .trim() - .replace(/\.$/, '') - .replace(',', '.'); - - if (!raw) { - return null; - } - - const parsed = Number(raw); - if (!Number.isFinite(parsed) || parsed <= 0) { - return null; - } - - const rounded = Math.round(parsed); - return Math.abs(parsed - rounded) < 1e-9 ? rounded : null; -} - -function sanitizeAssetCodeToken(value) { - return String(value || '') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .replace(/[đĐ]/g, 'd') - .toUpperCase() - .replace(/[^A-Z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 40); -} - -function generateImportAssetCodeFromRow(mapped, rowNumber = 0) { - const fromModel = sanitizeAssetCodeToken(mapped.model); - const fromSerial = sanitizeAssetCodeToken(mapped.serialNumber); - const fromName = sanitizeAssetCodeToken(mapped.assetName); - const base = fromModel || fromSerial || fromName || 'ASSET'; - const suffix = String(rowNumber || 0).padStart(4, '0'); - return `IMP-${base}-${suffix}`; -} - -function finalizeImportedAssetPayload(mapped, rowNumber = 0) { - const result = { ...mapped }; - if (!result.assetName) { - result.assetName = String(result.model || result.serialNumber || result.assetCode || '').trim(); - } - - if (!result.assetCode && result.assetName) { - result.assetCode = generateImportAssetCodeFromRow(result, rowNumber); - } - - return result; -} - -function buildAssetImportIndexMap(headerRow) { - const indexMap = { - stt: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.stt), - assetCode: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.assetCode), - assetName: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.assetName), - model: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.model), - serialNumber: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.serialNumber), - quantity: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.quantity), - importInPeriod: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.importInPeriod), - exportInPeriod: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.exportInPeriod), - endingBalance: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.endingBalance), - unit: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.unit), - department: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.department), - project: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.project), - location: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.location), - custodian: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.custodian), - purchaseDate: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.purchaseDate), - purchasePrice: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.purchasePrice), - status: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.status), - notes: inferAssetImportColumnIndex(headerRow, ASSET_IMPORT_ALIASES.notes) - }; - - if (indexMap.stt >= 0) { - if (indexMap.purchaseDate < 0) indexMap.purchaseDate = indexMap.stt + 1; - if (indexMap.assetCode < 0) indexMap.assetCode = indexMap.stt + 2; - if (indexMap.assetName < 0) indexMap.assetName = indexMap.stt + 3; - if (indexMap.model < 0) indexMap.model = indexMap.stt + 4; - if (indexMap.unit < 0) indexMap.unit = indexMap.stt + 5; - if (indexMap.quantity < 0) indexMap.quantity = indexMap.stt + 6; - if (indexMap.importInPeriod < 0) indexMap.importInPeriod = indexMap.stt + 7; - if (indexMap.exportInPeriod < 0) indexMap.exportInPeriod = indexMap.stt + 8; - if (indexMap.endingBalance < 0) indexMap.endingBalance = indexMap.stt + 9; - if (indexMap.notes < 0) indexMap.notes = indexMap.stt + 10; - if (indexMap.status < 0) indexMap.status = indexMap.stt + 11; - if (indexMap.location < 0) indexMap.location = indexMap.stt + 12; - if (indexMap.project < 0) indexMap.project = indexMap.stt + 13; - } - - return indexMap; -} - -function mapAssetImportMatrixRowsByIndex(matrixRows, headerRowIndex) { - const headerRow = Array.isArray(matrixRows[headerRowIndex]) ? matrixRows[headerRowIndex] : []; - if (!headerRow.length) { - return []; - } - - const indexMap = buildAssetImportIndexMap(headerRow); - if (indexMap.assetCode < 0 && indexMap.assetName < 0 && indexMap.model < 0 && indexMap.stt < 0) { - return []; - } - - const pick = (row, index) => { - if (index < 0 || !Array.isArray(row)) { - return ''; - } - return row[index] ?? ''; - }; - - return matrixRows - .slice(headerRowIndex + 1) - .filter(row => Array.isArray(row) && row.some(cell => String(cell ?? '').trim() !== '')) - .map((row, rowOffset) => { - const sttValue = parseAssetImportSttNumber(pick(row, indexMap.stt)); - if (indexMap.stt >= 0 && sttValue === null) { - return null; - } - - const endingBalance = parseAssetImportNumericValue( - pick(row, indexMap.endingBalance), - 0 - ); - - const mapped = { - assetCode: String(pick(row, indexMap.assetCode)).trim(), - assetName: String(pick(row, indexMap.assetName)).trim(), - model: String(pick(row, indexMap.model)).trim(), - serialNumber: String(pick(row, indexMap.serialNumber)).trim(), - quantity: parseAssetImportNumericValue(pick(row, indexMap.quantity), 0), - importInPeriod: parseAssetImportNumericValue(pick(row, indexMap.importInPeriod), 0), - exportInPeriod: parseAssetImportNumericValue(pick(row, indexMap.exportInPeriod), 0), - endingBalance, - unit: String(pick(row, indexMap.unit)).trim(), - department: String(pick(row, indexMap.department)).trim(), - project: String(pick(row, indexMap.project)).trim(), - location: String(pick(row, indexMap.location)).trim(), - custodian: String(pick(row, indexMap.custodian)).trim(), - purchaseDate: pick(row, indexMap.purchaseDate), - purchasePrice: pick(row, indexMap.purchasePrice), - status: String(pick(row, indexMap.status)).trim(), - notes: String(pick(row, indexMap.notes)).trim() - }; - - return finalizeImportedAssetPayload(mapped, headerRowIndex + rowOffset + 2); - }) - .filter(Boolean) - .filter(row => !isHeaderLikeAssetImportRow(row)) - .filter(row => row.assetCode && row.assetName); -} - -function parseAssetImportRowsFromMatrix(matrixRows) { - const rows = Array.isArray(matrixRows) ? matrixRows : []; - const maxScanRows = Math.min(rows.length, 300); - let bestRows = []; - - for (let rowIndex = 0; rowIndex < maxScanRows; rowIndex += 1) { - const row = Array.isArray(rows[rowIndex]) ? rows[rowIndex] : []; - if (!row.length) { - continue; - } - - const hasStt = row.some(cell => ASSET_IMPORT_ALIASES.stt.some(alias => isAssetImportHeaderMatch(cell, alias))); - const hasName = row.some(cell => ASSET_IMPORT_ALIASES.assetName.some(alias => isAssetImportHeaderMatch(cell, alias))); - const hasModel = row.some(cell => ASSET_IMPORT_ALIASES.model.some(alias => isAssetImportHeaderMatch(cell, alias))); - const hasQty = row.some(cell => ASSET_IMPORT_ALIASES.quantity.some(alias => isAssetImportHeaderMatch(cell, alias))); - - if (!hasStt || (!hasName && !hasModel && !hasQty)) { - continue; - } - - const candidateRows = mapAssetImportMatrixRowsByIndex(rows, rowIndex); - if (candidateRows.length > bestRows.length) { - bestRows = candidateRows; - } - } - - if (bestRows.length >= 3) { - return bestRows; - } - - let detectedSttCol = -1; - for (let rowIndex = 0; rowIndex < maxScanRows; rowIndex += 1) { - const row = Array.isArray(rows[rowIndex]) ? rows[rowIndex] : []; - const col = inferAssetImportColumnIndex(row, ASSET_IMPORT_ALIASES.stt); - if (col >= 0) { - detectedSttCol = col; - break; - } - } - - if (detectedSttCol < 0) { - detectedSttCol = 0; - } - - const sttRows = rows.filter(row => { - if (!Array.isArray(row)) { - return false; - } - - const sttValue = parseAssetImportSttNumber(row[detectedSttCol]); - if (sttValue === null) { - return false; - } - - return [2, 3, 4, 5, 9, 12] - .map(offset => detectedSttCol + offset) - .some(index => String(row[index] ?? '').trim() !== ''); - }); - - if (sttRows.length < 3) { - return bestRows; - } - - return sttRows - .map((row, idx) => { - const endingBalance = parseAssetImportNumericValue(row[detectedSttCol + 9] ?? '', 0); - const mapped = { - 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(), - model: String(row[detectedSttCol + 4] ?? '').trim(), - serialNumber: '', - quantity: parseAssetImportNumericValue(row[detectedSttCol + 6] ?? '', 0), - importInPeriod: parseAssetImportNumericValue(row[detectedSttCol + 7] ?? '', 0), - exportInPeriod: parseAssetImportNumericValue(row[detectedSttCol + 8] ?? '', 0), - endingBalance, - unit: String(row[detectedSttCol + 5] ?? '').trim(), - department: '', - project: String(row[detectedSttCol + 13] ?? '').trim(), - location: String(row[detectedSttCol + 12] ?? '').trim(), - custodian: '', - purchaseDate: row[detectedSttCol + 1] ?? '', - purchasePrice: '', - status: String(row[detectedSttCol + 11] ?? '').trim(), - notes: String(row[detectedSttCol + 10] ?? '').trim() - }; - - return finalizeImportedAssetPayload(mapped, idx + 2); - }) - .filter(row => !isHeaderLikeAssetImportRow(row)) - .filter(row => row.assetCode && row.assetName); -} - -function detectLikelySttColumn(matrixRows) { - const rows = Array.isArray(matrixRows) ? matrixRows : []; - const maxCols = Math.min( - rows.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0), - 40 - ); - - let bestColumn = -1; - let bestScore = 0; - - const scoreColumn = col => { - let validCount = 0; - let sequentialHits = 0; - let prev = null; - - for (const row of rows.slice(0, 500)) { - if (!Array.isArray(row)) { - continue; - } - - const value = parseAssetImportSttNumber(row[col]); - if (value === null) { - continue; - } - - validCount += 1; - if (prev !== null && value === prev + 1) { - sequentialHits += 1; - } - prev = value; - } - - return (validCount * 2) + (sequentialHits * 5); - }; - - for (let col = 0; col < maxCols; col += 1) { - const score = scoreColumn(col); - - if (score > bestScore) { - bestScore = score; - bestColumn = col; - } - } - - return bestScore >= 12 ? bestColumn : -1; -} - -function parseAssetImportRowsLoose(matrixRows) { - const rows = Array.isArray(matrixRows) ? matrixRows : []; - const sttCol = detectLikelySttColumn(rows); - if (sttCol < 0) { - return []; - } - - const dataRows = rows.filter(row => { - if (!Array.isArray(row)) { - return false; - } - - const stt = parseAssetImportSttNumber(row[sttCol]); - if (stt === null) { - return false; - } - - const hasCoreValue = [2, 3, 4, 5, 9, 12] - .map(offset => sttCol + offset) - .some(index => String(row[index] ?? '').trim() !== ''); - return hasCoreValue; - }); - - return dataRows - .map((row, idx) => { - const endingBalance = parseAssetImportNumericValue(row[sttCol + 9] ?? '', 0); - const mapped = { - 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(), - model: String(row[sttCol + 4] ?? '').trim(), - serialNumber: '', - quantity: parseAssetImportNumericValue(row[sttCol + 6] ?? '', 0), - importInPeriod: parseAssetImportNumericValue(row[sttCol + 7] ?? '', 0), - exportInPeriod: parseAssetImportNumericValue(row[sttCol + 8] ?? '', 0), - endingBalance, - unit: String(row[sttCol + 5] ?? '').trim(), - department: '', - project: String(row[sttCol + 13] ?? '').trim(), - location: String(row[sttCol + 12] ?? '').trim(), - custodian: '', - purchaseDate: row[sttCol + 1] ?? '', - purchasePrice: '', - status: String(row[sttCol + 11] ?? '').trim(), - notes: String(row[sttCol + 10] ?? '').trim() - }; - - return finalizeImportedAssetPayload(mapped, idx + 2); - }) - .filter(row => !isHeaderLikeAssetImportRow(row)) - .filter(row => row.assetCode && row.assetName); -} - -function parseAssetImportRows(matrixRows) { - const genericRows = parseAssetImportRowsByHeaderMap(matrixRows); - if (genericRows.length > 0) { - return genericRows; - } - - const strictRows = parseAssetImportRowsFromMatrix(matrixRows); - if (strictRows.length > 0) { - return strictRows; - } - - return parseAssetImportRowsLoose(matrixRows); -} - -function countNonEmptyMatrixRows(matrixRows) { - return (Array.isArray(matrixRows) ? matrixRows : []).filter( - row => Array.isArray(row) && row.some(cell => String(cell ?? '').trim() !== '') - ).length; -} - -function parseAssetImportRowsFromWorkbook(workbook) { - const sheetNames = Array.isArray(workbook?.SheetNames) ? workbook.SheetNames : []; - let bestRows = []; - let bestSheetName = ''; - let bestNonEmptyRows = 0; - const diagnostics = []; - - for (const sheetName of sheetNames) { - const sheet = workbook.Sheets?.[sheetName]; - if (!sheet) { - continue; - } - - const matrixRows = XLSX.utils.sheet_to_json(sheet, { - header: 1, - defval: '', - raw: false - }); - - const parsedRows = parseAssetImportRows(matrixRows); - const nonEmptyRows = countNonEmptyMatrixRows(matrixRows); - diagnostics.push({ - sheetName, - parsedRows: parsedRows.length, - nonEmptyRows - }); - - if (parsedRows.length > bestRows.length || (parsedRows.length === bestRows.length && nonEmptyRows > bestNonEmptyRows)) { - bestRows = parsedRows; - bestSheetName = sheetName; - bestNonEmptyRows = nonEmptyRows; - } - } - - return { - rows: bestRows, - sheetName: bestSheetName, - diagnostics - }; -} - // Middleware app.use(cors()); app.use(express.json()); @@ -1215,37 +358,6 @@ async function createTables() { FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE ) END`, - - // Asset Inventory Table - `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetInventory') - BEGIN - CREATE TABLE AssetInventory ( - AssetId INT PRIMARY KEY IDENTITY(1,1), - AssetCode NVARCHAR(100) NOT NULL UNIQUE, - AssetName NVARCHAR(255) NOT NULL, - Model NVARCHAR(255), - SerialNumber NVARCHAR(100), - Quantity INT NOT NULL DEFAULT 0, - ImportInPeriod INT NOT NULL DEFAULT 0, - ExportInPeriod INT NOT NULL DEFAULT 0, - EndingBalance INT NOT NULL DEFAULT 0, - Unit NVARCHAR(50), - Department NVARCHAR(100), - Project NVARCHAR(150), - Location NVARCHAR(150), - Custodian NVARCHAR(100), - Borrower NVARCHAR(255), - ExportedBy NVARCHAR(100), - PurchaseDate DATE NULL, - PurchasePrice DECIMAL(18,2) NULL, - Status NVARCHAR(30) NOT NULL DEFAULT 'in_use', - Notes NVARCHAR(MAX), - CreatedBy INT NULL, - CreatedDate DATETIME DEFAULT GETDATE(), - UpdatedDate DATETIME DEFAULT GETDATE(), - FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL - ) - END`, // AuditLog Table `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog') @@ -1282,34 +394,6 @@ async function createTables() { // Ensure new columns exist on Applications for migrations try { - await pool.request().query(`IF EXISTS ( - SELECT 1 - FROM sys.columns - WHERE object_id = OBJECT_ID('dbo.AssetInventory') - AND name = 'Model' - AND max_length < 510 - ) - ALTER TABLE AssetInventory ALTER COLUMN Model NVARCHAR(255) NULL;`); - - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ImportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ImportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ImportInPeriod DEFAULT(0);`); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ExportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ExportInPeriod DEFAULT(0);`); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','EndingBalance') IS NULL ALTER TABLE AssetInventory ADD EndingBalance INT NOT NULL CONSTRAINT DF_AssetInventory_EndingBalance DEFAULT(0);`); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Project') IS NULL ALTER TABLE AssetInventory ADD Project NVARCHAR(150) NULL;`); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Borrower') IS NULL ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;`); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportedBy') IS NULL ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;`); - await pool.request().query(`UPDATE AssetInventory SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));`); - await pool.request().query(`UPDATE AssetInventory SET Quantity = ISNULL(NULLIF(Quantity, 0), EndingBalance);`); - await pool.request().query(` - UPDATE ai - SET ai.ExportedBy = COALESCE(NULLIF(LTRIM(RTRIM(u.FullName)), ''), NULLIF(LTRIM(RTRIM(u.Username)), '')) - FROM AssetInventory ai - LEFT JOIN Users u ON ai.CreatedBy = u.UserId - WHERE ai.ExportedBy IS NULL - `); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Category') IS NOT NULL ALTER TABLE AssetInventory DROP COLUMN Category;`); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Brand') IS NOT NULL ALTER TABLE AssetInventory DROP COLUMN Brand;`); - await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','WarrantyUntil') IS NOT NULL ALTER TABLE AssetInventory DROP COLUMN WarrantyUntil;`); - await pool.request().query(`IF COL_LENGTH('dbo.Applications','Url') IS NULL ALTER TABLE Applications ADD Url NVARCHAR(255);`); await pool.request().query(`IF COL_LENGTH('dbo.Applications','Description') IS NULL ALTER TABLE Applications ADD Description NVARCHAR(500);`); await pool.request().query(`IF COL_LENGTH('dbo.Users','ViewPassword') IS NULL ALTER TABLE Users ADD ViewPassword NVARCHAR(1024);`); @@ -2341,335 +1425,6 @@ app.delete('/api/accounts/:id', async (req, res) => { } }); -// ========================================== -// API ROUTES - Asset Inventory -// ========================================== - -app.get('/api/assets', async (req, res) => { - try { - const result = await pool.request().query(` - SELECT AssetId, AssetCode, AssetName, Model, SerialNumber, - Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, - Unit, Department, Project, Location, Custodian, Borrower, ExportedBy, - PurchaseDate, PurchasePrice, Status, Notes, CreatedBy, CreatedDate, UpdatedDate - FROM AssetInventory - ORDER BY UpdatedDate DESC, AssetName ASC - `); - - res.json({ success: true, data: result.recordset }); - } catch (err) { - res.status(500).json({ success: false, message: err.message }); - } -}); - -app.get('/api/assets/:id', async (req, res) => { - try { - const result = await pool.request() - .input('assetId', sql.Int, req.params.id) - .query(` - SELECT AssetId, AssetCode, AssetName, Model, SerialNumber, - Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, - Unit, Department, Project, Location, Custodian, Borrower, ExportedBy, - PurchaseDate, PurchasePrice, Status, Notes, CreatedBy, CreatedDate, UpdatedDate - FROM AssetInventory - WHERE AssetId = @assetId - `); - - if (result.recordset.length === 0) { - return res.status(404).json({ success: false, message: 'Asset not found' }); - } - - res.json({ success: true, data: result.recordset[0] }); - } catch (err) { - res.status(500).json({ success: false, message: err.message }); - } -}); - -app.post('/api/assets', requireAssetOrAdmin, async (req, res) => { - try { - const payload = normalizeAssetPayload(req.body); - const createdBy = getUserIdFromRequest(req); - const exportedBy = await getUserDisplayNameById(createdBy); - - if (!payload.assetCode || !payload.assetName) { - return res.status(400).json({ success: false, message: 'Asset code and asset name are required' }); - } - - const result = await pool.request() - .input('assetCode', sql.NVarChar, payload.assetCode) - .input('assetName', sql.NVarChar, payload.assetName) - .input('model', sql.NVarChar, payload.model) - .input('serialNumber', sql.NVarChar, payload.serialNumber) - .input('quantity', sql.Int, payload.quantity) - .input('importInPeriod', sql.Int, payload.importInPeriod) - .input('exportInPeriod', sql.Int, payload.exportInPeriod) - .input('endingBalance', sql.Int, payload.endingBalance) - .input('unit', sql.NVarChar, payload.unit) - .input('department', sql.NVarChar, payload.department) - .input('project', sql.NVarChar, payload.project) - .input('location', sql.NVarChar, payload.location) - .input('custodian', sql.NVarChar, payload.custodian) - .input('borrower', sql.NVarChar, payload.borrower) - .input('exportedBy', sql.NVarChar, exportedBy) - .input('purchaseDate', sql.Date, payload.purchaseDate) - .input('purchasePrice', sql.Decimal(18, 2), payload.purchasePrice) - .input('status', sql.NVarChar, payload.status) - .input('notes', sql.NVarChar, payload.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 - ); - SELECT SCOPE_IDENTITY() AS AssetId; - `); - - res.json({ success: true, message: 'Asset created', assetId: result.recordset[0].AssetId }); - } catch (err) { - if (String(err.message || '').includes('UNIQUE')) { - return res.status(409).json({ success: false, message: 'Asset code already exists' }); - } - - res.status(500).json({ success: false, message: err.message }); - } -}); - -app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => { - try { - const payload = normalizeAssetPayload(req.body); - 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' }); - } - - await pool.request() - .input('assetId', sql.Int, req.params.id) - .input('assetCode', sql.NVarChar, payload.assetCode) - .input('assetName', sql.NVarChar, payload.assetName) - .input('model', sql.NVarChar, payload.model) - .input('serialNumber', sql.NVarChar, payload.serialNumber) - .input('quantity', sql.Int, payload.quantity) - .input('importInPeriod', sql.Int, payload.importInPeriod) - .input('exportInPeriod', sql.Int, payload.exportInPeriod) - .input('endingBalance', sql.Int, payload.endingBalance) - .input('unit', sql.NVarChar, payload.unit) - .input('department', sql.NVarChar, payload.department) - .input('project', sql.NVarChar, payload.project) - .input('location', sql.NVarChar, payload.location) - .input('custodian', sql.NVarChar, payload.custodian) - .input('borrower', sql.NVarChar, payload.borrower) - .input('exportedBy', sql.NVarChar, exportedBy) - .input('purchaseDate', sql.Date, payload.purchaseDate) - .input('purchasePrice', sql.Decimal(18, 2), payload.purchasePrice) - .input('status', sql.NVarChar, payload.status) - .input('notes', sql.NVarChar, payload.notes) - .query(` - UPDATE AssetInventory - SET AssetCode = @assetCode, - AssetName = @assetName, - Model = @model, - SerialNumber = @serialNumber, - Quantity = @quantity, - ImportInPeriod = @importInPeriod, - ExportInPeriod = @exportInPeriod, - EndingBalance = @endingBalance, - Unit = @unit, - Department = @department, - Project = @project, - Location = @location, - Custodian = @custodian, - Borrower = @borrower, - ExportedBy = @exportedBy, - PurchaseDate = @purchaseDate, - PurchasePrice = @purchasePrice, - Status = @status, - Notes = @notes, - UpdatedDate = GETDATE() - WHERE AssetId = @assetId - `); - - res.json({ success: true, message: 'Asset updated' }); - } catch (err) { - if (String(err.message || '').includes('UNIQUE')) { - return res.status(409).json({ success: false, message: 'Asset code already exists' }); - } - - res.status(500).json({ success: false, message: err.message }); - } -}); - -app.delete('/api/assets/:id', requireAssetOrAdmin, async (req, res) => { - try { - await pool.request() - .input('assetId', sql.Int, req.params.id) - .query('DELETE FROM AssetInventory WHERE AssetId = @assetId'); - - res.json({ success: true, message: 'Asset deleted' }); - } catch (err) { - res.status(500).json({ success: false, message: err.message }); - } -}); - -app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async (req, res) => { - let incomingRows = []; - let source = 'rows'; - let parseDiagnostics = []; - - try { - if (req.file?.buffer) { - const workbook = XLSX.read(req.file.buffer, { type: 'buffer' }); - if (!workbook.SheetNames?.length) { - return res.status(400).json({ success: false, message: 'Excel file does not contain a worksheet' }); - } - - const parsed = parseAssetImportRowsFromWorkbook(workbook); - incomingRows = parsed.rows; - parseDiagnostics = parsed.diagnostics; - source = parsed.sheetName ? `file:${parsed.sheetName}` : 'file'; - } else { - incomingRows = Array.isArray(req.body?.rows) ? req.body.rows : []; - } - } catch (err) { - return res.status(400).json({ success: false, message: `Cannot parse import file: ${err.message}` }); - } - - if (!incomingRows.length) { - if (req.file) { - console.warn('Asset import parse returned 0 rows', { - diagnostics: parseDiagnostics - }); - } - - return res.status(400).json({ - success: false, - message: req.file - ? 'Khong tim thay dong du lieu hop le trong file Excel. Vui long kiem tra dong STT va du lieu cot ten/model/ton cuoi ky.' - : 'Import data is empty', - diagnostics: req.file ? parseDiagnostics : undefined - }); - } - - const createdBy = getUserIdFromRequest(req); - const exportedBy = await getUserDisplayNameById(createdBy); - const normalizedRows = incomingRows - .map(row => normalizeAssetPayload(row)) - .filter(row => !isHeaderLikeAssetImportRow(row)) - .filter(row => row.assetCode && row.assetName); - - if (!normalizedRows.length) { - return res.status(400).json({ success: false, message: 'No valid rows found. Asset code and name are required.' }); - } - - const transaction = new sql.Transaction(pool); - let inserted = 0; - let updated = 0; - - try { - await transaction.begin(); - - for (const row of normalizedRows) { - const mergeResult = 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(` - MERGE AssetInventory AS target - USING (SELECT @assetCode AS AssetCode) AS source - ON target.AssetCode = source.AssetCode - WHEN MATCHED THEN - UPDATE SET - AssetName = @assetName, - Model = @model, - SerialNumber = @serialNumber, - Quantity = @quantity, - ImportInPeriod = @importInPeriod, - ExportInPeriod = @exportInPeriod, - EndingBalance = @endingBalance, - Unit = @unit, - Department = @department, - Project = @project, - Location = @location, - Custodian = @custodian, - Borrower = @borrower, - ExportedBy = @exportedBy, - PurchaseDate = @purchaseDate, - PurchasePrice = @purchasePrice, - Status = @status, - Notes = @notes, - UpdatedDate = GETDATE() - WHEN NOT MATCHED THEN - INSERT ( - 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 - ) - OUTPUT $action AS MergeAction; - `); - - const mergeAction = String(mergeResult.recordset?.[0]?.MergeAction || '').toUpperCase(); - if (mergeAction === 'INSERT') { - inserted += 1; - } else if (mergeAction === 'UPDATE') { - updated += 1; - } - } - - await transaction.commit(); - - res.json({ - success: true, - message: `Import completed. Inserted: ${inserted}, Updated: ${updated}`, - data: { - source, - totalReceived: incomingRows.length, - processed: normalizedRows.length, - inserted, - updated - } - }); - } catch (err) { - try { - await transaction.rollback(); - } catch (rollbackErr) { - // Ignore rollback errors if transaction is already completed. - } - res.status(500).json({ success: false, message: err.message }); - } -}); - // ========================================== // API ROUTES - Database Info // ========================================== diff --git a/database/setup.sql b/database/setup.sql index a992592..2d1031d 100644 --- a/database/setup.sql +++ b/database/setup.sql @@ -145,6 +145,7 @@ END -- =========================================== -- 6. CREATE INDEXES +-- 6. CREATE INDEXES -- =========================================== IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username') BEGIN @@ -171,10 +172,21 @@ BEGIN CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status); END +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_AssetCode') +BEGIN + CREATE INDEX IX_AssetInventory_AssetCode ON AssetInventory(AssetCode); +END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_Status') +BEGIN + CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status); +END + PRINT 'Indexes created successfully.'; -- =========================================== -- 7. INSERT INITIAL DATA +-- 7. INSERT INITIAL DATA -- =========================================== -- Check if admin user exists @@ -199,6 +211,7 @@ END -- =========================================== -- 8. DISPLAY DATABASE INFORMATION +-- 8. DISPLAY DATABASE INFORMATION -- =========================================== PRINT ''; PRINT '========================================'; diff --git a/public/js/app.js b/public/js/app.js index efa9bee..ca2795a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -15,6 +15,7 @@ class AccountManager { this.applications = []; this.users = []; this.assets = []; + this.assets = []; this.roles = []; this.accountPage = 1; this.accountPageSize = 9; @@ -24,6 +25,8 @@ class AccountManager { this.userPageSize = 9; this.assetPage = 1; this.assetPageSize = 10; + this.assetPage = 1; + this.assetPageSize = 10; this.apiBase = '/api'; this.currentPage = 'dashboard'; this.accountSearchTerm = ''; @@ -34,6 +37,9 @@ class AccountManager { this.assetSearchTerm = ''; this.assetStatusFilter = ''; this.selectedAssetIds = new Set(); + this.assetSearchTerm = ''; + this.assetStatusFilter = ''; + this.selectedAssetIds = new Set(); this.mobileBreakpoint = 900; this.boundResizeHandler = null; this.configureNotifications(); @@ -166,6 +172,8 @@ class AccountManager { if (usersNav) usersNav.style.display = ''; const usersSection = document.getElementById('usersSection'); if (usersSection) usersSection.style.display = ''; + const usersSection = document.getElementById('usersSection'); + if (usersSection) usersSection.style.display = ''; } this.setupEventListeners(); @@ -201,6 +209,12 @@ class AccountManager { this.setupAddButtonListeners(); this.setupFilters(); this.setupAssetPagerListeners(); + } else if (page === 'assets') { + mainContent.innerHTML = this.getAssetsContent(); + this.setupAssetRowListeners(); + this.setupAddButtonListeners(); + this.setupFilters(); + this.setupAssetPagerListeners(); } else if (page === 'accounts') { mainContent.innerHTML = this.getAccountsContent(); this.setupAccountRowListeners(); @@ -481,6 +495,7 @@ class AccountManager { const accountSearch = document.getElementById('accountSearch'); const appSearch = document.getElementById('appSearch'); const assetSearch = document.getElementById('assetSearch'); + const assetSearch = document.getElementById('assetSearch'); if (accountSearch && accountSearch.dataset.focused === 'true') { const pos = accountSearch.selectionStart || accountSearch.value.length; @@ -499,6 +514,12 @@ class AccountManager { assetSearch.focus(); assetSearch.setSelectionRange(pos, pos); } + + if (assetSearch && assetSearch.dataset.focused === 'true') { + const pos = assetSearch.selectionStart || assetSearch.value.length; + assetSearch.focus(); + assetSearch.setSelectionRange(pos, pos); + } } setupEventListeners() { @@ -3448,6 +3469,35 @@ class AccountManager { }); } + const assetSearch = document.getElementById('assetSearch'); + if (assetSearch) { + assetSearch.value = this.assetSearchTerm; + const handleAssetSearch = event => { + this.assetSearchTerm = event.target.value.toLowerCase(); + this.assetPage = 1; + this.renderAssetsTableBody(); + }; + + assetSearch.addEventListener('input', handleAssetSearch); + assetSearch.addEventListener('focus', () => { + assetSearch.dataset.focused = 'true'; + }); + assetSearch.addEventListener('blur', () => { + assetSearch.dataset.focused = 'false'; + }); + }); + } + + const assetStatusFilter = document.getElementById('assetStatusFilter'); + if (assetStatusFilter) { + assetStatusFilter.value = this.assetStatusFilter || ''; + assetStatusFilter.addEventListener('change', (e) => { + this.assetStatusFilter = String(e.target.value || '').toLowerCase(); + this.assetPage = 1; + this.renderAssetsTableBody(); + }); + } + const assetSearch = document.getElementById('assetSearch'); if (assetSearch) { assetSearch.value = this.assetSearchTerm; diff --git a/public/modals.html b/public/modals.html index 1370f1d..74fe02c 100644 --- a/public/modals.html +++ b/public/modals.html @@ -197,183 +197,3 @@ - - - - - - - - - - - -