// js/main.js // --- IMPORTS --- import { apiCall } from './api.js'; import { showMessage } from './utils.js'; import { renderAuthView, renderAdminDashboard, renderEmployeeDashboard, renderEditModal, renderChangePasswordModal, renderResetPasswordModal, renderRequestHistoryModal, handleViewNotesClick, renderArchiveView, renderTimeOffHistoryView, updatePendingRequestsList, renderEditTimeOffModal } from './ui.js'; // --- STATE MANAGEMENT --- let user = null; let authToken = null; let lastAdminTab = 'overview'; export { lastAdminTab }; // --- EVENT HANDLERS (The "Logic") --- async function handleAuthSubmit(e) { e.preventDefault(); const username = e.target.elements.username.value; const password = e.target.elements.password.value; const res = await apiCall('/login', 'POST', { username, password }); if (res && res.success) { localStorage.setItem('authToken', res.data.token); localStorage.setItem('user', JSON.stringify(res.data.user)); initializeApp(); } else { showMessage(res?.data?.message || 'Login failed. Please check your credentials.', 'error'); } } function handleSignOut(message) { localStorage.clear(); authToken = null; user = null; if (message) { showMessage(message, 'success'); } initializeApp(); } const handlePunch = () => { apiCall('/punch', 'POST').then(res => res.success && renderEmployeeDashboard()); }; async function handleChangePassword(e) { e.preventDefault(); const currentPassword = e.target.elements['modal-current-pw'].value; const newPassword = e.target.elements['modal-new-pw'].value; const res = await apiCall('/user/change-password', 'POST', { currentPassword, newPassword }); if (res.success) { showMessage(res.data.message, 'success'); document.getElementById('modal-container').innerHTML = ''; } } async function handleTimeOffRequest(e) { e.preventDefault(); const startDate = e.target.elements['start-date'].value; const endDate = e.target.elements['end-date'].value; const reason = e.target.elements['reason'].value; if (new Date(endDate) < new Date(startDate)) { return showMessage('End date cannot be before start date.', 'error'); } const res = await apiCall('/user/request-time-off', 'POST', { startDate, endDate, reason }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderEmployeeDashboard(); } } async function handleViewRequestHistoryClick() { const res = await apiCall('/user/time-off-requests/history'); if (res.success) { renderRequestHistoryModal(res.data); } } async function handleEditSubmit(e) { e.preventDefault(); const id = e.target.elements['edit-id'].value; const punch_in_time = new Date(e.target.elements['edit-in'].value).toISOString(); const punch_out_value = e.target.elements['edit-out'].value; const punch_out_time = punch_out_value ? new Date(punch_out_value).toISOString() : null; const res = await apiCall(`/admin/logs/${id}`, 'PUT', { punch_in_time, punch_out_time }); if (res.success) { document.getElementById('modal-container').innerHTML = ''; renderAdminDashboard(); showMessage('Entry updated successfully.', 'success'); } } const handleArchive = () => { if (confirm('Are you sure you want to archive all completed time entries? This action cannot be undone.')) { apiCall('/admin/archive', 'POST').then(res => { if(res.success) { showMessage('Records archived successfully.', 'success'); renderAdminDashboard(); } }); } }; async function handleCreateUser(e) { e.preventDefault(); const username = e.target.elements['new-username'].value; const password = e.target.elements['new-password'].value; const role = e.target.elements['new-user-role'].value; const res = await apiCall('/admin/create-user', 'POST', { username, password, role }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderAdminDashboard(); } } async function handleAddPunch(e) { e.preventDefault(); const selected = e.target.elements['add-punch-user']; const userId = selected.value; const username = selected.options[selected.selectedIndex].dataset.username; const punchInTime = new Date(e.target.elements['add-punch-in'].value).toISOString(); const punchOutValue = e.target.elements['add-punch-out'].value; const punchOutTime = punchOutValue ? new Date(punchOutValue).toISOString() : null; const res = await apiCall('/admin/add-punch', 'POST', { userId, username, punchInTime, punchOutTime }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderAdminDashboard(); } } async function handleAddNote(e) { e.preventDefault(); const userId = e.target.elements['note-user-select'].value; const noteText = e.target.elements['note-text'].value; if (!userId) { return showMessage('Please select an employee.', 'error'); } const res = await apiCall('/admin/notes', 'POST', { userId, noteText }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); } } async function handleResetPassword(e) { e.preventDefault(); const username = e.target.elements['reset-username'].value; const newPassword = e.target.elements['reset-new-pw'].value; const res = await apiCall('/admin/reset-password', 'POST', { username, newPassword }); if (res.success) { showMessage(res.data.message, 'success'); document.getElementById('modal-container').innerHTML = ''; } } function handleAdminDashboardClick(e) { const target = e.target.closest('button'); if (!target) return; const { id, userid, username, role, noteId } = target.dataset; switch (target.id) { case 'view-archives-btn': renderArchiveView(); return; case 'archive-btn': handleArchive(); return; case 'view-time-off-history-btn': renderTimeOffHistoryView(); return; case 'view-notes-btn': handleViewNotesClick(); return; } if (target.classList.contains('approve-request-btn') || target.classList.contains('deny-request-btn')) { const status = target.classList.contains('approve-request-btn') ? 'approved' : 'denied'; if (confirm(`Set this request to "${status}"?`)) { apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: status }) .then(res => { if (res.success) { showMessage(`Request ${status}.`, 'success'); apiCall('/admin/time-off-requests/pending').then(requestsRes => { if (requestsRes.success) { updatePendingRequestsList(requestsRes.data); } }); } }); } return; } if (target.classList.contains('set-pending-btn')) { if (confirm('Are you sure you want to move this request back to pending?')) { apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: 'pending' }) .then(res => { if (res.success) { showMessage('Request status set to pending.', 'success'); renderTimeOffHistoryView(); } }); } return; } if (target.classList.contains('admin-delete-request-btn')) { if (confirm('Are you sure you want to permanently delete this request? This cannot be undone.')) { apiCall(`/admin/time-off-requests/${id}`, 'DELETE') .then(res => { if (res.success) { showMessage('Request deleted.', 'success'); if (document.getElementById('tab-content-overview')) { apiCall('/admin/time-off-requests/pending').then(requestsRes => { if (requestsRes.success) { updatePendingRequestsList(requestsRes.data); } }); } else { renderTimeOffHistoryView(); } } }); } return; } if (target.classList.contains('edit-btn')) renderEditModal(id, handleEditSubmit); if (target.classList.contains('delete-btn') && confirm('Delete this time entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard()); if (target.classList.contains('force-clock-out-btn') && confirm(`Force clock out ${username}?`)) apiCall('/admin/force-clock-out', 'POST', { userId: userid }).then(res => res.success && renderAdminDashboard()); if (target.classList.contains('reset-pw-btn')) renderResetPasswordModal(username, handleResetPassword); if (target.classList.contains('change-role-btn')) { const newRole = role === 'admin' ? 'employee' : 'admin'; if(confirm(`Change ${username} to ${newRole}?`)) apiCall('/admin/update-role', 'POST', { username, newRole }).then(res => res.success && renderAdminDashboard()); } if (target.classList.contains('delete-user-btn') && confirm(`PERMANENTLY DELETE user '${username}'?`)) apiCall(`/admin/delete-user/${username}`, 'DELETE').then(res => res.success && renderAdminDashboard()); if (target.classList.contains('delete-note-btn')) { if (confirm('Delete this note?')) { apiCall(`/admin/notes/${noteId}`, 'DELETE').then(res => { if (res.success) { showMessage('Note deleted.', 'success'); handleViewNotesClick(); }});}} } async function handleEditTimeOffSubmit(e) { e.preventDefault(); const id = e.target.elements['edit-request-id'].value; const startDate = e.target.elements['edit-start-date'].value; const endDate = e.target.elements['edit-end-date'].value; const reason = e.target.elements['edit-reason'].value; if (new Date(endDate) < new Date(startDate)) { return showMessage('End date cannot be before start date.', 'error'); } const res = await apiCall(`/user/time-off-requests/${id}`, 'PUT', { startDate, endDate, reason }); if (res.success) { showMessage(res.data.message, 'success'); document.getElementById('modal-container').innerHTML = ''; renderEmployeeDashboard(); } } // --- LISTENER ATTACHMENT FUNCTIONS --- export function attachAuthFormListener() { const form = document.getElementById('auth-form'); if (form) form.addEventListener('submit', handleAuthSubmit); } export function attachEmployeeDashboardListeners() { const dashboard = document.getElementById('employee-dashboard'); if (!dashboard) return; dashboard.addEventListener('click', async (e) => { const target = e.target; if (target.id === 'punch-btn') { handlePunch(); } if (target.id === 'change-password-btn') { renderChangePasswordModal(handleChangePassword); } if (target.id === 'view-request-history-btn') { handleViewRequestHistoryClick(); } if (target.classList.contains('delete-request-btn')) { const id = target.dataset.id; if (confirm('Are you sure you want to delete this time off request?')) { const res = await apiCall(`/user/time-off-requests/${id}`, 'DELETE'); if (res.success) { showMessage(res.data.message, 'success'); renderEmployeeDashboard(); } } } if (target.classList.contains('edit-request-btn')) { const id = target.dataset.id; const res = await apiCall('/user/time-off-requests/history'); if (res.success) { const requestToEdit = res.data.find(r => r.id == id); if (requestToEdit) { renderEditTimeOffModal(requestToEdit, handleEditTimeOffSubmit); } } } }); const timeOffForm = document.getElementById('time-off-form'); if (timeOffForm) { timeOffForm.addEventListener('submit', handleTimeOffRequest); } } export function attachAdminDashboardListeners() { document.getElementById('admin-dashboard')?.addEventListener('click', handleAdminDashboardClick); document.getElementById('create-user-form')?.addEventListener('submit', handleCreateUser); document.getElementById('add-punch-form')?.addEventListener('submit', handleAddPunch); document.getElementById('add-note-form')?.addEventListener('submit', handleAddNote); setupTabbedInterface(); } // --- APP INITIALIZER --- function initializeApp() { authToken = localStorage.getItem('authToken'); const userString = localStorage.getItem('user'); user = userString ? JSON.parse(userString) : null; if (authToken && user) { const userControls = document.getElementById('nav-user-controls'); userControls.classList.remove('hidden'); userControls.querySelector('#welcome-message').textContent = `Welcome, ${user.username}`; if (user.role === 'admin') { renderAdminDashboard(); } else { renderEmployeeDashboard(); } } else { document.getElementById('nav-user-controls').classList.add('hidden'); renderAuthView(); } } // --- HELPERS --- function setupTabbedInterface() { const tabsContainer = document.getElementById('admin-tabs-nav'); const contentContainer = document.getElementById('admin-tabs-content'); if (!tabsContainer || !contentContainer) return; tabsContainer.addEventListener('click', (e) => { const clickedTab = e.target.closest('.tab-btn'); if (!clickedTab) return; const tabTarget = clickedTab.dataset.tab; lastAdminTab = tabTarget; tabsContainer.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active-tab'); }); clickedTab.classList.add('active-tab'); contentContainer.querySelectorAll('[id^="tab-content-"]').forEach(panel => { panel.classList.add('hidden'); }); document.getElementById(`tab-content-${tabTarget}`).classList.remove('hidden'); }); } // --- START THE APP --- if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => console.log('Service Worker registered! Scope:', registration.scope)) .catch(err => console.error('Service Worker registration failed:', err)); }); } document.getElementById('sign-out-btn').addEventListener('click', () => handleSignOut('You have been signed out.')); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && localStorage.getItem('user')) { initializeApp(); } }); initializeApp();