feat: employee calendar tab
This commit is contained in:
parent
61401d8dc7
commit
0421d840e3
@ -23,6 +23,7 @@ import {
|
|||||||
let user = null;
|
let user = null;
|
||||||
let authToken = null;
|
let authToken = null;
|
||||||
let lastAdminTab = 'overview';
|
let lastAdminTab = 'overview';
|
||||||
|
let lastEmployeeTab = 'dashboard';
|
||||||
export { lastAdminTab };
|
export { lastAdminTab };
|
||||||
|
|
||||||
// --- NOTIFICATION LOGIC ---
|
// --- NOTIFICATION LOGIC ---
|
||||||
@ -165,6 +166,26 @@ async function handleViewRequestHistoryClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadEmployeeCalendar() {
|
||||||
|
const iframe = document.getElementById('employee-calendar-iframe');
|
||||||
|
const empty = document.getElementById('employee-calendar-empty');
|
||||||
|
const link = document.getElementById('employee-calendar-link');
|
||||||
|
if (!iframe || !empty || !link) return;
|
||||||
|
const res = await apiCall('/calendar-url');
|
||||||
|
if (!res.success) return;
|
||||||
|
const url = res.data?.url || '';
|
||||||
|
if (!url) {
|
||||||
|
iframe.removeAttribute('src');
|
||||||
|
empty.classList.remove('hidden');
|
||||||
|
link.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
iframe.src = url;
|
||||||
|
link.href = url;
|
||||||
|
link.classList.remove('hidden');
|
||||||
|
empty.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
async function handleEditSubmit(e) {
|
async function handleEditSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = e.target.elements['edit-id'].value;
|
const id = e.target.elements['edit-id'].value;
|
||||||
@ -450,6 +471,16 @@ export function attachEmployeeDashboardListeners() {
|
|||||||
if (timeOffForm) {
|
if (timeOffForm) {
|
||||||
timeOffForm.addEventListener('submit', handleTimeOffRequest);
|
timeOffForm.addEventListener('submit', handleTimeOffRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupEmployeeTabs();
|
||||||
|
if (lastEmployeeTab !== 'dashboard') {
|
||||||
|
const tabsContainer = document.getElementById('employee-tabs-nav');
|
||||||
|
tabsContainer?.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active-tab'));
|
||||||
|
tabsContainer?.querySelector(`.tab-btn[data-tab="${lastEmployeeTab}"]`)?.classList.add('active-tab');
|
||||||
|
document.getElementById('tab-content-employee-dashboard')?.classList.add('hidden');
|
||||||
|
document.getElementById(`tab-content-employee-${lastEmployeeTab}`)?.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
if (lastEmployeeTab === 'calendar') loadEmployeeCalendar();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attachAdminDashboardListeners() {
|
export function attachAdminDashboardListeners() {
|
||||||
@ -527,6 +558,30 @@ function setupTabbedInterface() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupEmployeeTabs() {
|
||||||
|
const tabsContainer = document.getElementById('employee-tabs-nav');
|
||||||
|
const contentContainer = document.getElementById('employee-tabs-content');
|
||||||
|
if (!tabsContainer || !contentContainer) return;
|
||||||
|
|
||||||
|
tabsContainer.addEventListener('click', (e) => {
|
||||||
|
const clickedTab = e.target.closest('.tab-btn');
|
||||||
|
if (!clickedTab) return;
|
||||||
|
|
||||||
|
const tabTarget = clickedTab.dataset.tab;
|
||||||
|
lastEmployeeTab = tabTarget;
|
||||||
|
|
||||||
|
tabsContainer.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active-tab'));
|
||||||
|
clickedTab.classList.add('active-tab');
|
||||||
|
|
||||||
|
contentContainer.querySelectorAll('[id^="tab-content-employee-"]').forEach(panel => {
|
||||||
|
panel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
document.getElementById(`tab-content-employee-${tabTarget}`).classList.remove('hidden');
|
||||||
|
|
||||||
|
if (tabTarget === 'calendar') loadEmployeeCalendar();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- START THE APP ---
|
// --- START THE APP ---
|
||||||
// Register the service worker when the page loads
|
// Register the service worker when the page loads
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
143
public/js/ui.js
143
public/js/ui.js
@ -94,71 +94,92 @@ export async function renderEmployeeDashboard() {
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
mainViews.employee.innerHTML = `
|
mainViews.employee.innerHTML = `
|
||||||
<div class="max-w-4xl mx-auto space-y-8">
|
<div class="max-w-5xl mx-auto space-y-4">
|
||||||
<div class="bg-white rounded-xl shadow-md p-6">
|
<div id="employee-tabs-nav" class="flex space-x-4 border-b">
|
||||||
<div class="grid md:grid-cols-2 gap-6 items-center">
|
<button data-tab="dashboard" class="tab-btn py-3 px-4 active-tab">Dashboard</button>
|
||||||
<div class="p-6 rounded-lg text-white text-center h-48 flex flex-col justify-center ${punchedIn ? 'bg-red-500' : 'bg-green-500'}">
|
<button data-tab="calendar" class="tab-btn py-3 px-4">Calendar</button>
|
||||||
<h3 class="text-xl font-semibold">Current Status</h3>
|
</div>
|
||||||
<p class="text-3xl font-bold">${punchedIn ? 'Punched In' : 'Punched Out'}</p>
|
<div id="employee-tabs-content" class="space-y-8">
|
||||||
<p class="text-sm">${punchedIn ? 'Since:' : 'Last Punch:'} ${utils.formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}</p>
|
<div id="tab-content-employee-dashboard" class="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>
|
||||||
<div class="flex justify-center">
|
<div class="bg-white rounded-xl shadow-md p-6">
|
||||||
<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 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id="tab-content-employee-calendar" class="space-y-4 hidden">
|
||||||
<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">Notes from Admin</h3>
|
<div class="flex flex-wrap justify-between items-center gap-2 mb-4">
|
||||||
<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>
|
<h3 class="text-xl font-bold text-gray-700">Calendar</h3>
|
||||||
</div>
|
<a id="employee-calendar-link" class="text-sm text-blue-600 hover:underline hidden" target="_blank" rel="noopener">Open in new tab</a>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6">
|
</div>
|
||||||
<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="overflow-hidden rounded-lg border bg-white">
|
||||||
<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>
|
<iframe id="employee-calendar-iframe" title="Employee Calendar" style="width: 100%; height: 70vh; border: none;"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow-md p-6">
|
<p id="employee-calendar-empty" class="text-sm text-gray-500 mt-2 hidden">Calendar not configured.</p>
|
||||||
<div class="flex justify-between items-center mb-4">
|
<p class="text-xs text-gray-400 mt-1">If the calendar doesn't display, use the “Open in new tab” link.</p>
|
||||||
<h3 class="text-xl font-bold text-gray-700">Time Off Requests</h3>
|
</div>
|
||||||
<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>
|
</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>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
|||||||
@ -591,6 +591,15 @@ app.post('/api/admin/notify', authenticateToken, requireRole('admin'), async (re
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/calendar-url', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const storedUrl = await getSetting('nextcloud_calendar_embed_url');
|
||||||
|
res.json({ url: storedUrl || process.env.NEXTCLOUD_CALENDAR_EMBED_URL || null });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'Failed to fetch calendar settings.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/admin/archive', authenticateToken, requireRole('admin'), async (req, res) => {
|
app.post('/api/admin/archive', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const entriesToArchive = await db.all("SELECT * FROM time_entries WHERE status = 'out'");
|
const entriesToArchive = await db.all("SELECT * FROM time_entries WHERE status = 'out'");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user