467 lines
18 KiB
JavaScript
467 lines
18 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,
|
|
renderEditTimeOffModal
|
|
} from './ui.js';
|
|
|
|
// --- STATE MANAGEMENT ---
|
|
let user = null;
|
|
let authToken = null;
|
|
let lastAdminTab = 'overview';
|
|
export { lastAdminTab };
|
|
|
|
// --- NOTIFICATION LOGIC ---
|
|
|
|
// This helper function converts the VAPID public key for the browser
|
|
function urlBase64ToUint8Array(base64String) {
|
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
|
|
const rawData = window.atob(base64);
|
|
const outputArray = new Uint8Array(rawData.length);
|
|
for (let i = 0; i < rawData.length; ++i) {
|
|
outputArray[i] = rawData.charCodeAt(i);
|
|
}
|
|
return outputArray;
|
|
}
|
|
|
|
// This function handles the actual subscription process
|
|
async function subscribeToNotifications() {
|
|
try {
|
|
const publicVapidKey = process.env.PUBLIC_VAPID_KEY; // Make sure this is set in your .env
|
|
const token = localStorage.getItem('authToken');
|
|
|
|
if (!token || !publicVapidKey) {
|
|
return console.error('Auth token or VAPID key is missing.');
|
|
}
|
|
|
|
const register = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
|
|
const subscription = await register.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
|
|
});
|
|
|
|
await apiCall('/subscribe', 'POST', subscription);
|
|
showMessage('You are now subscribed to notifications!', 'success');
|
|
} catch (error) {
|
|
console.error('Error subscribing to notifications:', error);
|
|
showMessage('Failed to subscribe. Please ensure notifications are allowed for this site.', 'error');
|
|
}
|
|
}
|
|
|
|
// NEW: This function creates the pop-up prompt for the user
|
|
function promptForNotifications() {
|
|
// 1. Don't ask if permission is already granted or denied
|
|
if (Notification.permission !== 'default') {
|
|
return;
|
|
}
|
|
// 2. Don't ask if we've already prompted them before
|
|
if (localStorage.getItem('notificationPrompted')) {
|
|
return;
|
|
}
|
|
// 3. Wait a couple of seconds after login to ask
|
|
setTimeout(() => {
|
|
if (confirm("Enable notifications to receive important updates about your time-off requests and notes?")) {
|
|
subscribeToNotifications();
|
|
}
|
|
// Remember that we've prompted them, so we don't ask again
|
|
localStorage.setItem('notificationPrompted', 'true');
|
|
}, 2000);
|
|
}
|
|
|
|
// --- EVENT HANDLERS ---
|
|
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('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');
|
|
apiCall('/admin/time-off-requests/pending').then(requestsRes => {
|
|
if (requestsRes.success) {
|
|
updatePendingRequestsList(requestsRes.data);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
function handleTimeOffHistoryClick(e) {
|
|
const target = e.target.closest('button');
|
|
if (!target) return;
|
|
|
|
const { id } = target.dataset;
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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');
|
|
renderTimeOffHistoryView();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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();
|
|
}
|
|
|
|
export function attachTimeOffHistoryListeners() {
|
|
const historyView = document.getElementById('admin-time-off-history-view');
|
|
if (historyView) {
|
|
historyView.addEventListener('click', handleTimeOffHistoryClick);
|
|
}
|
|
}
|
|
|
|
// --- APP INITIALIZER ---
|
|
function initializeApp() {
|
|
authToken = localStorage.getItem('authToken');
|
|
const userString = localStorage.getItem('user');
|
|
user = userString ? JSON.parse(userString) : null;
|
|
|
|
const userControls = document.getElementById('nav-user-controls');
|
|
if (authToken && user) {
|
|
userControls.classList.remove('hidden');
|
|
userControls.querySelector('#welcome-message').textContent = `Welcome, ${user.username}`;
|
|
if (user.role === 'admin') {
|
|
renderAdminDashboard();
|
|
} else {
|
|
renderEmployeeDashboard();
|
|
}
|
|
// Ask for notification permission after user is logged in
|
|
promptForNotifications();
|
|
} else {
|
|
userControls.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 ---
|
|
// Register the service worker when the page loads
|
|
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(); |