This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

View File

@@ -0,0 +1,838 @@
"""
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())
}