add notes
This commit is contained in:
parent
901efb6bf6
commit
dc2369ce5e
@ -150,69 +150,86 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderEmployeeDashboard() {
|
async function renderEmployeeDashboard() {
|
||||||
clearInterval(employeeTimerInterval);
|
clearInterval(employeeTimerInterval);
|
||||||
const [statusRes, timeOffRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests')]);
|
// Fetch notes along with other data
|
||||||
if (!statusRes.success || !timeOffRes.success) return;
|
const [statusRes, timeOffRes, notesRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests'), apiCall('/user/notes')]);
|
||||||
const entries = statusRes.data;
|
if (!statusRes.success || !timeOffRes.success || !notesRes.success) return;
|
||||||
const requests = timeOffRes.data;
|
|
||||||
const last = entries[0];
|
const entries = statusRes.data;
|
||||||
const punchedIn = last?.status === 'in';
|
const requests = timeOffRes.data;
|
||||||
let totalMilliseconds = entries.reduce((acc, e) => {
|
const notes = notesRes.data; // Get notes data
|
||||||
return e.status === 'out' && e.punch_out_time ? acc + (new Date(e.punch_out_time) - new Date(e.punch_in_time)) : acc;
|
const last = entries[0];
|
||||||
}, 0);
|
const punchedIn = last?.status === 'in';
|
||||||
mainViews.employee.innerHTML = `
|
let totalMilliseconds = entries.reduce((acc, e) => {
|
||||||
<div class="max-w-4xl mx-auto space-y-8">
|
return e.status === 'out' && e.punch_out_time ? acc + (new Date(e.punch_out_time) - new Date(e.punch_in_time)) : acc;
|
||||||
<div class="bg-white rounded-xl shadow-md p-6">
|
}, 0);
|
||||||
<div class="grid md:grid-cols-2 gap-6 items-center">
|
|
||||||
<div class="p-6 rounded-lg text-white text-center h-48 flex flex-col justify-center ${punchedIn ? 'bg-red-500' : 'bg-green-500'}">
|
mainViews.employee.innerHTML = `
|
||||||
<h3 class="text-xl font-semibold">Current Status</h3>
|
<div class="max-w-4xl mx-auto space-y-8">
|
||||||
<p class="text-3xl font-bold">${punchedIn ? 'Punched In' : 'Punched Out'}</p>
|
<div class="bg-white rounded-xl shadow-md p-6">
|
||||||
<p class="text-sm">${punchedIn ? 'Since:' : 'Last Punch:'} ${formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}</p>
|
<div class="grid md:grid-cols-2 gap-6 items-center">
|
||||||
</div>
|
<div class="p-6 rounded-lg text-white text-center h-48 flex flex-col justify-center ${punchedIn ? 'bg-red-500' : 'bg-green-500'}">
|
||||||
<div class="flex justify-center">
|
<h3 class="text-xl font-semibold">Current Status</h3>
|
||||||
<button id="punch-btn" class="w-48 h-48 rounded-full text-white font-bold text-2xl transition-transform transform hover:scale-105 ${punchedIn ? 'bg-red-600' : 'bg-green-600'}">${punchedIn ? 'Punch Out' : 'Punch In'}</button>
|
<p class="text-3xl font-bold">${punchedIn ? 'Punched In' : 'Punched Out'}</p>
|
||||||
</div>
|
<p class="text-sm">${punchedIn ? 'Since:' : 'Last Punch:'} ${formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6">
|
<div class="flex justify-center">
|
||||||
<div class="flex justify-between items-center">
|
<button id="punch-btn" class="w-48 h-48 rounded-full text-white font-bold text-2xl transition-transform transform hover:scale-105 ${punchedIn ? 'bg-red-600' : 'bg-green-600'}">${punchedIn ? 'Punch Out' : 'Punch In'}</button>
|
||||||
<h3 class="text-xl font-bold text-gray-700">My Account</h3>
|
|
||||||
<button id="change-password-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Change Password</button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
|
|
||||||
<h4 class="font-semibold text-blue-800">My Total Hours (This Pay Period)</h4>
|
|
||||||
<p class="text-3xl font-bold text-blue-600" id="employee-total-hours">${formatDecimal(totalMilliseconds)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6">
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-700 mb-4">Time Off Requests</h3>
|
</div>
|
||||||
<form id="time-off-form" class="grid md:grid-cols-3 gap-4 items-end bg-gray-50 p-4 rounded-lg">
|
|
||||||
<div><label class="block text-sm font-medium">Start Date</label><input type="date" id="start-date" class="w-full p-2 border rounded" required></div>
|
<div class="bg-white rounded-xl shadow-md p-6">
|
||||||
<div><label class="block text-sm font-medium">End Date</label><input type="date" id="end-date" class="w-full p-2 border rounded" required></div>
|
<h3 class="text-xl font-bold text-gray-700 mb-4">Notes from Admin</h3>
|
||||||
<button type="submit" class="w-full bg-indigo-600 text-white p-2 rounded hover:bg-indigo-700">Submit Request</button>
|
<ul class="space-y-4 mt-4 max-h-60 overflow-y-auto">
|
||||||
<div class="md:col-span-3"><label class="block text-sm font-medium">Reason (optional)</label><input type="text" id="reason" placeholder="e.g., Vacation" class="w-full p-2 border rounded"></div>
|
${notes.length > 0 ? notes.map(note => `
|
||||||
</form>
|
<li class="bg-amber-50 p-3 rounded-lg border-l-4 border-amber-400">
|
||||||
<div class="mt-4 overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Dates</th><th class="p-2">Reason</th><th class="p-2">Status</th></tr></thead><tbody>${requests.map(r => `<tr class="border-t"><td class="p-2 whitespace-nowrap">${formatDate(r.start_date)} - ${formatDate(r.end_date)}</td><td class="p-2">${r.reason || ''}</td><td class="p-2 font-medium capitalize text-${r.status === 'approved' ? 'green' : r.status === 'denied' ? 'red' : 'gray'}-600">${r.status}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center p-4">No requests.</td></tr>'}</tbody></table></div>
|
<p class="text-gray-800 break-words">"${note.note_text}"</p>
|
||||||
</div>
|
<p class="text-xs text-gray-500 text-right mt-2">- ${note.admin_username} on ${formatDate(note.created_at)}</p>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6">
|
</li>
|
||||||
<h3 class="text-xl font-bold text-gray-700 mb-2">My Time Log</h3>
|
`).join('') : '<p class="text-gray-500 text-center">You have no new notes.</p>'}
|
||||||
<div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">In</th><th class="p-2">Out</th><th class="p-2">Duration (Hours)</th></tr></thead><tbody>${entries.map(e => `<tr class="border-t"><td class="p-2">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2" id="duration-${e.id}">${e.status === 'in' ? 'Running...' : formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center p-4">No entries.</td></tr>'}</tbody></table></div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
|
||||||
document.getElementById('punch-btn').addEventListener('click', handlePunch);
|
<div class="bg-white rounded-xl shadow-md p-6">
|
||||||
document.getElementById('change-password-btn').addEventListener('click', renderChangePasswordModal);
|
<div class="flex justify-between items-center">
|
||||||
document.getElementById('time-off-form').addEventListener('submit', handleTimeOffRequest);
|
<h3 class="text-xl font-bold text-gray-700">My Account</h3>
|
||||||
if (punchedIn) {
|
<button id="change-password-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Change Password</button>
|
||||||
const durationCell = document.getElementById(`duration-${last.id}`);
|
</div>
|
||||||
const totalHoursCell = document.getElementById('employee-total-hours');
|
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
|
||||||
const punchInTime = new Date(last.punch_in_time);
|
<h4 class="font-semibold text-blue-800">My Total Hours (This Pay Period)</h4>
|
||||||
employeeTimerInterval = setInterval(() => {
|
<p class="text-3xl font-bold text-blue-600" id="employee-total-hours">${formatDecimal(totalMilliseconds)}</p>
|
||||||
const elapsed = Date.now() - punchInTime.getTime();
|
</div>
|
||||||
if (durationCell) durationCell.textContent = formatDuration(elapsed);
|
</div>
|
||||||
if (totalHoursCell) totalHoursCell.textContent = formatDecimal(totalMilliseconds + elapsed);
|
<div class="bg-white rounded-xl shadow-md p-6">
|
||||||
}, 1000);
|
<h3 class="text-xl font-bold text-gray-700 mb-4">Time Off Requests</h3>
|
||||||
}
|
<form id="time-off-form" class="grid md:grid-cols-3 gap-4 items-end bg-gray-50 p-4 rounded-lg">
|
||||||
}
|
<div><label class="block text-sm font-medium">Start Date</label><input type="date" id="start-date" class="w-full p-2 border rounded" required></div>
|
||||||
|
<div><label class="block text-sm font-medium">End Date</label><input type="date" id="end-date" class="w-full p-2 border rounded" required></div>
|
||||||
|
<button type="submit" class="w-full bg-indigo-600 text-white p-2 rounded hover:bg-indigo-700">Submit Request</button>
|
||||||
|
<div class="md:col-span-3"><label class="block text-sm font-medium">Reason (optional)</label><input type="text" id="reason" placeholder="e.g., Vacation" class="w-full p-2 border rounded"></div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-4 overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Dates</th><th class="p-2">Reason</th><th class="p-2">Status</th></tr></thead><tbody>${requests.map(r => `<tr class="border-t"><td class="p-2 whitespace-nowrap">${formatDate(r.start_date)} - ${formatDate(r.end_date)}</td><td class="p-2">${r.reason || ''}</td><td class="p-2 font-medium capitalize text-${r.status === 'approved' ? 'green' : r.status === 'denied' ? 'red' : 'gray'}-600">${r.status}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center p-4">No requests.</td></tr>'}</tbody></table></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-bold text-gray-700 mb-2">My Time Log</h3>
|
||||||
|
<div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">In</th><th class="p-2">Out</th><th class="p-2">Duration (Hours)</th></tr></thead><tbody>${entries.map(e => `<tr class="border-t"><td class="p-2">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2" id="duration-${e.id}">${e.status === 'in' ? 'Running...' : formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center p-4">No entries.</td></tr>'}</tbody></table></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
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() {
|
async function renderAdminDashboard() {
|
||||||
const [logsRes, usersRes, requestsRes] = await Promise.all([apiCall('/admin/logs'), apiCall('/admin/users'), apiCall('/admin/time-off-requests/pending')]);
|
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;
|
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 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');
|
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 = `
|
mainViews.admin.innerHTML = `
|
||||||
<div class="max-w-6xl mx-auto space-y-8">
|
<div class="max-w-6xl mx-auto space-y-8">
|
||||||
<div class="bg-white rounded-xl shadow-md p-6"><div class="flex flex-wrap justify-between items-center gap-4"><h2 class="text-2xl font-bold">Admin Dashboard</h2><div class="flex-shrink-0 space-x-2"><button id="view-archives-btn" class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600">View Archives</button><button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600">Archive Records</button></div></div></div>
|
<div class="bg-white rounded-xl shadow-md p-6"><div class="flex flex-wrap justify-between items-center gap-4"><h2 class="text-2xl font-bold">Admin Dashboard</h2><div class="flex-shrink-0 space-x-2"><button id="view-archives-btn" class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600">View Archives</button><button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600">Archive Records</button></div></div></div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-bold text-gray-700 mb-4">Leave a Note for Employee</h3>
|
||||||
|
<form id="add-note-form" class="space-y-3 bg-gray-50 p-4 rounded-lg">
|
||||||
|
<select id="note-user-select" class="w-full p-2 border rounded" required>
|
||||||
|
<option value="">-- Select an Employee --</option>
|
||||||
|
${employeesOnly.map(u => `<option value="${u.id}">${u.username}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
<textarea id="note-text" placeholder="Write your note here..." class="w-full p-2 border rounded" rows="3" required></textarea>
|
||||||
|
<button type="submit" class="w-full bg-cyan-600 text-white p-2 rounded hover:bg-cyan-700">Submit Note</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-2">Currently Punched In</h3><ul class="border rounded-lg divide-y">${punchedInEntries.map(e => `<li class="flex flex-col items-start space-y-2 p-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0"><span class="font-medium text-gray-800">${e.username}</span><div class="flex items-center space-x-4"><span class="text-sm text-gray-500">Since: ${formatDateTime(e.punch_in_time)}</span><button class="force-clock-out-btn px-3 py-1 text-xs bg-red-500 text-white rounded whitespace-nowrap" data-userid="${e.user_id}" data-username="${e.username}">Force Clock Out</button></div></li>`).join('') || '<li class="p-4 text-center text-gray-500">None</li>'}</ul></div>
|
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-2">Currently Punched In</h3><ul class="border rounded-lg divide-y">${punchedInEntries.map(e => `<li class="flex flex-col items-start space-y-2 p-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0"><span class="font-medium text-gray-800">${e.username}</span><div class="flex items-center space-x-4"><span class="text-sm text-gray-500">Since: ${formatDateTime(e.punch_in_time)}</span><button class="force-clock-out-btn px-3 py-1 text-xs bg-red-500 text-white rounded whitespace-nowrap" data-userid="${e.user_id}" data-username="${e.username}">Force Clock Out</button></div></li>`).join('') || '<li class="p-4 text-center text-gray-500">None</li>'}</ul></div>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6"><div class="flex justify-between items-center mb-4"><h3 class="text-xl font-bold text-gray-700">Pending Time Off Requests</h3><button id="view-time-off-history-btn" class="px-4 py-2 text-sm bg-gray-200 rounded-lg hover:bg-gray-300">View History</button></div><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Dates</th><th class="p-2">Reason</th><th class="p-2">Actions</th></tr></thead><tbody>${pendingRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2 whitespace-nowrap">${formatDate(r.start_date)} - ${formatDate(r.end_date)}</td><td class="p-2">${r.reason||''}</td><td class="p-2"><div class="flex flex-col sm:flex-row gap-2"><button class="approve-request-btn font-medium text-green-600 hover:underline" data-id="${r.id}">Approve</button><button class="deny-request-btn font-medium text-red-600 hover:underline" data-id="${r.id}">Deny</button></div></td></tr>`).join('') || '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>'}</tbody></table></div></div>
|
<div class="bg-white rounded-xl shadow-md p-6"><div class="flex justify-between items-center mb-4"><h3 class="text-xl font-bold text-gray-700">Pending Time Off Requests</h3><button id="view-time-off-history-btn" class="px-4 py-2 text-sm bg-gray-200 rounded-lg hover:bg-gray-300">View History</button></div><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Dates</th><th class="p-2">Reason</th><th class="p-2">Actions</th></tr></thead><tbody>${pendingRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2 whitespace-nowrap">${formatDate(r.start_date)} - ${formatDate(r.end_date)}</td><td class="p-2">${r.reason||''}</td><td class="p-2"><div class="flex flex-col sm:flex-row gap-2"><button class="approve-request-btn font-medium text-green-600 hover:underline" data-id="${r.id}">Approve</button><button class="deny-request-btn font-medium text-red-600 hover:underline" data-id="${r.id}">Deny</button></div></td></tr>`).join('') || '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>'}</tbody></table></div></div>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-2">Hours by Employee</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Total Hours</th></tr></thead><tbody>${Object.entries(employeeTotals).map(([username, totalMs]) => `<tr class="border-t"><td class="p-2 font-medium">${username}</td><td class="p-2">${formatDecimal(totalMs)}</td></tr>`).join('') || '<tr><td colspan="2" class="text-center p-4">No data.</td></tr>'}</tbody></table></div></div>
|
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-2">Hours by Employee</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Total Hours</th></tr></thead><tbody>${Object.entries(employeeTotals).map(([username, totalMs]) => `<tr class="border-t"><td class="p-2 font-medium">${username}</td><td class="p-2">${formatDecimal(totalMs)}</td></tr>`).join('') || '<tr><td colspan="2" class="text-center p-4">No data.</td></tr>'}</tbody></table></div></div>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">Detailed Logs</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">In</th><th class="p-2">Out</th><th class="p-2">Duration</th><th class="p-2">Actions</th></tr></thead><tbody>${allTimeEntries.map(e => `<tr class="border-t"><td class="p-2">${e.username||'N/A'}</td><td class="p-2">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2" id="admin-duration-${e.id}">${e.punch_out_time ? formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2"><button class="edit-btn font-medium text-blue-600 hover:underline" data-id="${e.id}">Edit</button><button class="delete-btn font-medium text-red-600 hover:underline" data-id="${e.id}">Delete</button></div></td></tr>`).join('')}</tbody></table></div></div>
|
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">Detailed Logs</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">In</th><th class="p-2">Out</th><th class="p-2">Duration</th><th class="p-2">Actions</th></tr></thead><tbody>${allTimeEntries.map(e => `<tr class="border-t"><td class="p-2">${e.username||'N/A'}</td><td class="p-2">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2" id="admin-duration-${e.id}">${e.punch_out_time ? formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2"><button class="edit-btn font-medium text-blue-600 hover:underline" data-id="${e.id}">Edit</button><button class="delete-btn font-medium text-red-600 hover:underline" data-id="${e.id}">Delete</button></div></td></tr>`).join('')}</tbody></table></div></div>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">User & Payroll Management</h3><div class="grid md:grid-cols-2 gap-6"><form id="create-user-form" class="space-y-3 bg-gray-50 p-4 rounded-lg"><h4 class="font-semibold">Create User</h4><input type="text" id="new-username" placeholder="Username" class="w-full p-2 border rounded" required><input type="password" id="new-password" placeholder="Password" class="w-full p-2 border rounded" required><select id="new-user-role" class="w-full p-2 border rounded"><option value="employee">Employee</option><option value="admin">Admin</option></select><button type="submit" class="w-full bg-green-600 text-white p-2 rounded hover:bg-green-700">Create User</button></form>
|
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">User & Payroll Management</h3><div class="grid md:grid-cols-2 gap-6"><form id="create-user-form" class="space-y-3 bg-gray-50 p-4 rounded-lg"><h4 class="font-semibold">Create User</h4><input type="text" id="new-username" placeholder="Username" class="w-full p-2 border rounded" required><input type="password" id="new-password" placeholder="Password" class="w-full p-2 border rounded" required><select id="new-user-role" class="w-full p-2 border rounded"><option value="employee">Employee</option><option value="admin">Admin</option></select><button type="submit" class="w-full bg-green-600 text-white p-2 rounded hover:bg-green-700">Create User</button></form><form id="add-punch-form" class="space-y-3 bg-gray-50 p-4 rounded-lg"><h4 class="font-semibold">Add Manual Entry</h4><select id="add-punch-user" class="w-full p-2 border rounded" required>${allUsers.map(u => `<option value="${u.id}" data-username="${u.username}">${u.username}</option>`).join('')}</select><label class="text-sm">In (Required):</label><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><label class="text-sm">Out (Optional):</label><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded"><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded hover:bg-purple-700">Add Entry</button></form></div><div class="mt-6"><h4 class="font-semibold mb-2">Manage Users</h4><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Username</th><th class="p-2">Role</th><th class="p-2">Actions</th></tr></thead><tbody>${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2 capitalize">${u.role}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn font-medium text-blue-600 hover:underline" data-username="${u.username}">Reset PW</button><button class="change-role-btn font-medium text-purple-600 hover:underline" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn font-medium text-red-600 hover:underline" data-username="${u.username}">Delete</button>` : ''}`}</div></td></tr>`).join('')}</tbody></table></div></div></div>
|
||||||
<form id="add-punch-form" class="space-y-3 bg-gray-50 p-4 rounded-lg">
|
|
||||||
<h4 class="font-semibold">Add Manual Entry</h4>
|
|
||||||
<select id="add-punch-user" class="w-full p-2 border rounded" required>${allUsers.map(u => `<option value="${u.id}" data-username="${u.username}">${u.username}</option>`).join('')}</select>
|
|
||||||
<label class="text-sm">In (Required):</label>
|
|
||||||
<input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required>
|
|
||||||
<label class="text-sm">Out (Optional):</label>
|
|
||||||
<input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded">
|
|
||||||
<button type="submit" class="w-full bg-purple-600 text-white p-2 rounded hover:bg-purple-700">Add Entry</button>
|
|
||||||
</form></div>
|
|
||||||
<div class="mt-6"><h4 class="font-semibold mb-2">Manage Users</h4><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Username</th><th class="p-2">Role</th><th class="p-2">Actions</th></tr></thead><tbody>${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2 capitalize">${u.role}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn font-medium text-blue-600 hover:underline" data-username="${u.username}">Reset PW</button><button class="change-role-btn font-medium text-purple-600 hover:underline" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn font-medium text-red-600 hover:underline" data-username="${u.username}">Delete</button>` : ''}`}</div></td></tr>`).join('')}</tbody></table></div></div></div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
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); } });
|
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() {
|
function renderArchiveView() {
|
||||||
@ -315,6 +341,21 @@
|
|||||||
const username = selected.options[selected.selectedIndex].dataset.username;
|
const username = selected.options[selected.selectedIndex].dataset.username;
|
||||||
const punchInTime = new Date(e.target.elements['add-punch-in'].value).toISOString();
|
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
|
// Handle the optional punch-out time
|
||||||
const punchOutValue = e.target.elements['add-punch-out'].value;
|
const punchOutValue = e.target.elements['add-punch-out'].value;
|
||||||
const punchOutTime = punchOutValue ? new Date(punchOutValue).toISOString() : null;
|
const punchOutTime = punchOutValue ? new Date(punchOutValue).toISOString() : null;
|
||||||
|
|||||||
46
server.js
46
server.js
@ -71,6 +71,14 @@ async function initializeDatabase() {
|
|||||||
status TEXT NOT NULL DEFAULT 'pending',
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
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]);
|
const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]);
|
||||||
if (!adminUser) {
|
if (!adminUser) {
|
||||||
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
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.' });
|
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();
|
startServer();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user