!DOCTYPE html html lang=th class=h-full bg-gray-50 head meta charset=UTF-8 meta name=viewport content=width=device-width, initial-scale=1.0 titleระบบแดชบอร์ดติดตามข้อมูลพนักงานรายเดือน - กระทรวงสาธารณสุขtitle !-- Tailwind CSS v3 -- script src=httpscdn.tailwindcss.comscript !-- Google Fonts Sarabun & TH Sarabun New -- link rel=preconnect href=httpsfonts.googleapis.com link rel=preconnect href=httpsfonts.gstatic.com crossorigin link href=httpsfonts.googleapis.comcss2family=Sarabunwght@300;400;500;600;700&display=swap rel=stylesheet !-- Chart.js v4 -- script src=httpscdn.jsdelivr.netnpmchart.jsscript !-- Lucide Icons -- script src=httpsunpkg.comlucide@latestscript script tailwind.config = { theme { extend { colors { moph { deep '#003366', น้ำเงินกรมท่าราชการ teal '#008080', เขียวอมฟ้าสาธารณสุข tealLight '#e6f2f2', accent '#328CC1' } }, fontFamily { sarabun ['Sarabun', 'sans-serif'] } } } } script style body { font-family 'Sarabun', sans-serif; } Custom scrollbar to match sleek theme -webkit-scrollbar { width 6px; height 6px; } -webkit-scrollbar-track { background #f1f1f1; } -webkit-scrollbar-thumb { background #008080; border-radius 3px; } -webkit-scrollbar-thumbhover { background #003366; } style head body class=h-full flex flex-col overflow-x-hidden text-gray-800 !-- TOP NAVIGATION & HEADER -- header class=bg-gradient-to-r from-moph-deep to-moph-teal text-white shadow-md sticky top-0 z-40 div class=max-w-7xl mx-auto px-4 smpx-6 lgpx-8 py-3.5 flex flex-col smflex-row items-center justify-between gap-4 !-- Brand & Logo -- div class=flex items-center space-x-3.5 !-- MOPH Logo Simulation SVG -- div class=bg-white p-1.5 rounded-full shadow-inner flex-shrink-0 svg class=w-10 h-10 text-moph-teal viewBox=0 0 100 100 fill=none xmlns=httpwww.w3.org2000svg circle cx=50 cy=50 r=45 stroke=currentColor stroke-width=4 fill=#ffffff !-- Caduceus Rod of Asclepius symbol simulation (Medical snake rod) -- path d=M50 15 V80 stroke=currentColor stroke-width=5 stroke-linecap=round circle cx=50 cy=15 r=4 fill=currentColor path d=M35 30 Q50 20 50 35 T50 50 T65 65 stroke=#E53E3E stroke-width=3 fill=none stroke-linecap=round path d=M65 30 Q50 20 50 35 T50 50 T35 65 stroke=#E53E3E stroke-width=3 fill=none stroke-linecap=round path d=M50 80 L35 85 H65 L50 80Z fill=currentColor !-- Small Cross overlay -- rect x=47 y=40 width=6 height=16 fill=currentColor rx=1 rect x=42 y=45 width=16 height=6 fill=currentColor rx=1 svg div div h1 class=text-xl font-bold tracking-wideกระทรวงสาธารณสุขh1 p class=text-xs text-teal-100 font-lightระบบแดชบอร์ดติดตามข้อมูลพนักงานรายเดือน (MOPH Staff Dashboard)p div div !-- Header Action Buttons -- div class=flex flex-wrap items-center gap-2 button onclick=openModal('employeeModal') class=bg-white text-moph-deep hoverbg-teal-50 px-4 py-2 rounded-lg text-sm font-semibold shadow-sm transition-all duration-200 flex items-center gap-1.5 i data-lucide=user-plus class=w-4 h-4i spanเพิ่มข้อมูลบุคลากรspan button button onclick=confirmReset() class=bg-red-70050 hoverbg-red-700 text-white px-4 py-2 rounded-lg text-sm font-semibold border border-red-50020 shadow-sm transition-all duration-200 flex items-center gap-1.5 i data-lucide=rotate-ccw class=w-4 h-4i spanคืนค่าเริ่มต้นspan button div div header !-- MAIN BODY -- main class=flex-grow max-w-7xl w-full mx-auto px-4 smpx-6 lgpx-8 py-6 space-y-6 !-- GLOBAL FILTERS PANEL -- section class=bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex flex-col gap-4 div class=flex items-center space-x-2 text-moph-deep font-semibold i data-lucide=sliders-horizontal class=w-5 h-5i spanตัวเลือกคัดกรองข้อมูลระเบียบสาธารณสุขspan div div class=grid grid-cols-1 mdgrid-cols-3 gap-4 !-- Fiscal Year Filter -- div class=space-y-1.5 label for=filterYear class=text-xs font-medium text-gray-500ปีงบประมาณ ปี พ.ศ.label select id=filterYear onchange=applyFilters() class=w-full bg-gray-50 border border-gray-200 text-gray-700 py-2.5 px-3 rounded-lg focusoutline-none focusring-2 focusring-moph-teal text-sm option value=25692569 (ปีงบประมาณปัจจุบัน)option option value=25682568option select div !-- Month Filter -- div class=space-y-1.5 label for=filterMonth class=text-xs font-medium text-gray-500ประจำเดือนlabel select id=filterMonth onchange=applyFilters() class=w-full bg-gray-50 border border-gray-200 text-gray-700 py-2.5 px-3 rounded-lg focusoutline-none focusring-2 focusring-moph-teal text-sm option value=allทุกเดือนในไตรมาสนี้option option value=กรกฎาคม selectedกรกฎาคมoption option value=สิงหาคมสิงหาคมoption option value=กันยายนกันยายนoption select div !-- Department Filter -- div class=space-y-1.5 label for=filterDept class=text-xs font-medium text-gray-500ฝ่าย กลุ่มงานlabel select id=filterDept onchange=applyFilters() class=w-full bg-gray-50 border border-gray-200 text-gray-700 py-2.5 px-3 rounded-lg focusoutline-none focusring-2 focusring-moph-teal text-sm option value=allทั้งหมด (ทุกฝ่ายงาน)option option value=กลุ่มงานการแพทย์กลุ่มงานการแพทย์option option value=กลุ่มงานพยาบาลกลุ่มงานพยาบาลoption option value=กลุ่มงานเภสัชกรรมกลุ่มงานเภสัชกรรมoption option value=กลุ่มงานเทคนิคบริการกลุ่มงานเทคนิคบริการoption option value=กลุ่มงานบริหารทั่วไปกลุ่มงานบริหารทั่วไปoption select div div section !-- KPI CARDS GRID -- section class=grid grid-cols-1 smgrid-cols-2 lggrid-cols-4 gap-4 !-- KPI 1 Total Staff -- div class=bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex items-center justify-between div class=space-y-1 p class=text-xs font-semibold text-gray-400 tracking-wider uppercaseบุคลากรทั้งหมดในระบบp div class=flex items-baseline space-x-2 span id=kpiTotalStaff class=text-3xl font-bold text-moph-deep0span span class=text-xs text-gray-500รายspan div p class=text-[11px] text-gray-400 id=kpiGenderRatioชาย - หญิง -p div div class=p-3 bg-blue-50 text-moph-deep rounded-xl i data-lucide=users class=w-6 h-6i div div !-- KPI 2 Attendance Rate -- div class=bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex items-center justify-between div class=space-y-1 p class=text-xs font-semibold text-gray-400 tracking-wider uppercaseอัตราการมาปฏิบัติงานp div class=flex items-baseline space-x-2 span id=kpiAttendance class=text-3xl font-bold text-teal-6000%span div p class=text-[11px] text-teal-600 flex items-center gap-1 i data-lucide=trending-up class=w-3 h-3i spanอยู่ในเกณฑ์มาตรฐานความมั่นคงspan p div div class=p-3 bg-teal-50 text-teal-600 rounded-xl i data-lucide=calendar-check class=w-6 h-6i div div !-- KPI 3 Average Leave Days -- div class=bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex items-center justify-between div class=space-y-1 p class=text-xs font-semibold text-gray-400 tracking-wider uppercaseวันลาเฉลี่ยต่อคนp div class=flex items-baseline space-x-2 span id=kpiAvgLeave class=text-3xl font-bold text-amber-6000.0span span class=text-xs text-gray-500วันเดือนspan div p class=text-[11px] text-gray-400คำนวณจากยอดวันลาสะสมสะสมp div div class=p-3 bg-amber-50 text-amber-600 rounded-xl i data-lucide=clock class=w-6 h-6i div div !-- KPI 4 Retention Rate -- div class=bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex items-center justify-between div class=space-y-1 p class=text-xs font-semibold text-gray-400 tracking-wider uppercaseอัตราความมั่นคงบุคลากรp div class=flex items-baseline space-x-2 span id=kpiRetention class=text-3xl font-bold text-indigo-6000%span div p class=text-[11px] text-gray-400พ้นสภาพเฉลี่ยต่ำกว่าเป้าหมายกระทรวงp div div class=p-3 bg-indigo-50 text-indigo-600 rounded-xl i data-lucide=shield-check class=w-6 h-6i div div section !-- ANALYTICS CHARTS SECTION -- section class=grid grid-cols-1 lggrid-cols-2 gap-6 !-- Chart 1 Staff DistributionAttendance by Dept -- div class=bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex flex-col div class=flex items-center justify-between mb-4 border-b border-gray-100 pb-3 h3 class=text-sm font-bold text-moph-deep flex items-center gap-1.5 i data-lucide=bar-chart-3 class=w-4 h-4 text-moph-teali spanสถิติกำลังพลและสถานะแบ่งตามกลุ่มงานspan h3 span class=text-xs text-gray-400วิเคราะห์อัตราโครงสร้างspan div div class=flex-grow min-h-[250px] relative canvas id=barChartDeptcanvas div div !-- Chart 2 Leave Type Distribution -- div class=bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex flex-col div class=flex items-center justify-between mb-4 border-b border-gray-100 pb-3 h3 class=text-sm font-bold text-moph-deep flex items-center gap-1.5 i data-lucide=pie-chart class=w-4 h-4 text-moph-teali spanสัดส่วนประเภทการลาหยุดราชการประจำเดือนspan h3 span class=text-xs text-gray-400แยกตามประเภทการยื่นใบลาspan div div class=flex-grow min-h-[250px] max-h-[280px] relative flex justify-center canvas id=pieChartLeavecanvas div div section !-- DATA TABLE SECTION -- section class=bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden !-- Table Controls -- div class=p-5 border-b border-gray-100 flex flex-col lgflex-row items-center justify-between gap-4 bg-gray-5050 div class=flex flex-col smflex-row items-stretch smitems-center gap-3 w-full lgw-auto !-- Search Input -- div class=relative min-w-[280px] span class=absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none i data-lucide=search class=w-4 h-4 text-gray-400i span input type=text id=searchInput oninput=handleSearch() placeholder=ค้นหารหัส หรือชื่อ-นามสกุลบุคลากร... class=w-full bg-white border border-gray-200 rounded-lg pl-9 pr-4 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal div !-- Quick Status Filters -- div class=flex items-center space-x-1.5 border border-gray-200 bg-white p-1 rounded-lg button onclick=setQuickFilter('all') id=qf-all class=px-3 py-1 text-xs font-semibold rounded-md transition-all duration-200 bg-moph-teal text-whiteทั้งหมดbutton button onclick=setQuickFilter('ปกติ') id=qf-normal class=px-3 py-1 text-xs font-semibold rounded-md text-gray-600 hoverbg-gray-100 transition-all duration-200มาปกติbutton button onclick=setQuickFilter('ลา') id=qf-leave class=px-3 py-1 text-xs font-semibold rounded-md text-gray-600 hoverbg-gray-100 transition-all duration-200อยู่ระหว่างลาbutton button onclick=setQuickFilter('พ้นสภาพ') id=qf-dismiss class=px-3 py-1 text-xs font-semibold rounded-md text-gray-600 hoverbg-gray-100 transition-all duration-200พ้นสภาพbutton div div !-- Export CSV button -- div class=w-full lgw-auto flex justify-end button onclick=exportToCSV() class=w-full smw-auto bg-emerald-600 hoverbg-emerald-700 text-white px-4.5 py-2 rounded-lg text-sm font-semibold shadow-sm transition-all duration-200 flex items-center justify-center gap-2 i data-lucide=download-cloud class=w-4 h-4i spanส่งออกรายงาน (Export CSV)span button div div !-- Table responsive container -- div class=overflow-x-auto table class=w-full text-left border-collapse thead tr class=bg-moph-deep text-white text-xs font-semibold tracking-wider uppercase th class=py-4.5 px-6รหัสประจำตัวth th class=py-4.5 px-6ชื่อ - นามสกุลth th class=py-4.5 px-6ตำแหน่งวิชาชีพth th class=py-4.5 px-6กลุ่มงานฝ่ายth th class=py-4.5 px-6 text-centerสถานะเดือนนี้th th class=py-4.5 px-6 text-centerวันลาสะสม (วัน)th th class=py-4.5 px-6 text-centerการจัดการth tr thead tbody id=employeeTableBody class=divide-y divide-gray-100 text-sm !-- Table Rows will be rendered dynamically by JavaScript -- tbody table div !-- Empty State -- div id=emptyState class=hidden flex-col items-center justify-center py-12 px-4 text-center div class=p-3 bg-gray-100 text-gray-400 rounded-full mb-3 i data-lucide=folder-open class=w-8 h-8i div h4 class=text-base font-bold text-gray-700ไม่พบข้อมูลที่ค้นหาh4 p class=text-xs text-gray-400 mt-1โปรดตรวจสอบคำค้นหาหรือตัวกรองที่เลือกอีกครั้งp div !-- Table Pagination -- div class=p-5 border-t border-gray-100 flex flex-col smflex-row items-center justify-between gap-4 bg-gray-5050 span id=paginationInfo class=text-xs font-medium text-gray-500กำลังแสดงข้อมูลลำดับที่ 0 ถึง 0 จากทั้งหมด 0 รายการspan div class=flex items-center space-x-2 button onclick=changePage(-1) id=btnPrevPage class=p-1.5 border border-gray-200 rounded-lg hoverbg-gray-100 disabledopacity-50 disabledcursor-not-allowed transition-all i data-lucide=chevron-left class=w-4 h-4i button div id=pageNumbers class=flex items-center space-x-1 !-- Page numbers will be dynamically created -- div button onclick=changePage(1) id=btnNextPage class=p-1.5 border border-gray-200 rounded-lg hoverbg-gray-100 disabledopacity-50 disabledcursor-not-allowed transition-all i data-lucide=chevron-right class=w-4 h-4i button div div section main !-- FOOTER -- footer class=bg-gray-800 text-gray-400 text-xs py-5 mt-10 border-t border-gray-700 div class=max-w-7xl mx-auto px-4 smpx-6 lgpx-8 text-center space-y-2 p© 2569 ระบบแดชบอร์ดติดตามข้อมูลพนักงานรายเดือน กระทรวงสาธารณสุขp p class=text-gray-500พัฒนาด้วยสถาปัตยกรรมระดับ Production Grade เป็นไปตามข้อบัญญัติ PDPA การเก็บรักษาข้อมูลที่ปลอดภัย (Client-side Sandboxed LocalStorage)p div footer !-- ADDEDIT EMPLOYEE MODAL -- div id=employeeModal class=fixed inset-0 z-50 overflow-y-auto hidden aria-labelledby=modal-title role=dialog aria-modal=true div class=flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center smblock smp-0 !-- Background overlay -- div class=fixed inset-0 bg-gray-900 bg-opacity-50 transition-opacity onclick=closeModal('employeeModal')div !-- Center modal -- span class=hidden sminline-block smalign-middle smh-screen aria-hidden=true​span div class=inline-block align-bottom bg-white rounded-xl text-left overflow-hidden shadow-xl transform transition-all smmy-8 smalign-middle smmax-w-lg smw-full border border-gray-100 div class=bg-moph-deep text-white px-6 py-4 flex items-center justify-between h3 id=modalTitle class=text-base font-boldบันทึกแก้ไข ข้อมูลพนักงานสาธารณสุขh3 button onclick=closeModal('employeeModal') class=text-teal-100 hovertext-white transition-all i data-lucide=x class=w-5 h-5i button div !-- Form body -- form id=employeeForm onsubmit=handleFormSubmit(event) class=p-6 space-y-4 !-- ID Input -- div class=space-y-1.5 label for=empId class=text-xs font-semibold text-gray-600 blockรหัสประจำตัวบุคลากร span class=text-red-500spanlabel input type=text id=empId required placeholder=เช่น MOPH-1024 class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal uppercase p id=empIdError class=text-[11px] text-red-500 hiddenรหัสประจำตัวนี้มีอยู่ในระบบแล้ว โปรดใช้รหัสอื่นp div !-- Name Input -- div class=space-y-1.5 label for=empName class=text-xs font-semibold text-gray-600 blockชื่อ-นามสกุล span class=text-red-500spanlabel input type=text id=empName required placeholder=ระบุ ชื่อ และ นามสกุล class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal div !-- RolePosition -- div class=grid grid-cols-1 smgrid-cols-2 gap-4 div class=space-y-1.5 label for=empRole class=text-xs font-semibold text-gray-600 blockตำแหน่งทางวิชาชีพ span class=text-red-500spanlabel select id=empRole required class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal option value=นายแพทย์นายแพทย์ แพทย์หญิงoption option value=พยาบาลวิชาชีพพยาบาลวิชาชีพoption option value=เภสัชกรเภสัชกรoption option value=นักเทคนิคการแพทย์นักเทคนิคการแพทย์option option value=นักวิชาการสาธารณสุขนักวิชาการสาธารณสุขoption option value=เจ้าพนักงานธุรการเจ้าพนักงานธุรการoption select div div class=space-y-1.5 label for=empDept class=text-xs font-semibold text-gray-600 blockกลุ่มงาน ฝ่าย span class=text-red-500spanlabel select id=empDept required class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal option value=กลุ่มงานการแพทย์กลุ่มงานการแพทย์option option value=กลุ่มงานพยาบาลกลุ่มงานพยาบาลoption option value=กลุ่มงานเภสัชกรรมกลุ่มงานเภสัชกรรมoption option value=กลุ่มงานเทคนิคบริการกลุ่มงานเทคนิคบริการoption option value=กลุ่มงานบริหารทั่วไปกลุ่มงานบริหารทั่วไปoption select div div !-- Tel Input -- div class=space-y-1.5 label for=empTel class=text-xs font-semibold text-gray-600 blockเบอร์โทรศัพท์ติดต่อ (ไทย) span class=text-red-500spanlabel input type=tel id=empTel required placeholder=เช่น 0891234567 หรือ 021234567 pattern=^(0[2-9]d{7,8})$ class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal p class=text-[10px] text-gray-400กรอกเบอร์โทรไทย 9-10 หลัก โดยไม่ต้องใช้ขีด เช่น 0891234567p div !-- Month and Year selection inside edit form -- div class=grid grid-cols-1 smgrid-cols-2 gap-4 div class=space-y-1.5 label for=empMonth class=text-xs font-semibold text-gray-600 blockประจำเดือนข้อมูลlabel select id=empMonth class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal option value=กรกฎาคมกรกฎาคมoption option value=สิงหาคมสิงหาคมoption option value=กันยายนกันยายนoption select div div class=space-y-1.5 label for=empGender class=text-xs font-semibold text-gray-600 blockเพศlabel select id=empGender class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal option value=ชายชายoption option value=หญิงหญิงoption select div div !-- Attendance & Leave Status -- div class=grid grid-cols-1 smgrid-cols-2 gap-4 border-t border-gray-100 pt-3 div class=space-y-1.5 label for=empStatus class=text-xs font-semibold text-gray-600 blockสถานะปฏิบัติงานเดือนนี้label select id=empStatus class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal option value=ปกติปฏิบัติงานปกติoption option value=ลาป่วยลาป่วยoption option value=ลากิจลากิจoption option value=ลาพักผ่อนลาพักผ่อนoption option value=ลาคลอดลาคลอด ลาอื่นๆoption option value=พ้นสภาพพ้นสภาพบุคลากรoption select div div class=space-y-1.5 label for=empLeaveDays class=text-xs font-semibold text-gray-600 blockวันลาสะสมในเดือนนี้ (วัน)label input type=number id=empLeaveDays min=0 max=31 step=0.5 value=0 class=w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-2 text-sm focusoutline-none focusring-2 focusring-moph-teal div div !-- Footer buttons -- div class=flex items-center justify-end space-x-2.5 border-t border-gray-100 pt-4 button type=button onclick=closeModal('employeeModal') class=bg-gray-100 hoverbg-gray-200 text-gray-700 px-4 py-2 rounded-lg text-sm font-semibold transition-allยกเลิกbutton button type=submit class=bg-moph-teal hoverbg-teal-700 text-white px-5 py-2 rounded-lg text-sm font-semibold shadow-sm transition-allบันทึกข้อมูลbutton div form div div div !-- VIEW DETAILS MODAL -- div id=detailModal class=fixed inset-0 z-50 overflow-y-auto hidden aria-labelledby=modal-title role=dialog aria-modal=true div class=flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center smblock smp-0 div class=fixed inset-0 bg-gray-900 bg-opacity-50 transition-opacity onclick=closeModal('detailModal')div span class=hidden sminline-block smalign-middle smh-screen aria-hidden=true​span div class=inline-block align-bottom bg-white rounded-xl text-left overflow-hidden shadow-xl transform transition-all smmy-8 smalign-middle smmax-w-md smw-full border border-gray-100 div class=bg-moph-teal text-white px-6 py-4 flex items-center justify-between h3 class=text-base font-boldข้อมูลรายละเอียดบุคลากรh3 button onclick=closeModal('detailModal') class=text-teal-100 hovertext-white transition-all i data-lucide=x class=w-5 h-5i button div div class=p-6 space-y-4 div class=flex justify-center mb-4 div class=w-16 h-16 bg-moph-tealLight text-moph-teal rounded-full flex items-center justify-center i data-lucide=user class=w-8 h-8i div div div class=space-y-2 border-b border-gray-100 pb-3 h4 id=detName class=text-center font-bold text-lg text-moph-deep-h4 p id=detRole class=text-center text-xs text-gray-500 font-medium-p div div class=grid grid-cols-2 gap-4 text-xs div span class=text-gray-400 block font-semiboldรหัสประจำตัวspan span id=detId class=text-gray-700 font-medium text-sm-span div div span class=text-gray-400 block font-semiboldกลุ่มงานฝ่ายspan span id=detDept class=text-gray-700 font-medium text-sm-span div div span class=text-gray-400 block font-semiboldเบอร์โทรศัพท์span span id=detTel class=text-gray-700 font-medium text-sm-span div div span class=text-gray-400 block font-semiboldเพศspan span id=detGender class=text-gray-700 font-medium text-sm-span div div span class=text-gray-400 block font-semiboldสถานะเดือนนี้span span id=detStatus class=text-gray-700 font-medium text-sm-span div div span class=text-gray-400 block font-semiboldวันลาสะสมสะสมspan span id=detLeave class=text-gray-700 font-medium text-sm-span div div div class=flex justify-end pt-4 border-t border-gray-100 button onclick=closeModal('detailModal') class=bg-gray-100 hoverbg-gray-200 text-gray-700 px-5 py-2 rounded-lg text-xs font-bold transition-allปิดหน้าต่างbutton div div div div div !-- CUSTOM CONFIRM MODAL -- div id=confirmModal class=fixed inset-0 z-50 overflow-y-auto hidden aria-labelledby=modal-title role=dialog aria-modal=true div class=flex items-center justify-center min-h-screen px-4 div class=fixed inset-0 bg-gray-900 bg-opacity-50 transition-opacity onclick=closeModal('confirmModal')div div class=bg-white rounded-xl overflow-hidden shadow-xl transform transition-all max-w-sm w-full z-10 border border-gray-100 div class=p-6 text-center space-y-4 div class=mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 text-red-600 i data-lucide=alert-triangle class=h-6 w-6i div div class=space-y-2 h3 id=confirmTitle class=text-base font-bold text-gray-900ยืนยันการดำเนินการh3 p id=confirmMessage class=text-xs text-gray-500คุณแน่ใจหรือไม่ที่จะทำการย้อนระบบเป็นค่าเริ่มต้น ข้อมูลที่แก้ไขทั้งหมดจะสูญหายp div div div class=bg-gray-50 px-6 py-3.5 flex flex-row-reverse gap-2 button id=confirmBtnOk class=w-full inline-flex justify-center rounded-lg border border-transparent shadow-sm px-4 py-2 bg-red-600 text-sm font-semibold text-white hoverbg-red-700 transition-allยืนยันทำรายการbutton button onclick=closeModal('confirmModal') class=w-full inline-flex justify-center rounded-lg border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-semibold text-gray-700 hoverbg-gray-50 transition-allยกเลิกbutton div div div div !-- FLOATING TOAST NOTIFICATION (REPLACES ALERT) -- div id=toast class=fixed bottom-5 right-5 z-50 transform translate-y-10 opacity-0 pointer-events-none transition-all duration-300 flex items-center p-4 space-x-3 w-full max-w-xs bg-gray-800 text-white rounded-lg shadow-xl role=alert div id=toastIconContainer class=p-1 rounded bg-teal-500 text-white i id=toastIcon data-lucide=check class=w-5 h-5i div div id=toastMessage class=text-xs font-semiboldบันทึกข้อมูลสำเร็จ!div div !-- LOGIC APPLICATION JAVASCRIPT -- script MOCK DATA FOR MOPH STAFF - Initializing state const initialMockData = [ { id MOPH-001, name นพ. สมชาย รักดี, role นายแพทย์, dept กลุ่มงานการแพทย์, tel 0812345678, gender ชาย, status ปกติ, leaveDays 0, month กรกฎาคม, year 2569 }, { id MOPH-002, name พญ. นภาพร จิตมั่นคง, role นายแพทย์, dept กลุ่มงานการแพทย์, tel 0898765432, gender หญิง, status ปกติ, leaveDays 0, month กรกฎาคม, year 2569 }, { id MOPH-003, name นาง วิภาดา ใจงาม, role พยาบาลวิชาชีพ, dept กลุ่มงานพยาบาล, tel 0845678901, gender หญิง, status ลาพักผ่อน, leaveDays 3.5, month กรกฎาคม, year 2569 }, { id MOPH-004, name นส. อรพินท์ สุขสว่าง, role พยาบาลวิชาชีพ, dept กลุ่มงานพยาบาล, tel 0867890123, gender หญิง, status ปกติ, leaveDays 0, month กรกฎาคม, year 2569 }, { id MOPH-005, name นาย เกรียงไกร ชัยชนะ, role เภสัชกร, dept กลุ่มงานเภสัชกรรม, tel 0823456789, gender ชาย, status ปกติ, leaveDays 1, month กรกฎาคม, year 2569 }, { id MOPH-006, name นส. มยุรี ชลประธาน, role นักเทคนิคการแพทย์, dept กลุ่มงานเทคนิคบริการ, tel 0851234567, gender หญิง, status ลาป่วย, leaveDays 2, month กรกฎาคม, year 2569 }, { id MOPH-007, name นาย ปิยะ บุญรอด, role เจ้าพนักงานธุรการ, dept กลุ่มงานบริหารทั่วไป, tel 0832345678, gender ชาย, status ปกติ, leaveDays 0, month กรกฎาคม, year 2569 }, { id MOPH-008, name นส. กมลวรรณ ดวงแก้ว, role พยาบาลวิชาชีพ, dept กลุ่มงานพยาบาล, tel 0876543210, gender หญิง, status ลากิจ, leaveDays 1, month กรกฎาคม, year 2569 }, { id MOPH-009, name นพ. ธนา ทรัพย์แสนดี, role นายแพทย์, dept กลุ่มงานการแพทย์, tel 0887654321, gender ชาย, status ปกติ, leaveDays 0, month กรกฎาคม, year 2569 }, { id MOPH-010, name นาง รุ่งนภา ศรีทอง, role พยาบาลวิชาชีพ, dept กลุ่มงานพยาบาล, tel 0890123456, gender หญิง, status ปกติ, leaveDays 1.5, month กรกฎาคม, year 2569 }, { id MOPH-011, name นาย อภิชาติ เลิศศิลป์, role นักวิชาการสาธารณสุข, dept กลุ่มงานบริหารทั่วไป, tel 0811223344, gender ชาย, status ปกติ, leaveDays 0, month กรกฎาคม, year 2569 }, { id MOPH-012, name นส. ศรินยา บุญมี, role เภสัชกร, dept กลุ่มงานเภสัชกรรม, tel 0855667788, gender หญิง, status พ้นสภาพ, leaveDays 0, month กรกฎาคม, year 2569 }, { id MOPH-013, name นาย สิทธิชัย วงศ์สุวรรณ, role นักเทคนิคการแพทย์, dept กลุ่มงานเทคนิคบริการ, tel 0844332211, gender ชาย, status ปกติ, leaveDays 0.5, month กรกฎาคม, year 2569 }, { id MOPH-014, name นาง นันทนา รุ่งเรือง, role พยาบาลวิชาชีพ, dept กลุ่มงานพยาบาล, tel 0822334455, gender หญิง, status ลาพักผ่อน, leaveDays 4, month กรกฎาคม, year 2569 }, { id MOPH-015, name พญ. มุกดา เลิศล้ำ, role นายแพทย์, dept กลุ่มงานการแพทย์, tel 0866778899, gender หญิง, status ปกติ, leaveDays 0, month กรกฎาคม, year 2569 } ]; let employees = []; let filteredEmployees = []; let currentPage = 1; const rowsPerPage = 5; let activeQuickFilter = all; let editEmployeeId = null; Charts instances variables let barChartInstance = null; let pieChartInstance = null; Initialize App on Window Load window.onload = function() { loadFromLocalStorage(); lucide.createIcons(); applyFilters(); } State & LocalStorage Management function loadFromLocalStorage() { const stored = localStorage.getItem('moph_employees_data_prod'); if (stored) { try { employees = JSON.parse(stored); } catch (e) { employees = [...initialMockData]; saveToLocalStorage(); } } else { employees = [...initialMockData]; saveToLocalStorage(); } } function saveToLocalStorage() { localStorage.setItem('moph_employees_data_prod', JSON.stringify(employees)); } function resetToDefault() { employees = [...initialMockData]; saveToLocalStorage(); applyFilters(); showToast(คืนค่าเริ่มต้นระบบสำเร็จ!, check, bg-teal-500); } Input Sanitizer to block Script Injection XSS function sanitizeInput(str) { if (typeof str !== 'string') return str; return str.replace([&']g, function(match) { const map = { '&' '&', '' '<', '' '>', '' '"', ' ''' }; return map[match]; }); } Apply Global Filters, Search & Render Pipeline function applyFilters() { const filterYear = document.getElementById('filterYear').value; const filterMonth = document.getElementById('filterMonth').value; const filterDept = document.getElementById('filterDept').value; const searchValue = document.getElementById('searchInput').value.toLowerCase().trim(); filteredEmployees = employees.filter(emp = { const matchesYear = emp.year === filterYear; const matchesMonth = filterMonth === 'all' emp.month === filterMonth; const matchesDept = filterDept === 'all' emp.dept === filterDept; Matches quick status filter let matchesQuick = true; if (activeQuickFilter === ปกติ) { matchesQuick = emp.status === ปกติ; } else if (activeQuickFilter === ลา) { matchesQuick = [ลาป่วย, ลากิจ, ลาพักผ่อน, ลาคลอด].includes(emp.status); } else if (activeQuickFilter === พ้นสภาพ) { matchesQuick = emp.status === พ้นสภาพ; } const cleanSearch = sanitizeInput(searchValue); const matchesSearch = emp.name.toLowerCase().includes(cleanSearch) emp.id.toLowerCase().includes(cleanSearch) emp.role.toLowerCase().includes(cleanSearch); return matchesYear && matchesMonth && matchesDept && matchesQuick && matchesSearch; }); currentPage = 1; Reset to page 1 on active filter changes renderKPIs(); renderTable(); renderCharts(); } Handle Search Bar input with debounce style function handleSearch() { applyFilters(); } Set Quick Status Filters function setQuickFilter(status) { activeQuickFilter = status; Toggle active classes on buttons const buttons = { all document.getElementById('qf-all'), 'ปกติ' document.getElementById('qf-normal'), 'ลา' document.getElementById('qf-leave'), 'พ้นสภาพ' document.getElementById('qf-dismiss') }; Object.keys(buttons).forEach(key = { if (key === status) { buttons[key].className = px-3 py-1 text-xs font-semibold rounded-md transition-all duration-200 bg-moph-teal text-white shadow-sm; } else { buttons[key].className = px-3 py-1 text-xs font-semibold rounded-md text-gray-600 hoverbg-gray-100 transition-all duration-200; } }); applyFilters(); } Calculate and Render KPIs function renderKPIs() { const total = filteredEmployees.length; document.getElementById('kpiTotalStaff').innerText = total; MaleFemale ratio const males = filteredEmployees.filter(e = e.gender === ชาย).length; const females = filteredEmployees.filter(e = e.gender === หญิง).length; document.getElementById('kpiGenderRatio').innerText = `ชาย ${males} หญิง ${females}`; Attendance calculation (Active employees total) const leavesCount = filteredEmployees.filter(e = [ลาป่วย, ลากิจ, ลาพักผ่อน, ลาคลอด].includes(e.status)).length; const dismissed = filteredEmployees.filter(e = e.status === พ้นสภาพ).length; const activeStaff = total - dismissed; let attendanceRate = 100; if (activeStaff 0) { attendanceRate = ((activeStaff - leavesCount) activeStaff) 100; } else { attendanceRate = 0; } document.getElementById('kpiAttendance').innerText = `${attendanceRate.toFixed(1)}%`; Avg Leave days let totalLeaveDays = 0; filteredEmployees.forEach(e = { if (e.status !== พ้นสภาพ) { totalLeaveDays += Number(e.leaveDays 0); } }); const avgLeave = activeStaff 0 (totalLeaveDays activeStaff) 0; document.getElementById('kpiAvgLeave').innerText = avgLeave.toFixed(1); Retention Rate (Active employees total initial) let retentionRate = 100; if (total 0) { retentionRate = ((total - dismissed) total) 100; } document.getElementById('kpiRetention').innerText = `${retentionRate.toFixed(1)}%`; } Render Data Table with Pagination function renderTable() { const tableBody = document.getElementById('employeeTableBody'); const emptyState = document.getElementById('emptyState'); tableBody.innerHTML = ''; if (filteredEmployees.length === 0) { emptyState.classList.remove('hidden'); emptyState.classList.add('flex'); updatePagination(0, 0, 0); return; } else { emptyState.classList.add('hidden'); emptyState.classList.remove('flex'); } const startIndex = (currentPage - 1) rowsPerPage; const endIndex = Math.min(startIndex + rowsPerPage, filteredEmployees.length); const paginatedData = filteredEmployees.slice(startIndex, endIndex); paginatedData.forEach(emp = { let statusBadge = ; switch (emp.status) { case ปกติ statusBadge = `span class=inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200span class=w-1.5 h-1.5 rounded-full bg-emerald-500spanมาปกติspan`; break; case ลาป่วย statusBadge = `span class=inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-rose-50 text-rose-700 border border-rose-200span class=w-1.5 h-1.5 rounded-full bg-rose-500spanลาป่วยspan`; break; case ลากิจ statusBadge = `span class=inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-amber-50 text-amber-700 border border-amber-200span class=w-1.5 h-1.5 rounded-full bg-amber-500spanลากิจspan`; break; case ลาพักผ่อน statusBadge = `span class=inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-blue-50 text-blue-700 border border-blue-200span class=w-1.5 h-1.5 rounded-full bg-blue-500spanลาพักผ่อนspan`; break; case พ้นสภาพ statusBadge = `span class=inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700 border border-gray-200span class=w-1.5 h-1.5 rounded-full bg-gray-500spanพ้นสภาพspan`; break; default statusBadge = `span class=inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-purple-50 text-purple-700 border border-purple-200span class=w-1.5 h-1.5 rounded-full bg-purple-500span${emp.status}span`; } const tr = document.createElement('tr'); tr.className = hoverbg-slate-5080 transition-all duration-150 border-b border-gray-100; tr.innerHTML = ` td class=py-4 px-6 font-bold text-moph-deep${sanitizeInput(emp.id)}td td class=py-4 px-6 font-medium text-gray-800${sanitizeInput(emp.name)}td td class=py-4 px-6 text-gray-600${sanitizeInput(emp.role)}td td class=py-4 px-6 text-gray-600${sanitizeInput(emp.dept)}td td class=py-4 px-6 text-center${statusBadge}td td class=py-4 px-6 text-center font-bold text-gray-700${emp.status === 'พ้นสภาพ' '-' Number(emp.leaveDays).toFixed(1)}td td class=py-4 px-6 text-center div class=flex items-center justify-center space-x-1 button onclick=viewEmployeeDetails('${emp.id}') class=p-1.5 bg-sky-50 text-moph-accent rounded-md hoverbg-moph-accent hovertext-white transition-all duration-150 title=ดูรายละเอียด i data-lucide=eye class=w-4 h-4i button button onclick=openEditModal('${emp.id}') class=p-1.5 bg-teal-50 text-moph-teal rounded-md hoverbg-moph-teal hovertext-white transition-all duration-150 title=แก้ไขข้อมูล i data-lucide=edit-3 class=w-4 h-4i button button onclick=confirmDelete('${emp.id}') class=p-1.5 bg-red-50 text-red-600 rounded-md hoverbg-red-600 hovertext-white transition-all duration-150 title=ลบข้อมูล i data-lucide=trash-2 class=w-4 h-4i button div td `; tableBody.appendChild(tr); }); updatePagination(startIndex, endIndex, filteredEmployees.length); lucide.createIcons(); } Update Pagination controls state function updatePagination(start, end, total) { document.getElementById('paginationInfo').innerText = `กำลังแสดงข้อมูลลำดับที่ ${total === 0 0 start + 1} ถึง ${end} จากทั้งหมด ${total} รายการ`; const totalPages = Math.ceil(total rowsPerPage); document.getElementById('btnPrevPage').disabled = currentPage = 1; document.getElementById('btnNextPage').disabled = currentPage = totalPages totalPages === 0; const pageNumbersDiv = document.getElementById('pageNumbers'); pageNumbersDiv.innerHTML = ''; for (let i = 1; i = totalPages; i++) { const btn = document.createElement('button'); btn.className = `px-2.5 py-1 text-xs font-semibold rounded-md transition-all ${currentPage === i 'bg-moph-teal text-white' 'text-gray-600 hoverbg-gray-200'}`; btn.innerText = i; btn.onclick = function() { currentPage = i; renderTable(); }; pageNumbersDiv.appendChild(btn); } } function changePage(direction) { currentPage += direction; renderTable(); } Render Analytics Charts (MOPH Official Styles) function renderCharts() { Group staff by Department and status for Bar Chart const departments = [กลุ่มงานการแพทย์, กลุ่มงานพยาบาล, กลุ่มงานเภสัชกรรม, กลุ่มงานเทคนิคบริการ, กลุ่มงานบริหารทั่วไป]; const deptStats = departments.map(dept = { const list = filteredEmployees.filter(e = e.dept === dept); const active = list.filter(e = e.status === ปกติ).length; const leaves = list.filter(e = [ลาป่วย, ลากิจ, ลาพักผ่อน, ลาคลอด].includes(e.status)).length; return { dept, active, leaves }; }); Destroy previous charts if exists if (barChartInstance) barChartInstance.destroy(); if (pieChartInstance) pieChartInstance.destroy(); Chart 1 Bar Chart const ctxBar = document.getElementById('barChartDept').getContext('2d'); barChartInstance = new Chart(ctxBar, { type 'bar', data { labels departments.map(d = d.replace(กลุ่มงาน, )), datasets [ { label 'ปฏิบัติงานปกติ (คน)', data deptStats.map(s = s.active), backgroundColor '#008080', borderRadius 6 }, { label 'อยู่ระหว่างลา (คน)', data deptStats.map(s = s.leaves), backgroundColor '#D97706', borderRadius 6 } ] }, options { responsive true, maintainAspectRatio false, plugins { legend { position 'top', labels { font { family 'Sarabun', size 11 } } } }, scales { y { beginAtZero true, ticks { font { family 'Sarabun' }, stepSize 1 } }, x { ticks { font { family 'Sarabun', size 10 } } } } } }); Chart 2 Pie Chart (Leave types distribution) const sick = filteredEmployees.filter(e = e.status === ลาป่วย).length; const personal = filteredEmployees.filter(e = e.status === ลากิจ).length; const vac = filteredEmployees.filter(e = e.status === ลาพักผ่อน).length; const otherLeaves = filteredEmployees.filter(e = e.status === ลาคลอด).length; const ctxPie = document.getElementById('pieChartLeave').getContext('2d'); pieChartInstance = new Chart(ctxPie, { type 'doughnut', data { labels ['ลาป่วย', 'ลากิจ', 'ลาพักผ่อน', 'ลาอื่นๆ'], datasets [{ data [sick, personal, vac, otherLeaves], backgroundColor ['#f43f5e', '#f59e0b', '#3b82f6', '#a855f7'], borderWidth 2 }] }, options { responsive true, maintainAspectRatio false, plugins { legend { position 'bottom', labels { font { family 'Sarabun', size 11 } } } } } }); } Form Submission (AddEdit Employee) function handleFormSubmit(event) { event.preventDefault(); const empIdInput = document.getElementById('empId').value.trim().toUpperCase(); const empName = document.getElementById('empName').value.trim(); const empRole = document.getElementById('empRole').value; const empDept = document.getElementById('empDept').value; const empTel = document.getElementById('empTel').value.trim(); const empMonth = document.getElementById('empMonth').value; const empGender = document.getElementById('empGender').value; const empStatus = document.getElementById('empStatus').value; const empLeaveDays = Number(document.getElementById('empLeaveDays').value); Double validation check on ID for script injection or duplicate const sanitizedId = sanitizeInput(empIdInput); const sanitizedName = sanitizeInput(empName); const sanitizedTel = sanitizeInput(empTel); Check duplicate ID if adding new if (editEmployeeId === null) { const exist = employees.some(e = e.id === sanitizedId); if (exist) { document.getElementById('empIdError').classList.remove('hidden'); return; } } document.getElementById('empIdError').classList.add('hidden'); const currentYear = document.getElementById('filterYear').value; if (editEmployeeId !== null) { Update Existing const index = employees.findIndex(e = e.id === editEmployeeId); if (index !== -1) { employees[index] = { id sanitizedId, name sanitizedName, role empRole, dept empDept, tel sanitizedTel, gender empGender, status empStatus, leaveDays empLeaveDays, month empMonth, year currentYear }; showToast(อัปเดตข้อมูลพนักงานเรียบร้อยแล้ว!, check, bg-teal-500); } } else { Insert New employees.push({ id sanitizedId, name sanitizedName, role empRole, dept empDept, tel sanitizedTel, gender empGender, status empStatus, leaveDays empLeaveDays, month empMonth, year currentYear }); showToast(เพิ่มข้อมูลพนักงานสำเร็จ!, check, bg-emerald-500); } saveToLocalStorage(); closeModal('employeeModal'); applyFilters(); } Open Form in Edit Mode function openEditModal(id) { editEmployeeId = id; const emp = employees.find(e = e.id === id); if (!emp) return; document.getElementById('modalTitle').innerText = แก้ไขข้อมูลบุคลากรสาธารณสุข; document.getElementById('empId').value = emp.id; document.getElementById('empId').disabled = true; Lock ID during edit document.getElementById('empName').value = emp.name; document.getElementById('empRole').value = emp.role; document.getElementById('empDept').value = emp.dept; document.getElementById('empTel').value = emp.tel; document.getElementById('empGender').value = emp.gender; document.getElementById('empStatus').value = emp.status; document.getElementById('empLeaveDays').value = emp.leaveDays; document.getElementById('empMonth').value = emp.month กรกฎาคม; document.getElementById('empIdError').classList.add('hidden'); openModal('employeeModal'); } Reset and Open Form in Add Mode function openAddModal() { editEmployeeId = null; document.getElementById('modalTitle').innerText = เพิ่มข้อมูลบุคลากรสาธารณสุข; document.getElementById('employeeForm').reset(); document.getElementById('empId').disabled = false; document.getElementById('empIdError').classList.add('hidden'); openModal('employeeModal'); } View Detail Info function viewEmployeeDetails(id) { const emp = employees.find(e = e.id === id); if (!emp) return; document.getElementById('detId').innerText = emp.id; document.getElementById('detName').innerText = emp.name; document.getElementById('detRole').innerText = emp.role; document.getElementById('detDept').innerText = emp.dept; document.getElementById('detTel').innerText = emp.tel; document.getElementById('detGender').innerText = emp.gender; document.getElementById('detStatus').innerText = emp.status; document.getElementById('detLeave').innerText = `${Number(emp.leaveDays).toFixed(1)} วัน`; openModal('detailModal'); } Safe Confirmation Modals (Replaces native alerts) function confirmReset() { document.getElementById('confirmTitle').innerText = ยืนยันการคืนค่าเริ่มต้น; document.getElementById('confirmMessage').innerText = คุณต้องการรีเซ็ตระบบกลับเป็นฐานข้อมูลเริ่มต้นหรือไม่ การเปลี่ยนแปลงข้อมูลทั้งหมดของคุณจะหายไปอย่างสมบูรณ์; const btnOk = document.getElementById('confirmBtnOk'); btnOk.className = w-full inline-flex justify-center rounded-lg border border-transparent shadow-sm px-4 py-2 bg-red-600 text-sm font-semibold text-white hoverbg-red-700 transition-all; btnOk.onclick = function() { resetToDefault(); closeModal('confirmModal'); }; openModal('confirmModal'); } function confirmDelete(id) { document.getElementById('confirmTitle').innerText = ยืนยันการลบข้อมูลบุคลากร; document.getElementById('confirmMessage').innerText = `คุณแน่ใจหรือไม่ที่จะลบรายชื่อบุคลากรรหัส ${id} ออกจากระบบ การทำรายการนี้จะไม่สามารถย้อนคืนได้`; const btnOk = document.getElementById('confirmBtnOk'); btnOk.className = w-full inline-flex justify-center rounded-lg border border-transparent shadow-sm px-4 py-2 bg-red-600 text-sm font-semibold text-white hoverbg-red-700 transition-all; btnOk.onclick = function() { employees = employees.filter(e = e.id !== id); saveToLocalStorage(); applyFilters(); closeModal('confirmModal'); showToast(ลบข้อมูลพนักงานออกแล้ว, trash, bg-red-500); }; openModal('confirmModal'); } CSV Export Feature (With Thai Encoding BOM Support) function exportToCSV() { if (filteredEmployees.length === 0) { showToast(ไม่พบข้อมูลที่จะส่งออก!, alert-circle, bg-amber-500); return; } const currentYear = document.getElementById('filterYear').value; const currentMonth = document.getElementById('filterMonth').value; Generate header let csvContent = uFEFF; UTF-8 BOM to prevent Thai garbled text in Excel csvContent += รหัสประจำตัว,ชื่อ-นามสกุล,ตำแหน่ง,กลุ่มงาน,เพศ,สถานะเดือนนี้,วันลาสะสมสะสม,เดือนประมวลผล,ปีงบประมาณn; Generate rows filteredEmployees.forEach(emp = { const row = [ emp.id, emp.name, emp.role, emp.dept, emp.gender, emp.status, emp.leaveDays, emp.month, emp.year ].map(val = `${String(val).replace(g, '')}`).join(,); csvContent += row + n; }); Trigger file download const blob = new Blob([csvContent], { type 'textcsv;charset=utf-8;' }); const link = document.createElement(a); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute(href, url); link.setAttribute(download, `รายงานบุคลากร_สธ_${currentMonth}_${currentYear}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); showToast(ส่งออกข้อมูลเป็นไฟล์ CSV เรียบร้อยแล้ว, download, bg-teal-500); } } UI Utility Modals toggler function openModal(modalId) { if (modalId === 'employeeModal' && editEmployeeId === null) { Adjust for dynamic adding reset document.getElementById('modalTitle').innerText = เพิ่มข้อมูลบุคลากรสาธารณสุข; document.getElementById('employeeForm').reset(); document.getElementById('empId').disabled = false; } document.getElementById(modalId).classList.remove('hidden'); } function closeModal(modalId) { document.getElementById(modalId).classList.add('hidden'); } Custom Toast controller function showToast(message, iconName, bgClass) { const toast = document.getElementById('toast'); const toastMsg = document.getElementById('toastMessage'); const toastIcon = document.getElementById('toastIcon'); const toastContainer = document.getElementById('toastIconContainer'); toastMsg.innerText = message; toastIcon.setAttribute('data-lucide', iconName); toastContainer.className = `p-1 rounded text-white ${bgClass}`; Refresh icons inside toast lucide.createIcons(); Slide up & Fade in toast.classList.remove('translate-y-10', 'opacity-0', 'pointer-events-none'); toast.classList.add('translate-y-0', 'opacity-100'); Hide after 3 seconds setTimeout(() = { toast.classList.remove('translate-y-0', 'opacity-100'); toast.classList.add('translate-y-10', 'opacity-0', 'pointer-events-none'); }, 3000); } script body html