added some function for choose date
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+27
-2
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ZynkTime - Time Reporting</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=2">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=3">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
@@ -66,6 +66,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Entries Preview Table -->
|
||||
<div id="previewContainer" style="display: none; margin-top: 1.5rem;">
|
||||
<h4>Preview Time Entries</h4>
|
||||
<p class="text-sm text-muted mb-2">Click on a row or checkbox to exclude it from reporting.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50"><input type="checkbox" id="selectAllPreview" checked></th>
|
||||
<th>Date</th>
|
||||
<th>Hours</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="previewTableBody">
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2" align="right"><strong>Total Selected:</strong></td>
|
||||
<td><strong id="previewTotalHours">0.00</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Selection -->
|
||||
<div class="form-group">
|
||||
<label for="projectSelect">Select Project</label>
|
||||
@@ -155,6 +180,6 @@
|
||||
<p>Processing your request...</p>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js?v=2"></script>
|
||||
<script src="/static/js/app.js?v=3"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+96
-2
@@ -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 = `
|
||||
<td><input type="checkbox" ${item.selected ? 'checked' : ''} onclick="event.stopPropagation(); togglePreviewItem(${index})"></td>
|
||||
<td>${item.date}</td>
|
||||
<td>${item.hours.toFixed(2)}</td>
|
||||
`;
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user