From cba8f4b70308c2e538e63e68041f4b5f2b3d4d13 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 20 Apr 2026 09:40:16 +0700 Subject: [PATCH 01/49] =?UTF-8?q?fixed#2435=20=E0=B8=A3=E0=B8=B0=E0=B8=9A?= =?UTF-8?q?=E0=B8=9A=E0=B9=81=E0=B8=81=E0=B9=89=E0=B9=84=E0=B8=82=E0=B8=97?= =?UTF-8?q?=E0=B8=B0=E0=B9=80=E0=B8=9A=E0=B8=B5=E0=B8=A2=E0=B8=99=E0=B8=9B?= =?UTF-8?q?=E0=B8=A3=E0=B8=B0=E0=B8=A7=E0=B8=B1=E0=B8=95=E0=B8=B4=E0=B8=95?= =?UTF-8?q?=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=87/?= =?UTF-8?q?=E0=B9=80=E0=B8=87=E0=B8=B4=E0=B8=99=E0=B9=80=E0=B8=94=E0=B8=B7?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=20>>=20=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88?= =?UTF-8?q?=E0=B8=87/=E0=B9=80=E0=B8=87=E0=B8=B4=E0=B8=99=E0=B9=80?= =?UTF-8?q?=E0=B8=94=E0=B8=B7=E0=B8=AD=E0=B8=99=20(=E0=B8=8A=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=8A=E0=B9=88=E0=B8=AD=E0=B8=87=E0=B8=81?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=81=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1?= =?UTF-8?q?=E0=B8=B9=E0=B8=A5=E0=B8=AB=E0=B8=B2=E0=B8=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../04_registryPerson/views/edit/components/FormPosition.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/04_registryPerson/views/edit/components/FormPosition.vue b/src/modules/04_registryPerson/views/edit/components/FormPosition.vue index c30fec131..b6ef8c829 100644 --- a/src/modules/04_registryPerson/views/edit/components/FormPosition.vue +++ b/src/modules/04_registryPerson/views/edit/components/FormPosition.vue @@ -706,6 +706,7 @@ function classInput(val: boolean) { hide-bottom-space autocomplete="on" name="organization" + :label="`${'หน่วยงาน'}`" /> From 0ccda33b37954ce32256cf0a31db12165852e04b Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 20 Apr 2026 09:47:20 +0700 Subject: [PATCH 02/49] =?UTF-8?q?fixed#1565=20=E0=B8=A3=E0=B8=B0=E0=B8=9A?= =?UTF-8?q?=E0=B8=9A=E0=B9=81=E0=B8=88=E0=B9=89=E0=B8=87=E0=B8=9B=E0=B8=B1?= =?UTF-8?q?=E0=B8=8D=E0=B8=AB=E0=B8=B2=E0=B8=9A=E0=B8=B1=E0=B8=87=E0=B8=84?= =?UTF-8?q?=E0=B8=B1=E0=B8=9A=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B8=81=E0=B8=A3?= =?UTF-8?q?=E0=B8=AD=E0=B8=81=E0=B8=AD=E0=B8=B5=E0=B9=80=E0=B8=A1=E0=B8=A5?= =?UTF-8?q?=20=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=A3=E0=B8=B0=E0=B8=9A?= =?UTF-8?q?=E0=B8=B8=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84=E0=B8=A7=E0=B8=B2?= =?UTF-8?q?=E0=B8=A1=E0=B8=A7=E0=B9=88=E0=B8=B2=20admin=20=E0=B8=88?= =?UTF-8?q?=E0=B8=B0=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B8=95=E0=B9=88=E0=B8=AD?= =?UTF-8?q?=E0=B8=81=E0=B8=A5=E0=B8=B1=E0=B8=9A=E0=B8=97=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=AD=E0=B8=B5=E0=B9=80=E0=B8=A1=E0=B8=A5=E0=B9=80=E0=B8=9B?= =?UTF-8?q?=E0=B9=87=E0=B8=99=E0=B8=A5=E0=B8=B3=E0=B8=94=E0=B8=B1=E0=B8=9A?= =?UTF-8?q?=E0=B9=81=E0=B8=A3=E0=B8=81=E0=B8=81=E0=B8=A3=E0=B8=B8=E0=B8=93?= =?UTF-8?q?=E0=B8=B2=E0=B8=95=E0=B8=A3=E0=B8=A7=E0=B8=88=E0=B8=AA=E0=B8=AD?= =?UTF-8?q?=E0=B8=9A=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=AD=E0=B8=B5=E0=B9=80?= =?UTF-8?q?=E0=B8=A1=E0=B8=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dialogs/DialogDebug.vue | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/Dialogs/DialogDebug.vue b/src/components/Dialogs/DialogDebug.vue index e84ef99da..6443dff90 100644 --- a/src/components/Dialogs/DialogDebug.vue +++ b/src/components/Dialogs/DialogDebug.vue @@ -354,6 +354,11 @@ function onClose() {
+
+
+ ผู้ดูแลระบบจะติดต่อกลับผ่านทางอีเมลที่ท่านระบุ กรุณาตรวจสอบอีเมลของท่านเป็นระยะ +
+
@@ -378,12 +380,6 @@ function onClose() { v-model="formData.phone" class="inputgreen" hide-bottom-space - :rules="[ - () => - !!formData.email || - !!formData.phone || - 'กรุณากรอกอีเมลหรือเบอร์โทรติดต่อกลับ', - ]" />
From 519372056780d3c5a67983f47cc367ee51c7de0a Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 20 Apr 2026 09:55:27 +0700 Subject: [PATCH 03/49] fixed format email --- src/components/Dialogs/DialogDebug.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Dialogs/DialogDebug.vue b/src/components/Dialogs/DialogDebug.vue index 6443dff90..42230b6f4 100644 --- a/src/components/Dialogs/DialogDebug.vue +++ b/src/components/Dialogs/DialogDebug.vue @@ -368,7 +368,11 @@ function onClose() { class="inputgreen" hide-bottom-space :rules="[ - (val: string) => !!val || 'กรุณากรอกอีเมลติดต่อกลับ', + (val: string) => !!val || 'กรุณากรอกที่อยู่อีเมล', + (val: string) => { + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailPattern.test(val) || 'กรุณากรอกที่อยู่อีเมลในรูปแบบที่ถูกต้อง'; + } ]" /> From 1de3d3575254732e4c72bf792a95a744c0cd4149 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Tue, 21 Apr 2026 14:14:38 +0700 Subject: [PATCH 04/49] refactor(retirement): download retirementReport --- .../01_retirement/RetirementDetail.vue | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/modules/06_retirement/components/01_retirement/RetirementDetail.vue b/src/modules/06_retirement/components/01_retirement/RetirementDetail.vue index 2f773e5e1..08d05ff47 100644 --- a/src/modules/06_retirement/components/01_retirement/RetirementDetail.vue +++ b/src/modules/06_retirement/components/01_retirement/RetirementDetail.vue @@ -370,18 +370,32 @@ async function uploadFile(event: any, date: any) { */ async function downloadAttachment(type: string, id: string) { showLoader(); - await http - .get(config.API.reportRetireList(type, id)) - .then(async (res) => { - const data = res.data.result; - await genReport(data, `รายชื่อผู้เกษียณอายุราชการ`, type); - }) - .catch(async (e) => { - messageError($q, JSON.parse(await e.response.data.text())); - }) - .finally(() => { - hideLoader(); - }); + try { + const response = await http.get( + config.API.retirementReport + `/${type}/${id}`, + { + headers: { + accept: + type === "pdf" + ? "application/pdf" + : "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "content-Type": "application/json", + }, + responseType: "blob", + } + ); + const blob = response.data; + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `รายชื่อผู้เกษียณอายุราชการ.${type}`; + link.click(); + URL.revokeObjectURL(url); + } catch (e) { + messageError($q, e); + } finally { + hideLoader(); + } } // ยืนยันการแก้ไขข้อมูล From 5bf1f2f197d51b1785fd0b157b50ac92ae05172b Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 23 Apr 2026 10:43:08 +0700 Subject: [PATCH 05/49] =?UTF-8?q?fixed=20=E0=B8=AA=E0=B8=B4=E0=B8=97?= =?UTF-8?q?=E0=B8=98=E0=B8=B4=E0=B9=8C=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=A5?= =?UTF-8?q?=E0=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/07_LeaveHistory/DialogForm.vue | 29 +++++++++---------- .../09_leave/views/07_LeaveHistoryMain.vue | 8 ++--- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue b/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue index b33009b20..f62052426 100644 --- a/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue +++ b/src/modules/09_leave/components/07_LeaveHistory/DialogForm.vue @@ -453,7 +453,7 @@ watch(modal, async (val) => { -
+
{ dense outlined label="จำนวนสิทธิ์การลา" - hide-bottom-space - :rules="[(val: string) => !val || /^\d+(\.\d*)?$/.test(val) || 'กรุณากรอกเฉพาะตัวเลข']" + :rules="[(val: string) => !val || /^\d+$/.test(val) || 'กรุณากรอกเฉพาะตัวเลขที่เป็นจำนวนเต็ม']" + hint="* สำหรับลาพักผ่อนเท่านั้น คือสิทธิ์ลาประจำปี + สิทธิ์สะสม" />
@@ -471,10 +471,11 @@ watch(modal, async (val) => { v-model="formData.leaveDaysUsed" dense outlined - label="ที่ใช้ไป (วัน)" - hide-bottom-space + label="ที่ใช้ไปทั้งหมด (วัน)" :rules="[(val: string) => !val || /^\d+(\.\d*)?$/.test(val) || 'กรุณากรอกเฉพาะตัวเลข']" + hint="* จำนวนวันรวม การลาที่บันทึกในระบบและการลาย้อนหลังในปีงบประมาณนี้" /> +
{ v-model="formData.leaveCount" dense outlined - label="ที่ใช้ไป (ครั้ง)" - hide-bottom-space - mask="#" - reverse-fill-mask + label="ที่ใช้ไปทั้งหมด (ครั้ง)" + :rules="[(val: string) => !val || /^\d+$/.test(val) || 'กรุณากรอกเฉพาะตัวเลขที่เป็นจำนวนเต็ม']" + hint="* จำนวนครั้งรวม การลาที่บันทึกในระบบและการลาย้อนหลังในปีงบประมาณนี้" />
@@ -495,9 +495,9 @@ watch(modal, async (val) => { v-model="formData.beginningLeaveDays" dense outlined - label="ยกมา (วัน)" - hide-bottom-space + label="จำนวนวันลาก่อนใช้งานระบบ" :rules="[(val: string) => !val || /^\d+(\.\d*)?$/.test(val) || 'กรุณากรอกเฉพาะตัวเลข']" + hint="* จำนวนวันรวมการลาในปีงบประมาณนี้ที่เกิดขึ้นก่อนเริ่มใช้ระบบ" />
@@ -507,10 +507,9 @@ watch(modal, async (val) => { v-model="formData.beginningLeaveCount" dense outlined - label="ยกมา (ครั้ง)" - hide-bottom-space - mask="#" - reverse-fill-mask + label="จำนวนครั้งที่ลาก่อนใช้งานระบบ" + :rules="[(val: string) => !val || /^\d+$/.test(val) || 'กรุณากรอกเฉพาะตัวเลขที่เป็นจำนวนเต็ม']" + hint="* จำนวนครั้งของการลาในปีงบประมาณนี้ที่เกิดขึ้นก่อนเริ่มใช้ระบบ" /> diff --git a/src/modules/09_leave/views/07_LeaveHistoryMain.vue b/src/modules/09_leave/views/07_LeaveHistoryMain.vue index 8fdfcc774..190c80628 100644 --- a/src/modules/09_leave/views/07_LeaveHistoryMain.vue +++ b/src/modules/09_leave/views/07_LeaveHistoryMain.vue @@ -83,7 +83,7 @@ const columns = ref([ { name: "leaveDaysUsed", align: "left", - label: "ที่ใช้ไป (วัน)", + label: "ที่ใช้ไปทั้งหมด (วัน)", sortable: true, field: "leaveDaysUsed", headerStyle: "font-size: 14px", @@ -92,7 +92,7 @@ const columns = ref([ { name: "leaveCount", align: "left", - label: "ที่ใช้ไป (ครั้ง)", + label: "ที่ใช้ไปทั้งหมด (ครั้ง)", sortable: true, field: "leaveCount", headerStyle: "font-size: 14px", @@ -101,7 +101,7 @@ const columns = ref([ { name: "beginningLeaveDays", align: "left", - label: "ยกมา (วัน)", + label: "จำนวนวันลาก่อนใช้งานระบบ", sortable: true, field: "beginningLeaveDays", headerStyle: "font-size: 14px", @@ -110,7 +110,7 @@ const columns = ref([ { name: "beginningLeaveCount", align: "left", - label: "ยกมา (ครั้ง)", + label: "จำนวนครั้งที่ลาก่อนใช้งานระบบ", sortable: true, field: "beginningLeaveCount", headerStyle: "font-size: 14px", From cf053cb306dded6c957e5ccd6a96eaf781b58f95 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Thu, 23 Apr 2026 16:42:31 +0700 Subject: [PATCH 06/49] feat(registry-edit): uploadFile profileSalaryTemp --- src/api/02_organizational/api.organization.ts | 2 + .../views/edit/components/Table.vue | 65 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/api/02_organizational/api.organization.ts b/src/api/02_organizational/api.organization.ts index bcb7e54d9..b30ec3ec1 100644 --- a/src/api/02_organizational/api.organization.ts +++ b/src/api/02_organizational/api.organization.ts @@ -192,6 +192,8 @@ export default { `${orgProfile}/keycloak/permissionProfile/${rootId}`, profileidPosition: (type: string) => `${orgProfile}${type}/profileid/position`, + uploadProfile: (type: string, id: string) => + `${organization}/upload/${type}-profileSalaryTemp/${id}`, workflowCommanderOperate: `${workflow}/commander/operate`, workflowCommanderSign: `${workflow}/commander/sign`, diff --git a/src/modules/04_registryPerson/views/edit/components/Table.vue b/src/modules/04_registryPerson/views/edit/components/Table.vue index 7d99fd725..74849426c 100644 --- a/src/modules/04_registryPerson/views/edit/components/Table.vue +++ b/src/modules/04_registryPerson/views/edit/components/Table.vue @@ -847,6 +847,58 @@ async function validateAndSave( } } +/** + * ฟังก์ชันอัปโหลดไฟล์ Excel + * ส่งไฟล์ Excel ไปยัง API โดยตรง + */ +function handUploadFile() { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".xlsx,.xls"; + + input.onchange = async (e: Event) => { + try { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + + // ตรวจสอบชนิดไฟล์โดยตรวจสอบนามสกุล + 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", + }, + } + ); + + success($q, "อัปโหลดไฟล์สำเร็จ"); + await fetchData(); + } catch (error) { + messageError($q, error); + } finally { + hideLoader(); + } + }; + + input.click(); +} + onMounted(async () => { await Promise.all([fetchData(), fetchType()]); }); @@ -886,6 +938,15 @@ onMounted(async () => { >
+ + อัปโหลดไฟล์แก้ไข + { icon="download" :disable="rows.length == 0" @click="exportToExcel()" - /> + > + ดาวน์โหลดไฟล์ +
Date: Thu, 23 Apr 2026 17:22:01 +0700 Subject: [PATCH 07/49] refactor(registry-edit): exportExcel --- package.json | 1 + .../interface/response/Edit.ts | 1 + .../04_registryPerson/utils/exportPosition.ts | 157 ++++++++++++++++++ .../views/edit/components/Table.vue | 114 ++++++------- 4 files changed, 217 insertions(+), 56 deletions(-) create mode 100644 src/modules/04_registryPerson/utils/exportPosition.ts diff --git a/package.json b/package.json index 6e2ddd2c2..088a0ead7 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "axios": "^1.6.7", "bma-org-chart": "^0.0.8", "esri-loader": "^3.7.0", + "exceljs": "^4.4.0", "html-to-image": "^1.11.13", "keycloak-js": "^20.0.2", "moment": "^2.29.4", diff --git a/src/modules/04_registryPerson/interface/response/Edit.ts b/src/modules/04_registryPerson/interface/response/Edit.ts index 8af17bae8..d9deaf0bb 100644 --- a/src/modules/04_registryPerson/interface/response/Edit.ts +++ b/src/modules/04_registryPerson/interface/response/Edit.ts @@ -70,6 +70,7 @@ interface DataPosition { status: string; posNumCodeSitAbb: string; posNumCodeSit: string; + positionExecutiveField: string; } export type { DataSalaryPos, DataPosition }; diff --git a/src/modules/04_registryPerson/utils/exportPosition.ts b/src/modules/04_registryPerson/utils/exportPosition.ts new file mode 100644 index 000000000..144033429 --- /dev/null +++ b/src/modules/04_registryPerson/utils/exportPosition.ts @@ -0,0 +1,157 @@ +import ExcelJS from "exceljs"; +import { useEditPosDataStore } from "@/modules/04_registryPerson/stores/Edit"; + +import type { DataPosition } from "@/modules/04_registryPerson/interface/response/Edit"; + +const store = useEditPosDataStore(); + +// ฟังก์ชันแปลงวันที่จาก ISO format เป็นรูปแบบ dd/mm/yyyy (เช่น 18/05/2564) +function formatDateToDDMMYYYY(dateString: string | null | Date): string { + if (!dateString) return ""; + + const date = new Date(dateString); + if (isNaN(date.getTime())) return ""; + + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear() + 543; // แปลงเป็นปีพุทธศักราช + + return `${day}/${month}/${year}`; +} + +export async function exportToExcelPosition(data: DataPosition[]) { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("รายการประวัติตำแหน่งเงินเดือน"); + + // --- ส่วนที่ 1: สร้าง Master Data Sheet สำหรับอ้างอิง ID --- + // เราจะซ่อนแผ่นงานนี้ไว้ (hidden) เพื่อใช้ทำ Dropdown และ VLOOKUP + const masterSheet = workbook.addWorksheet("MasterData", { state: "hidden" }); + const masterData = store.commandCodeData; // [{id: 1, name: "ย้าย"}, ...] + + masterData.forEach((item, index) => { + masterSheet.getCell(`A${index + 1}`).value = item.name; + masterSheet.getCell(`B${index + 1}`).value = item.id; + }); + + // --- ส่วนที่ 2: กำหนด Columns --- + worksheet.columns = [ + { header: "ลำดับ", key: "no", width: 8 }, + { header: "วันที่คำสั่งมีผล", key: "commandDateAffect", width: 18 }, + { header: "ตำแหน่งในสายงาน", key: "positionName", width: 25 }, + { header: "ตำแหน่งประเภท", key: "positionType", width: 18 }, + { header: "ระดับ", key: "positionLevel", width: 12 }, + { header: "ระดับซี", key: "positionCee", width: 12 }, + { header: "สายงาน", key: "positionLine", width: 20 }, + { header: "ด้าน/สาขา", key: "positionPathSide", width: 15 }, + { header: "ตำแหน่งทางการบริหาร", key: "positionExecutive", width: 20 }, + { header: "ด้านทางการบริหาร", key: "positionExecutiveField", width: 20 }, + { header: "เงินเดือน", key: "amount", width: 15 }, + { header: "เงินค่าตอบแทนรายเดือน", key: "mouthSalaryAmount", width: 15 }, + { header: "เงินประจำตำแหน่ง", key: "positionSalaryAmount", width: 15 }, + { header: "เงินค่าตอบแทนพิเศษ", key: "amountSpecial", width: 15 }, + { header: "หน่วยงาน", key: "organization", width: 30 }, + { header: "ส่วนราชการระดับ 1", key: "orgChild1", width: 20 }, + { header: "ส่วนราชการระดับ 2", key: "orgChild2", width: 20 }, + { header: "ส่วนราชการระดับ 3", key: "orgChild3", width: 20 }, + { header: "ส่วนราชการระดับ 4", key: "orgChild4", width: 20 }, + { header: "ตัวย่อเลขที่ตำแหน่ง", key: "posNoAbb", width: 15 }, + { header: "เลขที่ตำแหน่ง", key: "posNo", width: 15 }, + { header: "หน่วยงานที่ออกคำสั่ง", key: "posNumCodeSit", width: 20 }, + { + header: "ตัวย่อหน่วยงานที่ออกคำสั่ง", + key: "posNumCodeSitAbb", + width: 15, + }, + { header: "เลขที่คำสั่ง", key: "commandNo", width: 15 }, + { header: "ปีเลขที่คำสั่ง", key: "commandYear", width: 12 }, + { header: "วันที่ลงนาม", key: "commandDateSign", width: 18 }, + { header: "ประเภทคำสั่ง", key: "commandCodeName", width: 25 }, // AA + { header: "หมายเหตุ", key: "remark", width: 20 }, + { header: "commandId", key: "commandId", width: 20 }, // AC (ลำดับที่ 29) + { header: "commandCode", key: "commandCode", width: 20 }, // AD (ลำดับที่ 30) + ]; + + // 3. Map ข้อมูล + const newData = data.map((e, index) => ({ + no: index + 1, + commandDateAffect: e.commandDateAffect + ? formatDateToDDMMYYYY(e.commandDateAffect) + : "", + positionName: e.positionName, + positionType: e.positionType, + positionLevel: e.positionLevel, + positionCee: e.positionCee, + positionLine: e.positionLine || "", + positionPathSide: e.positionPathSide || "", + positionExecutive: e.positionExecutive, + positionExecutiveField: e.positionExecutiveField || "", + amount: e.amount || 0, + mouthSalaryAmount: e.mouthSalaryAmount || 0, + positionSalaryAmount: e.positionSalaryAmount || 0, + amountSpecial: e.amountSpecial || 0, + organization: e.orgRoot, + orgChild1: e.orgChild1, + orgChild2: e.orgChild2, + orgChild3: e.orgChild3, + orgChild4: e.orgChild4, + posNoAbb: e.posNoAbb, + posNo: e.posNo, + posNumCodeSit: e.posNumCodeSit, + posNumCodeSitAbb: e.posNumCodeSitAbb, + commandNo: e.commandNo, + commandYear: e.commandYear ? Number(e.commandYear) + 543 : "", + commandDateSign: e.commandDateSign + ? formatDateToDDMMYYYY(e.commandDateSign) + : "", + commandCodeName: store.convertCommandCodeName(e.commandCode), + remark: e.remark, + commandId: e.commandId || "", + commandCode: e.commandCode || "", // ใส่ค่าเริ่มต้นไว้ + })); + + worksheet.addRows(newData); + + // 4. ตกแต่งสี, Dropdown และ สูตร VLOOKUP + newData.forEach((_, index) => { + const rowIndex = index + 2; + + // --- ตั้งค่า Format สำหรับเซลล์วันที่ --- + // เซลล์ B (commandDateAffect) และ Z (commandDateSign) + const dateAffectCell = worksheet.getCell(`B${rowIndex}`); + const dateSignCell = worksheet.getCell(`Z${rowIndex}`); + + [dateAffectCell, dateSignCell].forEach((cell) => { + cell.numFmt = "dd/mm/yyyy"; + }); + + // --- ทำ Dropdown คอลัมน์ AA --- + // อ้างอิงรายการจาก MasterData Sheet จะทำให้ Excel ทำงานได้เสถียรกว่า (กรณีรายการเยอะ) + worksheet.getCell(`AA${rowIndex}`).dataValidation = { + type: "list", + allowBlank: true, + formulae: [`MasterData!$A$1:$A$${masterData.length}`], + }; + + // --- ผูกสูตรให้ commandCode (AD) เปลี่ยนตามการเลือกใน AA --- + // AA คือประเภทคำสั่ง, AD คือ commandCode + worksheet.getCell(`AD${rowIndex}`).value = { + formula: `=IFERROR(VLOOKUP(AA${rowIndex}, MasterData!$A$1:$B$${masterData.length}, 2, FALSE), "")`, + }; + }); + + // 5. สไตล์ Header + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).alignment = { vertical: "middle", horizontal: "center" }; + + // 6. เขียนไฟล์ + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "รายการประวัติตำแหน่งเงินเดือน.xlsx"; + a.click(); + window.URL.revokeObjectURL(url); +} diff --git a/src/modules/04_registryPerson/views/edit/components/Table.vue b/src/modules/04_registryPerson/views/edit/components/Table.vue index 74849426c..470c010ac 100644 --- a/src/modules/04_registryPerson/views/edit/components/Table.vue +++ b/src/modules/04_registryPerson/views/edit/components/Table.vue @@ -8,6 +8,7 @@ import config from "@/app.config"; import { useRoute } from "vue-router"; import { useCounterMixin } from "@/stores/mixin"; import { useEditPosDataStore } from "@/modules/04_registryPerson/stores/Edit"; +import { exportToExcelPosition } from "@/modules/04_registryPerson/utils/exportPosition"; import type { QTableColumn } from "quasar"; import type { FormDataSalary } from "@/modules/04_registryPerson/interface/index/Edit"; @@ -430,66 +431,67 @@ function classColorRow(isDelete: boolean, isEdit: boolean, isEntry: boolean) { /** ฟังก์ชันดาวน์โหลดไฟล Excel */ function exportToExcel() { - const newData = rows.value.map((e: DataPosition) => { - return { - commandDateAffect: date2Thai(e.commandDateAffect), - positionName: e.positionName, - positionType: e.positionType, - positionLevel: e.positionLevel - ? e.positionLevel - : e.positionCee - ? e.positionCee - : "", - positionExecutive: e.positionExecutive, - amount: e.amount, - mouthSalaryAmount: e.mouthSalaryAmount, - positionSalaryAmount: e.positionSalaryAmount, - organization: findOrgName({ - root: e.orgRoot, - child1: e.orgChild1, - child2: e.orgChild2, - child3: e.orgChild3, - child4: e.orgChild4, - }), - posNo: - e.posNoAbb && e.posNo - ? `${e.posNoAbb} ${e.posNo}` - : e.posNo - ? e.posNo - : "", - posNumCodeSit: - e.posNumCodeSitAbb && e.posNumCodeSit - ? `${e.posNumCodeSit} (${e.posNumCodeSitAbb})` - : e.posNumCodeSit - ? e.posNumCodeSit - : "", - commandNo: - e.commandNo && e.commandYear - ? `${e.commandNo}/${Number(e.commandYear) + 543}` - : "", - commandDateSign: date2Thai(e.commandDateSign), - commandCode: store.convertCommandCodeName(e.commandCode), - remark: e.remark, - }; - }); + exportToExcelPosition(rows.value); + // const newData = rows.value.map((e: DataPosition) => { + // return { + // commandDateAffect: date2Thai(e.commandDateAffect), + // positionName: e.positionName, + // positionType: e.positionType, + // positionLevel: e.positionLevel + // ? e.positionLevel + // : e.positionCee + // ? e.positionCee + // : "", + // positionExecutive: e.positionExecutive, + // amount: e.amount, + // mouthSalaryAmount: e.mouthSalaryAmount, + // positionSalaryAmount: e.positionSalaryAmount, + // organization: findOrgName({ + // root: e.orgRoot, + // child1: e.orgChild1, + // child2: e.orgChild2, + // child3: e.orgChild3, + // child4: e.orgChild4, + // }), + // posNo: + // e.posNoAbb && e.posNo + // ? `${e.posNoAbb} ${e.posNo}` + // : e.posNo + // ? e.posNo + // : "", + // posNumCodeSit: + // e.posNumCodeSitAbb && e.posNumCodeSit + // ? `${e.posNumCodeSit} (${e.posNumCodeSitAbb})` + // : e.posNumCodeSit + // ? e.posNumCodeSit + // : "", + // commandNo: + // e.commandNo && e.commandYear + // ? `${e.commandNo}/${Number(e.commandYear) + 543}` + // : "", + // commandDateSign: date2Thai(e.commandDateSign), + // commandCode: store.convertCommandCodeName(e.commandCode), + // remark: e.remark, + // }; + // }); - const headers = columns.value.map((item: any) => item.label) || []; // หัวคอลัมน์ภาษาไทย - const worksheet = XLSX.utils.json_to_sheet(newData, { - header: visibleColumns.value, - }); + // const headers = columns.value.map((item: any) => item.label) || []; // หัวคอลัมน์ภาษาไทย + // const worksheet = XLSX.utils.json_to_sheet(newData, { + // header: visibleColumns.value, + // }); - //แทรกหัวคอลัมน์ภาษาไทย (ใช้ A1, B1, C1 แทน) - XLSX.utils.sheet_add_aoa(worksheet, [headers], { origin: "A1" }); + // //แทรกหัวคอลัมน์ภาษาไทย (ใช้ A1, B1, C1 แทน) + // XLSX.utils.sheet_add_aoa(worksheet, [headers], { origin: "A1" }); - // Create a new workbook and append the worksheet - const workbook = XLSX.utils.book_new(); + // // Create a new workbook and append the worksheet + // const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet( - workbook, - worksheet, - `รายการประวัติตำแหน่งเงินเดือน` - ); - XLSX.writeFile(workbook, "รายการประวัติตำแหน่งเงินเดือน.xlsx"); + // XLSX.utils.book_append_sheet( + // workbook, + // worksheet, + // `รายการประวัติตำแหน่งเงินเดือน` + // ); + // XLSX.writeFile(workbook, "รายการประวัติตำแหน่งเงินเดือน.xlsx"); } const commandCodeOptions = ref(store.commandCodeData); //รายการปรเภทคำสั่ง 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 08/49] 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 09/49] 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 10/49] 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 11/49] 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 12/49] 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 @@ + +