From fa6f041f12be3b4b3cde927628e9df4f15d9e217 Mon Sep 17 00:00:00 2001 From: julle Date: Sat, 6 Jun 2026 09:05:26 +0200 Subject: [PATCH] added some function for choose date --- main.py | 37 ++++++++++++++++- static/css/style.css | 56 +++++++++++++++++++++++++ static/index.html | 29 ++++++++++++- static/js/app.js | 98 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 215 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index ac9696b..2a0fabc 100644 --- a/main.py +++ b/main.py @@ -107,9 +107,40 @@ def list_projects(): return projects +@app.post("/preview-time") +async def preview_time_from_csv(file: UploadFile = File(...)): + """ + Parse a file and return the extracted work time without submitting. + """ + content = await file.read() + filename = (file.filename or "").lower() + content_type = (file.content_type or "").lower() + + try: + if filename.endswith(".xlsx") or "spreadsheetml" in content_type: + rows = parse_xlsx(content) + else: + rows = parse_csv(content.decode('utf-8-sig')) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to parse file: {str(e)}") + + work_time = extract_work_time(rows) + + if not work_time: + raise HTTPException( + status_code=400, + detail="No valid work time found in the uploaded file." + ) + + return [{"date": k, "hours": v} for k, v in sorted(work_time.items())] + + @app.post("/report-time", response_model=TimeReportResponse) async def report_time_from_csv( project_name: str, + excluded_dates: str = "", file: UploadFile = File(...) ): """ @@ -168,10 +199,14 @@ async def report_time_from_csv( work_time = extract_work_time(rows) + if excluded_dates: + excluded_list = [d.strip() for d in excluded_dates.split(",")] + work_time = {k: v for k, v in work_time.items() if k not in excluded_list} + if not work_time: raise HTTPException( status_code=400, - detail="No valid work time found in the uploaded CSV." + detail="No valid work time found in the uploaded CSV after filtering." ) # Submit events diff --git a/static/css/style.css b/static/css/style.css index 0198f83..091492e 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -242,6 +242,62 @@ body::before { color: var(--text-muted); } +.text-sm { + font-size: 0.875rem; +} + +.text-muted { + color: var(--text-muted); +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.table-responsive { + overflow-x: auto; +} + +.preview-table { + width: 100%; + border-collapse: collapse; + margin-top: 0.5rem; + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.preview-table th, .preview-table td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.preview-table th { + background: rgba(255, 255, 255, 0.04); + font-weight: 600; + color: var(--text-primary); +} + +.preview-table tr { + transition: var(--transition); + cursor: pointer; +} + +.preview-table tbody tr:hover { + background: rgba(255, 255, 255, 0.06); +} + +.preview-table tbody tr.excluded { + opacity: 0.5; + background: rgba(239, 68, 68, 0.05); +} + +.preview-table tbody tr.excluded td { + text-decoration: line-through; +} + /* File Preview */ .file-preview { margin-top: 1.5rem; diff --git a/static/index.html b/static/index.html index a9312c5..d07d3f5 100644 --- a/static/index.html +++ b/static/index.html @@ -4,7 +4,7 @@ ZynkTime - Time Reporting - + @@ -66,6 +66,31 @@ + + +
@@ -155,6 +180,6 @@

Processing your request...

- + diff --git a/static/js/app.js b/static/js/app.js index eaf59b9..40ef0b5 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2,6 +2,7 @@ let selectedFile = null; let projects = {}; let lastEvents = []; +let previewData = []; // DOM Elements const uploadZone = document.getElementById('uploadZone'); @@ -203,18 +204,105 @@ function setFile(file) { 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() { - submitBtn.disabled = !(selectedFile && projectSelect.value); + const hasSelectedDates = previewData.length === 0 || previewData.some(item => item.selected); + submitBtn.disabled = !(selectedFile && projectSelect.value && hasSelectedDates); } function setDefaultDateRange() { @@ -238,10 +326,16 @@ async function handleSubmit() { const projectName = projectSelect.value; + const excludedDates = previewData + .filter(item => !item.selected) + .map(item => item.date) + .join(','); + showLoading(true); try { - const response = await fetch(`/report-time?project_name=${encodeURIComponent(projectName)}`, { + const url = `/report-time?project_name=${encodeURIComponent(projectName)}&excluded_dates=${encodeURIComponent(excludedDates)}`; + const response = await fetch(url, { method: 'POST', body: formData });