// 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 = []; let allEmployeeEntries = []; // Convenience alias const esc = utils.escapeHtml; // --- 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 = `

Employee Login

`; 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); allEmployeeEntries = entries; 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)}

Notes from Admin

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

    - ${esc(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('') || ''}
Dates Reason Status Actions
${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)} ${esc(r.reason)} ${esc(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.
`; 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 = `

Admin Dashboard

Currently Punched In

    ${punchedInEntries.map(e => { const un = esc(e.username); return `
  • ${un}
    Since: ${utils.formatDateTime(e.punch_in_time)}
  • `; }).join('') || '
  • None
  • '}

Pending Time Off Requests

${pendingRequests.map(r => ` `).join('') || ''}
EmployeeDatesReasonActions
${esc(r.username)} ${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)} ${esc(r.reason)}
No pending requests.

Employee Notes

`; 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(); renderAdminLogsContent(); 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 = '

Loading calendar...

'; const res = await apiCall('/admin/calendar-url'); if (res.success && res.data.url) { calendarContainer.innerHTML = '
'; document.getElementById('admin-cal-frame').src = res.data.url; } else { calendarContainer.innerHTML = `

Calendar Not Configured

Please configure the Nextcloud calendar embed URL in the Admin Settings tab.

`; } } export function renderArchiveView() { apiCall('/admin/archives').then(res => { if (!res.success) return; showView('archive'); mainViews.archive.innerHTML = `

Archived Logs

${res.data.map(e => ``).join('') || ''}
EmployeeInOutDuration (Hrs)Archived On
${esc(e.username) || 'N/A'}${utils.formatDateTime(e.punch_in_time)}${utils.formatDateTime(e.punch_out_time)}${utils.formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}${utils.formatDateTime(e.archived_at)}
No archived entries found.
`; 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 = `

Time Off History

${res.data.map(r => ` `).join('') || ''}
Employee Dates Reason Status Actions
${esc(r.username)} ${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)} ${esc(r.reason)} ${esc(r.status)}
No history.
`; document.getElementById('back-to-dash-btn').addEventListener('click', renderAdminDashboard); attachTimeOffHistoryListeners(); }); } // --- MODAL RENDER FUNCTIONS --- function renderModal(title, formHTML, submitHandler) { modalContainer.innerHTML = ``; 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 = `
`; renderModal(`Edit Entry for ${esc(entry.username)}`, formHTML, submitHandler); } export function renderChangePasswordModal(submitHandler) { const formHTML = ``; renderModal('Change My Password', formHTML, submitHandler); } export function renderResetPasswordModal(username, submitHandler) { const safeUsername = esc(username); const formHTML = ``; renderModal(`Reset Password for ${safeUsername}`, formHTML, submitHandler); } export function renderRequestHistoryModal(requests) { const modalBody = `
${requests.map(r => ``).join('') || ''}
DatesReasonStatus
${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)}${esc(r.reason)}${esc(r.status)}
No history found.
`; modalContainer.innerHTML = ``; 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 = `
`; 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 = `

Showing Notes for ${esc(document.getElementById('note-user-select').options[document.getElementById('note-user-select').selectedIndex].text)}

`; } else { container.innerHTML = '

No notes found for this employee.

'; } } else { container.innerHTML = '

Could not load notes.

'; } } export function renderAdminLogsContent(startDate, endDate) { const container = document.getElementById('admin-logs-content'); if (!container) return; let filtered = allTimeEntries; if (startDate) { const start = new Date(startDate + 'T00:00:00'); filtered = filtered.filter(e => new Date(e.punch_in_time) >= start); } if (endDate) { const end = new Date(endDate + 'T23:59:59.999'); filtered = filtered.filter(e => new Date(e.punch_in_time) <= end); } const employeeTotals = filtered.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; }, {}); container.innerHTML = `

Hours by Employee

${Object.entries(employeeTotals).map(([username, totalMs]) => ``).join('') || ''}
EmployeeTotal Hours
${esc(username)}${utils.formatDecimal(totalMs)}
No data.

Detailed Logs

${filtered.map(e => ``).join('') || ''}
EmployeeInOutDurationActions
${esc(e.username) || 'N/A'}${utils.formatDateTime(e.punch_in_time)}${utils.formatDateTime(e.punch_out_time)}${e.punch_out_time ? utils.formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}
${e.status === 'out' ? `` : ''}
No entries in selected range.
`; } export function renderEmployeeLogsSection(startDate, endDate) { const tbody = document.getElementById('employee-log-tbody'); const filteredHours = document.getElementById('employee-log-filtered-hours'); if (!tbody) return; let filtered = allEmployeeEntries; if (startDate) { const start = new Date(startDate + 'T00:00:00'); filtered = filtered.filter(e => new Date(e.punch_in_time) >= start); } if (endDate) { const end = new Date(endDate + 'T23:59:59.999'); filtered = filtered.filter(e => new Date(e.punch_in_time) <= end); } tbody.innerHTML = filtered.map(e => `${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))}`).join('') || 'No entries in selected range.'; if (filteredHours) { if (startDate || endDate) { const total = filtered.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); filteredHours.textContent = `${utils.formatDecimal(total)} hrs in range`; filteredHours.classList.remove('hidden'); } else { filteredHours.classList.add('hidden'); } } } export function updatePendingRequestsList(requests) { const tableBody = document.querySelector('#tab-content-overview table tbody'); if (!tableBody) return; if (requests.length === 0) { tableBody.innerHTML = 'No pending requests.'; return; } tableBody.innerHTML = requests.map(r => ` ${esc(r.username)} ${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)} ${esc(r.reason)}
`).join(''); }