Move data export and data formatting functions into own modules.

This commit is contained in:
2025-07-01 11:00:25 +02:00
committed by Jens Luedicke
parent 141c7d4ee4
commit 63de80f752
3 changed files with 595 additions and 571 deletions

385
app.py
View File

@@ -1,5 +1,13 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project
from data_formatting import (
format_duration, prepare_export_data, prepare_team_hours_export_data,
format_table_data, format_graph_data, format_team_data
)
from data_export import (
export_to_csv, export_to_excel, export_team_hours_to_csv, export_team_hours_to_excel,
export_analytics_csv, export_analytics_excel
)
import logging import logging
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
import os import os
@@ -1452,152 +1460,6 @@ def get_date_range(period, start_date_str=None, end_date_str=None):
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValueError('Invalid date format') raise ValueError('Invalid date format')
def format_duration(seconds):
"""Format duration in seconds to HH:MM:SS format."""
if seconds is None:
return '00:00:00'
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
return f"{hours:d}:{minutes:02d}:{seconds:02d}"
def prepare_export_data(entries):
"""Prepare time entries data for export."""
data = []
for entry in entries:
row = {
'Date': entry.arrival_time.strftime('%Y-%m-%d'),
'Project Code': entry.project.code if entry.project else '',
'Project Name': entry.project.name if entry.project else '',
'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)': format_duration(entry.duration) if entry.duration is not None else 'In progress',
'Break Duration (HH:MM:SS)': format_duration(entry.total_break_duration),
'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,
'Notes': entry.notes if entry.notes else ''
}
data.append(row)
return data
def export_to_csv(data, filename):
"""Export data to CSV format."""
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment;filename={filename}.csv'}
)
def export_to_excel(data, filename):
"""Export data to Excel format with formatting."""
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"
)
def prepare_team_hours_export_data(team, team_data, date_range):
"""Prepare team hours data for export."""
export_data = []
for member_data in team_data:
user = member_data['user']
daily_hours = member_data['daily_hours']
# Create base row with member info
row = {
'Team': team['name'],
'Member': user['username'],
'Email': user['email'],
'Total Hours': member_data['total_hours']
}
# Add daily hours columns
for date_str in date_range:
formatted_date = datetime.strptime(date_str, '%Y-%m-%d').strftime('%m/%d/%Y')
row[formatted_date] = daily_hours.get(date_str, 0.0)
export_data.append(row)
return export_data
def export_team_hours_to_csv(data, filename):
"""Export team hours data to CSV format."""
if not data:
return None
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment;filename={filename}.csv'}
)
def export_team_hours_to_excel(data, filename, team_name):
"""Export team hours data to Excel format with formatting."""
if not data:
return None
df = pd.DataFrame(data)
output = io.BytesIO()
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, sheet_name=f'{team_name} Hours', index=False)
# Get the workbook and worksheet objects
workbook = writer.book
worksheet = writer.sheets[f'{team_name} Hours']
# Create formats
header_format = workbook.add_format({
'bold': True,
'text_wrap': True,
'valign': 'top',
'fg_color': '#4CAF50',
'font_color': 'white',
'border': 1
})
# Auto-adjust columns' width and apply formatting
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)
# Apply header formatting
worksheet.write(0, i, col, header_format)
output.seek(0)
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=f"{filename}.xlsx"
)
@app.route('/download_export') @app.route('/download_export')
def download_export(): def download_export():
@@ -1763,101 +1625,6 @@ def get_filtered_analytics_data(user, mode, start_date=None, end_date=None, proj
return query.order_by(TimeEntry.arrival_time.desc()).all() return query.order_by(TimeEntry.arrival_time.desc()).all()
def format_table_data(entries):
"""Format data for table view"""
formatted_entries = []
for entry in entries:
formatted_entry = {
'id': entry.id,
'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',
'duration': format_duration(entry.duration) if entry.duration else 'In progress',
'break_duration': format_duration(entry.total_break_duration),
'project_code': entry.project.code if entry.project else None,
'project_name': entry.project.name if entry.project else 'No Project',
'notes': entry.notes or '',
'user_name': entry.user.username
}
formatted_entries.append(formatted_entry)
return {'entries': formatted_entries}
def format_graph_data(entries, granularity='daily'):
"""Format data for graph visualization"""
from collections import defaultdict
# Group data by date
daily_data = defaultdict(lambda: {'total_hours': 0, 'projects': defaultdict(int)})
project_totals = defaultdict(int)
for entry in entries:
if entry.departure_time and entry.duration:
date_key = entry.arrival_time.strftime('%Y-%m-%d')
hours = entry.duration / 3600 # Convert seconds to hours
daily_data[date_key]['total_hours'] += hours
project_name = entry.project.name if entry.project else 'No Project'
daily_data[date_key]['projects'][project_name] += hours
project_totals[project_name] += hours
# Format time series data
time_series = []
for date, data in sorted(daily_data.items()):
time_series.append({
'date': date,
'hours': round(data['total_hours'], 2)
})
# Format project distribution
project_distribution = [
{'project': project, 'hours': round(hours, 2)}
for project, hours in project_totals.items()
]
return {
'timeSeries': time_series,
'projectDistribution': project_distribution,
'totalHours': sum(project_totals.values()),
'totalDays': len(daily_data)
}
def format_team_data(entries, granularity='daily'):
"""Format data for team view"""
from collections import defaultdict
# Group by user and date
user_data = defaultdict(lambda: {'daily_hours': defaultdict(float), 'total_hours': 0})
for entry in entries:
if entry.departure_time and entry.duration:
date_key = entry.arrival_time.strftime('%Y-%m-%d')
hours = entry.duration / 3600
user_data[entry.user.username]['daily_hours'][date_key] += hours
user_data[entry.user.username]['total_hours'] += hours
# Format for frontend
team_data = []
for username, data in user_data.items():
team_data.append({
'username': username,
'daily_hours': dict(data['daily_hours']),
'total_hours': round(data['total_hours'], 2)
})
return {'team_data': team_data}
def format_duration(seconds):
"""Format duration in seconds to HH:MM:SS"""
if not seconds:
return "00:00:00"
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
@app.route('/api/analytics/export') @app.route('/api/analytics/export')
@login_required @login_required
@@ -1902,142 +1669,6 @@ def analytics_export():
flash('Error generating export', 'error') flash('Error generating export', 'error')
return redirect(url_for('analytics')) return redirect(url_for('analytics'))
def export_analytics_csv(entries, view_type, mode):
"""Export analytics data as CSV"""
output = io.StringIO()
if view_type == 'team':
# Team summary CSV
writer = csv.writer(output)
writer.writerow(['Team Member', 'Total Hours', 'Total Entries'])
# Group by user
user_data = {}
for entry in entries:
if entry.departure_time and entry.duration:
username = entry.user.username
if username not in user_data:
user_data[username] = {'hours': 0, 'entries': 0}
user_data[username]['hours'] += entry.duration / 3600
user_data[username]['entries'] += 1
for username, data in user_data.items():
writer.writerow([username, f"{data['hours']:.2f}", data['entries']])
else:
# Detailed entries CSV
writer = csv.writer(output)
headers = ['Date', 'Arrival Time', 'Departure Time', 'Duration', 'Break Duration', 'Project Code', 'Project Name', 'Notes']
if mode == 'team':
headers.insert(1, 'User')
writer.writerow(headers)
for entry in entries:
row = [
entry.arrival_time.strftime('%Y-%m-%d'),
entry.arrival_time.strftime('%H:%M:%S'),
entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active',
format_duration(entry.duration) if entry.duration else 'In progress',
format_duration(entry.total_break_duration),
entry.project.code if entry.project else '',
entry.project.name if entry.project else 'No Project',
entry.notes or ''
]
if mode == 'team':
row.insert(1, entry.user.username)
writer.writerow(row)
output.seek(0)
filename = f"analytics_{view_type}_{mode}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment; filename={filename}'}
)
def export_analytics_excel(entries, view_type, mode):
"""Export analytics data as Excel"""
try:
output = io.BytesIO()
if view_type == 'team':
# Team summary Excel
user_data = {}
for entry in entries:
if entry.departure_time and entry.duration:
username = entry.user.username
if username not in user_data:
user_data[username] = {'hours': 0, 'entries': 0}
user_data[username]['hours'] += entry.duration / 3600
user_data[username]['entries'] += 1
df = pd.DataFrame([
{
'Team Member': username,
'Total Hours': f"{data['hours']:.2f}",
'Total Entries': data['entries']
}
for username, data in user_data.items()
])
else:
# Detailed entries Excel
data_list = []
for entry in entries:
row_data = {
'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',
'Duration': format_duration(entry.duration) if entry.duration else 'In progress',
'Break Duration': format_duration(entry.total_break_duration),
'Project Code': entry.project.code if entry.project else '',
'Project Name': entry.project.name if entry.project else 'No Project',
'Notes': entry.notes or ''
}
if mode == 'team':
row_data['User'] = entry.user.username
data_list.append(row_data)
df = pd.DataFrame(data_list)
# Write to Excel with formatting
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, sheet_name='Analytics Data', index=False)
# Get workbook and worksheet
workbook = writer.book
worksheet = writer.sheets['Analytics Data']
# Add formatting
header_format = workbook.add_format({
'bold': True,
'text_wrap': True,
'valign': 'top',
'fg_color': '#D7E4BD',
'border': 1
})
# Write headers with formatting
for col_num, value in enumerate(df.columns.values):
worksheet.write(0, col_num, value, header_format)
# Auto-adjust column widths
for i, col in enumerate(df.columns):
max_len = max(df[col].astype(str).apply(len).max(), len(col)) + 2
worksheet.set_column(i, i, min(max_len, 50))
output.seek(0)
filename = f"analytics_{view_type}_{mode}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return Response(
output.getvalue(),
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={'Content-Disposition': f'attachment; filename={filename}'}
)
except Exception as e:
logger.error(f"Error creating Excel export: {str(e)}")
flash('Error generating Excel export.')
return redirect(url_for('analytics'))
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, port=5050) app.run(debug=True, port=5050)

246
data_export.py Normal file
View File

@@ -0,0 +1,246 @@
"""
Data export utilities for TimeTrack application.
Handles exporting time entries and analytics data to various file formats (CSV, Excel).
"""
import io
import csv
import pandas as pd
from flask import Response, send_file
from datetime import datetime
from data_formatting import format_duration
def export_to_csv(data, filename):
"""Export data to CSV format."""
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment;filename={filename}.csv'}
)
def export_to_excel(data, filename):
"""Export data to Excel format with formatting."""
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"
)
def export_team_hours_to_csv(data, filename):
"""Export team hours data to CSV format."""
if not data:
return None
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment;filename={filename}.csv'}
)
def export_team_hours_to_excel(data, filename, team_name):
"""Export team hours data to Excel format with formatting."""
if not data:
return None
df = pd.DataFrame(data)
output = io.BytesIO()
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, sheet_name=f'{team_name} Hours', index=False)
# Get the workbook and worksheet objects
workbook = writer.book
worksheet = writer.sheets[f'{team_name} Hours']
# Create formats
header_format = workbook.add_format({
'bold': True,
'text_wrap': True,
'valign': 'top',
'fg_color': '#4CAF50',
'font_color': 'white',
'border': 1
})
# Auto-adjust columns' width and apply formatting
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)
# Apply header formatting
worksheet.write(0, i, col, header_format)
output.seek(0)
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=f"{filename}.xlsx"
)
def export_analytics_csv(entries, view_type, mode):
"""Export analytics data as CSV."""
output = io.StringIO()
if view_type == 'team':
# Team summary CSV
writer = csv.writer(output)
writer.writerow(['Team Member', 'Total Hours', 'Total Entries'])
# Group by user
user_data = {}
for entry in entries:
if entry.departure_time and entry.duration:
username = entry.user.username
if username not in user_data:
user_data[username] = {'hours': 0, 'entries': 0}
user_data[username]['hours'] += entry.duration / 3600
user_data[username]['entries'] += 1
for username, data in user_data.items():
writer.writerow([username, f"{data['hours']:.2f}", data['entries']])
else:
# Detailed entries CSV
writer = csv.writer(output)
headers = ['Date', 'Arrival Time', 'Departure Time', 'Duration', 'Break Duration', 'Project Code', 'Project Name', 'Notes']
if mode == 'team':
headers.insert(1, 'User')
writer.writerow(headers)
for entry in entries:
row = [
entry.arrival_time.strftime('%Y-%m-%d'),
entry.arrival_time.strftime('%H:%M:%S'),
entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active',
format_duration(entry.duration) if entry.duration else 'In progress',
format_duration(entry.total_break_duration),
entry.project.code if entry.project else '',
entry.project.name if entry.project else 'No Project',
entry.notes or ''
]
if mode == 'team':
row.insert(1, entry.user.username)
writer.writerow(row)
output.seek(0)
filename = f"analytics_{view_type}_{mode}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment; filename={filename}'}
)
def export_analytics_excel(entries, view_type, mode):
"""Export analytics data as Excel."""
try:
output = io.BytesIO()
if view_type == 'team':
# Team summary Excel
user_data = {}
for entry in entries:
if entry.departure_time and entry.duration:
username = entry.user.username
if username not in user_data:
user_data[username] = {'hours': 0, 'entries': 0}
user_data[username]['hours'] += entry.duration / 3600
user_data[username]['entries'] += 1
df = pd.DataFrame([
{
'Team Member': username,
'Total Hours': f"{data['hours']:.2f}",
'Total Entries': data['entries']
}
for username, data in user_data.items()
])
else:
# Detailed entries Excel
data_list = []
for entry in entries:
row_data = {
'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',
'Duration': format_duration(entry.duration) if entry.duration else 'In progress',
'Break Duration': format_duration(entry.total_break_duration),
'Project Code': entry.project.code if entry.project else '',
'Project Name': entry.project.name if entry.project else 'No Project',
'Notes': entry.notes or ''
}
if mode == 'team':
row_data['User'] = entry.user.username
data_list.append(row_data)
df = pd.DataFrame(data_list)
# Write to Excel with formatting
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, sheet_name='Analytics Data', index=False)
# Get workbook and worksheet
workbook = writer.book
worksheet = writer.sheets['Analytics Data']
# Add formatting
header_format = workbook.add_format({
'bold': True,
'text_wrap': True,
'valign': 'top',
'fg_color': '#D7E4BD',
'border': 1
})
# Write headers with formatting
for col_num, value in enumerate(df.columns.values):
worksheet.write(0, col_num, value, header_format)
# Auto-adjust column widths
for i, col in enumerate(df.columns):
max_len = max(df[col].astype(str).apply(len).max(), len(col)) + 2
worksheet.set_column(i, i, min(max_len, 50))
output.seek(0)
filename = f"analytics_{view_type}_{mode}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return Response(
output.getvalue(),
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={'Content-Disposition': f'attachment; filename={filename}'}
)
except Exception as e:
raise Exception(f"Error creating Excel export: {str(e)}")

147
data_formatting.py Normal file
View File

@@ -0,0 +1,147 @@
"""
Data formatting utilities for TimeTrack application.
Handles conversion of time entries and analytics data to various display formats.
"""
from datetime import datetime
from collections import defaultdict
def format_duration(seconds):
"""Format duration in seconds to HH:MM:SS format."""
if seconds is None:
return '00:00:00'
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
return f"{hours:d}:{minutes:02d}:{seconds:02d}"
def prepare_export_data(entries):
"""Prepare time entries data for export."""
data = []
for entry in entries:
row = {
'Date': entry.arrival_time.strftime('%Y-%m-%d'),
'Project Code': entry.project.code if entry.project else '',
'Project Name': entry.project.name if entry.project else '',
'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)': format_duration(entry.duration) if entry.duration is not None else 'In progress',
'Break Duration (HH:MM:SS)': format_duration(entry.total_break_duration),
'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,
'Notes': entry.notes if entry.notes else ''
}
data.append(row)
return data
def prepare_team_hours_export_data(team, team_data, date_range):
"""Prepare team hours data for export."""
export_data = []
for member_data in team_data:
user = member_data['user']
daily_hours = member_data['daily_hours']
# Create base row with member info
row = {
'Team': team['name'],
'Member': user['username'],
'Email': user['email'],
'Total Hours': member_data['total_hours']
}
# Add daily hours columns
for date_str in date_range:
formatted_date = datetime.strptime(date_str, '%Y-%m-%d').strftime('%m/%d/%Y')
row[formatted_date] = daily_hours.get(date_str, 0.0)
export_data.append(row)
return export_data
def format_table_data(entries):
"""Format data for table view in analytics."""
formatted_entries = []
for entry in entries:
formatted_entry = {
'id': entry.id,
'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',
'duration': format_duration(entry.duration) if entry.duration else 'In progress',
'break_duration': format_duration(entry.total_break_duration),
'project_code': entry.project.code if entry.project else None,
'project_name': entry.project.name if entry.project else 'No Project',
'notes': entry.notes or '',
'user_name': entry.user.username
}
formatted_entries.append(formatted_entry)
return {'entries': formatted_entries}
def format_graph_data(entries, granularity='daily'):
"""Format data for graph visualization in analytics."""
# Group data by date
daily_data = defaultdict(lambda: {'total_hours': 0, 'projects': defaultdict(int)})
project_totals = defaultdict(int)
for entry in entries:
if entry.departure_time and entry.duration:
date_key = entry.arrival_time.strftime('%Y-%m-%d')
hours = entry.duration / 3600 # Convert seconds to hours
daily_data[date_key]['total_hours'] += hours
project_name = entry.project.name if entry.project else 'No Project'
daily_data[date_key]['projects'][project_name] += hours
project_totals[project_name] += hours
# Format time series data
time_series = []
for date, data in sorted(daily_data.items()):
time_series.append({
'date': date,
'hours': round(data['total_hours'], 2)
})
# Format project distribution
project_distribution = [
{'project': project, 'hours': round(hours, 2)}
for project, hours in project_totals.items()
]
return {
'timeSeries': time_series,
'projectDistribution': project_distribution,
'totalHours': sum(project_totals.values()),
'totalDays': len(daily_data)
}
def format_team_data(entries, granularity='daily'):
"""Format data for team view in analytics."""
# Group by user and date
user_data = defaultdict(lambda: {'daily_hours': defaultdict(float), 'total_hours': 0})
for entry in entries:
if entry.departure_time and entry.duration:
date_key = entry.arrival_time.strftime('%Y-%m-%d')
hours = entry.duration / 3600
user_data[entry.user.username]['daily_hours'][date_key] += hours
user_data[entry.user.username]['total_hours'] += hours
# Format for frontend
team_data = []
for username, data in user_data.items():
team_data.append({
'username': username,
'daily_hours': dict(data['daily_hours']),
'total_hours': round(data['total_hours'], 2)
})
return {'team_data': team_data}