diff --git a/app.py b/app.py index d5245b4..e23c4e6 100644 --- a/app.py +++ b/app.py @@ -1296,6 +1296,109 @@ def team_hours_data(): 'start_date': start_date.strftime('%Y-%m-%d'), 'end_date': end_date.strftime('%Y-%m-%d') }) +======= +@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__': app.run(debug=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2bf148d..ad8c5a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ Flask-SQLAlchemy==2.5.1 SQLAlchemy==1.4.23 python-dotenv==0.19.0 pyotp==2.6.0 -qrcode[pil]==7.3.1 \ No newline at end of file +qrcode[pil]==7.3.1 diff --git a/static/css/style.css b/static/css/style.css index 39fbe19..125ce04 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -127,19 +127,20 @@ main { } .form-group { - margin-bottom: 15px; + margin-bottom: 1rem; } .form-group label { display: block; - margin-bottom: 5px; + margin-bottom: 0.5rem; font-weight: bold; } .form-group input, -.form-group textarea { +.form-group textarea, +.form-group select { width: 100%; - padding: 8px; + padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; } @@ -158,8 +159,8 @@ button { border: none; border-radius: 4px; cursor: pointer; - background-color: #4CAF50; - color: white; + font-size: 1rem; + text-align: center; } .btn-primary { @@ -706,4 +707,30 @@ input[type="time"]::-webkit-datetime-edit { display: flex; align-items: center; gap: 5px; -} \ No newline at end of file +} +======= +.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; +} diff --git a/templates/export.html b/templates/export.html new file mode 100644 index 0000000..dfa22e0 --- /dev/null +++ b/templates/export.html @@ -0,0 +1,48 @@ +{% extends "layout.html" %} + +{% block content %} +