127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import xlsx from 'xlsx'
|
|
import ProductService from '#services/product_service'
|
|
import LogService from '#services/log_service'
|
|
import NotificationService from '#services/notification_service'
|
|
import type { NotificationType } from '#models/notification'
|
|
|
|
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<ImportSummary> {
|
|
const wb = xlsx.readFile(filePath)
|
|
const sheet = wb.Sheets[wb.SheetNames[0]]
|
|
const rows = xlsx.utils.sheet_to_json<Record<string, any>>(sheet, { defval: null })
|
|
return this.processRows(rows, username)
|
|
}
|
|
|
|
static async importFromBuffer(buffer: Buffer, username: string): Promise<ImportSummary> {
|
|
const wb = xlsx.read(buffer, { type: 'buffer' })
|
|
const sheet = wb.Sheets[wb.SheetNames[0]]
|
|
const rows = xlsx.utils.sheet_to_json<Record<string, any>>(sheet, { defval: null })
|
|
return this.processRows(rows, username)
|
|
}
|
|
|
|
private static async processRows(
|
|
rows: Record<string, any>[],
|
|
username: string
|
|
): Promise<ImportSummary> {
|
|
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 },
|
|
})
|
|
|
|
// Thông báo cho người dùng: cảnh báo nếu import trùng / có dòng lỗi, ngược lại báo thành công.
|
|
const type: NotificationType =
|
|
summary.failed > 0 ? 'error' : summary.updated > 0 ? 'warning' : 'success'
|
|
const parts: string[] = []
|
|
if (summary.created > 0) parts.push(`${summary.created} sản phẩm mới`)
|
|
if (summary.updated > 0) parts.push(`${summary.updated} sản phẩm đã tồn tại (import trùng, đã cập nhật)`)
|
|
if (summary.failed > 0) parts.push(`${summary.failed} dòng lỗi`)
|
|
await NotificationService.notify({
|
|
type,
|
|
title:
|
|
summary.updated > 0
|
|
? 'Import trùng sản phẩm đã có'
|
|
: summary.failed > 0
|
|
? 'Import Excel có lỗi'
|
|
: 'Import Excel thành công',
|
|
message: `Đã xử lý ${summary.total} dòng: ${parts.join(', ') || 'không có thay đổi'}.`,
|
|
meta: {
|
|
total: summary.total,
|
|
created: summary.created,
|
|
updated: summary.updated,
|
|
failed: summary.failed,
|
|
},
|
|
})
|
|
|
|
return summary
|
|
}
|
|
|
|
private static normalizeKeys(row: Record<string, any>): Record<string, any> {
|
|
const out: Record<string, any> = {}
|
|
for (const [k, v] of Object.entries(row)) {
|
|
out[String(k).trim().toLowerCase()] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
private static validateRow(raw: Record<string, any>): 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
|
|
}
|
|
}
|