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();
});