-
-
-
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
-
-
| Dates | Reason | Status |
${requests.map(r => `| ${formatDate(r.start_date)} - ${formatDate(r.end_date)} | ${r.reason || ''} | ${r.status} |
`).join('') || '| No requests. |
'}
-
-
-
My Time Log
-
| In | Out | Duration (Hours) |
${entries.map(e => `| ${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))} |
`).join('') || '| 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);
- }
- }
+
+
+
+
+
+
+
+
My Account
+
+
+
+
My Total Hours (This Pay Period)
+
${formatDecimal(totalMilliseconds)}
+
+
+
+
Time Off Requests
+
+
| Dates | Reason | Status |
${requests.map(r => `| ${formatDate(r.start_date)} - ${formatDate(r.end_date)} | ${r.reason || ''} | ${r.status} |
`).join('') || '| No requests. |
'}
+
+
+
My Time Log
+
| In | Out | Duration (Hours) |
${entries.map(e => `| ${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))} |
`).join('') || '| 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
+
+
+
Pending Time Off Requests
| Employee | Dates | Reason | Actions |
${pendingRequests.map(r => `| ${r.username} | ${formatDate(r.start_date)} - ${formatDate(r.end_date)} | ${r.reason||''} | |
`).join('') || '| No pending requests. |
'}
Hours by Employee
| Employee | Total Hours |
${Object.entries(employeeTotals).map(([username, totalMs]) => `| ${username} | ${formatDecimal(totalMs)} |
`).join('') || '| No data. |
'}
Detailed Logs
| Employee | In | Out | Duration | Actions |
${allTimeEntries.map(e => `| ${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' : '...'} | |
`).join('')}
-
+
`;
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();