""" Deadline reporting and dashboard services Provides comprehensive reporting and analytics for deadline management """ from typing import List, Dict, Any, Optional, Tuple from datetime import datetime, date, timezone, timedelta from sqlalchemy.orm import Session, joinedload from sqlalchemy import and_, func, or_, desc, case, extract from decimal import Decimal from app.models import ( Deadline, DeadlineHistory, User, Employee, File, Rolodex, DeadlineType, DeadlinePriority, DeadlineStatus, NotificationFrequency ) from app.utils.logging import app_logger logger = app_logger class DeadlineReportService: """Service for deadline reporting and analytics""" def __init__(self, db: Session): self.db = db def generate_upcoming_deadlines_report( self, start_date: date = None, end_date: date = None, employee_id: Optional[str] = None, user_id: Optional[int] = None, deadline_type: Optional[DeadlineType] = None, priority: Optional[DeadlinePriority] = None ) -> Dict[str, Any]: """Generate comprehensive upcoming deadlines report""" if start_date is None: start_date = date.today() if end_date is None: end_date = start_date + timedelta(days=30) # Build query query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date.between(start_date, end_date) ) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) if deadline_type: query = query.filter(Deadline.deadline_type == deadline_type) if priority: query = query.filter(Deadline.priority == priority) deadlines = query.options( joinedload(Deadline.file), joinedload(Deadline.client), joinedload(Deadline.assigned_to_user), joinedload(Deadline.assigned_to_employee) ).order_by( Deadline.deadline_date.asc(), Deadline.priority.desc() ).all() # Group deadlines by week weeks = {} for deadline in deadlines: # Calculate week start (Monday) days_since_monday = deadline.deadline_date.weekday() week_start = deadline.deadline_date - timedelta(days=days_since_monday) week_key = week_start.strftime("%Y-%m-%d") if week_key not in weeks: weeks[week_key] = { "week_start": week_start, "week_end": week_start + timedelta(days=6), "deadlines": [], "counts": { "total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0 } } deadline_data = { "id": deadline.id, "title": deadline.title, "deadline_date": deadline.deadline_date, "deadline_time": deadline.deadline_time, "priority": deadline.priority.value, "deadline_type": deadline.deadline_type.value, "file_no": deadline.file_no, "client_name": self._get_client_name(deadline), "assigned_to": self._get_assigned_to(deadline), "court_name": deadline.court_name, "case_number": deadline.case_number, "days_until": (deadline.deadline_date - date.today()).days } weeks[week_key]["deadlines"].append(deadline_data) weeks[week_key]["counts"]["total"] += 1 weeks[week_key]["counts"][deadline.priority.value] += 1 # Sort weeks by date sorted_weeks = sorted(weeks.values(), key=lambda x: x["week_start"]) # Calculate summary statistics total_deadlines = len(deadlines) priority_breakdown = {} type_breakdown = {} for priority in DeadlinePriority: count = sum(1 for d in deadlines if d.priority == priority) priority_breakdown[priority.value] = count for deadline_type in DeadlineType: count = sum(1 for d in deadlines if d.deadline_type == deadline_type) type_breakdown[deadline_type.value] = count return { "report_period": { "start_date": start_date, "end_date": end_date, "days": (end_date - start_date).days + 1 }, "filters": { "employee_id": employee_id, "user_id": user_id, "deadline_type": deadline_type.value if deadline_type else None, "priority": priority.value if priority else None }, "summary": { "total_deadlines": total_deadlines, "priority_breakdown": priority_breakdown, "type_breakdown": type_breakdown }, "weeks": sorted_weeks } def generate_overdue_report( self, cutoff_date: date = None, employee_id: Optional[str] = None, user_id: Optional[int] = None ) -> Dict[str, Any]: """Generate report of overdue deadlines""" if cutoff_date is None: cutoff_date = date.today() query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date < cutoff_date ) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) overdue_deadlines = query.options( joinedload(Deadline.file), joinedload(Deadline.client), joinedload(Deadline.assigned_to_user), joinedload(Deadline.assigned_to_employee) ).order_by( Deadline.deadline_date.asc() ).all() # Group by days overdue overdue_groups = { "1-3_days": [], "4-7_days": [], "8-30_days": [], "over_30_days": [] } for deadline in overdue_deadlines: days_overdue = (cutoff_date - deadline.deadline_date).days deadline_data = { "id": deadline.id, "title": deadline.title, "deadline_date": deadline.deadline_date, "priority": deadline.priority.value, "deadline_type": deadline.deadline_type.value, "file_no": deadline.file_no, "client_name": self._get_client_name(deadline), "assigned_to": self._get_assigned_to(deadline), "days_overdue": days_overdue } if days_overdue <= 3: overdue_groups["1-3_days"].append(deadline_data) elif days_overdue <= 7: overdue_groups["4-7_days"].append(deadline_data) elif days_overdue <= 30: overdue_groups["8-30_days"].append(deadline_data) else: overdue_groups["over_30_days"].append(deadline_data) return { "report_date": cutoff_date, "filters": { "employee_id": employee_id, "user_id": user_id }, "summary": { "total_overdue": len(overdue_deadlines), "by_timeframe": { "1-3_days": len(overdue_groups["1-3_days"]), "4-7_days": len(overdue_groups["4-7_days"]), "8-30_days": len(overdue_groups["8-30_days"]), "over_30_days": len(overdue_groups["over_30_days"]) } }, "overdue_groups": overdue_groups } def generate_completion_report( self, start_date: date, end_date: date, employee_id: Optional[str] = None, user_id: Optional[int] = None ) -> Dict[str, Any]: """Generate deadline completion performance report""" # Get all deadlines that were due within the period query = self.db.query(Deadline).filter( Deadline.deadline_date.between(start_date, end_date) ) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) deadlines = query.options( joinedload(Deadline.assigned_to_user), joinedload(Deadline.assigned_to_employee) ).all() # Calculate completion statistics total_deadlines = len(deadlines) completed_on_time = 0 completed_late = 0 still_pending = 0 missed = 0 completion_by_priority = {} completion_by_type = {} completion_by_assignee = {} for deadline in deadlines: # Determine completion status if deadline.status == DeadlineStatus.COMPLETED: if deadline.completed_date and deadline.completed_date.date() <= deadline.deadline_date: completed_on_time += 1 status = "on_time" else: completed_late += 1 status = "late" elif deadline.status == DeadlineStatus.PENDING: if deadline.deadline_date < date.today(): missed += 1 status = "missed" else: still_pending += 1 status = "pending" elif deadline.status == DeadlineStatus.CANCELLED: status = "cancelled" else: status = "other" # Track by priority priority_key = deadline.priority.value if priority_key not in completion_by_priority: completion_by_priority[priority_key] = { "total": 0, "on_time": 0, "late": 0, "missed": 0, "pending": 0, "cancelled": 0 } completion_by_priority[priority_key]["total"] += 1 completion_by_priority[priority_key][status] += 1 # Track by type type_key = deadline.deadline_type.value if type_key not in completion_by_type: completion_by_type[type_key] = { "total": 0, "on_time": 0, "late": 0, "missed": 0, "pending": 0, "cancelled": 0 } completion_by_type[type_key]["total"] += 1 completion_by_type[type_key][status] += 1 # Track by assignee assignee = self._get_assigned_to(deadline) or "Unassigned" if assignee not in completion_by_assignee: completion_by_assignee[assignee] = { "total": 0, "on_time": 0, "late": 0, "missed": 0, "pending": 0, "cancelled": 0 } completion_by_assignee[assignee]["total"] += 1 completion_by_assignee[assignee][status] += 1 # Calculate completion rates completed_total = completed_on_time + completed_late on_time_rate = (completed_on_time / completed_total * 100) if completed_total > 0 else 0 completion_rate = (completed_total / total_deadlines * 100) if total_deadlines > 0 else 0 return { "report_period": { "start_date": start_date, "end_date": end_date }, "filters": { "employee_id": employee_id, "user_id": user_id }, "summary": { "total_deadlines": total_deadlines, "completed_on_time": completed_on_time, "completed_late": completed_late, "still_pending": still_pending, "missed": missed, "on_time_rate": round(on_time_rate, 2), "completion_rate": round(completion_rate, 2) }, "breakdown": { "by_priority": completion_by_priority, "by_type": completion_by_type, "by_assignee": completion_by_assignee } } def generate_workload_report( self, target_date: date = None, days_ahead: int = 30 ) -> Dict[str, Any]: """Generate workload distribution report by assignee""" if target_date is None: target_date = date.today() end_date = target_date + timedelta(days=days_ahead) # Get pending deadlines in the timeframe deadlines = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date.between(target_date, end_date) ).options( joinedload(Deadline.assigned_to_user), joinedload(Deadline.assigned_to_employee), joinedload(Deadline.file), joinedload(Deadline.client) ).all() # Group by assignee workload_by_assignee = {} unassigned_deadlines = [] for deadline in deadlines: assignee = self._get_assigned_to(deadline) if not assignee: unassigned_deadlines.append({ "id": deadline.id, "title": deadline.title, "deadline_date": deadline.deadline_date, "priority": deadline.priority.value, "deadline_type": deadline.deadline_type.value, "file_no": deadline.file_no }) continue if assignee not in workload_by_assignee: workload_by_assignee[assignee] = { "total_deadlines": 0, "critical": 0, "high": 0, "medium": 0, "low": 0, "overdue": 0, "due_this_week": 0, "due_next_week": 0, "deadlines": [] } # Count by priority workload_by_assignee[assignee]["total_deadlines"] += 1 workload_by_assignee[assignee][deadline.priority.value] += 1 # Count by timeframe days_until = (deadline.deadline_date - target_date).days if days_until < 0: workload_by_assignee[assignee]["overdue"] += 1 elif days_until <= 7: workload_by_assignee[assignee]["due_this_week"] += 1 elif days_until <= 14: workload_by_assignee[assignee]["due_next_week"] += 1 workload_by_assignee[assignee]["deadlines"].append({ "id": deadline.id, "title": deadline.title, "deadline_date": deadline.deadline_date, "priority": deadline.priority.value, "deadline_type": deadline.deadline_type.value, "file_no": deadline.file_no, "days_until": days_until }) # Sort assignees by workload sorted_assignees = sorted( workload_by_assignee.items(), key=lambda x: (x[1]["critical"] + x[1]["high"], x[1]["total_deadlines"]), reverse=True ) return { "report_date": target_date, "timeframe_days": days_ahead, "summary": { "total_assignees": len(workload_by_assignee), "total_deadlines": len(deadlines), "unassigned_deadlines": len(unassigned_deadlines) }, "workload_by_assignee": dict(sorted_assignees), "unassigned_deadlines": unassigned_deadlines } def generate_trends_report( self, start_date: date, end_date: date, granularity: str = "month" # "week", "month", "quarter" ) -> Dict[str, Any]: """Generate deadline trends and analytics over time""" # Get all deadlines created within the period deadlines = self.db.query(Deadline).filter( func.date(Deadline.created_at) >= start_date, func.date(Deadline.created_at) <= end_date ).all() # Group by time periods periods = {} for deadline in deadlines: created_date = deadline.created_at.date() if granularity == "week": # Get Monday of the week days_since_monday = created_date.weekday() period_start = created_date - timedelta(days=days_since_monday) period_key = period_start.strftime("%Y-W%U") elif granularity == "month": period_key = created_date.strftime("%Y-%m") elif granularity == "quarter": quarter = (created_date.month - 1) // 3 + 1 period_key = f"{created_date.year}-Q{quarter}" else: period_key = created_date.strftime("%Y-%m-%d") if period_key not in periods: periods[period_key] = { "total_created": 0, "completed": 0, "missed": 0, "pending": 0, "by_type": {}, "by_priority": {}, "avg_completion_days": 0 } periods[period_key]["total_created"] += 1 # Track completion status if deadline.status == DeadlineStatus.COMPLETED: periods[period_key]["completed"] += 1 elif deadline.status == DeadlineStatus.PENDING and deadline.deadline_date < date.today(): periods[period_key]["missed"] += 1 else: periods[period_key]["pending"] += 1 # Track by type and priority type_key = deadline.deadline_type.value priority_key = deadline.priority.value if type_key not in periods[period_key]["by_type"]: periods[period_key]["by_type"][type_key] = 0 periods[period_key]["by_type"][type_key] += 1 if priority_key not in periods[period_key]["by_priority"]: periods[period_key]["by_priority"][priority_key] = 0 periods[period_key]["by_priority"][priority_key] += 1 # Calculate trends sorted_periods = sorted(periods.items()) return { "report_period": { "start_date": start_date, "end_date": end_date, "granularity": granularity }, "summary": { "total_periods": len(periods), "total_deadlines": len(deadlines) }, "trends": { "by_period": sorted_periods } } # Private helper methods def _get_client_name(self, deadline: Deadline) -> Optional[str]: """Get formatted client name from deadline""" if deadline.client: return f"{deadline.client.first or ''} {deadline.client.last or ''}".strip() elif deadline.file and deadline.file.owner: return f"{deadline.file.owner.first or ''} {deadline.file.owner.last or ''}".strip() return None def _get_assigned_to(self, deadline: Deadline) -> Optional[str]: """Get assigned person name from deadline""" if deadline.assigned_to_user: return deadline.assigned_to_user.username elif deadline.assigned_to_employee: employee = deadline.assigned_to_employee return f"{employee.first_name or ''} {employee.last_name or ''}".strip() return None class DeadlineDashboardService: """Service for deadline dashboard widgets and summaries""" def __init__(self, db: Session): self.db = db self.report_service = DeadlineReportService(db) def get_dashboard_widgets( self, user_id: Optional[int] = None, employee_id: Optional[str] = None ) -> Dict[str, Any]: """Get all dashboard widgets for deadline management""" today = date.today() return { "summary_cards": self._get_summary_cards(user_id, employee_id), "upcoming_deadlines": self._get_upcoming_deadlines_widget(user_id, employee_id), "overdue_alerts": self._get_overdue_alerts_widget(user_id, employee_id), "priority_breakdown": self._get_priority_breakdown_widget(user_id, employee_id), "recent_completions": self._get_recent_completions_widget(user_id, employee_id), "weekly_calendar": self._get_weekly_calendar_widget(today, user_id, employee_id) } def _get_summary_cards( self, user_id: Optional[int] = None, employee_id: Optional[str] = None ) -> List[Dict[str, Any]]: """Get summary cards for dashboard""" base_query = self.db.query(Deadline).filter(Deadline.status == DeadlineStatus.PENDING) if user_id: base_query = base_query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: base_query = base_query.filter(Deadline.assigned_to_employee_id == employee_id) today = date.today() # Calculate counts total_pending = base_query.count() overdue = base_query.filter(Deadline.deadline_date < today).count() due_today = base_query.filter(Deadline.deadline_date == today).count() due_this_week = base_query.filter( Deadline.deadline_date.between(today, today + timedelta(days=7)) ).count() return [ { "title": "Total Pending", "value": total_pending, "icon": "calendar", "color": "blue" }, { "title": "Overdue", "value": overdue, "icon": "exclamation-triangle", "color": "red" }, { "title": "Due Today", "value": due_today, "icon": "clock", "color": "orange" }, { "title": "Due This Week", "value": due_this_week, "icon": "calendar-week", "color": "green" } ] def _get_upcoming_deadlines_widget( self, user_id: Optional[int] = None, employee_id: Optional[str] = None, limit: int = 5 ) -> Dict[str, Any]: """Get upcoming deadlines widget""" query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date >= date.today() ) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) deadlines = query.options( joinedload(Deadline.file), joinedload(Deadline.client) ).order_by( Deadline.deadline_date.asc(), Deadline.priority.desc() ).limit(limit).all() return { "title": "Upcoming Deadlines", "deadlines": [ { "id": d.id, "title": d.title, "deadline_date": d.deadline_date, "priority": d.priority.value, "deadline_type": d.deadline_type.value, "file_no": d.file_no, "client_name": self.report_service._get_client_name(d), "days_until": (d.deadline_date - date.today()).days } for d in deadlines ] } def _get_overdue_alerts_widget( self, user_id: Optional[int] = None, employee_id: Optional[str] = None ) -> Dict[str, Any]: """Get overdue alerts widget""" query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date < date.today() ) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) overdue_deadlines = query.options( joinedload(Deadline.file), joinedload(Deadline.client) ).order_by( Deadline.deadline_date.asc() ).limit(10).all() return { "title": "Overdue Deadlines", "count": len(overdue_deadlines), "deadlines": [ { "id": d.id, "title": d.title, "deadline_date": d.deadline_date, "priority": d.priority.value, "file_no": d.file_no, "client_name": self.report_service._get_client_name(d), "days_overdue": (date.today() - d.deadline_date).days } for d in overdue_deadlines ] } def _get_priority_breakdown_widget( self, user_id: Optional[int] = None, employee_id: Optional[str] = None ) -> Dict[str, Any]: """Get priority breakdown widget""" base_query = self.db.query(Deadline).filter(Deadline.status == DeadlineStatus.PENDING) if user_id: base_query = base_query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: base_query = base_query.filter(Deadline.assigned_to_employee_id == employee_id) breakdown = {} for priority in DeadlinePriority: count = base_query.filter(Deadline.priority == priority).count() breakdown[priority.value] = count return { "title": "Priority Breakdown", "breakdown": breakdown } def _get_recent_completions_widget( self, user_id: Optional[int] = None, employee_id: Optional[str] = None, days_back: int = 7 ) -> Dict[str, Any]: """Get recent completions widget""" cutoff_date = date.today() - timedelta(days=days_back) query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.COMPLETED, func.date(Deadline.completed_date) >= cutoff_date ) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) completed = query.options( joinedload(Deadline.file), joinedload(Deadline.client) ).order_by( Deadline.completed_date.desc() ).limit(5).all() return { "title": "Recently Completed", "count": len(completed), "deadlines": [ { "id": d.id, "title": d.title, "deadline_date": d.deadline_date, "completed_date": d.completed_date.date() if d.completed_date else None, "priority": d.priority.value, "file_no": d.file_no, "client_name": self.report_service._get_client_name(d), "on_time": d.completed_date.date() <= d.deadline_date if d.completed_date else False } for d in completed ] } def _get_weekly_calendar_widget( self, week_start: date, user_id: Optional[int] = None, employee_id: Optional[str] = None ) -> Dict[str, Any]: """Get weekly calendar widget""" # Adjust to Monday days_since_monday = week_start.weekday() monday = week_start - timedelta(days=days_since_monday) sunday = monday + timedelta(days=6) query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date.between(monday, sunday) ) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) deadlines = query.options( joinedload(Deadline.file), joinedload(Deadline.client) ).order_by( Deadline.deadline_date.asc(), Deadline.deadline_time.asc() ).all() # Group by day calendar_days = {} for i in range(7): day = monday + timedelta(days=i) calendar_days[day.strftime("%Y-%m-%d")] = { "date": day, "day_name": day.strftime("%A"), "deadlines": [] } for deadline in deadlines: day_key = deadline.deadline_date.strftime("%Y-%m-%d") if day_key in calendar_days: calendar_days[day_key]["deadlines"].append({ "id": deadline.id, "title": deadline.title, "deadline_time": deadline.deadline_time, "priority": deadline.priority.value, "deadline_type": deadline.deadline_type.value, "file_no": deadline.file_no }) return { "title": "This Week", "week_start": monday, "week_end": sunday, "days": list(calendar_days.values()) }