// State
let selectedFile = null;
let projects = {};
let lastEvents = [];
// 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');
// 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();
}
function removeFile() {
selectedFile = null;
fileInput.value = '';
uploadZone.style.display = 'block';
filePreview.style.display = 'none';
updateSubmitButton();
}
function updateSubmitButton() {
submitBtn.disabled = !(selectedFile && projectSelect.value);
}
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;
showLoading(true);
try {
const response = await fetch(`/report-time?project_name=${encodeURIComponent(projectName)}`, {
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
`;
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 = `
${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
';
return;
}
eventsList.innerHTML = '';
events.forEach(event => {
const approved = event.approved;
const item = document.createElement('div');
item.className = 'event-item';
item.innerHTML = `
${event.date || 'Unknown date'}
Hours: ${event.hours ?? '-'}
Activity: ${event.activity?.id ?? '-'}
Project: ${event["client-project"]?.id ?? '-'}
${approved ? 'Approved' : 'Pending'}
`;
eventsList.appendChild(item);
});
}
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);
}
}