Files
TODO_list_system/backend/routes/reports.py
beabigegg b0c86302ff 1ST
2025-08-29 16:25:46 +08:00

372 lines
13 KiB
Python

"""
Reports API Routes
提供待辦清單的統計報表和分析
"""
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from datetime import datetime, date, timedelta
from sqlalchemy import func, and_, or_
from models import (
db, TodoItem, TodoItemResponsible, TodoItemFollower,
TodoAuditLog, TodoUserPref
)
from utils.logger import get_logger
import calendar
reports_bp = Blueprint('reports', __name__)
logger = get_logger(__name__)
@reports_bp.route('/summary', methods=['GET'])
@jwt_required()
def get_summary():
"""Get user's todo summary"""
try:
identity = get_jwt_identity()
# Count todos by status for current user
query = TodoItem.query.filter(
or_(
TodoItem.creator_ad == identity,
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == identity),
TodoItem.followers.any(TodoItemFollower.ad_account == identity)
)
)
total = query.count()
completed = query.filter(TodoItem.status == 'DONE').count()
in_progress = query.filter(TodoItem.status == 'IN_PROGRESS').count()
new = query.filter(TodoItem.status == 'NEW').count()
# Overdue todos
today = date.today()
overdue = query.filter(
and_(
TodoItem.due_date < today,
TodoItem.status != 'DONE'
)
).count()
# Due today
due_today = query.filter(
and_(
TodoItem.due_date == today,
TodoItem.status != 'DONE'
)
).count()
# Due this week
week_end = today + timedelta(days=7)
due_this_week = query.filter(
and_(
TodoItem.due_date.between(today, week_end),
TodoItem.status != 'DONE'
)
).count()
# Priority distribution
high_priority = query.filter(TodoItem.priority == 'HIGH').count()
medium_priority = query.filter(TodoItem.priority == 'MEDIUM').count()
low_priority = query.filter(TodoItem.priority == 'LOW').count()
# Completion rate
completion_rate = (completed / total * 100) if total > 0 else 0
return jsonify({
'summary': {
'total': total,
'completed': completed,
'in_progress': in_progress,
'new': new,
'overdue': overdue,
'due_today': due_today,
'due_this_week': due_this_week,
'completion_rate': round(completion_rate, 1)
},
'priority_distribution': {
'high': high_priority,
'medium': medium_priority,
'low': low_priority
}
}), 200
except Exception as e:
logger.error(f"Error fetching summary: {str(e)}")
return jsonify({'error': 'Failed to fetch summary'}), 500
@reports_bp.route('/activity', methods=['GET'])
@jwt_required()
def get_activity():
"""Get user's activity over time"""
try:
identity = get_jwt_identity()
days = request.args.get('days', 30, type=int)
# Get date range
end_date = date.today()
start_date = end_date - timedelta(days=days-1)
# Query audit logs for the user
logs = db.session.query(
func.date(TodoAuditLog.timestamp).label('date'),
func.count(TodoAuditLog.id).label('count'),
TodoAuditLog.action
).filter(
and_(
TodoAuditLog.actor_ad == identity,
func.date(TodoAuditLog.timestamp) >= start_date
)
).group_by(
func.date(TodoAuditLog.timestamp),
TodoAuditLog.action
).all()
# Organize by date and action
activity_data = {}
for log in logs:
date_str = log.date.isoformat()
if date_str not in activity_data:
activity_data[date_str] = {'CREATE': 0, 'UPDATE': 0, 'DELETE': 0}
activity_data[date_str][log.action] = log.count
# Fill in missing dates
current_date = start_date
while current_date <= end_date:
date_str = current_date.isoformat()
if date_str not in activity_data:
activity_data[date_str] = {'CREATE': 0, 'UPDATE': 0, 'DELETE': 0}
current_date += timedelta(days=1)
return jsonify({
'activity': activity_data,
'period': {
'start_date': start_date.isoformat(),
'end_date': end_date.isoformat(),
'days': days
}
}), 200
except Exception as e:
logger.error(f"Error fetching activity: {str(e)}")
return jsonify({'error': 'Failed to fetch activity'}), 500
@reports_bp.route('/productivity', methods=['GET'])
@jwt_required()
def get_productivity():
"""Get productivity metrics"""
try:
identity = get_jwt_identity()
# Get date ranges
today = date.today()
week_start = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)
# Base query for user's todos
base_query = TodoItem.query.filter(
or_(
TodoItem.creator_ad == identity,
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == identity)
)
)
# Today's completions
today_completed = base_query.filter(
and_(
func.date(TodoItem.completed_at) == today,
TodoItem.status == 'DONE'
)
).count()
# This week's completions
week_completed = base_query.filter(
and_(
func.date(TodoItem.completed_at) >= week_start,
TodoItem.status == 'DONE'
)
).count()
# This month's completions
month_completed = base_query.filter(
and_(
func.date(TodoItem.completed_at) >= month_start,
TodoItem.status == 'DONE'
)
).count()
# Average completion time (for completed todos)
completed_todos = base_query.filter(
and_(
TodoItem.status == 'DONE',
TodoItem.completed_at.isnot(None)
)
).all()
avg_completion_days = 0
if completed_todos:
total_days = 0
count = 0
for todo in completed_todos:
if todo.completed_at and todo.created_at:
days = (todo.completed_at.date() - todo.created_at.date()).days
total_days += days
count += 1
avg_completion_days = round(total_days / count, 1) if count > 0 else 0
# On-time completion rate (within due date)
on_time_todos = base_query.filter(
and_(
TodoItem.status == 'DONE',
TodoItem.due_date.isnot(None),
TodoItem.completed_at.isnot(None),
func.date(TodoItem.completed_at) <= TodoItem.due_date
)
).count()
total_due_todos = base_query.filter(
and_(
TodoItem.status == 'DONE',
TodoItem.due_date.isnot(None)
)
).count()
on_time_rate = (on_time_todos / total_due_todos * 100) if total_due_todos > 0 else 0
return jsonify({
'productivity': {
'today_completed': today_completed,
'week_completed': week_completed,
'month_completed': month_completed,
'avg_completion_days': avg_completion_days,
'on_time_rate': round(on_time_rate, 1),
'total_with_due_dates': total_due_todos,
'on_time_count': on_time_todos
}
}), 200
except Exception as e:
logger.error(f"Error fetching productivity: {str(e)}")
return jsonify({'error': 'Failed to fetch productivity metrics'}), 500
@reports_bp.route('/team-overview', methods=['GET'])
@jwt_required()
def get_team_overview():
"""Get team overview for todos created by current user"""
try:
identity = get_jwt_identity()
# Get todos created by current user
created_todos = TodoItem.query.filter(TodoItem.creator_ad == identity)
# Get unique responsible users from these todos
responsible_stats = db.session.query(
TodoItemResponsible.ad_account,
func.count(TodoItem.id).label('total'),
func.sum(func.case([(TodoItem.status == 'DONE', 1)], else_=0)).label('completed'),
func.sum(func.case([(TodoItem.status == 'IN_PROGRESS', 1)], else_=0)).label('in_progress'),
func.sum(func.case([
(and_(TodoItem.due_date < date.today(), TodoItem.status != 'DONE'), 1)
], else_=0)).label('overdue')
).join(
TodoItem, TodoItemResponsible.todo_id == TodoItem.id
).filter(
TodoItem.creator_ad == identity
).group_by(
TodoItemResponsible.ad_account
).all()
team_stats = []
for stat in responsible_stats:
completion_rate = (stat.completed / stat.total * 100) if stat.total > 0 else 0
team_stats.append({
'ad_account': stat.ad_account,
'total_assigned': stat.total,
'completed': stat.completed,
'in_progress': stat.in_progress,
'overdue': stat.overdue,
'completion_rate': round(completion_rate, 1)
})
return jsonify({
'team_overview': team_stats,
'summary': {
'total_team_members': len(team_stats),
'total_assigned_todos': sum(stat['total_assigned'] for stat in team_stats),
'total_completed': sum(stat['completed'] for stat in team_stats),
'total_overdue': sum(stat['overdue'] for stat in team_stats)
}
}), 200
except Exception as e:
logger.error(f"Error fetching team overview: {str(e)}")
return jsonify({'error': 'Failed to fetch team overview'}), 500
@reports_bp.route('/monthly-trends', methods=['GET'])
@jwt_required()
def get_monthly_trends():
"""Get monthly trends for the past year"""
try:
identity = get_jwt_identity()
months = request.args.get('months', 12, type=int)
# Calculate date range
today = date.today()
start_date = today.replace(day=1) - timedelta(days=30 * (months - 1))
# Base query
base_query = TodoItem.query.filter(
or_(
TodoItem.creator_ad == identity,
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == identity)
)
)
# Get monthly statistics
monthly_data = db.session.query(
func.year(TodoItem.created_at).label('year'),
func.month(TodoItem.created_at).label('month'),
func.count(TodoItem.id).label('created'),
func.sum(func.case([(TodoItem.status == 'DONE', 1)], else_=0)).label('completed')
).filter(
and_(
func.date(TodoItem.created_at) >= start_date,
or_(
TodoItem.creator_ad == identity,
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == identity)
)
)
).group_by(
func.year(TodoItem.created_at),
func.month(TodoItem.created_at)
).order_by(
func.year(TodoItem.created_at),
func.month(TodoItem.created_at)
).all()
# Format the data
trends = []
for data in monthly_data:
month_name = calendar.month_name[data.month]
completion_rate = (data.completed / data.created * 100) if data.created > 0 else 0
trends.append({
'year': data.year,
'month': data.month,
'month_name': month_name,
'created': data.created,
'completed': data.completed,
'completion_rate': round(completion_rate, 1)
})
return jsonify({
'trends': trends,
'period': {
'months': months,
'start_date': start_date.isoformat(),
'end_date': today.isoformat()
}
}), 200
except Exception as e:
logger.error(f"Error fetching monthly trends: {str(e)}")
return jsonify({'error': 'Failed to fetch monthly trends'}), 500