diff --git a/public/index.html b/public/index.html index b9d00a1..70bfa3a 100644 --- a/public/index.html +++ b/public/index.html @@ -54,13 +54,14 @@ const messageBox = document.getElementById('message-box'), loadingSpinner = document.getElementById('loading-spinner'), modalContainer = document.getElementById('modal-container'); let authToken, user, allTimeEntries = [], allUsers = [], employeeTimerInterval = null; let adminTimerIntervals = []; - + // --- Helper Functions --- const showLoading = (show) => { loadingSpinner.innerHTML = show ? `
` : ''; }; const showMessage = (message, type = 'success') => { messageBox.innerHTML = ``; messageBox.classList.remove('hidden'); }; const formatDecimal = (ms) => ms ? (ms / 3600000).toFixed(2) : '0.00'; - const formatDateTime = (s) => s ? new Date(s).toLocaleString() : 'N/A'; - const formatDate = (s) => s ? new Date(s).toLocaleDateString() : 'N/A'; + // UPDATED: Shorter date formats for mobile friendliness + const formatDateTime = (s) => s ? new Date(s).toLocaleString(undefined, { month: '2-digit', day: '2-digit', year: '2-digit', hour: 'numeric', minute: '2-digit' }) : 'N/A'; + const formatDate = (s) => s ? new Date(s).toLocaleDateString(undefined, { month: '2-digit', day: '2-digit', year: '2-digit' }) : 'N/A'; const toLocalISO = (d) => { if (!d) return ''; const date = new Date(d); return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); }; const formatDuration = (ms) => { if (!ms || ms < 0) return '00:00:00'; const totalSeconds = Math.floor(ms / 1000); const h = Math.floor(totalSeconds / 3600); const m = Math.floor((totalSeconds % 3600) / 60); const s = totalSeconds % 60; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; }; @@ -72,30 +73,20 @@ try { showLoading(true); const response = await fetch(`${API_BASE_URL}${endpoint}`, { method, headers, body: body ? JSON.stringify(body) : null }); - if (response.status === 401) { handleSignOut('Your session has expired. Please log in again.'); return { success: false }; } - const text = await response.text(); let data; - try { data = text ? JSON.parse(text) : null; } catch (error) { - if (!response.ok) { - throw new Error(text || `Request failed with status ${response.status}`); - } + if (!response.ok) { throw new Error(text || `Request failed with status ${response.status}`); } return { success: true, data: text }; } - - if (!response.ok) { - throw new Error(data.message || `An unknown error occurred.`); - } - + if (!response.ok) { throw new Error(data.message || `An unknown error occurred.`); } return { success: true, data }; - } catch (error) { showMessage(error.message, 'error'); return { success: false }; @@ -105,23 +96,25 @@ } // --- View Management --- -// --- View Management --- -const showView = (viewName) => { - // Clear all timers when the view changes - clearInterval(employeeTimerInterval); - adminTimerIntervals.forEach(clearInterval); - adminTimerIntervals = []; - - Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName)); -} + const showView = (viewName) => { + clearInterval(employeeTimerInterval); + adminTimerIntervals.forEach(clearInterval); + adminTimerIntervals = []; + Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName)); + } // --- UI Rendering --- function updateUI() { try { + // UPDATED: Adjust main container padding for mobile + const mainContainer = document.querySelector('main.container'); + if (mainContainer) { + mainContainer.classList.remove('px-6'); + mainContainer.classList.add('px-4', 'sm:px-6'); + } const storedUser = localStorage.getItem('user'); authToken = localStorage.getItem('authToken'); user = storedUser ? JSON.parse(storedUser) : null; - if (authToken && user) { navUserControls.classList.remove('hidden'); welcomeMessage.textContent = `Welcome, ${user.username}`; @@ -160,7 +153,6 @@ const showView = (viewName) => { clearInterval(employeeTimerInterval); const [statusRes, timeOffRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests')]); if (!statusRes.success || !timeOffRes.success) return; - const entries = statusRes.data; const requests = timeOffRes.data; const last = entries[0]; @@ -168,7 +160,6 @@ const showView = (viewName) => { let totalMilliseconds = entries.reduce((acc, e) => { return e.status === 'out' && e.punch_out_time ? acc + (new Date(e.punch_out_time) - new Date(e.punch_in_time)) : acc; }, 0); - mainViews.employee.innerHTML = `
@@ -201,7 +192,7 @@ const showView = (viewName) => {
-
${requests.map(r => ``).join('') || ''}
DatesReasonStatus
${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason || ''}${r.status}
No requests.
+
${requests.map(r => ``).join('') || ''}
DatesReasonStatus
${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason || ''}${r.status}
No requests.

My Time Log

@@ -211,7 +202,6 @@ const showView = (viewName) => { document.getElementById('punch-btn').addEventListener('click', handlePunch); document.getElementById('change-password-btn').addEventListener('click', renderChangePasswordModal); document.getElementById('time-off-form').addEventListener('submit', handleTimeOffRequest); - if (punchedIn) { const durationCell = document.getElementById(`duration-${last.id}`); const totalHoursCell = document.getElementById('employee-total-hours'); @@ -225,105 +215,30 @@ const showView = (viewName) => { } async function renderAdminDashboard() { - const [logsRes, usersRes, requestsRes] = await Promise.all([apiCall('/admin/logs'), apiCall('/admin/users'), apiCall('/admin/time-off-requests/pending')]); - if (!logsRes.success || !usersRes.success || !requestsRes.success) return; - - allTimeEntries = logsRes.data; - allUsers = usersRes.data; - const pendingRequests = requestsRes.data; - - const employeeTotals = allTimeEntries.reduce((acc, entry) => { - const dur = entry.punch_out_time ? (new Date(entry.punch_out_time) - new Date(entry.punch_in_time)) : (Date.now() - new Date(entry.punch_in_time)); - acc[entry.username] = (acc[entry.username] || 0) + dur; - return acc; - }, {}); - - const punchedInEntries = allTimeEntries.filter(e => e.status === 'in'); - - mainViews.admin.innerHTML = ` -
-
-
-

Admin Dashboard

-
- - -
-
-
-
-

Currently Punched In

-
    ${punchedInEntries.map(e => ` -
  • - ${e.username} -
    - Since: ${formatDateTime(e.punch_in_time)} - -
    -
  • `).join('') || '
  • None
  • '} -
-
-
-
-

Pending Time Off Requests

- -
-
${pendingRequests.map(r => ``).join('') || ''}
EmployeeDatesReasonActions
${r.username}${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason||''}
No pending requests.
-
-
-

Hours by Employee

-
${Object.entries(employeeTotals).map(([username, totalMs]) => ``).join('') || ''}
EmployeeTotal Hours
${username}${formatDecimal(totalMs)}
No data.
-
-
-

Detailed Logs

-
${allTimeEntries.map(e => ``).join('')}
EmployeeInOutDurationActions
${e.username||'N/A'}${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${e.punch_out_time ? formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}
-
-
-

User & Payroll Management

-
-

Create User

-

Add Manual Punch

-
-

Manage Users

${allUsers.map(u => ``).join('')}
UsernameRoleActions
${u.username}${u.role}${u.isPrimary ? `Primary Admin` : `${u.username !== user.username ? `` : ''}`}
-
-
`; - - // ** NEW: Start timers for all currently punched-in users ** - punchedInEntries.forEach(entry => { - const durationCell = document.getElementById(`admin-duration-${entry.id}`); - if (durationCell) { - const punchInTime = new Date(entry.punch_in_time); - const intervalId = setInterval(() => { - durationCell.textContent = formatDuration(Date.now() - punchInTime.getTime()); - }, 1000); - adminTimerIntervals.push(intervalId); // Store the timer to clear it later + const [logsRes, usersRes, requestsRes] = await Promise.all([apiCall('/admin/logs'), apiCall('/admin/users'), apiCall('/admin/time-off-requests/pending')]); + if (!logsRes.success || !usersRes.success || !requestsRes.success) return; + allTimeEntries = logsRes.data; allUsers = usersRes.data; const pendingRequests = requestsRes.data; + const employeeTotals = allTimeEntries.reduce((acc, entry) => { const dur = entry.punch_out_time ? (new Date(entry.punch_out_time) - new Date(entry.punch_in_time)) : (Date.now() - new Date(entry.punch_in_time)); acc[entry.username] = (acc[entry.username] || 0) + dur; return acc; }, {}); + const punchedInEntries = allTimeEntries.filter(e => e.status === 'in'); + mainViews.admin.innerHTML = ` +
+

Admin Dashboard

+

Currently Punched In

    ${punchedInEntries.map(e => `
  • ${e.username}
    Since: ${formatDateTime(e.punch_in_time)}
  • `).join('') || '
  • None
  • '}
+

Pending Time Off Requests

${pendingRequests.map(r => ``).join('') || ''}
EmployeeDatesReasonActions
${r.username}${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason||''}
No pending requests.
+

Hours by Employee

${Object.entries(employeeTotals).map(([username, totalMs]) => ``).join('') || ''}
EmployeeTotal Hours
${username}${formatDecimal(totalMs)}
No data.
+

Detailed Logs

${allTimeEntries.map(e => ``).join('')}
EmployeeInOutDurationActions
${e.username||'N/A'}${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${e.punch_out_time ? formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}
${/* UPDATED */''}
+

User & Payroll Management

Create User

Add Manual Punch

Manage Users

${allUsers.map(u => ``).join('')}
UsernameRoleActions
${u.username}${u.role}
${/* UPDATED */''}${u.isPrimary ? `Primary Admin` : `${u.username !== user.username ? `` : ''}`}
+
`; + punchedInEntries.forEach(entry => { const durationCell = document.getElementById(`admin-duration-${entry.id}`); if (durationCell) { const punchInTime = new Date(entry.punch_in_time); const intervalId = setInterval(() => { durationCell.textContent = formatDuration(Date.now() - punchInTime.getTime()); }, 1000); adminTimerIntervals.push(intervalId); } }); + document.getElementById('archive-btn').addEventListener('click', handleArchive); document.getElementById('view-archives-btn').addEventListener('click', renderArchiveView); document.getElementById('view-time-off-history-btn').addEventListener('click', renderTimeOffHistoryView); document.getElementById('create-user-form').addEventListener('submit', handleCreateUser); document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch); document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick); } - }); - - document.getElementById('archive-btn').addEventListener('click', handleArchive); - document.getElementById('view-archives-btn').addEventListener('click', renderArchiveView); - document.getElementById('view-time-off-history-btn').addEventListener('click', renderTimeOffHistoryView); - document.getElementById('create-user-form').addEventListener('submit', handleCreateUser); - document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch); - document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick); -} function renderArchiveView() { apiCall('/admin/archives').then(res => { if (!res.success) return; showView('archive'); mainViews.archive.innerHTML = ` -
-
-

Archived Logs

- -
-
- - ${res.data.map(e => ``).join('') || ''} -
EmployeeInOutDuration (Hrs)Archived On
${e.username||'N/A'}${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}${formatDateTime(e.archived_at)}
No archived entries found.
-
-
`; +

Archived Logs

${res.data.map(e => ``).join('') || ''}
EmployeeInOutDuration (Hrs)Archived On
${e.username||'N/A'}${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}${formatDateTime(e.archived_at)}
No archived entries found.
`; document.getElementById('back-to-dash-btn').addEventListener('click', () => { showView('admin'); renderAdminDashboard(); }); }); } @@ -333,34 +248,13 @@ const showView = (viewName) => { if (!res.success) return; showView('timeOffHistory'); mainViews.timeOffHistory.innerHTML = ` -
-
-

Time Off History

- -
-
- - ${res.data.map(r => ``).join('') || ''} -
EmployeeDatesReasonStatus
${r.username}${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason||''}${r.status}
No history.
-
-
`; +

Time Off History

${res.data.map(r => ``).join('') || ''}
EmployeeDatesReasonStatus
${r.username}${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason||''}${r.status}
No history.
`; document.getElementById('back-to-dash-btn').addEventListener('click', () => { showView('admin'); renderAdminDashboard(); }); }); } function renderModal(title, formHTML, submitHandler) { - modalContainer.innerHTML = ` - `; + modalContainer.innerHTML = ``; document.getElementById('modal-form').addEventListener('submit', submitHandler); document.querySelector('.cancel-modal-btn').addEventListener('click', () => modalContainer.innerHTML = ''); document.querySelector('.modal-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) modalContainer.innerHTML = ''; }); @@ -369,56 +263,27 @@ const showView = (viewName) => { function renderEditModal(id) { const entry = allTimeEntries.find(e => e.id == id); if (!entry) { showMessage('Could not find entry to edit.', 'error'); return; } - const formHTML = ` - -
-
`; + const formHTML = `
`; renderModal(`Edit Entry for ${entry.username}`, formHTML, handleEditSubmit); } function renderChangePasswordModal() { - const formHTML = ` - - `; + const formHTML = ``; renderModal('Change My Password', formHTML, handleChangePassword); } function renderResetPasswordModal(username) { - const formHTML = ` - - `; + const formHTML = ``; renderModal(`Reset Password for ${username}`, formHTML, handleResetPassword); } - // --- 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.success) { - authToken = res.data.token; - user = res.data.user; - localStorage.setItem('authToken', authToken); - localStorage.setItem('user', JSON.stringify(user)); - updateUI(); - } - } - - const handleSignOut = (message) => { - localStorage.clear(); - authToken = null; - user = null; - updateUI(); - if (message) showMessage(message, 'success'); - }; - + // --- Event Handlers (no changes needed below this line, but included for completeness) --- + 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.success) { authToken = res.data.token; user = res.data.user; localStorage.setItem('authToken', authToken); localStorage.setItem('user', JSON.stringify(user)); updateUI(); } } + const handleSignOut = (message) => { localStorage.clear(); authToken = null; user = null; updateUI(); if (message) showMessage(message, 'success'); }; const handlePunch = () => apiCall('/punch', 'POST').then(res => res.success && renderEmployeeDashboard()); - function handleAdminDashboardClick(e) { const target = e.target; const { id, userid, username, role } = target.dataset; - if (target.classList.contains('edit-btn')) renderEditModal(id); if (target.classList.contains('delete-btn') && confirm('Are you sure you want to delete this time entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard()); if (target.classList.contains('force-clock-out-btn') && confirm(`Are you sure you want to force clock out ${username}?`)) apiCall('/admin/force-clock-out', 'POST', { userId: userid }).then(res => res.success && renderAdminDashboard()); @@ -428,111 +293,18 @@ const showView = (viewName) => { if (target.classList.contains('approve-request-btn')) { if (confirm('Set this request to "approved"?')) apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: 'approved' }).then(res => res.success && renderAdminDashboard()); } if (target.classList.contains('deny-request-btn')) { if (confirm('Set this request to "denied"?')) apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: 'denied' }).then(res => res.success && renderAdminDashboard()); } } - - 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) { - modalContainer.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 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'); - modalContainer.innerHTML = ''; - } - } - - 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'); - modalContainer.innerHTML = ''; - } - } - - 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 punchOutTime = new Date(e.target.elements['add-punch-out'].value).toISOString(); - 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 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 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) { modalContainer.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 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'); modalContainer.innerHTML = ''; } } + 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'); modalContainer.innerHTML = ''; } } + 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 punchOutTime = new Date(e.target.elements['add-punch-out'].value).toISOString(); 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 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(); } } // --- Initializer --- signOutBtn.addEventListener('click', () => handleSignOut('You have been signed out.')); - - // Adds a listener to refresh data when the tab becomes visible again - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible' && user) { - console.log("Tab is visible, refreshing UI."); - updateUI(); - } - }); - - // Initial load - updateUI(); + document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && user) { console.log("Tab is visible, refreshing UI."); updateUI(); } }); + updateUI(); // Initial load