Move data export and data formatting functions into own modules.
This commit is contained in:
385
app.py
385
app.py
@@ -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
246
data_export.py
Normal 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
147
data_formatting.py
Normal 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}
|
||||||
Reference in New Issue
Block a user