feat: employee calendar tab

This commit is contained in:
chris 2026-02-11 20:52:41 -05:00
parent 61401d8dc7
commit 0421d840e3
3 changed files with 146 additions and 61 deletions

View File

@ -23,6 +23,7 @@ import {
let user = null;
let authToken = null;
let lastAdminTab = 'overview';
let lastEmployeeTab = 'dashboard';
export { lastAdminTab };
// --- 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) {
e.preventDefault();
const id = e.target.elements['edit-id'].value;
@ -450,6 +471,16 @@ export function attachEmployeeDashboardListeners() {
if (timeOffForm) {
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() {
@ -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 ---
// Register the service worker when the page loads
if ('serviceWorker' in navigator) {

View File

@ -94,7 +94,13 @@ export async function renderEmployeeDashboard() {
}, 0);
mainViews.employee.innerHTML = `
<div class="max-w-4xl mx-auto space-y-8">
<div class="max-w-5xl mx-auto space-y-4">
<div id="employee-tabs-nav" class="flex space-x-4 border-b">
<button data-tab="dashboard" class="tab-btn py-3 px-4 active-tab">Dashboard</button>
<button data-tab="calendar" class="tab-btn py-3 px-4">Calendar</button>
</div>
<div id="employee-tabs-content" class="space-y-8">
<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'}">
@ -160,6 +166,21 @@ export async function renderEmployeeDashboard() {
<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 id="tab-content-employee-calendar" class="space-y-4 hidden">
<div class="bg-white rounded-xl shadow-md p-6">
<div class="flex flex-wrap justify-between items-center gap-2 mb-4">
<h3 class="text-xl font-bold text-gray-700">Calendar</h3>
<a id="employee-calendar-link" class="text-sm text-blue-600 hover:underline hidden" target="_blank" rel="noopener">Open in new tab</a>
</div>
<div class="overflow-hidden rounded-lg border bg-white">
<iframe id="employee-calendar-iframe" title="Employee Calendar" style="width: 100%; height: 70vh; border: none;"></iframe>
</div>
<p id="employee-calendar-empty" class="text-sm text-gray-500 mt-2 hidden">Calendar not configured.</p>
<p class="text-xs text-gray-400 mt-1">If the calendar doesn't display, use the Open in new tab link.</p>
</div>
</div>
</div>
</div>`;
attachEmployeeDashboardListeners();

View File

@ -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) => {
try {
const entriesToArchive = await db.all("SELECT * FROM time_entries WHERE status = 'out'");