timepulse/public/js/main.js
2025-08-10 09:31:24 -04:00

326 lines
13 KiB
JavaScript

// 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
} from './ui.js';
// --- STATE MANAGEMENT ---
let user = null;
let authToken = null;
// --- 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) {
// --- THIS IS THE FIX ---
// You were missing these two lines to save the data
localStorage.setItem('authToken', res.data.token);
localStorage.setItem('user', JSON.stringify(res.data.user));
// --- END OF FIX ---
initializeApp(); // Now this will find the data it needs
} 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 = '';
}
}
// This single handler uses event delegation for all buttons on the admin dashboard
function handleAdminDashboardClick(e) {
const target = e.target.closest('button'); // Find the closest button that was clicked
if (!target) return; // Ignore clicks that aren't on or inside a button
const { id, userid, username, role, noteId } = target.dataset;
// Handle buttons by their ID
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;
}
// --- THIS IS THE UPDATED LOGIC ---
// It now calls the targeted update function instead of renderAdminDashboard()
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');
// After success, just fetch the new list of requests and update the table
apiCall('/admin/time-off-requests/pending').then(requestsRes => {
if (requestsRes.success) {
updatePendingRequestsList(requestsRes.data);
}
});
}
});
}
return; // Stop the function here
}
// --- END OF UPDATED LOGIC ---
// Handle other buttons by their class name
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(); }});}}
}
// --- LISTENER ATTACHMENT FUNCTIONS ---
export function attachAuthFormListener() {
const form = document.getElementById('auth-form');
if (form) form.addEventListener('submit', handleAuthSubmit);
}
export function attachEmployeeDashboardListeners() {
document.getElementById('punch-btn')?.addEventListener('click', handlePunch);
document.getElementById('change-password-btn')?.addEventListener('click', () => renderChangePasswordModal(handleChangePassword));
document.getElementById('time-off-form')?.addEventListener('submit', handleTimeOffRequest);
document.getElementById('view-request-history-btn')?.addEventListener('click', handleViewRequestHistoryClick);
}
export function attachAdminDashboardListeners() {
// This master listener handles all buttons
document.getElementById('admin-dashboard')?.addEventListener('click', handleAdminDashboardClick);
// Listeners for forms that need to prevent default submission
document.getElementById('create-user-form')?.addEventListener('submit', handleCreateUser);
document.getElementById('add-punch-form')?.addEventListener('submit', handleAddPunch);
document.getElementById('add-note-form')?.addEventListener('submit', handleAddNote);
// Make the tabs interactive
setupTabbedInterface();
}
// --- APP INITIALIZER ---
function initializeApp() {
console.log("A. Inside initializeApp()."); // Checkpoint A
authToken = localStorage.getItem('authToken');
const userString = localStorage.getItem('user');
user = userString ? JSON.parse(userString) : null;
console.log("B. Auth Token found:", !!authToken); // Checkpoint B
console.log("C. User data found:", user); // Checkpoint C
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') {
console.log("D. User is an admin, calling renderAdminDashboard()."); // Checkpoint D
renderAdminDashboard();
} else {
console.log("E. User is an employee, calling renderEmployeeDashboard()."); // Checkpoint E
renderEmployeeDashboard();
}
} else {
console.log("F. No user or token, calling renderAuthView()."); // Checkpoint F
document.getElementById('nav-user-controls').classList.add('hidden');
renderAuthView();
}
console.log("G. initializeApp() finished."); // Checkpoint G
}
// --- 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;
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 ---
// Register the service worker for PWA functionality
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));
});
}
// Attach global listeners that are always present on the page
document.getElementById('sign-out-btn').addEventListener('click', () => handleSignOut('You have been signed out.'));
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && localStorage.getItem('user')) {
initializeApp();
}
});
// Initial call to start the app on page load
initializeApp();