From 0421d840e3f359ddcc7a91142967ea8ba77a11d6 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 11 Feb 2026 20:52:41 -0500 Subject: [PATCH] feat: employee calendar tab --- public/js/main.js | 55 ++++++++++++++++++ public/js/ui.js | 143 ++++++++++++++++++++++++++-------------------- server.js | 9 +++ 3 files changed, 146 insertions(+), 61 deletions(-) diff --git a/public/js/main.js b/public/js/main.js index d727d9c..0d4e13e 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -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) { diff --git a/public/js/ui.js b/public/js/ui.js index 422a744..00b3e3a 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -94,71 +94,92 @@ export async function renderEmployeeDashboard() { }, 0); mainViews.employee.innerHTML = ` -
-
-
-
-

Current Status

-

${punchedIn ? 'Punched In' : 'Punched Out'}

-

${punchedIn ? 'Since:' : 'Last Punch:'} ${utils.formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}

+
+
+ + +
+
+
+
+
+
+

Current Status

+

${punchedIn ? 'Punched In' : 'Punched Out'}

+

${punchedIn ? 'Since:' : 'Last Punch:'} ${utils.formatDateTime(punchedIn ? last.punch_in_time : last?.punch_out_time)}

+
+
+ +
+
-
- +
+

Notes from Admin

+
    ${notes.length > 0 ? notes.map(note => `
  • "${note.note_text}"

    - ${note.admin_username} on ${utils.formatDate(note.created_at)}

  • `).join('') : '

    You have no new notes.

    '}
+
+
+

My Account

+

My Total Hours (This Pay Period)

${utils.formatDecimal(totalMilliseconds)}

+
+
+
+

Time Off Requests

+ +
+
+
+
+ +
+
+
+ + + + + + + + + + + ${requests.map(r => ` + + + + + + + `).join('') || ''} + +
DatesReasonStatusActions
${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)}${r.reason || ''}${r.status} + ${r.status === 'pending' ? ` +
+ + +
+ ` : ''} +
No upcoming or pending requests.
+
+
+
+

My Time Log

+
${entries.map(e => ``).join('') || ''}
InOutDuration (Hours)
${utils.formatDateTime(e.punch_in_time)}${utils.formatDateTime(e.punch_out_time)}${e.status === 'in' ? 'Running...' : utils.formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}
No entries.
-
-
-

Notes from Admin

-
    ${notes.length > 0 ? notes.map(note => `
  • "${note.note_text}"

    - ${note.admin_username} on ${utils.formatDate(note.created_at)}

  • `).join('') : '

    You have no new notes.

    '}
-
-
-

My Account

-

My Total Hours (This Pay Period)

${utils.formatDecimal(totalMilliseconds)}

-
-
-
-

Time Off Requests

- + -
-
-
- -
-
-
- - - - - - - - - - - ${requests.map(r => ` - - - - - - - `).join('') || ''} - -
DatesReasonStatusActions
${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)}${r.reason || ''}${r.status} - ${r.status === 'pending' ? ` -
- - -
- ` : ''} -
No upcoming or pending requests.
-
-
-
-

My Time Log

-
${entries.map(e => ``).join('') || ''}
InOutDuration (Hours)
${utils.formatDateTime(e.punch_in_time)}${utils.formatDateTime(e.punch_out_time)}${e.status === 'in' ? 'Running...' : utils.formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}
No entries.
`; diff --git a/server.js b/server.js index 3c10190..f12352a 100644 --- a/server.js +++ b/server.js @@ -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'");