just need port, no api path

This commit is contained in:
chris 2025-07-30 09:07:58 -04:00
parent 4d518830fb
commit a954ec88f9
2 changed files with 54 additions and 93 deletions

BIN
data/timetracker.db Normal file

Binary file not shown.

View File

@ -41,6 +41,7 @@
<div id="employee-dashboard" class="hidden"></div> <div id="employee-dashboard" class="hidden"></div>
<div id="admin-dashboard" class="hidden"></div> <div id="admin-dashboard" class="hidden"></div>
<div id="admin-archive-view" class="hidden"></div> <div id="admin-archive-view" class="hidden"></div>
<div id="admin-time-off-history-view" class="hidden"></div>
</main> </main>
<div id="modal-container"></div> <div id="modal-container"></div>
@ -48,11 +49,11 @@
<script> <script>
// --- DOM Elements & State --- // --- DOM Elements & State ---
const mainViews = { auth: document.getElementById('auth-view'), employee: document.getElementById('employee-dashboard'), admin: document.getElementById('admin-dashboard'), archive: document.getElementById('admin-archive-view') }; const mainViews = { auth: document.getElementById('auth-view'), employee: document.getElementById('employee-dashboard'), admin: document.getElementById('admin-dashboard'), archive: document.getElementById('admin-archive-view'), timeOffHistory: document.getElementById('admin-time-off-history-view') };
const navUserControls = document.getElementById('nav-user-controls'), welcomeMessage = document.getElementById('welcome-message'), signOutBtn = document.getElementById('sign-out-btn'); const navUserControls = document.getElementById('nav-user-controls'), welcomeMessage = document.getElementById('welcome-message'), signOutBtn = document.getElementById('sign-out-btn');
const messageBox = document.getElementById('message-box'), loadingSpinner = document.getElementById('loading-spinner'), modalContainer = document.getElementById('modal-container'); const messageBox = document.getElementById('message-box'), loadingSpinner = document.getElementById('loading-spinner'), modalContainer = document.getElementById('modal-container');
let authToken = localStorage.getItem('authToken'), user = JSON.parse(localStorage.getItem('user')), allTimeEntries = [], allUsers = [], employeeTimerInterval = null; let authToken = localStorage.getItem('authToken'), user = JSON.parse(localStorage.getItem('user')), allTimeEntries = [], allUsers = [], employeeTimerInterval = null;
// --- Helper Functions --- // --- Helper Functions ---
const showLoading = (show) => loadingSpinner.innerHTML = show ? `<div class="inline-block animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>` : ''; const showLoading = (show) => loadingSpinner.innerHTML = show ? `<div class="inline-block animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>` : '';
const showMessage = (message, type = 'success') => { messageBox.innerHTML = `<div class="p-4 mb-4 text-sm rounded-lg flex items-center justify-between ${type === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}" role="alert"><span>${message}</span><button onclick="messageBox.classList.add('hidden')" class="font-bold text-lg">&times;</button></div>`; messageBox.classList.remove('hidden'); }; const showMessage = (message, type = 'success') => { messageBox.innerHTML = `<div class="p-4 mb-4 text-sm rounded-lg flex items-center justify-between ${type === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}" role="alert"><span>${message}</span><button onclick="messageBox.classList.add('hidden')" class="font-bold text-lg">&times;</button></div>`; messageBox.classList.remove('hidden'); };
@ -61,38 +62,28 @@
const formatDate = (s) => s ? new Date(s).toLocaleDateString() : 'N/A'; const formatDate = (s) => s ? new Date(s).toLocaleDateString() : '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 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 s = Math.floor(ms / 1000); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; }; const formatDuration = (ms) => { if (!ms || ms < 0) return '00:00:00'; const s = Math.floor(ms / 1000); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; };
// --- API Calls --- // --- API Calls ---
// MODIFIED: This now points to the full server address, including the port.
const API_BASE_URL = `http://${window.location.hostname}:3000/api`;
async function apiCall(endpoint, method = 'GET', body = null) { async function apiCall(endpoint, method = 'GET', body = null) {
const headers = { 'Content-Type': 'application/json' }; const headers = { 'Content-Type': 'application/json' };
if (authToken) headers['Authorization'] = `Bearer ${authToken}`; if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
try { try {
showLoading(true); showLoading(true);
const response = await fetch(`/api${endpoint}`, { method, headers, body: body ? JSON.stringify(body) : null }); 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 }; }
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
handleSignOut('Your session has expired. Please log in again.');
return { success: false };
}
const text = await response.text();
const data = text ? JSON.parse(text) : {};
throw new Error(data.message || `HTTP error ${response.status}`);
}
const text = await response.text(); const text = await response.text();
if (!response.ok) throw new Error(JSON.parse(text).message || `HTTP error ${response.status}`);
return { success: true, data: text ? JSON.parse(text) : null }; return { success: true, data: text ? JSON.parse(text) : null };
} catch (error) { } catch (error) { showMessage(error.message, 'error'); return { success: false };
showMessage(error.message, 'error'); } finally { showLoading(false); }
return { success: false };
} finally {
showLoading(false);
}
} }
// --- View Management --- // --- View Management ---
const showView = (viewName) => { clearInterval(employeeTimerInterval); Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName)); } const showView = (viewName) => { clearInterval(employeeTimerInterval); Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName)); }
// --- UI Rendering --- // --- UI Rendering ---
function updateUI() { function updateUI() {
if (authToken && user) { if (authToken && user) {
@ -105,12 +96,12 @@
renderAuthView(); renderAuthView();
} }
} }
function renderAuthView() { function renderAuthView() {
mainViews.auth.innerHTML = `<div class="max-w-md mx-auto mt-10 p-8 border rounded-xl shadow-lg bg-white"><h2 class="text-3xl font-bold text-center text-gray-800 mb-6">Employee Login</h2><form id="auth-form" class="space-y-4"><input type="text" id="username" placeholder="Username" class="w-full p-2 border rounded" required><input type="password" id="password" placeholder="Password" class="w-full p-2 border rounded" required><button type="submit" class="w-full bg-blue-600 text-white p-2 rounded">Log In</button></form></div>`; mainViews.auth.innerHTML = `<div class="max-w-md mx-auto mt-10 p-8 border rounded-xl shadow-lg bg-white"><h2 class="text-3xl font-bold text-center text-gray-800 mb-6">Employee Login</h2><form id="auth-form" class="space-y-4"><input type="text" id="username" placeholder="Username" class="w-full p-2 border rounded" required><input type="password" id="password" placeholder="Password" class="w-full p-2 border rounded" required><button type="submit" class="w-full bg-blue-600 text-white p-2 rounded">Log In</button></form></div>`;
document.getElementById('auth-form').addEventListener('submit', handleAuthSubmit); document.getElementById('auth-form').addEventListener('submit', handleAuthSubmit);
} }
async function renderEmployeeDashboard() { async function renderEmployeeDashboard() {
clearInterval(employeeTimerInterval); clearInterval(employeeTimerInterval);
const [statusRes, timeOffRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests')]); const [statusRes, timeOffRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests')]);
@ -121,10 +112,7 @@
const last = entries[0]; const last = entries[0];
const punchedIn = last?.status === 'in'; const punchedIn = last?.status === 'in';
let totalMilliseconds = entries.reduce((acc, e) => e.status === 'out' ? acc + (new Date(e.punch_out_time) - new Date(e.punch_in_time)) : acc, 0); let totalMilliseconds = entries.reduce((acc, e) => e.status === 'out' ? acc + (new Date(e.punch_out_time) - new Date(e.punch_in_time)) : acc, 0);
// --- NEW: Get today's date for the min attribute on date inputs ---
const today = new Date().toISOString().split('T')[0];
mainViews.employee.innerHTML = ` mainViews.employee.innerHTML = `
<div class="max-w-4xl mx-auto space-y-8"> <div class="max-w-4xl mx-auto space-y-8">
<div class="bg-white rounded-xl shadow-md p-6"> <div class="bg-white rounded-xl shadow-md p-6">
@ -140,8 +128,8 @@
<div class="bg-white rounded-xl shadow-md p-6"> <div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-xl font-bold text-gray-700 mb-4">Time Off Requests</h3> <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"> <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 min="${today}"></div> <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 min="${today}"></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">Submit Request</button> <button type="submit" class="w-full bg-indigo-600 text-white p-2 rounded">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> <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> </form>
@ -155,82 +143,56 @@
document.getElementById('punch-btn').addEventListener('click', handlePunch); document.getElementById('punch-btn').addEventListener('click', handlePunch);
document.getElementById('change-password-btn').addEventListener('click', renderChangePasswordModal); document.getElementById('change-password-btn').addEventListener('click', renderChangePasswordModal);
document.getElementById('time-off-form').addEventListener('submit', handleTimeOffRequest); document.getElementById('time-off-form').addEventListener('submit', handleTimeOffRequest);
if (punchedIn) { if (punchedIn) {
const durationCell = document.getElementById(`duration-${last.id}`), totalHoursCell = document.getElementById('employee-total-hours'), punchInTime = new Date(last.punch_in_time); const durationCell = document.getElementById(`duration-${last.id}`), totalHoursCell = document.getElementById('employee-total-hours'), punchInTime = new Date(last.punch_in_time);
employeeTimerInterval = setInterval(() => { const elapsed = new Date() - punchInTime; durationCell.textContent = formatDuration(elapsed); totalHoursCell.textContent = formatDecimal(totalMilliseconds + elapsed); }, 1000); employeeTimerInterval = setInterval(() => { const elapsed = new Date() - punchInTime; durationCell.textContent = formatDuration(elapsed); 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')]); 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; if (!logsRes.success || !usersRes.success || !requestsRes.success) return;
allTimeEntries = logsRes.data; allUsers = usersRes.data; const allRequests = requestsRes.data; allTimeEntries = logsRes.data; allUsers = usersRes.data; const pendingRequests = requestsRes.data;
const employeeTotals = allTimeEntries.reduce((acc, entry) => { const dur = (entry.status === 'out' ? new Date(entry.punch_out_time) - new Date(entry.punch_in_time) : new Date() - 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.status === 'out' ? new Date(entry.punch_out_time) - new Date(entry.punch_in_time) : new Date() - 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');
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 justify-between items-center mb-4"><h2 class="text-2xl font-bold">Admin Dashboard</h2><div class="space-x-2"><button id="view-archives-btn" class="px-4 py-2 bg-gray-500 text-white rounded-lg">View Archives</button><button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg">Archive Records</button></div></div></div> <div class="bg-white rounded-xl shadow-md p-6"><div class="flex justify-between items-center mb-4"><h2 class="text-2xl font-bold">Admin Dashboard</h2><div class="space-x-2"><button id="view-archives-btn" class="px-4 py-2 bg-gray-500 text-white rounded-lg">View Archives</button><button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg">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-2">Currently Punched In</h3><ul id="punched-in-list" class="border rounded-lg divide-y">${punchedInEntries.map(e => `<li class="flex justify-between items-center p-3"><span class="font-medium">${e.username}</span><div class="flex items-center space-x-4"><span class="text-sm text-gray-600">Since: ${formatDateTime(e.punch_in_time)}</span><button class="force-clock-out-btn px-2 py-1 text-xs bg-red-500 text-white rounded" data-userid="${e.user_id}">Clock Out</button></div></li>`).join('') || '<li class="p-4 text-center">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 id="punched-in-list" class="border rounded-lg divide-y">${punchedInEntries.map(e => `<li class="flex justify-between items-center p-3"><span class="font-medium">${e.username}</span><div class="flex items-center space-x-4"><span class="text-sm text-gray-600">Since: ${formatDateTime(e.punch_in_time)}</span><button class="force-clock-out-btn px-2 py-1 text-xs bg-red-500 text-white rounded" data-userid="${e.user_id}">Clock Out</button></div></li>`).join('') || '<li class="p-4 text-center">None</li>'}</ul></div>
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">Time Off Requests</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">Dates</th><th class="p-2">Reason</th><th class="p-2">Status</th><th class="p-2">Actions</th></tr></thead><tbody id="time-off-requests-table">${allRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2">${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><td class="p-2 space-x-2">${r.status === 'pending' ? `<button class="approve-request-btn text-green-600" data-id="${r.id}">Approve</button><button class="deny-request-btn text-red-600" data-id="${r.id}">Deny</button>` : ''}<button class="delete-request-btn text-gray-500" data-id="${r.id}">Delete</button></td></tr>`).join('') || '<tr><td colspan="5" class="text-center p-4">No 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">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 id="time-off-requests-table">${pendingRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2">${formatDate(r.start_date)} - ${formatDate(r.end_date)}</td><td class="p-2">${r.reason||''}</td><td class="p-2 space-x-1"><button class="approve-request-btn text-green-600" data-id="${r.id}">Approve</button><button class="deny-request-btn text-red-600" data-id="${r.id}">Deny</button></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.keys(employeeTotals).map(u => `<tr class="border-t"><td class="p-2 font-medium">${u}</td><td class="p-2">${formatDecimal(employeeTotals[u])}</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-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.keys(employeeTotals).map(u => `<tr class="border-t"><td class="p-2 font-medium">${u}</td><td class="p-2">${formatDecimal(employeeTotals[u])}</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"><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 id="admin-table">${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">${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td><td class="p-2 space-x-2"><button class="edit-btn text-blue-600" data-id="${e.id}">Edit</button><button class="delete-btn text-red-600" data-id="${e.id}">Delete</button></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"><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 id="admin-table">${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">${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td><td class="p-2 space-x-2"><button class="edit-btn text-blue-600" data-id="${e.id}">Edit</button><button class="delete-btn text-red-600" data-id="${e.id}">Delete</button></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">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 Punch</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><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded" required><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded">Add Punch</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 id="manage-users-table">${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2">${u.role}</td><td class="p-2 space-x-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn text-blue-600" data-username="${u.username}">Reset PW</button><button class="change-role-btn text-purple-600" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn text-red-600" data-username="${u.username}">Delete</button>` : ''}`}</td></tr>`).join('')}</tbody></table></div></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">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 Punch</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><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded" required><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded">Add Punch</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 id="manage-users-table">${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2">${u.role}</td><td class="p-2 space-x-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn text-blue-600" data-username="${u.username}">Reset PW</button><button class="change-role-btn text-purple-600" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn text-red-600" data-username="${u.username}">Delete</button>` : ''}`}</td></tr>`).join('')}</tbody></table></div></div></div>
</div>`; </div>`;
document.getElementById('archive-btn').addEventListener('click', handleArchive); document.getElementById('archive-btn').addEventListener('click', handleArchive);
document.getElementById('view-archives-btn').addEventListener('click', renderArchiveView); 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('create-user-form').addEventListener('submit', handleCreateUser);
document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch); document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch);
document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick); document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick);
} }
async function renderArchiveView() { function renderArchiveView() {
showView('archive'); apiCall('/admin/archives').then(res => {
const res = await apiCall('/admin/archives'); if (!res.success) return;
if (!res.success) { showView('archive');
mainViews.archive.innerHTML = `<div class="text-center p-4">Could not load archives.</div>`; mainViews.archive.innerHTML = `<div class="max-w-6xl mx-auto bg-white rounded-xl shadow-md p-6"><div class="flex justify-between items-center mb-4"><h2 class="text-2xl font-bold">Archived Logs</h2><button id="back-to-dash-btn" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">Back to Dashboard</button></div><div class="overflow-x-auto"><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">Archived On</th></tr></thead><tbody>${res.data.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">${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td><td class="p-2">${formatDateTime(e.archived_at)}</td></tr>`).join('') || '<tr><td colspan="5" class="text-center p-4">No archived entries found.</td></tr>'}</tbody></table></div></div>`;
return; document.getElementById('back-to-dash-btn').addEventListener('click', () => { showView('admin'); renderAdminDashboard(); });
} });
const archives = res.data; }
mainViews.archive.innerHTML = ` function renderTimeOffHistoryView() {
<div class="max-w-6xl mx-auto space-y-6"> apiCall('/admin/time-off-requests/history').then(res => {
<div class="bg-white rounded-xl shadow-md p-6"> if (!res.success) return;
<div class="flex justify-between items-center mb-4"> showView('timeOffHistory');
<h2 class="text-2xl font-bold">Archived Records</h2> mainViews.timeOffHistory.innerHTML = `
<button id="back-to-admin-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">← Back to Dashboard</button> <div class="max-w-6xl mx-auto bg-white rounded-xl shadow-md p-6">
</div> <div class="flex justify-between items-center mb-4"><h2 class="text-2xl font-bold">Time Off History</h2><button id="back-to-dash-btn" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">Back to Dashboard</button></div>
<div class="overflow-x-auto border rounded-lg"> <div class="overflow-x-auto"><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">Status</th></tr></thead><tbody>${res.data.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2">${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' : 'red'}-600">${r.status}</td></tr>`).join('') || '<tr><td colspan="4" class="text-center p-4">No history.</td></tr>'}</tbody></table></div>
<table class="min-w-full text-sm text-left"> </div>`;
<thead class="bg-gray-50"> document.getElementById('back-to-dash-btn').addEventListener('click', () => { showView('admin'); renderAdminDashboard(); });
<tr>
<th class="p-2">Employee</th>
<th class="p-2">Punch In</th>
<th class="p-2">Punch Out</th>
<th class="p-2">Duration (Hours)</th>
<th class="p-2">Archived At</th>
</tr>
</thead>
<tbody>
${archives.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">${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td>
<td class="p-2">${formatDateTime(e.archived_at)}</td>
</tr>
`).join('') || '<tr><td colspan="5" class="text-center p-4">No archived records found.</td></tr>'}
</tbody>
</table>
</div>
</div>
</div>`;
document.getElementById('back-to-admin-btn').addEventListener('click', () => {
showView('admin');
renderAdminDashboard();
}); });
} }
@ -239,7 +201,7 @@
document.getElementById('modal-form').addEventListener('submit', submitHandler); document.getElementById('modal-form').addEventListener('submit', submitHandler);
document.querySelector('.cancel-modal-btn').addEventListener('click', () => modalContainer.innerHTML = ''); document.querySelector('.cancel-modal-btn').addEventListener('click', () => modalContainer.innerHTML = '');
} }
function renderEditModal(id) { function renderEditModal(id) {
const entry = allTimeEntries.find(e => e.id == id); const entry = allTimeEntries.find(e => e.id == id);
const formHTML = `<input type="hidden" id="edit-id" value="${entry.id}"><div><label class="font-medium">Punch In</label><input type="datetime-local" id="edit-in" value="${toLocalISO(entry.punch_in_time)}" class="w-full p-2 border rounded" required></div><div><label class="font-medium">Punch Out</label><input type="datetime-local" id="edit-out" value="${toLocalISO(entry.punch_out_time)}" class="w-full p-2 border rounded"></div>`; const formHTML = `<input type="hidden" id="edit-id" value="${entry.id}"><div><label class="font-medium">Punch In</label><input type="datetime-local" id="edit-in" value="${toLocalISO(entry.punch_in_time)}" class="w-full p-2 border rounded" required></div><div><label class="font-medium">Punch Out</label><input type="datetime-local" id="edit-out" value="${toLocalISO(entry.punch_out_time)}" class="w-full p-2 border rounded"></div>`;
@ -250,12 +212,12 @@
const formHTML = `<input type="password" id="modal-current-pw" placeholder="Current Password" class="w-full p-2 border rounded" required><input type="password" id="modal-new-pw" placeholder="New Password" class="w-full p-2 border rounded" required>`; const formHTML = `<input type="password" id="modal-current-pw" placeholder="Current Password" class="w-full p-2 border rounded" required><input type="password" id="modal-new-pw" placeholder="New Password" class="w-full p-2 border rounded" required>`;
renderModal('Change My Password', formHTML, handleChangePassword); renderModal('Change My Password', formHTML, handleChangePassword);
} }
function renderResetPasswordModal(username) { function renderResetPasswordModal(username) {
const formHTML = `<input type="hidden" id="reset-username" value="${username}"><input type="password" id="reset-new-pw" placeholder="New Password" class="w-full p-2 border rounded" required>`; const formHTML = `<input type="hidden" id="reset-username" value="${username}"><input type="password" id="reset-new-pw" placeholder="New Password" class="w-full p-2 border rounded" required>`;
renderModal(`Reset Password for ${username}`, formHTML, handleResetPassword); renderModal(`Reset Password for ${username}`, formHTML, handleResetPassword);
} }
// --- Event Handlers --- // --- Event Handlers ---
async function handleAuthSubmit(e) { e.preventDefault(); const username = e.target.elements.username.value, 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(); } } async function handleAuthSubmit(e) { e.preventDefault(); const username = e.target.elements.username.value, 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, 'error'); }; const handleSignOut = (message) => { localStorage.clear(); authToken = null; user = null; updateUI(); if (message) showMessage(message, 'error'); };
@ -264,7 +226,7 @@
function handleAdminDashboardClick(e) { function handleAdminDashboardClick(e) {
const target = e.target; const target = e.target;
const { id, userid, username, role } = target.dataset; const { id, userid, username, role } = target.dataset;
if (target.classList.contains('edit-btn')) renderEditModal(id); if (target.classList.contains('edit-btn')) renderEditModal(id);
if (target.classList.contains('delete-btn') && confirm('Delete entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard()); if (target.classList.contains('delete-btn') && confirm('Delete entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard());
if (target.classList.contains('force-clock-out-btn') && confirm('Force clock out?')) apiCall('/admin/force-clock-out', 'POST', { userId: userid }).then(res => res.success && renderAdminDashboard()); if (target.classList.contains('force-clock-out-btn') && confirm('Force clock out?')) apiCall('/admin/force-clock-out', 'POST', { userId: userid }).then(res => res.success && renderAdminDashboard());
@ -273,18 +235,17 @@
if (target.classList.contains('delete-user-btn') && confirm(`PERMANENTLY DELETE user '${username}' and all their data? This cannot be undone.`)) apiCall(`/admin/delete-user/${username}`, 'DELETE').then(res => res.success && renderAdminDashboard()); if (target.classList.contains('delete-user-btn') && confirm(`PERMANENTLY DELETE user '${username}' and all their data? This cannot be undone.`)) apiCall(`/admin/delete-user/${username}`, 'DELETE').then(res => res.success && renderAdminDashboard());
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('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()); } 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()); }
// --- NEW: Handle deleting a time-off request ---
if (target.classList.contains('delete-request-btn')) { if (confirm(`Permanently delete this request?`)) apiCall(`/admin/time-off-requests/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard()); }
} }
async function handleEditSubmit(e) { e.preventDefault(); const id = e.target.elements['edit-id'].value, punch_in_time = new Date(e.target.elements['edit-in'].value).toISOString(), punch_out_time = e.target.elements['edit-out'].value ? new Date(e.target.elements['edit-out'].value).toISOString() : null; const res = await apiCall(`/admin/logs/${id}`, 'PUT', { punch_in_time, punch_out_time }); if (res.success) { modalContainer.innerHTML = ''; renderAdminDashboard(); } } async function handleEditSubmit(e) { e.preventDefault(); const id = e.target.elements['edit-id'].value, punch_in_time = new Date(e.target.elements['edit-in'].value).toISOString(), punch_out_time = e.target.elements['edit-out'].value ? new Date(e.target.elements['edit-out'].value).toISOString() : null; const res = await apiCall(`/admin/logs/${id}`, 'PUT', { punch_in_time, punch_out_time }); if (res.success) { modalContainer.innerHTML = ''; renderAdminDashboard(); } }
const handleArchive = () => confirm('Archive all completed entries?') && apiCall('/admin/archive', 'POST').then(res => res.success && renderAdminDashboard()); const handleArchive = () => confirm('Archive all completed entries?') && apiCall('/admin/archive', 'POST').then(res => res.success && renderAdminDashboard());
async function handleCreateUser(e) { e.preventDefault(); const username = e.target.elements['new-username'].value, password = e.target.elements['new-password'].value, 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 handleCreateUser(e) { e.preventDefault(); const username = e.target.elements['new-username'].value, password = e.target.elements['new-password'].value, 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, 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 handleChangePassword(e) { e.preventDefault(); const currentPassword = e.target.elements['modal-current-pw'].value, 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, 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 handleResetPassword(e) { e.preventDefault(); const username = e.target.elements['reset-username'].value, 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 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, endDate = e.target.elements['end-date'].value, 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(); } } const handleViewArchivesBtn = renderArchiveView;
async function handleTimeOffRequest(e) { e.preventDefault(); const startDate = e.target.elements['start-date'].value, endDate = e.target.elements['end-date'].value, reason = e.target.elements['reason'].value; 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 --- // --- Initializer ---
signOutBtn.addEventListener('click', () => handleSignOut()); signOutBtn.addEventListener('click', () => handleSignOut());
updateUI(); updateUI();