From 874bedf7ebf2760671fc6add9716b1c271889f9a Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 24 Apr 2026 09:23:46 +0700 Subject: [PATCH 1/5] refactor(registry-edit): permisson handUploadFile --- .../04_registryPerson/views/edit/components/Table.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/04_registryPerson/views/edit/components/Table.vue b/src/modules/04_registryPerson/views/edit/components/Table.vue index 470c010ac..ab1a275aa 100644 --- a/src/modules/04_registryPerson/views/edit/components/Table.vue +++ b/src/modules/04_registryPerson/views/edit/components/Table.vue @@ -941,6 +941,11 @@ onMounted(async () => {
Date: Fri, 24 Apr 2026 09:52:14 +0700 Subject: [PATCH 2/5] feat(leave): UI Show Card leaveWaitingSummary --- .../components/05_Leave/DetailLeavePage.vue | 28 ++++++++++++++----- .../components/05_Leave/DetailLeaveReject.vue | 28 ++++++++++++++----- .../09_leave/interface/request/leave.ts | 1 + 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/modules/09_leave/components/05_Leave/DetailLeavePage.vue b/src/modules/09_leave/components/05_Leave/DetailLeavePage.vue index 1ba2c0a2b..4f4b2ee46 100644 --- a/src/modules/09_leave/components/05_Leave/DetailLeavePage.vue +++ b/src/modules/09_leave/components/05_Leave/DetailLeavePage.vue @@ -82,6 +82,7 @@ const formData = reactive({ status: "", //สถานะการของลา leaveLimit: 0, //โควต้าลา(แต่ละประเภท)หน่วยเป็นวัน leaveSummary: 0, //ลาป่วยไปแล้ว(แต่ละประเภท)หน่วยเป็นวัน + leaveWaitingSummary: 0, //ลาอยู่ระหว่างการพิจารณา(แต่ละประเภท)หน่วยเป็นวัน leaveRemain: 0, //คงเหลือโควต้า(แต่ละประเภท)หน่วยเป็นวัน leaveWrote: "", //เขียนที่ leaveAddress: "", //สถานที่ติดต่อขณะลา @@ -391,6 +392,9 @@ async function fetchDetailLeave(paramsId: string) { formData.leaveRange = data.leaveRange; formData.commanderPosition = data.commanderPosition; formData.leaveRangeEnd = data.leaveRangeEnd; + formData.leaveWaitingSummary = data.leaveWaitingSummary + ? data.leaveWaitingSummary + : "0"; keycloakUserId.value = data.keycloakUserId; rows.value = { commanders: data.commanders, @@ -773,40 +777,50 @@ onMounted(async () => {
-
+
{{ formData.leaveLimit }}
- ได้รับ + ได้รับ
-
+
{{ formData.leaveSummary }}
- ใช้ไป + ใช้ไป
{{ formData.leaveRemain }}
- คงเหลือ + คงเหลือ +
+
+
+
+ +
+ {{ formData.leaveWaitingSummary }} +
+
+ อยู่ระหว่างการพิจารณา
diff --git a/src/modules/09_leave/components/05_Leave/DetailLeaveReject.vue b/src/modules/09_leave/components/05_Leave/DetailLeaveReject.vue index 6b0e4e44b..6653eed35 100644 --- a/src/modules/09_leave/components/05_Leave/DetailLeaveReject.vue +++ b/src/modules/09_leave/components/05_Leave/DetailLeaveReject.vue @@ -151,6 +151,7 @@ const formData = reactive({ leaveSubTypeName: "", commanderPosition: "", leaveRangeEnd: "", + leaveWaitingSummary: 0, //ลาอยู่ระหว่างการพิจารณา(แต่ละประเภท)หน่วยเป็นวัน }); const isLoadData = ref(false); @@ -217,6 +218,9 @@ async function fetchDetailLeave(paramsId: string) { formData.leaveLimit = data.leaveLimit; formData.leaveSummary = data.leaveSummary; formData.leaveRemain = data.leaveRemain; + formData.leaveWaitingSummary = data.leaveWaitingSummary + ? data.leaveWaitingSummary + : 0; formData.leaveWrote = data.leaveWrote; formData.leaveAddress = data.leaveAddress; formData.leaveNumber = data.leaveNumber; @@ -626,40 +630,50 @@ onMounted(async () => {
-
+
{{ formData.leaveLimit }}
- ได้รับ + ได้รับ
-
+
{{ formData.leaveSummary }}
- ใช้ไป + ใช้ไป
{{ formData.leaveRemain }}
- คงเหลือ + คงเหลือ +
+
+
+
+ +
+ {{ formData.leaveWaitingSummary }} +
+
+ อยู่ระหว่างการพิจารณา
diff --git a/src/modules/09_leave/interface/request/leave.ts b/src/modules/09_leave/interface/request/leave.ts index 95b81143e..27426c709 100644 --- a/src/modules/09_leave/interface/request/leave.ts +++ b/src/modules/09_leave/interface/request/leave.ts @@ -41,6 +41,7 @@ interface FormData { status: string; //สถานะการของลา leaveLimit: number; //โควต้าลา(แต่ละประเภท)หน่วยเป็นวัน leaveSummary: number; //ลาป่วยไปแล้ว(แต่ละประเภท)หน่วยเป็นวัน + leaveWaitingSummary: number; //ลาอยู่ระหว่างการพิจารณา(แต่ละประเภท)หน่วยเป็นวัน leaveRemain: number; //คงเหลือโควต้า(แต่ละประเภท)หน่วยเป็นวัน // leaveStartDate: Date | null; //*วัน เดือน ปีเริ่มต้นลา // leaveEndDate: Date | null; //*วัน เดือน ปีสิ้นสุดลา From 9132efed119adedc7a866c45d5c531364a8ae879 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 24 Apr 2026 14:06:01 +0700 Subject: [PATCH 3/5] feat(registry-edit): previewFile excel Position --- .../04_registryPerson/utils/excelParser.ts | 177 ++++++++++++ .../edit/components/DialogExcelPreview.vue | 263 ++++++++++++++++++ .../views/edit/components/Table.vue | 106 ++++--- 3 files changed, 512 insertions(+), 34 deletions(-) create mode 100644 src/modules/04_registryPerson/utils/excelParser.ts create mode 100644 src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue 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..c100d9ba9 --- /dev/null +++ b/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue @@ -0,0 +1,263 @@ + + + diff --git a/src/modules/04_registryPerson/views/edit/components/Table.vue b/src/modules/04_registryPerson/views/edit/components/Table.vue index ab1a275aa..f7cb3a8d6 100644 --- a/src/modules/04_registryPerson/views/edit/components/Table.vue +++ b/src/modules/04_registryPerson/views/edit/components/Table.vue @@ -21,6 +21,7 @@ import type { import DialogForm from "@/modules/04_registryPerson/views/edit/components/DialogForm.vue"; import DialogSort from "@/modules/04_registryPerson/views/edit/components/DialogSort.vue"; +import DialogExcelPreview from "@/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue"; import CurruncyInput from "@/components/CurruncyInput.vue"; const $q = useQuasar(); @@ -53,6 +54,11 @@ const amountRef = ref(null); const amountSpecialRef = ref(null); const currencyPopupRef = ref(null); +// Excel Preview +const excelPreviewModal = ref(false); +const selectedExcelFile = ref(null); +const isParsingExcel = ref(false); + //Table const isLoad = ref(true); const rowIndex = ref(0); @@ -851,7 +857,7 @@ async function validateAndSave( /** * ฟังก์ชันอัปโหลดไฟล์ Excel - * ส่งไฟล์ Excel ไปยัง API โดยตรง + * เปิด preview modal ก่อนอัปโหลด */ function handUploadFile() { const input = document.createElement("input"); @@ -859,48 +865,73 @@ function handUploadFile() { input.accept = ".xlsx,.xls"; input.onchange = async (e: Event) => { - try { - const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) return; + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; - // ตรวจสอบชนิดไฟล์โดยตรวจสอบนามสกุล - const validExtensions = [".xlsx", ".xls"]; - const fileExtension = file.name - .substring(file.name.lastIndexOf(".")) - .toLowerCase(); + // ตรวจสอบชนิดไฟล์โดยตรวจสอบนามสกุล + const validExtensions = [".xlsx", ".xls"]; + const fileExtension = file.name + .substring(file.name.lastIndexOf(".")) + .toLowerCase(); - if (!validExtensions.includes(fileExtension)) { - messageError("กรุณาเลือกไฟล์ Excel เท่านั้น (.xlsx, .xls)"); - return; - } - - showLoader(); - const type = empType.value === "officer" ? "office" : "employee"; - const formData = new FormData(); - formData.append("file", file); - - await http.post( - config.API.uploadProfile(type, profileId.value), - formData, - { - headers: { - "Content-Type": "multipart/form-data", + if (!validExtensions.includes(fileExtension)) { + messageError($q, { + response: { + data: { + title: "กรุณาเลือกไฟล์ Excel เท่านั้น (.xlsx, .xls)", }, - } - ); - - success($q, "อัปโหลดไฟล์สำเร็จ"); - await fetchData(); - } catch (error) { - messageError($q, error); - } finally { - hideLoader(); + }, + }); + return; } + + // เก็บไฟล์และเปิด preview modal + selectedExcelFile.value = file; + excelPreviewModal.value = true; }; input.click(); } +/** + * ฟังก์ชันยืนยันการอัปโหลดไฟล์ + * ส่งไฟล์ไปยัง API เมื่อ user ยืนยันจาก preview modal + */ +async function onConfirmUpload(file: File) { + try { + isParsingExcel.value = true; + showLoader(); + + const type = empType.value === "officer" ? "office" : "employee"; + const formData = new FormData(); + formData.append("file", file); + + await http.post(config.API.uploadProfile(type, profileId.value), formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + success($q, "อัปโหลดไฟล์สำเร็จ"); + await fetchData(); + excelPreviewModal.value = false; + } catch (error) { + messageError($q, error); + } finally { + hideLoader(); + isParsingExcel.value = false; + } +} + +/** + * ฟังก์ชันยกเลิกการอัปโหลด + * ปิด preview modal และล้างค่า + */ +function onCancelUpload() { + selectedExcelFile.value = null; + excelPreviewModal.value = false; +} + onMounted(async () => { await Promise.all([fetchData(), fetchType()]); }); @@ -1794,6 +1825,13 @@ onMounted(async () => { :fetch-data="fetchData" :columns="columns" /> + + From 7211081d14e20339c151fe9c3d804f48218cbbba Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 24 Apr 2026 14:29:32 +0700 Subject: [PATCH 4/5] refactor(leave-history): replace overflow-hidden with overflow-auto for better scrollability --- src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue b/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue index f62052426..60ca9409e 100644 --- a/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue +++ b/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue @@ -386,7 +386,7 @@ watch(modal, async (val) => { -
+
{ :rules="[(val: string) => !val || /^\d+(\.\d*)?$/.test(val) || 'กรุณากรอกเฉพาะตัวเลข']" hint="* จำนวนวันรวม การลาที่บันทึกในระบบและการลาย้อนหลังในปีงบประมาณนี้" /> -
Date: Fri, 24 Apr 2026 14:51:31 +0700 Subject: [PATCH 5/5] refactor: dialogConfirm onConfirmUpload --- .../edit/components/DialogExcelPreview.vue | 17 ++++++++++++----- .../views/edit/components/Table.vue | 1 - 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue b/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue index c100d9ba9..326a767c1 100644 --- a/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue +++ b/src/modules/04_registryPerson/views/edit/components/DialogExcelPreview.vue @@ -1,11 +1,16 @@