From 901efb6bf64fd94fbb599d6e2a555f396c0a8bb8 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 2 Aug 2025 09:22:29 -0400 Subject: [PATCH] updated add punch admin --- public/index.html | 64 +++++++++++++++++++++++++++++++++-------------- server.js | 40 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/public/index.html b/public/index.html index 70bfa3a..170a911 100644 --- a/public/index.html +++ b/public/index.html @@ -215,23 +215,33 @@ } 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' : '...'}
${/* 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); - } + 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 Entry

+ + + + + + +
+

Manage Users

${allUsers.map(u => ``).join('')}
UsernameRoleActions
${u.username}${u.role}
${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); +} function renderArchiveView() { apiCall('/admin/archives').then(res => { @@ -298,8 +308,24 @@ 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 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(); + + // Handle the optional punch-out time + 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 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.')); diff --git a/server.js b/server.js index 6cc89a1..0b7724b 100644 --- a/server.js +++ b/server.js @@ -225,6 +225,46 @@ app.get('/api/admin/users', authenticateToken, requireRole('admin'), async (req, } }); +app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), async (req, res) => { + try { + const { userId, username, punchInTime, punchOutTime } = req.body; + + if (!userId || !punchInTime) { + return res.status(400).json({ message: "User and Punch In time are required." }); + } + + // SCENARIO 1: A "punch-in only" was submitted + if (!punchOutTime) { + // First, ensure this user doesn't already have an active punch + const existingPunch = await db.get("SELECT id FROM time_entries WHERE user_id = ? AND status = 'in'", [userId]); + if (existingPunch) { + return res.status(409).json({ message: `${username} is already punched in. Please edit their existing entry.` }); + } + + // If clear, insert the new active punch + await db.run( + 'INSERT INTO time_entries (user_id, username, punch_in_time, status) VALUES (?, ?, ?, ?)', + [userId, username, punchInTime, 'in'] + ); + return res.status(201).json({ message: `Active punch successfully started for ${username}.` }); + } + + // SCENARIO 2: A complete entry (in and out) was submitted + if (new Date(punchOutTime) < new Date(punchInTime)) { + return res.status(400).json({ message: "Punch out time cannot be before punch in time." }); + } + await db.run( + 'INSERT INTO time_entries (user_id, username, punch_in_time, punch_out_time, status) VALUES (?, ?, ?, ?, ?)', + [userId, username, punchInTime, punchOutTime, 'out'] + ); + res.status(201).json({ message: `Completed entry added successfully for ${username}.` }); + + } catch (err) { + console.error("Error adding manual punch:", err); + res.status(500).json({ message: 'Failed to add manual punch.' }); + } +}); + // Gets all time entries for the detailed log view app.get('/api/admin/logs', authenticateToken, requireRole('admin'), async (req, res) => { try {