diff --git a/src/modules/04_registryPerson/utils/excelParser.ts b/src/modules/04_registryPerson/utils/excelParser.ts new file mode 100644 index 000000000..b954edfc0 --- /dev/null +++ b/src/modules/04_registryPerson/utils/excelParser.ts @@ -0,0 +1,177 @@ +import * as XLSX from "xlsx"; + +export interface ExcelPreviewData { + fileName: string; + fileSize: number; + sheetNames: string[]; + headers: string[]; + rows: Array>; + totalRows: number; + previewRows: number; +} + +export interface ParseOptions { + maxPreviewRows?: number; + sheetIndex?: number; +} + +const DEFAULT_CONFIG = { + MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB + MAX_PREVIEW_ROWS: 0, // 0 = ไม่จำกัด แสดงทั้งหมด + ALLOWED_EXTENSIONS: [".xlsx", ".xls"], +}; + +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; +} + +function getFileExtension(fileName: string): string { + return fileName.substring(fileName.lastIndexOf(".")).toLowerCase(); +} + +function validateFile(file: File): { message: string } | null { + const extension = getFileExtension(file.name); + + if (!DEFAULT_CONFIG.ALLOWED_EXTENSIONS.includes(extension)) { + return { + message: `กรุณาเลือกไฟล์ Excel เท่านั้น (${DEFAULT_CONFIG.ALLOWED_EXTENSIONS.join(", ")})`, + }; + } + + if (file.size > DEFAULT_CONFIG.MAX_FILE_SIZE) { + const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2); + const maxSizeMB = (DEFAULT_CONFIG.MAX_FILE_SIZE / (1024 * 1024)).toFixed(2); + return { + message: `ขนาดไฟล์เกิน ${maxSizeMB}MB (ไฟล์ของคุณ: ${fileSizeMB}MB)`, + }; + } + + return null; +} + +export async function parseExcelFile( + file: File, + options: ParseOptions = {} +): Promise { + const maxPreviewRows = options.maxPreviewRows ?? DEFAULT_CONFIG.MAX_PREVIEW_ROWS; + const sheetIndex = options.sheetIndex ?? 0; + + // Validate file + const validationError = validateFile(file); + if (validationError) { + throw new Error(validationError.message); + } + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e) => { + try { + const data = e.target?.result; + if (!data) { + reject(new Error("ไม่สามารถอ่านไฟล์ได้")); + return; + } + + const workbook = XLSX.read(data, { type: "binary" }); + + if (workbook.SheetNames.length === 0) { + reject(new Error("ไฟล์ไม่มีข้อมูล")); + return; + } + + const sheetName = workbook.SheetNames[sheetIndex]; + const worksheet = workbook.Sheets[sheetName]; + + // ใช้ sheet_to_json กับ raw: true เพื่ออ่าน raw values ก่อน + const jsonData = XLSX.utils.sheet_to_json(worksheet, { + header: 1, + defval: "", + raw: true, // อ่าน raw values + }) as any[][]; + + // หลังจากได้ raw data แล้ว ต้องไปอ่านค่าจาก formula ที่ cells โดยตรง + // เพราะบาง formula อาจจะยังไม่ถูกคำนวณ + const range = XLSX.utils.decode_range(worksheet["!ref"] || "A1"); + + // อ่านค่าจาก cells ที่มี formula (column สุดท้าย) + for (let row = range.s.r; row <= range.e.r; row++) { + for (let col = range.s.c; col <= range.e.c; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); + const cell = worksheet[cellAddress]; + + // ถ้ามี formula ให้ใช้ผลลัพธ์ที่คำนวณแล้ว + if (cell && cell.f && !cell.v) { + // ลองใช้ cell.w (formatted value) ถ้ามี + if (cell.w) { + // แปลง array index ให้ถูกต้อง (row + 1 เพราะมี header row) + const arrayRowIndex = row - range.s.r; + if (jsonData[arrayRowIndex] && jsonData[arrayRowIndex][col] !== undefined) { + jsonData[arrayRowIndex][col] = cell.w; + } + } + } + } + } + + if (jsonData.length === 0) { + reject(new Error("ไฟล์ไม่มีข้อมูล")); + return; + } + + // Extract headers from first row (ภาษาไทย) + const headers = jsonData[0].map((h: any) => String(h ?? "")); + + // Extract data rows (skip header row) + const dataRows = jsonData.slice(1).filter((row) => row.some((cell) => cell !== "")); + + if (dataRows.length === 0) { + reject(new Error("ไฟล์ไม่มีข้อมูล")); + return; + } + + // Convert to array of objects - ใช้ header จาก Excel เป็น field names โดยตรง + // ถ้า maxPreviewRows = 0 จะแสดงทั้งหมด มิฉะนั้นจะแสดงตามจำนวนที่กำหนด + const rowsToProcess = maxPreviewRows === 0 ? dataRows : dataRows.slice(0, maxPreviewRows); + const rows = rowsToProcess.map((row, rowIndex) => { + const obj: Record = { + id: `row_${rowIndex}`, + }; + + // ใช้ header เป็น field names โดยตรง + headers.forEach((header, index) => { + const cellValue = row[index] ?? ""; + // ใช้ค่าจาก Excel โดยตรง ไม่แปลงค่า + obj[header] = cellValue; + }); + + return obj; + }); + + resolve({ + fileName: file.name, + fileSize: file.size, + sheetNames: workbook.SheetNames, + headers, + rows, + totalRows: dataRows.length, + previewRows: rows.length, + }); + } catch (error) { + reject(new Error("ไม่สามารถอ่านไฟล์ Excel ได้ กรุณาตรวจสอบไฟล์อีกครั้ง")); + } + }; + + reader.onerror = () => { + reject(new Error("ไม่สามารถอ่านไฟล์ได้")); + }; + + reader.readAsBinaryString(file); + }); +} + +export { formatFileSize }; diff --git a/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue b/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue new file mode 100644 index 000000000..326a767c1 --- /dev/null +++ b/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue @@ -0,0 +1,270 @@ + + + diff --git a/src/modules/04_registryPerson/views/edit/components/Table.vue b/src/modules/04_registryPerson/views/edit/components/Table.vue index 470c010ac..f470254c6 100644 --- a/src/modules/04_registryPerson/views/edit/components/Table.vue +++ b/src/modules/04_registryPerson/views/edit/components/Table.vue @@ -1,7 +1,6 @@