import xlsx from 'xlsx' import ProductService from '#services/product_service' import LogService from '#services/log_service' export interface ImportRowResult { row: number sku?: string status: 'created' | 'updated' | 'error' message?: string } export interface ImportSummary { total: number created: number updated: number failed: number rows: ImportRowResult[] } const VALID_HEADERS = ['sku', 'condition', 'qty', 'price'] /** * Import sản phẩm từ file Excel. Cột yêu cầu: sku, condition, qty, price. */ export default class ImportService { static async importFromFile(filePath: string, username: string): Promise { const wb = xlsx.readFile(filePath) const sheet = wb.Sheets[wb.SheetNames[0]] const rows = xlsx.utils.sheet_to_json>(sheet, { defval: null }) return this.processRows(rows, username) } static async importFromBuffer(buffer: Buffer, username: string): Promise { const wb = xlsx.read(buffer, { type: 'buffer' }) const sheet = wb.Sheets[wb.SheetNames[0]] const rows = xlsx.utils.sheet_to_json>(sheet, { defval: null }) return this.processRows(rows, username) } private static async processRows( rows: Record[], username: string ): Promise { const summary: ImportSummary = { total: rows.length, created: 0, updated: 0, failed: 0, rows: [] } for (let i = 0; i < rows.length; i++) { const rowNo = i + 2 // +1 header, +1 1-based const raw = this.normalizeKeys(rows[i]) const error = this.validateRow(raw) if (error) { summary.failed++ summary.rows.push({ row: rowNo, sku: raw.sku, status: 'error', message: error }) continue } try { const { created } = await ProductService.upsert({ sku: String(raw.sku).trim(), condition: String(raw.condition).trim(), qty: Number(raw.qty), price: Number(raw.price), }) created ? summary.created++ : summary.updated++ summary.rows.push({ row: rowNo, sku: raw.sku, status: created ? 'created' : 'updated' }) } catch (e: any) { summary.failed++ summary.rows.push({ row: rowNo, sku: raw.sku, status: 'error', message: e.message }) } } await LogService.record({ username, actionName: 'Import Excel', action: 'import', meta: { total: summary.total, created: summary.created, updated: summary.updated, failed: summary.failed }, }) return summary } private static normalizeKeys(row: Record): Record { const out: Record = {} for (const [k, v] of Object.entries(row)) { out[String(k).trim().toLowerCase()] = v } return out } private static validateRow(raw: Record): string | null { for (const h of VALID_HEADERS) { if (raw[h] === null || raw[h] === undefined || raw[h] === '') { return `Thiếu cột "${h}"` } } if (Number.isNaN(Number(raw.qty))) return 'qty không hợp lệ' if (Number.isNaN(Number(raw.price))) return 'price không hợp lệ' return null } }