commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 21:15:41 2025 +0200 Disable resuming of old time entries. commit 3e3ec2f01cb7943622b819a19179388078ae1315 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 20:59:19 2025 +0200 Refactor db migrations. commit 15a51a569da36c6b7c9e01ab17b6fdbdee6ad994 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 19:58:04 2025 +0200 Apply new style for Time Tracking view. commit 77e5278b303e060d2b03853b06277f8aa567ae68 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:06:04 2025 +0200 Allow direct registrations as a Company. commit 188a8772757cbef374243d3a5f29e4440ddecabe Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:04:45 2025 +0200 Add email invitation feature. commit d9ebaa02aa01b518960a20dccdd5a327d82f30c6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 17:12:32 2025 +0200 Apply common style for Company, User, Team management pages. commit 81149caf4d8fc6317e2ab1b4f022b32fc5aa6d22 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 16:44:32 2025 +0200 Move export functions to own module. commit 1a26e19338e73f8849c671471dd15cc3c1b1fe82 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 15:51:15 2025 +0200 Split up models.py. commit 61f1ccd10f721b0ff4dc1eccf30c7a1ee13f204d Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 12:05:28 2025 +0200 Move utility function into own modules. commit 84b341ed35e2c5387819a8b9f9d41eca900ae79f Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:44:24 2025 +0200 Refactor auth functions use. commit 923e311e3da5b26d85845c2832b73b7b17c48adb Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:35:52 2025 +0200 Refactor route nameing and fix bugs along the way. commit f0a5c4419c340e62a2615c60b2a9de28204d2995 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 10:34:33 2025 +0200 Fix URL endpoints in announcement template. commit b74d74542a1c8dc350749e4788a9464d067a88b5 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:25:53 2025 +0200 Move announcements to own module. commit 9563a28021ac46c82c04fe4649b394dbf96f92c7 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:16:30 2025 +0200 Combine Company view and edit templates. commit 6687c373e681d54e4deab6b2582fed5cea9aadf6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 08:17:42 2025 +0200 Move Users, Company and System Administration to own modules. commit 8b7894a2e3eb84bb059f546648b6b9536fea724e Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:40:57 2025 +0200 Move Teams and Projects to own modules. commit d11bf059d99839ecf1f5d7020b8c8c8a2454c00b Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:09:33 2025 +0200 Move Tasks and Sprints to own modules.
208 lines
7.6 KiB
Python
208 lines
7.6 KiB
Python
"""
|
|
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)
|
|
}
|
|
} |