487 lines
36 KiB
JavaScript

// js/ui.js
// --- IMPORTS ---
import { apiCall } from './api.js';
import * as utils from './utils.js';
import {
attachAuthFormListener,
attachAdminDashboardListeners,
attachEmployeeDashboardListeners,
attachTimeOffHistoryListeners,
lastAdminTab
} from './main.js';
// --- DOM ELEMENT SELECTORS ---
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');
const welcomeMessage = document.getElementById('welcome-message');
const modalContainer = document.getElementById('modal-container');
// --- MODULE-LEVEL STATE ---
let employeeTimerInterval = null;
let adminTimerIntervals = [];
let allTimeEntries = [];
let allUsers = [];
// --- VIEW MANAGEMENT ---
export function showView(viewName) {
clearInterval(employeeTimerInterval);
adminTimerIntervals.forEach(clearInterval);
adminTimerIntervals = [];
Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName));
}
// --- MASTER UI UPDATE FUNCTION ---
export function updateUI() {
try {
const storedUser = localStorage.getItem('user');
const authToken = localStorage.getItem('authToken');
const user = storedUser ? JSON.parse(storedUser) : null;
if (authToken && user) {
navUserControls.classList.remove('hidden');
welcomeMessage.textContent = `Welcome, ${user.username}`;
if (user.role === 'admin') {
renderAdminDashboard();
} else {
renderEmployeeDashboard();
}
} else {
navUserControls.classList.add('hidden');
renderAuthView();
}
} catch (error) {
console.error("Corrupted session data. Clearing and reloading.", error);
localStorage.clear();
window.location.reload();
}
}
// --- RENDER FUNCTIONS ---
export function renderAuthView() {
showView('auth');
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 hover:bg-blue-700">Log In</button>
</form>
</div>`;
attachAuthFormListener();
}
export async function renderEmployeeDashboard() {
showView('employee');
clearInterval(employeeTimerInterval);
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;
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 = `
<div class="max-w-4xl mx-auto space-y-8">
<div class="bg-white rounded-xl shadow-md p-6">
<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'}">
<h3 class="text-xl font-semibold">Current Status</h3>
<p class="text-3xl font-bold">${punchedIn ? 'Punched In' : 'Punched Out'}</p>
<p class="text-sm">${punchedIn ? 'Since:' : 'Last Punch:'} ${utils.formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}</p>
</div>
<div class="flex justify-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>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-xl font-bold text-gray-700 mb-4">Notes from Admin</h3>
<ul class="space-y-4 mt-4 max-h-60 overflow-y-auto">${notes.length > 0 ? notes.map(note => `<li class="bg-amber-50 p-3 rounded-lg border-l-4 border-amber-400"><p class="text-gray-800 break-words">"${note.note_text}"</p><p class="text-xs text-gray-500 text-right mt-2">- ${note.admin_username} on ${utils.formatDate(note.created_at)}</p></li>`).join('') : '<p class="text-gray-500 text-center">You have no new notes.</p>'}</ul>
</div>
<div class="bg-white rounded-xl shadow-md p-6">
<div class="flex justify-between items-center"><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">${utils.formatDecimal(totalMilliseconds)}</p></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">Time Off Requests</h3>
<button id="view-request-history-btn" class="text-sm px-3 py-1 bg-gray-200 rounded-lg hover:bg-gray-300">View History</button>
</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><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>
<th class="p-2">Actions</th>
</tr>
</thead>
<tbody id="time-off-requests-tbody">
${requests.map(r => `
<tr class="border-t">
<td class="p-2 whitespace-nowrap">${utils.formatDate(r.start_date)} - ${utils.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">
${r.status === 'pending' ? `
<div class="flex gap-2">
<button class="edit-request-btn font-medium text-blue-600 hover:underline" data-id="${r.id}">Edit</button>
<button class="delete-request-btn font-medium text-red-600 hover:underline" data-id="${r.id}">Delete</button>
</div>
` : ''}
</td>
</tr>
`).join('') || '<tr><td colspan="4" class="text-center p-4">No upcoming or 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">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">${utils.formatDateTime(e.punch_in_time)}</td><td class="p-2">${utils.formatDateTime(e.punch_out_time)}</td><td class="p-2" id="duration-${e.id}">${e.status === 'in' ? 'Running...' : utils.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>`;
attachEmployeeDashboardListeners();
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 = utils.formatDuration(elapsed);
if (totalHoursCell) totalHoursCell.textContent = utils.formatDecimal(totalMilliseconds + elapsed);
}, 1000);
}
}
export async function renderAdminDashboard() {
showView('admin');
const [logsRes, usersRes, requestsRes] = await Promise.all([apiCall('/admin/logs'), apiCall('/admin/users'), apiCall('/admin/time-off-requests/pending')]);
if (!logsRes.success || !usersRes.success || !requestsRes.success) return;
allTimeEntries = logsRes.data;
allUsers = usersRes.data;
const pendingRequests = requestsRes.data;
const user = JSON.parse(localStorage.getItem('user'));
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 employeesOnly = allUsers.filter(u => u.role === 'employee');
mainViews.admin.innerHTML = `
<div class="max-w-6xl mx-auto space-y-4">
<div class="bg-white rounded-xl shadow-md p-4">
<h2 class="text-2xl font-bold">Admin Dashboard</h2>
</div>
<div id="admin-tabs-nav" class="flex border-b border-gray-200 bg-white rounded-t-lg px-4">
<button data-tab="overview" class="tab-btn active-tab py-3 px-4">Overview</button>
<button data-tab="logs" class="tab-btn py-3 px-4">Time Logs</button>
<button data-tab="users" class="tab-btn py-3 px-4">User Management</button>
<button data-tab="calendar" class="tab-btn py-3 px-4">Calendar</button>
<button data-tab="settings" class="tab-btn py-3 px-4">Settings</button>
</div>
<div id="admin-tabs-content" class="bg-white rounded-b-lg shadow-md p-6">
<div id="tab-content-overview" class="space-y-8">
<div><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: ${utils.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>
<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">${utils.formatDate(r.start_date)} - ${utils.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>
<button class="admin-delete-request-btn font-medium text-gray-500 hover:underline" data-id="${r.id}">Delete</button>
</div>
</td>
</tr>
`).join('') || '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>'}
</tbody>
</table>
</div>
</div>
<div>
<h3 class="text-xl font-bold text-gray-700 mb-4">Employee Notes</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 a new note here..." class="w-full p-2 border rounded" rows="3" required></textarea>
<div class="flex gap-2"><button type="submit" class="w-full bg-cyan-600 text-white p-2 rounded hover:bg-cyan-700">Submit Note</button><button type="button" id="view-notes-btn" class="w-full bg-gray-600 text-white p-2 rounded hover:bg-gray-700">View Notes</button></div>
</form>
<div id="employee-notes-container" class="mt-4"></div>
</div>
</div>
<div id="tab-content-logs" class="space-y-8 hidden">
<div class="flex flex-wrap gap-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 Time Records</button>
</div>
<div><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">${utils.formatDecimal(totalMs)}</td></tr>`).join('') || '<tr><td colspan="2" class="text-center p-4">No data.</td></tr>'}</tbody></table></div></div>
<div><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">${utils.formatDateTime(e.punch_in_time)}</td><td class="p-2">${utils.formatDateTime(e.punch_out_time)}</td><td class="p-2" id="admin-duration-${e.id}">${e.punch_out_time ? utils.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>
${e.status === 'out' ? `<button class="archive-log-btn font-medium text-amber-600 hover:underline" data-id="${e.id}">Archive</button>` : ''}</div></td></tr>`).join('')}</tbody></table></div></div>
</div>
<div id="tab-content-users" class="space-y-8 hidden">
<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><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 id="tab-content-calendar" class="space-y-8 hidden">
</div>
<div id="tab-content-settings" class="space-y-6 hidden">
<div class="max-w-2xl">
<h3 class="text-xl font-bold text-gray-700 mb-2">Calendar Settings</h3>
<p class="text-sm text-gray-500 mb-4">Set the Nextcloud calendar embed URL to display the calendar in the admin dashboard.</p>
<form id="calendar-settings-form" class="space-y-3 bg-gray-50 p-4 rounded-lg">
<label for="calendar-embed-url" class="block text-sm font-medium text-gray-700">Embed URL</label>
<input id="calendar-embed-url" name="calendar-embed-url" type="url" placeholder="https://your-nextcloud.example.com/apps/calendar/p/..." class="w-full p-2 border rounded" />
<div class="flex gap-2">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Save</button>
<button type="button" id="calendar-clear-btn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">Clear</button>
</div>
</form>
<div class="mt-6">
<h4 class="text-lg font-semibold text-gray-700 mb-2">Live Preview</h4>
<div class="overflow-hidden rounded-lg border bg-white">
<iframe id="calendar-preview-iframe" title="Calendar Preview" style="width: 100%; height: 60vh; border: none;"></iframe>
</div>
<p id="calendar-preview-empty" class="text-sm text-gray-500 mt-2 hidden">Paste a valid embed URL to preview.</p>
</div>
</div>
</div>
</div>
</div>
`;
if (lastAdminTab && lastAdminTab !== 'overview') {
document.querySelector('.tab-btn[data-tab="overview"]').classList.remove('active-tab');
document.getElementById('tab-content-overview').classList.add('hidden');
document.querySelector(`.tab-btn[data-tab="${lastAdminTab}"]`).classList.add('active-tab');
document.getElementById(`tab-content-${lastAdminTab}`).classList.remove('hidden');
}
attachAdminDashboardListeners();
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 = utils.formatDuration(Date.now() - punchInTime.getTime());
}, 1000);
adminTimerIntervals.push(intervalId);
}
});
}
export async function renderCalendarView() {
const calendarContainer = document.getElementById('tab-content-calendar');
calendarContainer.innerHTML = '<p>Loading calendar...</p>';
const res = await apiCall('/admin/calendar-url');
if (res.success && res.data.url) {
calendarContainer.innerHTML = `
<iframe src="${res.data.url}" style="width: 100%; height: 80vh; border: none;"></iframe>
`;
} else {
calendarContainer.innerHTML = `
<div class="text-center p-8">
<h3 class="text-xl font-bold text-gray-700">Calendar Not Configured</h3>
<p class="text-gray-500 mt-2">Please configure the Nextcloud calendar embed URL in the Admin Settings tab.</p>
</div>
`;
}
}
export function renderArchiveView() {
apiCall('/admin/archives').then(res => {
if (!res.success) return;
showView('archive');
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 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 (Hrs)</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">${utils.formatDateTime(e.punch_in_time)}</td><td class="p-2">${utils.formatDateTime(e.punch_out_time)}</td><td class="p-2">${utils.formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td><td class="p-2">${utils.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>`;
document.getElementById('back-to-dash-btn').addEventListener('click', renderAdminDashboard);
});
}
export function renderTimeOffHistoryView() {
apiCall('/admin/time-off-requests/history').then(res => {
if (!res.success) return;
showView('timeOffHistory');
mainViews.timeOffHistory.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">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">
<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>
${res.data.map(r => `
<tr class="border-t">
<td class="p-2">${r.username}</td>
<td class="p-2 whitespace-nowrap">${utils.formatDate(r.start_date)} - ${utils.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>
<td class="p-2">
<div class="flex flex-col sm:flex-row gap-2">
<button class="set-pending-btn font-medium text-blue-600 hover:underline" data-id="${r.id}">Set to Pending</button>
<button class="admin-delete-request-btn font-medium text-red-600 hover:underline" data-id="${r.id}">Delete</button>
</div>
</td>
</tr>
`).join('') || '<tr><td colspan="5" class="text-center p-4">No history.</td></tr>'}
</tbody>
</table>
</div>
</div>`;
document.getElementById('back-to-dash-btn').addEventListener('click', renderAdminDashboard);
attachTimeOffHistoryListeners();
});
}
// --- MODAL RENDER FUNCTIONS ---
function renderModal(title, formHTML, submitHandler) {
modalContainer.innerHTML = `<div class="modal-overlay" role="dialog" aria-modal="true"><div class="modal-content"><h3 class="text-xl font-bold mb-4">${title}</h3><form id="modal-form" class="space-y-4">${formHTML}<div class="flex justify-end space-x-2 pt-4"><button type="button" class="cancel-modal-btn px-4 py-2 bg-gray-300 rounded-lg hover:bg-gray-400">Cancel</button><button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Save</button></div></form></div></div>`;
document.getElementById('modal-form').addEventListener('submit', submitHandler);
document.querySelector('.cancel-modal-btn').addEventListener('click', () => modalContainer.innerHTML = '');
document.querySelector('.modal-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) modalContainer.innerHTML = ''; });
}
export function renderEditModal(id, submitHandler) {
const entry = allTimeEntries.find(e => e.id == id);
if (!entry) { utils.showMessage('Could not find entry to edit.', 'error'); return; }
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="${utils.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="${utils.toLocalISO(entry.punch_out_time)}" class="w-full p-2 border rounded"></div>`;
renderModal(`Edit Entry for ${entry.username}`, formHTML, submitHandler);
}
export function renderChangePasswordModal(submitHandler) {
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, submitHandler);
}
export function renderResetPasswordModal(username, submitHandler) {
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, submitHandler);
}
export function renderRequestHistoryModal(requests) {
const modalBody = `<div class="max-h-[70vh] overflow-y-auto"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50 sticky top-0"><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">${utils.formatDate(r.start_date)} - ${utils.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="3" class="text-center p-4">No history found.</td></tr>'}</tbody></table></div>`;
modalContainer.innerHTML = `<div class="modal-overlay"><div class="modal-content"><div class="flex justify-between items-center mb-4"><h3 class="text-xl font-bold">Full Time Off History</h3><button class="cancel-modal-btn font-bold text-2xl">&times;</button></div>${modalBody}</div></div>`;
document.querySelector('.cancel-modal-btn').addEventListener('click', () => modalContainer.innerHTML = '');
document.querySelector('.modal-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) modalContainer.innerHTML = ''; });
}
export function renderEditTimeOffModal(request, submitHandler) {
const startDate = new Date(request.start_date).toISOString().split('T')[0];
const endDate = new Date(request.end_date).toISOString().split('T')[0];
const formHTML = `
<input type="hidden" id="edit-request-id" value="${request.id}">
<div>
<label class="block text-sm font-medium">Start Date</label>
<input type="date" id="edit-start-date" class="w-full p-2 border rounded" value="${startDate}" required>
</div>
<div>
<label class="block text-sm font-medium">End Date</label>
<input type="date" id="edit-end-date" class="w-full p-2 border rounded" value="${endDate}" required>
</div>
<div>
<label class="block text-sm font-medium">Reason (optional)</label>
<input type="text" id="edit-reason" placeholder="e.g., Vacation" class="w-full p-2 border rounded" value="${request.reason || ''}">
</div>
`;
renderModal('Edit Time Off Request', formHTML, submitHandler);
}
// --- UI HELPER FUNCTIONS ---
export async function handleViewNotesClick() {
const userId = document.getElementById('note-user-select').value;
const container = document.getElementById('employee-notes-container');
if (!userId) {
return utils.showMessage('Please select an employee to view their notes.', 'error');
}
container.innerHTML = 'Loading notes...';
const res = await apiCall(`/admin/notes/${userId}`);
if (res.success) {
if (res.data.length > 0) {
container.innerHTML = `
<h4 class="font-semibold mb-2 text-gray-600">Showing Notes for ${document.getElementById('note-user-select').options[document.getElementById('note-user-select').selectedIndex].text}</h4>
<ul class="space-y-3 max-h-70 overflow-y-auto border rounded-lg p-2 bg-gray-50">
${res.data.map(note => `
<li class="bg-white p-3 rounded-lg shadow-sm flex justify-between items-start">
<div>
<p class="text-gray-800 break-words">"${note.note_text}"</p>
<p class="text-xs text-gray-500 mt-2">- ${note.admin_username} on ${utils.formatDate(note.created_at)}</p>
</div>
<button class="delete-note-btn text-red-500 hover:text-red-700 flex-shrink-0 ml-4" data-note-id="${note.id}">&times;</button>
</li>
`).join('')}
</ul>`;
} else {
container.innerHTML = '<p class="text-gray-500 text-center">No notes found for this employee.</p>';
}
} else {
container.innerHTML = '<p class="text-red-500 text-center">Could not load notes.</p>';
}
}
export function updatePendingRequestsList(requests) {
const tableBody = document.querySelector('#tab-content-overview table tbody');
if (!tableBody) return;
if (requests.length === 0) {
tableBody.innerHTML = '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>';
return;
}
tableBody.innerHTML = requests.map(r => `
<tr class="border-t">
<td class="p-2">${r.username}</td>
<td class="p-2 whitespace-nowrap">${utils.formatDate(r.start_date)} - ${utils.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>
<button class="admin-delete-request-btn font-medium text-gray-500 hover:underline" data-id="${r.id}">Delete</button>
</div>
</td>
</tr>
`).join('');
}