document.addEventListener('DOMContentLoaded', () => { // --- STATE MANAGEMENT --- const state = { currentView: 'dashboard', // 'dashboard', 'employees', 'recruitment', 'tasks', 'employeeProfile', 'leave' currentEmployeeId: null, modal: { isOpen: false, type: null, }, mockData: { employees: [ {id: 1, name: 'สมชาย ใจดี', position: 'Software Developer', department: 'Technology', avatar: 'สจ', startDate: '2022-01-15', email: 'somchai.j@example.com', phone: '081-234-5678'}, {id: 2, name: 'สมหญิง จริงใจ', position: 'HR Manager', department: 'Human Resources', avatar: 'สจ', startDate: '2021-06-01', email: 'somyhing.j@example.com', phone: '082-345-6789'}, {id: 3, name: 'มานี มีนา', position: 'Marketing', department: 'Marketing', avatar: 'มน', startDate: '2022-08-20', email: 'manee.m@example.com', phone: '083-456-7890'}, {id: 4, name: 'ปิติ ยินดี', position: 'Data Analyst', department: 'Technology', avatar: 'ปย', startDate: '2023-02-10', email: 'piti.y@example.com', phone: '084-567-8901'}, {id: 5, name: 'เอบีซี ดีอีเอฟ', position: 'Sales', department: 'Sales', avatar: 'อด', startDate: '2022-11-05', email: 'abc.d@example.com', phone: '085-678-9012'}, {id: 6, name: 'สุดา พาเพลิน', position: 'Accountant', department: 'Finance', avatar: 'สพ', startDate: '2020-03-30', email: 'suda.p@example.com', phone: '086-789-0123'}, {id: 7, name: 'ดวงดี มีโชค', position: 'Software Developer', department: 'Technology', avatar: 'ดม', startDate: '2023-07-19', email: 'duangdee.m@example.com', phone: '087-890-1234'}, ], leaveRequests: [ {id: 1, employeeId: 1, employeeName: 'สมชาย ใจดี', type: 'ลาป่วย (Sick Leave)', startDate: '2023-11-20', endDate: '2023-11-21', status: 'pending'}, {id: 2, employeeId: 3, employeeName: 'มานี มีนา', type: 'ลากิจ (Personal Leave)', startDate: '2023-11-25', endDate: '2023-11-25', status: 'pending'}, {id: 3, employeeId: 4, employeeName: 'ปิติ ยินดี', type: 'ลาพักร้อน (Annual Leave)', startDate: '2023-12-01', endDate: '2023-12-05', status: 'approved'}, {id: 4, employeeId: 6, employeeName: 'สุดา พาเพลิน', type: 'ลาป่วย (Sick Leave)', startDate: '2023-11-15', endDate: '2023-11-15', status: 'rejected'}, ], candidates: [ {id: 1, name: 'วีระ กล้าหาญ', position: 'Software Developer', status: 'interview', date: '2023-10-15'}, {id: 2, name: 'ใจดี มีสุข', position: 'Project Manager', status: 'hired', date: '2023-10-20'}, {id: 3, name: 'เมตตา กรุณา', position: 'UX/UI Designer', status: 'new', date: '2023-11-01'}, {id: 4, name: 'กล้าณรงค์ วงศ์ดี', position: 'Data Analyst', status: 'new', date: '2023-11-05'}, {id: 5, name: 'ทักษิณ ชินวัตร', position: 'Sales', status: 'interview', date: '2023-11-10'}, ], tasks: [ {id: 1, name: 'สัมภาษณ์งานคุณวีระ', completed: false}, {id: 2, name: 'เตรียมเอกสารสัญญาจ้างคุณใจดี', completed: true}, ] } }; // --- DOM Elements --- const appContent = document.getElementById('app-content'); const desktopContent = document.getElementById('desktop-content'); const headerTitle = document.getElementById('header-title'); const modalContainer = document.createElement('div'); modalContainer.id = 'modal-container'; document.body.appendChild(modalContainer); // --- NAVIGATION SETUP --- function setupNavigation() { // Desktop navigation const desktopNavItems = document.querySelectorAll('.desktop-nav .nav-item'); desktopNavItems.forEach(item => { item.addEventListener('click', (e) => { const viewName = e.target.closest('.nav-item').id.replace('nav-', ''); navigateTo(viewName); }); }); // Mobile navigation const mobileNavItems = document.querySelectorAll('.mobile-nav .nav-button'); mobileNavItems.forEach(item => { item.addEventListener('click', (e) => { const viewName = e.target.closest('.nav-button').id.replace('nav-', '').replace('-mobile', ''); navigateTo(viewName); }); }); } function navigateTo(viewName) { state.currentView = viewName; render(); } // Setup navigation on load setupNavigation(); // --- TEMPLATING ENGINE --- async function fetchTemplate(path) { const response = await fetch(path); if (!response.ok) throw new Error(`Template not found: ${path}`); return await response.text(); } function interpolate(template, data) { return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => data[key.trim()] || ''); } // --- DATA MANIPULATION --- function addEmployee(name, position) { const newId = state.mockData.employees.length > 0 ? Math.max(...state.mockData.employees.map(e => e.id)) + 1 : 1; const avatar = name.split(' ').map(n => n[0]).join('').toUpperCase(); const department = 'General'; // Default department state.mockData.employees.push({ id: newId, name, position, department, avatar, startDate: new Date().toISOString().split('T')[0], email: `${name.toLowerCase().replace(' ', '.')}@company.com`, phone: '0XX-XXX-XXXX' }); showNotification(`เพิ่มพนักงาน ${name} เรียบร้อยแล้ว`, 'success'); } function addCandidate(name, position) { const newId = state.mockData.candidates.length > 0 ? Math.max(...state.mockData.candidates.map(c => c.id)) + 1 : 1; state.mockData.candidates.push({ id: newId, name, position, status: 'new', date: new Date().toISOString().split('T')[0] }); showNotification(`เพิ่มผู้สมัคร ${name} เรียบร้อยแล้ว`, 'success'); } function addTask(name) { const newId = state.mockData.tasks.length > 0 ? Math.max(...state.mockData.tasks.map(t => t.id)) + 1 : 1; state.mockData.tasks.push({id: newId, name, completed: false}); showNotification(`เพิ่มงาน "${name}" เรียบร้อยแล้ว`, 'success'); } function toggleTask(taskId) { const task = state.mockData.tasks.find(t => t.id === taskId); if (task) { task.completed = !task.completed; showNotification( `งาน "${task.name}" ${task.completed ? 'เสร็จสิ้น' : 'ยกเลิกการเสร็จสิ้น'}แล้ว`, task.completed ? 'success' : 'info' ); } } function approveLeave(leaveId) { const request = state.mockData.leaveRequests.find(r => r.id === leaveId); if (request) { request.status = 'approved'; showNotification(`อนุมัติการลาของ ${request.employeeName} เรียบร้อยแล้ว`, 'success'); } } function rejectLeave(leaveId) { const request = state.mockData.leaveRequests.find(r => r.id === leaveId); if (request) { request.status = 'rejected'; showNotification(`ปฏิเสธการลาของ ${request.employeeName} เรียบร้อยแล้ว`, 'info'); } } function updateModalVisibility() { const modalContainer = document.getElementById('modal-container'); if (!modalContainer) return; if (state.modal && state.modal.isOpen) { modalContainer.classList.remove('hidden'); document.body.classList.add('modal-open'); } else { modalContainer.classList.add('hidden'); document.body.classList.remove('modal-open'); } } // --- RENDER FUNCTIONS --- async function render() { // Update header title const titleMap = { 'dashboard': 'Dashboard', 'employees': 'พนักงาน', 'recruitment': 'สรรหาบุคลากร', 'tasks': 'งาน', 'leave': 'การลา', 'employeeProfile': 'ข้อมูลพนักงาน' }; headerTitle.textContent = titleMap[state.currentView] || state.currentView; // Update navigation states document.querySelectorAll('.nav-button, .nav-item').forEach(btn => btn.classList.remove('active')); // Update desktop nav const desktopNavItem = document.getElementById(`nav-${state.currentView}`); if (desktopNavItem) desktopNavItem.classList.add('active'); // Update mobile nav const mobileNavItem = document.getElementById(`nav-${state.currentView}-mobile`); if (mobileNavItem) mobileNavItem.classList.add('active'); // Clear content areas before rendering new content if (appContent) appContent.innerHTML = ''; if (desktopContent) desktopContent.innerHTML = ''; // Render content based on current view switch (state.currentView) { case 'dashboard': await renderDashboard(); break; case 'employees': await renderEmployees(); break; case 'recruitment': await renderRecruitment(); break; case 'tasks': await renderTasks(); break; case 'employeeProfile': await renderEmployeeProfile(); break; case 'leave': await renderLeave(); break; } await renderModal(); updateModalVisibility(); } async function renderDashboard() { const {employees, candidates, tasks} = state.mockData; const template = await fetchTemplate('templates/dashboard.html'); // Summary Cards Data const summaryItems = [ {number: employees.length, label: 'พนักงานทั้งหมด'}, {number: candidates.filter(c => c.status === 'new').length, label: 'ผู้สมัครใหม่'}, {number: tasks.filter(t => !t.completed).length, label: 'งานที่ต้องทำ'}, {number: state.mockData.leaveRequests.filter(r => r.status === 'pending').length, label: 'รออนุมัติลา'} ]; const summary_items_html = summaryItems.map(item => `
${item.number}
${item.label}
` ).join(''); // Task List Data const urgentTasks = tasks.filter(t => !t.completed); const task_list_html = urgentTasks.map(task => `
${task.name}
งานที่รอดำเนินการ
`).join('') || '

ไม่มีงานค้างที่ต้องทำ
'; // Render the template for both mobile and desktop const contentHtml = interpolate(template, { summary_items: summary_items_html, task_list: task_list_html, urgent_count: urgentTasks.length }); if (appContent) appContent.innerHTML = contentHtml; if (desktopContent) desktopContent.innerHTML = contentHtml; // --- Chart Rendering --- // This needs to run AFTER the canvas elements are in the DOM renderDepartmentChart(); renderCandidateChart(); } function renderDepartmentChart() { const ctx = document.getElementById('departmentChart')?.getContext('2d'); if (!ctx) return; const departmentCounts = state.mockData.employees.reduce((acc, emp) => { acc[emp.department] = (acc[emp.department] || 0) + 1; return acc; }, {}); new Chart(ctx, { type: 'pie', data: { labels: Object.keys(departmentCounts), datasets: [{ label: 'พนักงาน', data: Object.values(departmentCounts), backgroundColor: ['#4a90e2', '#50e3c2', '#f5a623', '#bd10e0', '#9013fe'], hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'top', } } } }); } function renderCandidateChart() { const ctx = document.getElementById('candidateChart')?.getContext('2d'); if (!ctx) return; const candidateCountsByMonth = state.mockData.candidates.reduce((acc, can) => { const month = new Date(can.date).toLocaleString('default', {month: 'short'}); acc[month] = (acc[month] || 0) + 1; return acc; }, {}); const sortedMonths = Object.keys(candidateCountsByMonth).sort((a, b) => new Date(`1 ${a} 2023`) - new Date(`1 ${b} 2023`)); const sortedCounts = sortedMonths.map(month => candidateCountsByMonth[month]); new Chart(ctx, { type: 'bar', data: { labels: sortedMonths, datasets: [{ label: 'จำนวนผู้สมัคร', data: sortedCounts, backgroundColor: '#50e3c2', borderColor: '#4a90e2', borderWidth: 1 }] }, options: { scales: { y: { beginAtZero: true } }, responsive: true, maintainAspectRatio: true, } }); } async function renderEmployees() { const template = await fetchTemplate('templates/employees.html'); const employee_list_html = state.mockData.employees.map(emp => `
${emp.avatar}

${emp.name}

${emp.position}

${emp.department}

`).join(''); const contentHtml = interpolate(template, {employee_list: employee_list_html}); if (appContent) appContent.innerHTML = contentHtml; if (desktopContent) desktopContent.innerHTML = contentHtml; } async function renderEmployeeProfile() { const employee = state.mockData.employees.find(e => e.id === state.currentEmployeeId); if (!employee) { // Handle case where employee not found, maybe redirect to employee list state.currentView = 'employees'; await render(); return; } const template = await fetchTemplate('templates/employee-profile.html'); const contentHtml = interpolate(template, employee); if (appContent) appContent.innerHTML = contentHtml; if (desktopContent) desktopContent.innerHTML = contentHtml; } async function renderRecruitment() { const template = await fetchTemplate('templates/recruitment.html'); const getStatusText = (status) => { switch (status) { case 'new': return 'ใหม่'; case 'interview': return 'สัมภาษณ์'; case 'hired': return 'รับเข้าทำงาน'; default: return status; } }; const candidate_list_html = state.mockData.candidates.map(can => `

${can.name}

${can.position}

วันที่สมัคร: ${can.date}

${getStatusText(can.status)}
`).join(''); const contentHtml = interpolate(template, {candidate_list: candidate_list_html}); if (appContent) appContent.innerHTML = contentHtml; if (desktopContent) desktopContent.innerHTML = contentHtml; } async function renderLeave() { const template = await fetchTemplate('templates/leave.html'); const pendingCount = state.mockData.leaveRequests.filter(req => req.status === 'pending').length; const getStatusText = (status) => { switch (status) { case 'pending': return 'รออนุมัติ'; case 'approved': return 'อนุมัติแล้ว'; case 'rejected': return 'ปฏิเสธ'; default: return status; } }; const leave_requests_html = state.mockData.leaveRequests.map(req => { let actionsOrStatus; if (req.status === 'pending') { actionsOrStatus = `
`; } else { actionsOrStatus = `${getStatusText(req.status)}`; } return `
${req.employeeName}
${req.type}
${req.startDate} ถึง ${req.endDate}
${actionsOrStatus}
`; }).join(''); const contentHtml = interpolate(template, { leave_requests: leave_requests_html, pending_count: pendingCount }); if (appContent) appContent.innerHTML = contentHtml; if (desktopContent) desktopContent.innerHTML = contentHtml; } async function renderTasks() { const template = await fetchTemplate('templates/tasks.html'); const task_list_html = state.mockData.tasks.map(task => `
${task.name}
${task.completed ? 'เสร็จแล้ว' : 'รอดำเนินการ'}
${!task.completed ? '' : 'เสร็จแล้ว'}
`).join(''); const contentHtml = interpolate(template, {task_list: task_list_html}); if (appContent) appContent.innerHTML = contentHtml; if (desktopContent) desktopContent.innerHTML = contentHtml; } async function renderModal() { if (!state.modal.isOpen) { modalContainer.innerHTML = ''; updateModalVisibility(); return; } const template = await fetchTemplate('templates/modal.html'); let title, formContent; switch (state.modal.type) { case 'addEmployee': title = 'เพิ่มพนักงานใหม่'; formContent = `
`; break; case 'addCandidate': title = 'เพิ่มผู้สมัครใหม่'; formContent = `
`; break; case 'addTask': title = 'เพิ่มงานใหม่'; formContent = `
`; break; default: return; } modalContainer.innerHTML = interpolate(template, {modal_title: title, form_content: formContent}); updateModalVisibility(); } // --- EVENT LISTENERS --- appContent.addEventListener('click', (e) => { const target = e.target.closest('[data-action]'); if (!target) return; const {action, modalType, taskId, employeeId, leaveId} = target.dataset; switch (action) { case 'openModal': state.modal.isOpen = true; state.modal.type = modalType; break; case 'toggleTask': toggleTask(parseInt(taskId)); break; case 'viewProfile': state.currentView = 'employeeProfile'; state.currentEmployeeId = parseInt(employeeId); break; case 'backToEmployees': state.currentView = 'employees'; state.currentEmployeeId = null; break; case 'approveLeave': approveLeave(parseInt(leaveId)); break; case 'rejectLeave': rejectLeave(parseInt(leaveId)); break; } render(); }); // Event listener for desktop content if (desktopContent) { desktopContent.addEventListener('click', (e) => { const target = e.target.closest('[data-action]'); if (!target) return; const {action, modalType, taskId, employeeId, leaveId} = target.dataset; switch (action) { case 'openModal': state.modal.isOpen = true; state.modal.type = modalType; break; case 'toggleTask': toggleTask(parseInt(taskId)); break; case 'viewProfile': state.currentView = 'employeeProfile'; state.currentEmployeeId = parseInt(employeeId); break; case 'backToEmployees': state.currentView = 'employees'; state.currentEmployeeId = null; break; case 'approveLeave': approveLeave(parseInt(leaveId)); break; case 'rejectLeave': rejectLeave(parseInt(leaveId)); break; } render(); }); } modalContainer.addEventListener('click', (e) => { const target = e.target; if (target.dataset.action === 'closeModal' || target.classList.contains('modal-overlay')) { state.modal.isOpen = false; render(); updateModalVisibility(); } }); modalContainer.addEventListener('submit', (e) => { e.preventDefault(); if (e.target.id === 'modal-form') { switch (state.modal.type) { case 'addEmployee': addEmployee(document.getElementById('employee-name').value, document.getElementById('employee-position').value); break; case 'addCandidate': addCandidate(document.getElementById('candidate-name').value, document.getElementById('candidate-position').value); break; case 'addTask': addTask(document.getElementById('task-name').value); break; } state.modal.isOpen = false; render(); } }); // Add global functions to window for inline onclick handlers window.toggleTask = toggleTask; window.approveLeave = approveLeave; window.rejectLeave = rejectLeave; // Enhanced event delegation for better performance document.addEventListener('click', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (!action) return; const target = e.target.closest('[data-action]'); switch (action) { case 'openModal': state.modal.isOpen = true; state.modal.type = target.dataset.modalType; render(); break; case 'closeModal': state.modal.isOpen = false; state.modal.type = null; render(); break; case 'viewProfile': state.currentEmployeeId = parseInt(target.dataset.employeeId); state.currentView = 'employeeProfile'; render(); break; case 'toggleTask': toggleTask(parseInt(target.dataset.taskId)); render(); break; case 'approveLeave': approveLeave(parseInt(target.dataset.leaveId)); render(); break; case 'rejectLeave': rejectLeave(parseInt(target.dataset.leaveId)); render(); break; } }); // Add form submission handling document.addEventListener('submit', (e) => { e.preventDefault(); const form = e.target; if (form.id === 'modal-form') { handleFormSubmission(); } }); function handleFormSubmission() { const formType = state.modal.type; switch (formType) { case 'addEmployee': const empName = document.getElementById('employee-name').value; const empPosition = document.getElementById('employee-position').value; if (empName && empPosition) { addEmployee(empName, empPosition); state.modal.isOpen = false; state.modal.type = null; render(); } break; case 'addCandidate': const canName = document.getElementById('candidate-name').value; const canPosition = document.getElementById('candidate-position').value; if (canName && canPosition) { addCandidate(canName, canPosition); state.modal.isOpen = false; state.modal.type = null; render(); } break; case 'addTask': const taskName = document.getElementById('task-name').value; if (taskName) { addTask(taskName); state.modal.isOpen = false; state.modal.type = null; render(); } break; } } // Add search functionality document.addEventListener('input', (e) => { if (e.target.classList.contains('search-input')) { handleSearch(e.target.value); } }); function handleSearch(query) { // Simple search implementation - could be enhanced const searchableElements = document.querySelectorAll('.employee-card, .candidate-card, .task-item, .leave-request-item'); searchableElements.forEach(element => { const text = element.textContent.toLowerCase(); const shouldShow = text.includes(query.toLowerCase()); element.style.display = shouldShow ? '' : 'none'; }); } // Add keyboard navigation document.addEventListener('keydown', (e) => { // ESC key to close modal if (e.key === 'Escape' && state.modal.isOpen) { state.modal.isOpen = false; state.modal.type = null; render(); updateModalVisibility(); } // Number keys for quick navigation (1-5) if (e.key >= '1' && e.key <= '5' && !e.target.matches('input, textarea')) { const views = ['dashboard', 'employees', 'recruitment', 'tasks', 'leave']; const viewIndex = parseInt(e.key) - 1; if (views[viewIndex]) { navigateTo(views[viewIndex]); } } }); // Add loading states for better UX function showLoading(element) { element.classList.add('loading-state'); } function hideLoading(element) { element.classList.remove('loading-state'); } // Add notification system function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.innerHTML = `
${message}
`; document.body.appendChild(notification); // Auto remove after 4 seconds setTimeout(() => { notification.style.animation = 'fadeOut 0.3s ease-out forwards'; setTimeout(() => notification.remove(), 300); }, 4000); } // --- INITIAL RENDER --- render(); });