!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=truespan
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=truespan
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