PDPA Notice: ระบบนี้ทำงานบน Client-Side Browser Storage โปรดหลีกเลี่ยงการบันทึกข้อมูลส่วนบุคคลที่อ่อนไหว เช่น เลขบัตรประชาชน หรือข้อมูลทางการเงิน
|
เลื่อนซ้าย-ขวาเพื่อดูข้อมูลตารางเพิ่มเติม
// Render Dynamic KPI Cards - Secure textContent implementation
function renderKPIs() {
const totalBudget = filteredItems.reduce((acc, curr) => acc + Number(curr.budget || 0), 0);
const totalStaff = filteredItems.length;
// Unique courses
const uniqueCourses = new Set(filteredItems.map(i => i.course)).size;
// Success Rate
const completedCount = filteredItems.filter(i => i.status === 'เสร็จสิ้น').length;
const successRate = totalStaff > 0 ? ((completedCount / totalStaff) * 100).toFixed(1) : 0;
// Secure rendering using textContent (Preventing DOM-based XSS)
document.getElementById('kpi-total-budget').textContent = `฿${totalBudget.toLocaleString()}`;
document.getElementById('kpi-total-staff').textContent = `${totalStaff} คน`;
document.getElementById('kpi-total-courses').textContent = `${uniqueCourses} หลักสูตร`;
document.getElementById('kpi-success-rate').textContent = `${successRate}%`;
}
// Backup Data Management: Export JSON
function exportBackupData() {
if (hrItems.length === 0) {
showToast("ไม่มีข้อมูลสำหรับสำรอง", "error");
return;
}
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(hrItems, null, 2));
const downloadAnchor = document.createElement('a');
downloadAnchor.setAttribute("href", dataStr);
downloadAnchor.setAttribute("download", `HRD_Backup_${new Date().toISOString().slice(0,10)}.json`);
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
downloadAnchor.remove();
showToast("สำรองข้อมูลเป็นไฟล์ JSON สำเร็จ", "success");
}
// Restore Data Management: Import JSON
function importBackupData(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const importedData = JSON.parse(e.target.result);
if (Array.isArray(importedData)) {
hrItems = importedData;
saveToLocalStorage();
applyFilters();
showToast("นำเข้าข้อมูลสำรองสำเร็จ", "success");
} else {
showToast("รูปแบบไฟล์ JSON ไม่ถูกต้อง", "error");
}
} catch (err) {
showToast("เกิดข้อผิดพลาดในการอ่านไฟล์ JSON", "error");
}
};
reader.readAsText(file);
event.target.value = ''; // Reset input
}
// Initialize App on DOM Load
window.addEventListener('DOMContentLoaded', () => {
loadInitialData();
initCharts();
applyFilters();
});
// Load data from LocalStorage or Mock Data
function loadInitialData() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
hrItems = JSON.parse(stored);
} else {
hrItems = [...mockData];
saveToLocalStorage();
}
} catch (e) {
console.error("Failed to load local storage, falling back to mock data.", e);
hrItems = [...mockData];
}
}
function saveToLocalStorage() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(hrItems));
} catch (e) {
showToast("ไม่สามารถบันทึกข้อมูลลง LocalStorage ได้", "error");
}
}
// Apply Filters & Search Query
function applyFilters() {
const yearVal = document.getElementById('filter-year').value;
const deptVal = document.getElementById('filter-department').value;
const skillVal = document.getElementById('filter-skill').value;
const searchVal = document.getElementById('search-input').value.trim().toLowerCase();
filteredItems = hrItems.filter(item => {
const matchYear = yearVal === 'ALL' || item.year === yearVal;
const matchDept = deptVal === 'ALL' || item.dept === deptVal;
const matchSkill = skillVal === 'ALL' || item.skill === skillVal;
const matchSearch = !searchVal ||
item.name.toLowerCase().includes(searchVal) ||
item.course.toLowerCase().includes(searchVal) ||
item.position.toLowerCase().includes(searchVal);
return matchYear && matchDept && matchSkill && matchSearch;
});
renderTable();
renderKPIs();
updateCharts();
}
function resetFilters() {
document.getElementById('filter-year').value = 'ALL';
document.getElementById('filter-department').value = 'ALL';
document.getElementById('filter-skill').value = 'ALL';
document.getElementById('search-input').value = '';
applyFilters();
}
// Render Table Rows
function renderTable() {
const tbody = document.getElementById('table-body');
const emptyState = document.getElementById('empty-state');
tbody.innerHTML = '';
if (filteredItems.length === 0) {
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
filteredItems.forEach(item => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-slate-50 transition border-b border-slate-100';
// Status Badge styling
let statusBadge = '';
if (item.status === 'เสร็จสิ้น') {
statusBadge = ` เสร็จสิ้น`;
} else if (item.status === 'กำลังดำเนินการ') {
statusBadge = ` กำลังดำเนินการ`;
} else {
statusBadge = ` ยังไม่เริ่ม`;
}
// Skill Badge
let skillBadge = `${escapeHTML(item.skill)}`;
tr.innerHTML = `
${escapeHTML(item.year)} |
${escapeHTML(item.name)} |
${escapeHTML(item.dept)}
${escapeHTML(item.position)}
|
${escapeHTML(item.course)} |
${skillBadge} |
฿${Number(item.budget).toLocaleString()} |
${statusBadge} |
|
`;
tbody.appendChild(tr);
});
}
// Render Dynamic KPI Cards
function renderKPIs() {
const totalBudget = filteredItems.reduce((acc, curr) => acc + Number(curr.budget || 0), 0);
const totalStaff = filteredItems.length;
// Unique courses
const uniqueCourses = new Set(filteredItems.map(i => i.course)).size;
// Success Rate
const completedCount = filteredItems.filter(i => i.status === 'เสร็จสิ้น').length;
const successRate = totalStaff > 0 ? ((completedCount / totalStaff) * 100).toFixed(1) : 0;
document.getElementById('kpi-total-budget').innerText = `฿${totalBudget.toLocaleString()}`;
document.getElementById('kpi-total-staff').innerText = `${totalStaff} คน`;
document.getElementById('kpi-total-courses').innerText = `${uniqueCourses} หลักสูตร`;
document.getElementById('kpi-success-rate').innerText = `${successRate}%`;
}
// Chart.js Initialization
function initCharts() {
const ctxA = document.getElementById('skillBudgetChart').getContext('2d');
skillChartInstance = new Chart(ctxA, {
type: 'doughnut',
data: {
labels: ['Hard Skill', 'Soft Skill', 'Leadership'],
datasets: [{
data: [0, 0, 0],
backgroundColor: ['#0F172A', '#F97316', '#3B82F6'],
borderWidth: 2,
borderColor: '#FFFFFF'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { font: { family: 'Sarabun' } } }
}
}
});
const ctxB = document.getElementById('deptProgressChart').getContext('2d');
deptChartInstance = new Chart(ctxB, {
type: 'bar',
data: {
labels: ['IT', 'HR', 'Sales', 'Finance', 'Operations'],
datasets: [{
label: 'จำนวนคนอบรม',
data: [0, 0, 0, 0, 0],
backgroundColor: '#1E293B',
borderRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, ticks: { precision: 0, font: { family: 'Sarabun' } } },
x: { ticks: { font: { family: 'Sarabun' } } }
},
plugins: {
legend: { display: false }
}
}
});
}
// Update Charts with filtered data
function updateCharts() {
if (!skillChartInstance || !deptChartInstance) return;
// Update Chart A: Skill Budget
const skillBudgets = { 'Hard Skill': 0, 'Soft Skill': 0, 'Leadership': 0 };
filteredItems.forEach(i => {
if (skillBudgets[i.skill] !== undefined) {
skillBudgets[i.skill] += Number(i.budget || 0);
}
});
skillChartInstance.data.datasets[0].data = [
skillBudgets['Hard Skill'],
skillBudgets['Soft Skill'],
skillBudgets['Leadership']
];
skillChartInstance.update();
// Update Chart B: Department Participants
const depts = ['IT', 'HR', 'Sales', 'Finance', 'Operations'];
const deptCounts = { 'IT': 0, 'HR': 0, 'Sales': 0, 'Finance': 0, 'Operations': 0 };
filteredItems.forEach(i => {
if (deptCounts[i.dept] !== undefined) {
deptCounts[i.dept] += 1;
}
});
deptChartInstance.data.datasets[0].data = depts.map(d => deptCounts[d]);
deptChartInstance.update();
}
// Modal Management
function openAddModal() {
document.getElementById('modal-title').innerText = 'เพิ่มแผนพัฒนาบุคลากร';
document.getElementById('form-id').value = '';
document.getElementById('hr-form').reset();
document.getElementById('modal-form').classList.remove('hidden');
}
function openEditModal(id) {
const item = hrItems.find(i => i.id === id);
if (!item) return;
document.getElementById('modal-title').innerText = 'แก้ไขแผนพัฒนาบุคลากร';
document.getElementById('form-id').value = item.id;
document.getElementById('form-year').value = item.year;
document.getElementById('form-department').value = item.dept;
document.getElementById('form-name').value = item.name;
document.getElementById('form-position').value = item.position;
document.getElementById('form-course').value = item.course;
document.getElementById('form-skill').value = item.skill;
document.getElementById('form-budget').value = item.budget;
document.getElementById('form-status').value = item.status;
document.getElementById('modal-form').classList.remove('hidden');
}
function closeModal() {
document.getElementById('modal-form').classList.add('hidden');
}
// Save Item (Create or Update)
function saveItem(e) {
e.preventDefault();
const id = document.getElementById('form-id').value;
const newItem = {
id: id || Date.now().toString(),
year: document.getElementById('form-year').value,
dept: document.getElementById('form-department').value,
name: document.getElementById('form-name').value.trim(),
position: document.getElementById('form-position').value.trim(),
course: document.getElementById('form-course').value.trim(),
skill: document.getElementById('form-skill').value,
budget: Number(document.getElementById('form-budget').value),
status: document.getElementById('form-status').value
};
if (id) {
// Update
const idx = hrItems.findIndex(i => i.id === id);
if (idx !== -1) hrItems[idx] = newItem;
showToast("อัปเดตข้อมูลสำเร็จ", "success");
} else {
// Create
hrItems.unshift(newItem);
showToast("บันทึกข้อมูลใหม่สำเร็จ", "success");
}
saveToLocalStorage();
applyFilters();
closeModal();
}
// Delete Confirm Dialog
function confirmDelete(id) {
pendingDeleteId = id;
const item = hrItems.find(i => i.id === id);
if (!item) return;
document.getElementById('confirm-title').innerText = `ยืนยันการลบแผนพัฒนา`;
document.getElementById('confirm-desc').innerText = `คุณแน่ใจหรือไม่ว่าต้องการลบหลักสูตร "${item.course}" ของคุณ ${item.name}?`;
document.getElementById('btn-confirm-action').onclick = executeDelete;
document.getElementById('modal-confirm').classList.remove('hidden');
}
function executeDelete() {
if (!pendingDeleteId) return;
hrItems = hrItems.filter(i => i.id !== pendingDeleteId);
saveToLocalStorage();
applyFilters();
closeConfirmModal();
showToast("ลบข้อมูลสำเร็จ", "success");
}
function closeConfirmModal() {
document.getElementById('modal-confirm').classList.add('hidden');
pendingDeleteId = null;
}
// Reset to Initial Mock Data
function confirmResetMockData() {
document.getElementById('confirm-title').innerText = `คืนค่าข้อมูลตัวอย่าง`;
document.getElementById('confirm-desc').innerText = `คุณแน่ใจหรือไม่ว่าต้องการล้างข้อมูลปัจจุบันทั้งหมด และกลับไปใช้ข้อมูลตัวอย่างเริ่มต้น?`;
document.getElementById('btn-confirm-action').onclick = () => {
hrItems = [...mockData];
saveToLocalStorage();
applyFilters();
closeConfirmModal();
showToast("รีเซ็ตข้อมูลกลับสู่ค่าเริ่มต้นเรียบร้อย", "success");
};
document.getElementById('modal-confirm').classList.remove('hidden');
}
// Data Export: Excel (SheetJS)
function exportToExcel() {
if (filteredItems.length === 0) {
showToast("ไม่มีข้อมูลสำหรับส่งออก", "error");
return;
}
const exportData = filteredItems.map(item => ({
'ปีงบประมาณ': item.year,
'ชื่อ-นามสกุล': item.name,
'แผนก': item.dept,
'ตำแหน่ง': item.position,
'ชื่อหลักสูตร': item.course,
'ประเภท Skill': item.skill,
'งบประมาณ (บาท)': item.budget,
'สถานะ': item.status
}));
const worksheet = XLSX.utils.json_to_sheet(exportData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "HRD_Plan");
XLSX.writeFile(workbook, `HRD_Development_Plan_${new Date().toISOString().slice(0,10)}.xlsx`);
showToast("ส่งออกไฟล์ Excel สำเร็จ", "success");
}
// Data Export: PDF (html2pdf)
function exportToPDF() {
const element = document.getElementById('export-area');
const opt = {
margin: [0.3, 0.3, 0.3, 0.3],
filename: `HRD_Dashboard_Report_${new Date().toISOString().slice(0,10)}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'in', format: 'a4', orientation: 'landscape' }
};
showToast("กำลังสร้างเอกสาร PDF...", "info");
html2pdf().set(opt).from(element).save().then(() => {
showToast("ส่งออกไฟล์ PDF สำเร็จ", "success");
}).catch(err => {
console.error("PDF Export error:", err);
showToast("เกิดข้อผิดพลาดในการสร้าง PDF", "error");
});
}
// Custom Toast Notification System
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
let bgClass = 'bg-slate-800 text-white';
let icon = 'fa-info-circle';
if (type === 'success') {
bgClass = 'bg-emerald-600 text-white';
icon = 'fa-check-circle';
} else if (type === 'error') {
bgClass = 'bg-rose-600 text-white';
icon = 'fa-exclamation-circle';
}
toast.className = `pointer-events-auto flex items-center gap-3 p-4 rounded-xl shadow-lg ${bgClass} transition-all duration-300 transform translate-y-2 opacity-0 text-sm font-medium`;
toast.innerHTML = ` ${escapeHTML(message)}`;
container.appendChild(toast);
// Animate in
setTimeout(() => {
toast.classList.remove('translate-y-2', 'opacity-0');
}, 10);
// Remove after 3s
setTimeout(() => {
toast.classList.add('opacity-0', 'translate-y-2');
setTimeout(() => toast.remove(), 300);
}, 3000);
}