372 lines
13 KiB
Python
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 |