177 lines
6.3 KiB
TypeScript
177 lines
6.3 KiB
TypeScript
import * as XLSX from "xlsx";
|
|
|
|
export interface ExcelPreviewData {
|
|
fileName: string;
|
|
fileSize: number;
|
|
sheetNames: string[];
|
|
headers: string[];
|
|
rows: Array<Record<string, any>>;
|
|
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<ExcelPreviewData> {
|
|
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<string, any> = {
|
|
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 };
|