// State let selectedFile = null; let projects = {}; let lastEvents = []; let previewData = []; // DOM Elements const uploadZone = document.getElementById('uploadZone'); const fileInput = document.getElementById('fileInput'); const browseLink = document.getElementById('browseLink'); const filePreview = document.getElementById('filePreview'); const fileName = document.getElementById('fileName'); const fileSize = document.getElementById('fileSize'); const removeFileBtn = document.getElementById('removeFile'); const projectSelect = document.getElementById('projectSelect'); const submitBtn = document.getElementById('submitBtn'); const statusBadge = document.getElementById('statusBadge'); const resultsSection = document.getElementById('resultsSection'); const resultsContent = document.getElementById('resultsContent'); const projectsList = document.getElementById('projectsList'); const refreshProjectsBtn = document.getElementById('refreshProjects'); const loadingOverlay = document.getElementById('loadingOverlay'); const toastContainer = document.getElementById('toastContainer'); const startDateInput = document.getElementById('startDate'); const endDateInput = document.getElementById('endDate'); const loadEventsBtn = document.getElementById('loadEvents'); const approveEventsBtn = document.getElementById('approveEvents'); const eventsList = document.getElementById('eventsList'); const eventsTotal = document.getElementById('eventsTotal'); // Initialize document.addEventListener('DOMContentLoaded', () => { checkHealth(); loadProjects(); setDefaultDateRange(); setupEventListeners(); }); // Event Listeners function setupEventListeners() { // File upload uploadZone.addEventListener('click', () => fileInput.click()); browseLink.addEventListener('click', (e) => { e.stopPropagation(); fileInput.click(); }); fileInput.addEventListener('change', handleFileSelect); removeFileBtn.addEventListener('click', (e) => { e.stopPropagation(); removeFile(); }); // Drag and drop uploadZone.addEventListener('dragover', handleDragOver); uploadZone.addEventListener('dragleave', handleDragLeave); uploadZone.addEventListener('drop', handleDrop); // Submit submitBtn.addEventListener('click', handleSubmit); // Refresh projects refreshProjectsBtn.addEventListener('click', () => { refreshProjectsBtn.classList.add('spin'); loadProjects().finally(() => { setTimeout(() => refreshProjectsBtn.classList.remove('spin'), 500); }); }); // Approvals loadEventsBtn.addEventListener('click', fetchEvents); approveEventsBtn.addEventListener('click', approveEvents); } // API Functions async function checkHealth() { try { const response = await fetch('/health'); const data = await response.json(); const statusText = statusBadge.querySelector('.status-text'); if (data.status === 'healthy') { statusBadge.classList.add('healthy'); statusText.textContent = 'Connected'; } else { statusBadge.classList.add('unhealthy'); statusText.textContent = 'No API Key'; showToast('Please configure KLEER_API_KEY environment variable', 'warning'); } } catch (error) { statusBadge.classList.add('unhealthy'); statusBadge.querySelector('.status-text').textContent = 'Offline'; showToast('Cannot connect to server', 'error'); } } async function loadProjects() { try { const response = await fetch('/projects'); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } projects = await response.json(); populateProjectSelect(); displayProjects(); } catch (error) { console.error('Error loading projects:', error); projectSelect.innerHTML = ''; projectsList.innerHTML = `
Failed to load projects: ${error.message}
`; showToast('Failed to load projects', 'error'); } } function populateProjectSelect() { projectSelect.innerHTML = ''; Object.keys(projects).forEach(projectName => { const option = document.createElement('option'); option.value = projectName; option.textContent = projectName; projectSelect.appendChild(option); }); projectSelect.disabled = false; updateSubmitButton(); } function displayProjects() { const projectNames = Object.keys(projects); if (projectNames.length === 0) { projectsList.innerHTML = '
No projects found
'; return; } projectsList.innerHTML = ''; projectNames.forEach(projectName => { const project = projects[projectName]; const projectItem = document.createElement('div'); projectItem.className = 'project-item'; projectItem.innerHTML = `
${projectName}
ID: ${project.id} Activity: ${project.activity.id || 'N/A'}
`; projectsList.appendChild(projectItem); }); } // File Handling function handleFileSelect(e) { const file = e.target.files[0]; if (file) { if (isSupportedFile(file.name)) { setFile(file); } else { showToast('Please select a CSV or XLSX file', 'warning'); fileInput.value = ''; } } } function handleDragOver(e) { e.preventDefault(); uploadZone.classList.add('drag-over'); } function handleDragLeave(e) { e.preventDefault(); uploadZone.classList.remove('drag-over'); } function handleDrop(e) { e.preventDefault(); uploadZone.classList.remove('drag-over'); const file = e.dataTransfer.files[0]; if (file) { if (isSupportedFile(file.name)) { setFile(file); } else { showToast('Please drop a CSV or XLSX file', 'warning'); } } } function isSupportedFile(fileName) { const lower = fileName.toLowerCase(); return lower.endsWith('.csv') || lower.endsWith('.xlsx'); } function setFile(file) { selectedFile = file; // Show file preview uploadZone.style.display = 'none'; filePreview.style.display = 'block'; fileName.textContent = file.name; fileSize.textContent = formatFileSize(file.size); updateSubmitButton(); generatePreview(file); } async function generatePreview(file) { const formData = new FormData(); formData.append('file', file); showLoading(true); try { const response = await fetch('/preview-time', { method: 'POST', body: formData }); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || `HTTP ${response.status}`); } renderPreview(data); } catch (error) { console.error('Preview error:', error); showToast('Failed to generate preview: ' + error.message, 'error'); document.getElementById('previewContainer').style.display = 'none'; } finally { showLoading(false); } } function renderPreview(data) { previewData = data.map(item => ({...item, selected: true})); const container = document.getElementById('previewContainer'); container.style.display = 'block'; updatePreviewTable(); document.getElementById('selectAllPreview').onchange = (e) => { const checked = e.target.checked; previewData.forEach(item => item.selected = checked); updatePreviewTable(); }; } // Ensure togglePreviewItem is available globally for inline onclick handlers window.togglePreviewItem = function(index) { previewData[index].selected = !previewData[index].selected; // Update "select all" checkbox state const allSelected = previewData.every(item => item.selected); const noneSelected = previewData.every(item => !item.selected); const selectAllCheckbox = document.getElementById('selectAllPreview'); selectAllCheckbox.checked = allSelected; selectAllCheckbox.indeterminate = !allSelected && !noneSelected; updatePreviewTable(); }; function updatePreviewTable() { const tbody = document.getElementById('previewTableBody'); tbody.innerHTML = ''; let totalHours = 0; let selectedCount = 0; previewData.forEach((item, index) => { if (item.selected) { totalHours += item.hours; selectedCount++; } const tr = document.createElement('tr'); if (!item.selected) tr.classList.add('excluded'); tr.innerHTML = ` ${item.date} ${item.hours.toFixed(2)} `; tr.onclick = () => togglePreviewItem(index); tbody.appendChild(tr); }); document.getElementById('previewTotalHours').textContent = totalHours.toFixed(2); updateSubmitButton(); } function removeFile() { selectedFile = null; previewData = []; fileInput.value = ''; uploadZone.style.display = 'block'; filePreview.style.display = 'none'; document.getElementById('previewContainer').style.display = 'none'; updateSubmitButton(); } function updateSubmitButton() { const hasSelectedDates = previewData.length === 0 || previewData.some(item => item.selected); submitBtn.disabled = !(selectedFile && projectSelect.value && hasSelectedDates); } function setDefaultDateRange() { const today = new Date(); const start = new Date(today); start.setDate(today.getDate() - 7); startDateInput.value = start.toISOString().slice(0, 10); endDateInput.value = today.toISOString().slice(0, 10); } // Submit Handler async function handleSubmit() { if (!selectedFile || !projectSelect.value) { showToast('Please select a file and project', 'warning'); return; } const formData = new FormData(); formData.append('file', selectedFile); const projectName = projectSelect.value; const excludedDates = previewData .filter(item => !item.selected) .map(item => item.date) .join(','); showLoading(true); try { const url = `/report-time?project_name=${encodeURIComponent(projectName)}&excluded_dates=${encodeURIComponent(excludedDates)}`; const response = await fetch(url, { method: 'POST', body: formData }); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || `HTTP ${response.status}`); } // Show success displayResults(data); showToast('Time reported successfully!', 'success'); // Reset form removeFile(); projectSelect.value = ''; updateSubmitButton(); } catch (error) { console.error('Error submitting:', error); showToast(`Error: ${error.message}`, 'error'); } finally { showLoading(false); } } // Results Display function displayResults(data) { resultsContent.innerHTML = `

${data.message}

${data.reported_days}
Days Reported
${data.total_hours.toFixed(2)}
Total Hours
${data.project_name}
Project
${data.user_name}
User
`; resultsSection.style.display = 'block'; resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } // Utility Functions function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } function showLoading(show) { loadingOverlay.style.display = show ? 'flex' : 'none'; } function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${getToastIcon(type)} ${message} `; toastContainer.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideInRight 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, 3000); } function getToastIcon(type) { const icons = { success: '', error: '', warning: '', info: '' }; return icons[type] || icons.info; } // Project select change handler projectSelect.addEventListener('change', updateSubmitButton); // Approvals helpers function renderEvents(events) { if (!events || events.length === 0) { eventsList.innerHTML = '
No events found for range
'; eventsTotal.style.display = 'none'; return; } let totalHours = 0; eventsList.innerHTML = ''; events.forEach(event => { const approved = event.approved; const hours = parseFloat(event.hours) || 0; totalHours += hours; const item = document.createElement('div'); item.className = 'event-item'; item.innerHTML = `
${event.date || 'Unknown date'}
Hours: ${hours.toFixed(2)} Activity: ${event.activity?.id ?? '-'} Project: ${event["client-project"]?.id ?? '-'}
${approved ? 'Approved' : 'Pending'}
`; eventsList.appendChild(item); }); eventsTotal.innerHTML = `Total Hours: ${totalHours.toFixed(2)}`; eventsTotal.style.display = 'block'; } function getDateRange() { const startDate = startDateInput.value; const endDate = endDateInput.value; if (!startDate || !endDate) { showToast('Please select start and end dates', 'warning'); return null; } if (new Date(startDate) > new Date(endDate)) { showToast('Start date cannot be after end date', 'warning'); return null; } return { startDate, endDate }; } async function fetchEvents() { const range = getDateRange(); if (!range) return; showLoading(true); try { const response = await fetch(`/events?start_date=${range.startDate}&end_date=${range.endDate}`); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || `HTTP ${response.status}`); } lastEvents = data.events || []; renderEvents(lastEvents); showToast('Events loaded', 'success'); } catch (error) { console.error('Error fetching events:', error); showToast(`Failed to load events: ${error.message}`, 'error'); } finally { showLoading(false); } } async function approveEvents() { const range = getDateRange(); if (!range) return; showLoading(true); try { const response = await fetch( `/approve-events?start_date=${range.startDate}&end_date=${range.endDate}`, { method: 'POST' } ); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || `HTTP ${response.status}`); } showToast('Events approved', 'success'); await fetchEvents(); } catch (error) { console.error('Error approving events:', error); showToast(`Failed to approve events: ${error.message}`, 'error'); } finally { showLoading(false); } }