From dc2369ce5e0027605ad39cb3bf1cfe7b4e527511 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 2 Aug 2025 09:34:40 -0400 Subject: [PATCH] add notes --- public/index.html | 187 ++++++++++++++++++++++++++++------------------ server.js | 46 +++++++++++- 2 files changed, 159 insertions(+), 74 deletions(-) diff --git a/public/index.html b/public/index.html index 170a911..6cfe1c8 100644 --- a/public/index.html +++ b/public/index.html @@ -150,69 +150,86 @@ } async function renderEmployeeDashboard() { - 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]; - const punchedIn = last?.status === 'in'; - 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 = ` -
-
-
-
-

Current Status

-

${punchedIn ? 'Punched In' : 'Punched Out'}

-

${punchedIn ? 'Since:' : 'Last Punch:'} ${formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}

-
-
- -
-
+ clearInterval(employeeTimerInterval); + // Fetch notes along with other data + const [statusRes, timeOffRes, notesRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests'), apiCall('/user/notes')]); + if (!statusRes.success || !timeOffRes.success || !notesRes.success) return; + + const entries = statusRes.data; + const requests = timeOffRes.data; + const notes = notesRes.data; // Get notes data + const last = entries[0]; + const punchedIn = last?.status === 'in'; + 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 = ` +
+
+
+
+

Current Status

+

${punchedIn ? 'Punched In' : 'Punched Out'}

+

${punchedIn ? 'Since:' : 'Last Punch:'} ${formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}

-
-
-

My Account

- -
-
-

My Total Hours (This Pay Period)

-

${formatDecimal(totalMilliseconds)}

-
+
+
-
-

Time Off Requests

-
-
-
- -
-
-
${requests.map(r => ``).join('') || ''}
DatesReasonStatus
${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason || ''}${r.status}
No requests.
-
-
-

My Time Log

-
${entries.map(e => ``).join('') || ''}
InOutDuration (Hours)
${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${e.status === 'in' ? 'Running...' : formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}
No entries.
-
-
`; - 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'); - const punchInTime = new Date(last.punch_in_time); - employeeTimerInterval = setInterval(() => { - const elapsed = Date.now() - punchInTime.getTime(); - if (durationCell) durationCell.textContent = formatDuration(elapsed); - if (totalHoursCell) totalHoursCell.textContent = formatDecimal(totalMilliseconds + elapsed); - }, 1000); - } - } +
+
+ +
+

Notes from Admin

+
    + ${notes.length > 0 ? notes.map(note => ` +
  • +

    "${note.note_text}"

    +

    - ${note.admin_username} on ${formatDate(note.created_at)}

    +
  • + `).join('') : '

    You have no new notes.

    '} +
+
+ +
+
+

My Account

+ +
+
+

My Total Hours (This Pay Period)

+

${formatDecimal(totalMilliseconds)}

+
+
+
+

Time Off Requests

+
+
+
+ +
+
+
${requests.map(r => ``).join('') || ''}
DatesReasonStatus
${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason || ''}${r.status}
No requests.
+
+
+

My Time Log

+
${entries.map(e => ``).join('') || ''}
InOutDuration (Hours)
${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${e.status === 'in' ? 'Running...' : formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}
No entries.
+
+
`; + 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'); + const punchInTime = new Date(last.punch_in_time); + employeeTimerInterval = setInterval(() => { + const elapsed = Date.now() - punchInTime.getTime(); + if (durationCell) durationCell.textContent = formatDuration(elapsed); + if (totalHoursCell) totalHoursCell.textContent = formatDecimal(totalMilliseconds + elapsed); + }, 1000); + } +} async function renderAdminDashboard() { const [logsRes, usersRes, requestsRes] = await Promise.all([apiCall('/admin/logs'), apiCall('/admin/users'), apiCall('/admin/time-off-requests/pending')]); @@ -220,27 +237,36 @@ 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'); + + // Filter out admins from the "leave a note" dropdown + const employeesOnly = allUsers.filter(u => u.role === 'employee'); + mainViews.admin.innerHTML = `

Admin Dashboard

+ +
+

Leave a Note for Employee

+
+ + + +
+
+

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 ? `` : ''}`}
+

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); + 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('add-note-form').addEventListener('submit', handleAddNote); // Add listener for new form + document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick); } function renderArchiveView() { @@ -315,6 +341,21 @@ const username = selected.options[selected.selectedIndex].dataset.username; const punchInTime = new Date(e.target.elements['add-punch-in'].value).toISOString(); + 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('/api/admin/notes', 'POST', { userId, noteText }); + if (res.success) { + showMessage(res.data.message, 'success'); + e.target.reset(); + } +} // Handle the optional punch-out time const punchOutValue = e.target.elements['add-punch-out'].value; const punchOutTime = punchOutValue ? new Date(punchOutValue).toISOString() : null; diff --git a/server.js b/server.js index 0b7724b..059b66e 100644 --- a/server.js +++ b/server.js @@ -71,6 +71,14 @@ async function initializeDatabase() { status TEXT NOT NULL DEFAULT 'pending', FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE )`); + await db.exec(`CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + admin_username TEXT NOT NULL, + employee_user_id INTEGER NOT NULL, + note_text TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_user_id) REFERENCES users (id) ON DELETE CASCADE +)`); const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]); if (!adminUser) { const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10); @@ -342,7 +350,43 @@ app.delete('/api/admin/logs/:id', authenticateToken, requireRole('admin'), async res.status(500).json({ message: 'Failed to delete time entry.' }); } }); - // Other admin routes (logs, users, roles, etc.) stay the same... + +// Admin creates a note for an employee +app.post('/api/admin/notes', authenticateToken, requireRole('admin'), async (req, res) => { + try { + const { userId, noteText } = req.body; + const adminUsername = req.user.username; // Get admin's username from their token + + if (!userId || !noteText) { + return res.status(400).json({ message: "Employee and note text are required." }); + } + + await db.run( + 'INSERT INTO notes (admin_username, employee_user_id, note_text) VALUES (?, ?, ?)', + [adminUsername, userId, noteText] + ); + + res.status(201).json({ message: "Note successfully posted." }); + } catch (err) { + console.error("Error posting note:", err); + res.status(500).json({ message: 'Failed to post note.' }); + } +}); + +// Employee fetches their notes +app.get('/api/user/notes', authenticateToken, async (req, res) => { + try { + const notes = await db.all( + "SELECT admin_username, note_text, created_at FROM notes WHERE employee_user_id = ? ORDER BY created_at DESC", + [req.user.id] + ); + res.json(notes); + } catch (err) { + console.error("Error fetching notes:", err); + res.status(500).json({ message: 'Failed to fetch notes.' }); + } +}); + } startServer();