Add Export to CSV or Excel.

This commit is contained in:
Jens Luedicke
2025-06-28 12:54:48 +02:00
parent dc4229e468
commit b9aa7ad413
6 changed files with 202 additions and 11 deletions

110
app.py
View File

@@ -1,8 +1,11 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, send_file, Response
from models import db, TimeEntry, WorkConfig from models import db, TimeEntry, WorkConfig
from datetime import datetime, time
import os import os
from sqlalchemy import func from sqlalchemy import func
import csv
import io
import pandas as pd
from datetime import datetime, time, timedelta
app = Flask(__name__) app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
@@ -292,5 +295,108 @@ def resume_entry(entry_id):
'total_break_duration': entry_to_resume.total_break_duration 'total_break_duration': entry_to_resume.total_break_duration
}) })
@app.route('/export')
def export():
return render_template('export.html', title='Export Data')
@app.route('/download_export')
def download_export():
# Get parameters
export_format = request.args.get('format', 'csv')
period = request.args.get('period')
# Handle date range
if period:
# Quick export options
today = datetime.now().date()
if period == 'today':
start_date = today
end_date = today
elif period == 'week':
start_date = today - timedelta(days=today.weekday())
end_date = today
elif period == 'month':
start_date = today.replace(day=1)
end_date = today
elif period == 'all':
# Get the earliest entry date
earliest_entry = TimeEntry.query.order_by(TimeEntry.arrival_time).first()
start_date = earliest_entry.arrival_time.date() if earliest_entry else today
end_date = today
else:
# Custom date range
try:
start_date = datetime.strptime(request.args.get('start_date'), '%Y-%m-%d').date()
end_date = datetime.strptime(request.args.get('end_date'), '%Y-%m-%d').date()
except (ValueError, TypeError):
flash('Invalid date format. Please use YYYY-MM-DD format.')
return redirect(url_for('export'))
# Query entries within the date range
start_datetime = datetime.combine(start_date, time.min)
end_datetime = datetime.combine(end_date, time.max)
entries = TimeEntry.query.filter(
TimeEntry.arrival_time >= start_datetime,
TimeEntry.arrival_time <= end_datetime
).order_by(TimeEntry.arrival_time).all()
if not entries:
flash('No entries found for the selected date range.')
return redirect(url_for('export'))
# Prepare data for export
data = []
for entry in entries:
row = {
'Date': entry.arrival_time.strftime('%Y-%m-%d'),
'Arrival Time': entry.arrival_time.strftime('%H:%M:%S'),
'Departure Time': entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active',
'Work Duration (HH:MM:SS)': f"{entry.duration//3600:d}:{(entry.duration%3600)//60:02d}:{entry.duration%60:02d}" if entry.duration is not None else 'In progress',
'Break Duration (HH:MM:SS)': f"{entry.total_break_duration//3600:d}:{(entry.total_break_duration%3600)//60:02d}:{entry.total_break_duration%60:02d}" if entry.total_break_duration is not None else '00:00:00',
'Work Duration (seconds)': entry.duration if entry.duration is not None else 0,
'Break Duration (seconds)': entry.total_break_duration if entry.total_break_duration is not None else 0
}
data.append(row)
# Generate filename
filename = f"timetrack_export_{start_date.strftime('%Y%m%d')}_to_{end_date.strftime('%Y%m%d')}"
# Export based on format
if export_format == 'csv':
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
response = Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment;filename={filename}.csv'}
)
return response
elif export_format == 'excel':
# Convert to DataFrame and export as Excel
df = pd.DataFrame(data)
output = io.BytesIO()
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, sheet_name='TimeTrack Data', index=False)
# Auto-adjust columns' width
worksheet = writer.sheets['TimeTrack Data']
for i, col in enumerate(df.columns):
column_width = max(df[col].astype(str).map(len).max(), len(col)) + 2
worksheet.set_column(i, i, column_width)
output.seek(0)
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=f"{filename}.xlsx"
)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

View File

@@ -7,3 +7,5 @@ click==8.0.1
Flask-SQLAlchemy==2.5.1 Flask-SQLAlchemy==2.5.1
SQLAlchemy==1.4.23 SQLAlchemy==1.4.23
python-dotenv==0.19.0 python-dotenv==0.19.0
pandas
xlsxwriter

View File

@@ -69,19 +69,20 @@ main {
} }
.form-group { .form-group {
margin-bottom: 15px; margin-bottom: 1rem;
} }
.form-group label { .form-group label {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 0.5rem;
font-weight: bold; font-weight: bold;
} }
.form-group input, .form-group input,
.form-group textarea { .form-group textarea,
.form-group select {
width: 100%; width: 100%;
padding: 8px; padding: 0.5rem;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
} }
@@ -96,12 +97,16 @@ button {
} }
.btn { .btn {
padding: 8px 16px; display: inline-block;
background-color: #4CAF50;
color: white;
padding: 0.5rem 1rem;
text-decoration: none;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
background-color: #4CAF50; font-size: 1rem;
color: white; text-align: center;
} }
.btn:hover { .btn:hover {
@@ -414,3 +419,29 @@ input[type="time"]::-webkit-datetime-edit {
margin-top: 4px; margin-top: 4px;
font-style: italic; font-style: italic;
} }
.export-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.export-section {
background-color: #f9f9f9;
padding: 1.5rem;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.export-section h3 {
color: #4CAF50;
margin-top: 0;
margin-bottom: 1rem;
}
.quick-export-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}

48
templates/export.html Normal file
View File

@@ -0,0 +1,48 @@
{% extends "layout.html" %}
{% block content %}
<div class="timetrack-container">
<h2>Export Time Entries</h2>
<div class="export-options">
<div class="export-section">
<h3>Date Range</h3>
<form action="{{ url_for('download_export') }}" method="get">
<div class="form-group">
<label for="start_date">Start Date:</label>
<input type="date" id="start_date" name="start_date" required>
</div>
<div class="form-group">
<label for="end_date">End Date:</label>
<input type="date" id="end_date" name="end_date" required>
</div>
<div class="form-group">
<label for="format">Export Format:</label>
<select id="format" name="format">
<option value="csv">CSV</option>
<option value="excel">Excel</option>
</select>
</div>
<button type="submit" class="btn">Generate Export</button>
</form>
</div>
<div class="export-section">
<h3>Quick Export</h3>
<div class="quick-export-buttons">
<a href="{{ url_for('download_export', period='today', format='csv') }}" class="btn">Today (CSV)</a>
<a href="{{ url_for('download_export', period='today', format='excel') }}" class="btn">Today (Excel)</a>
<a href="{{ url_for('download_export', period='week', format='csv') }}" class="btn">This Week (CSV)</a>
<a href="{{ url_for('download_export', period='week', format='excel') }}" class="btn">This Week (Excel)</a>
<a href="{{ url_for('download_export', period='month', format='csv') }}" class="btn">This Month (CSV)</a>
<a href="{{ url_for('download_export', period='month', format='excel') }}" class="btn">This Month (Excel)</a>
<a href="{{ url_for('download_export', period='all', format='csv') }}" class="btn">All Time (CSV)</a>
<a href="{{ url_for('download_export', period='all', format='excel') }}" class="btn">All Time (Excel)</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -3,6 +3,9 @@
{% block content %} {% block content %}
<div class="timetrack-container"> <div class="timetrack-container">
<h2>Complete Time Entry History</h2> <h2>Complete Time Entry History</h2>
<div class="export-button-container">
<a href="{{ url_for('export') }}" class="btn">Export Data</a>
</div>
<div class="history-section"> <div class="history-section">
{% if entries %} {% if entries %}

View File

@@ -12,6 +12,7 @@
<ul> <ul>
<li><a href="{{ url_for('home') }}">Home</a></li> <li><a href="{{ url_for('home') }}">Home</a></li>
<li><a href="{{ url_for('history') }}">Complete History</a></li> <li><a href="{{ url_for('history') }}">Complete History</a></li>
<li><a href="{{ url_for('export') }}">Export Data</a></li>
<li><a href="{{ url_for('config') }}" {% if title == 'Configuration' %}class="active"{% endif %}>Configuration</a></li> <li><a href="{{ url_for('config') }}" {% if title == 'Configuration' %}class="active"{% endif %}>Configuration</a></li>
<li><a href="{{ url_for('about') }}">About</a></li> <li><a href="{{ url_for('about') }}">About</a></li>
</ul> </ul>