""" 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} def format_burndown_data(tasks, start_date, end_date): """Format data for burndown chart visualization.""" from datetime import datetime, timedelta from models import Task, TaskStatus if not tasks: return {'burndown': {'dates': [], 'remaining': [], 'ideal': []}} # Convert string dates to datetime objects if needed if isinstance(start_date, str): start_date = datetime.strptime(start_date, '%Y-%m-%d').date() if isinstance(end_date, str): end_date = datetime.strptime(end_date, '%Y-%m-%d').date() # Generate date range current_date = start_date dates = [] while current_date <= end_date: dates.append(current_date.strftime('%Y-%m-%d')) current_date += timedelta(days=1) total_tasks = len(tasks) if total_tasks == 0: return {'burndown': {'dates': dates, 'remaining': [0] * len(dates), 'ideal': [0] * len(dates)}} # Calculate ideal burndown (linear decrease from total to 0) total_days = len(dates) ideal_burndown = [] for i in range(total_days): remaining_ideal = total_tasks - (total_tasks * i / (total_days - 1)) if total_days > 1 else 0 ideal_burndown.append(max(0, round(remaining_ideal, 1))) # Calculate actual remaining tasks for each date actual_remaining = [] for date_str in dates: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() # Count tasks not completed by this date remaining_count = 0 for task in tasks: # Task is remaining if: # 1. It's not completed, OR # 2. It was completed after this date if task.status != TaskStatus.DONE: remaining_count += 1 elif task.completed_date and task.completed_date > date_obj: remaining_count += 1 actual_remaining.append(remaining_count) return { 'burndown': { 'dates': dates, 'remaining': actual_remaining, 'ideal': ideal_burndown, 'total_tasks': total_tasks, 'tasks_completed': total_tasks - (actual_remaining[-1] if actual_remaining else total_tasks) } }