Files
delphi-database/app/api/deadlines.py
HotSwapp bac8cc4bd5 changes
2025-08-18 20:20:04 -05:00

1103 lines
40 KiB
Python

"""
Deadline management API endpoints
"""
from typing import List, Optional, Dict, Any
from datetime import date, datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field, ConfigDict
from app.database.base import get_db
from app.models import (
Deadline, DeadlineTemplate, DeadlineHistory, User,
DeadlineType, DeadlinePriority, DeadlineStatus, NotificationFrequency
)
from app.services.deadlines import DeadlineService, DeadlineTemplateService, DeadlineManagementError
from app.services.deadline_notifications import DeadlineNotificationService, DeadlineAlertManager
from app.services.deadline_reports import DeadlineReportService, DeadlineDashboardService
from app.services.deadline_calendar import DeadlineCalendarService, CalendarExportService
from app.auth.security import get_current_user
from app.utils.logging import app_logger
router = APIRouter()
logger = app_logger
# Pydantic schemas for requests/responses
class DeadlineCreateRequest(BaseModel):
"""Request to create a new deadline"""
title: str = Field(..., min_length=1, max_length=200, description="Deadline title")
description: Optional[str] = Field(None, description="Detailed description")
deadline_date: date = Field(..., description="Deadline date")
deadline_time: Optional[datetime] = Field(None, description="Specific deadline time")
deadline_type: DeadlineType = Field(DeadlineType.OTHER, description="Type of deadline")
priority: DeadlinePriority = Field(DeadlinePriority.MEDIUM, description="Priority level")
file_no: Optional[str] = Field(None, description="Associated file number")
client_id: Optional[str] = Field(None, description="Associated client ID")
assigned_to_user_id: Optional[int] = Field(None, description="Assigned user ID")
assigned_to_employee_id: Optional[str] = Field(None, description="Assigned employee ID")
court_name: Optional[str] = Field(None, description="Court name if applicable")
case_number: Optional[str] = Field(None, description="Case number if applicable")
advance_notice_days: int = Field(7, ge=0, le=365, description="Days advance notice")
notification_frequency: NotificationFrequency = Field(NotificationFrequency.WEEKLY, description="Notification frequency")
class DeadlineUpdateRequest(BaseModel):
"""Request to update a deadline"""
title: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = None
deadline_date: Optional[date] = None
deadline_time: Optional[datetime] = None
deadline_type: Optional[DeadlineType] = None
priority: Optional[DeadlinePriority] = None
assigned_to_user_id: Optional[int] = None
assigned_to_employee_id: Optional[str] = None
court_name: Optional[str] = None
case_number: Optional[str] = None
advance_notice_days: Optional[int] = Field(None, ge=0, le=365)
notification_frequency: Optional[NotificationFrequency] = None
class DeadlineCompleteRequest(BaseModel):
"""Request to complete a deadline"""
completion_notes: Optional[str] = Field(None, description="Notes about completion")
class DeadlineExtendRequest(BaseModel):
"""Request to extend a deadline"""
new_deadline_date: date = Field(..., description="New deadline date")
extension_reason: Optional[str] = Field(None, description="Reason for extension")
extension_granted_by: Optional[str] = Field(None, description="Who granted the extension")
class DeadlineCancelRequest(BaseModel):
"""Request to cancel a deadline"""
cancellation_reason: Optional[str] = Field(None, description="Reason for cancellation")
class DeadlineResponse(BaseModel):
"""Response for deadline data"""
id: int
title: str
description: Optional[str] = None
deadline_date: date
deadline_time: Optional[datetime] = None
deadline_type: DeadlineType
priority: DeadlinePriority
status: DeadlineStatus
file_no: Optional[str] = None
client_id: Optional[str] = None
assigned_to_user_id: Optional[int] = None
assigned_to_employee_id: Optional[str] = None
court_name: Optional[str] = None
case_number: Optional[str] = None
advance_notice_days: int
notification_frequency: NotificationFrequency
completed_date: Optional[datetime] = None
completion_notes: Optional[str] = None
original_deadline_date: Optional[date] = None
extension_reason: Optional[str] = None
extension_granted_by: Optional[str] = None
created_at: datetime
updated_at: datetime
is_overdue: bool = False
days_until_deadline: int = 0
model_config = ConfigDict(from_attributes=True)
class DeadlineTemplateCreateRequest(BaseModel):
"""Request to create a deadline template"""
name: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = None
deadline_type: DeadlineType = Field(..., description="Type of deadline")
priority: DeadlinePriority = Field(DeadlinePriority.MEDIUM, description="Default priority")
default_title_template: Optional[str] = Field(None, description="Title template with placeholders")
default_description_template: Optional[str] = Field(None, description="Description template")
default_advance_notice_days: int = Field(7, ge=0, le=365)
default_notification_frequency: NotificationFrequency = Field(NotificationFrequency.WEEKLY)
days_from_file_open: Optional[int] = Field(None, ge=0, description="Days from file open date")
days_from_event: Optional[int] = Field(None, ge=0, description="Days from triggering event")
class DeadlineTemplateResponse(BaseModel):
"""Response for deadline template data"""
id: int
name: str
description: Optional[str] = None
deadline_type: DeadlineType
priority: DeadlinePriority
default_title_template: Optional[str] = None
default_description_template: Optional[str] = None
default_advance_notice_days: int
default_notification_frequency: NotificationFrequency
days_from_file_open: Optional[int] = None
days_from_event: Optional[int] = None
active: bool
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class DeadlineStatisticsResponse(BaseModel):
"""Response for deadline statistics"""
total_deadlines: int
pending_deadlines: int
completed_deadlines: int
overdue_deadlines: int
completion_rate: float
priority_breakdown: Dict[str, int]
type_breakdown: Dict[str, int]
upcoming: Dict[str, int]
class DeadlineFromTemplateRequest(BaseModel):
"""Request to create deadline from template"""
template_id: int = Field(..., description="Template ID to use")
file_no: Optional[str] = Field(None, description="File number for context")
client_id: Optional[str] = Field(None, description="Client ID for context")
deadline_date: Optional[date] = Field(None, description="Override calculated deadline date")
title: Optional[str] = Field(None, description="Override template title")
description: Optional[str] = Field(None, description="Override template description")
priority: Optional[DeadlinePriority] = Field(None, description="Override template priority")
assigned_to_user_id: Optional[int] = Field(None, description="Assign to specific user")
assigned_to_employee_id: Optional[str] = Field(None, description="Assign to specific employee")
# Deadline CRUD endpoints
@router.post("/", response_model=DeadlineResponse)
async def create_deadline(
request: DeadlineCreateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create a new deadline"""
try:
service = DeadlineService(db)
deadline = service.create_deadline(
title=request.title,
description=request.description,
deadline_date=request.deadline_date,
deadline_time=request.deadline_time,
deadline_type=request.deadline_type,
priority=request.priority,
file_no=request.file_no,
client_id=request.client_id,
assigned_to_user_id=request.assigned_to_user_id,
assigned_to_employee_id=request.assigned_to_employee_id,
court_name=request.court_name,
case_number=request.case_number,
advance_notice_days=request.advance_notice_days,
notification_frequency=request.notification_frequency,
created_by_user_id=current_user.id
)
# Add computed properties
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
return response
except DeadlineManagementError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.get("/", response_model=List[DeadlineResponse])
async def get_deadlines(
file_no: Optional[str] = Query(None, description="Filter by file number"),
client_id: Optional[str] = Query(None, description="Filter by client ID"),
assigned_to_user_id: Optional[int] = Query(None, description="Filter by assigned user"),
assigned_to_employee_id: Optional[str] = Query(None, description="Filter by assigned employee"),
deadline_type: Optional[DeadlineType] = Query(None, description="Filter by deadline type"),
priority: Optional[DeadlinePriority] = Query(None, description="Filter by priority"),
status: Optional[DeadlineStatus] = Query(None, description="Filter by status"),
upcoming_days: Optional[int] = Query(None, ge=1, le=365, description="Filter upcoming deadlines within N days"),
overdue_only: bool = Query(False, description="Show only overdue deadlines"),
limit: int = Query(100, ge=1, le=500, description="Maximum number of results"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get deadlines with optional filtering"""
service = DeadlineService(db)
if overdue_only:
deadlines = service.get_overdue_deadlines(
user_id=assigned_to_user_id,
employee_id=assigned_to_employee_id
)
elif upcoming_days:
deadlines = service.get_upcoming_deadlines(
days_ahead=upcoming_days,
user_id=assigned_to_user_id,
employee_id=assigned_to_employee_id,
priority=priority,
deadline_type=deadline_type
)
else:
# Build custom query
query = db.query(Deadline)
if file_no:
query = query.filter(Deadline.file_no == file_no)
if client_id:
query = query.filter(Deadline.client_id == client_id)
if assigned_to_user_id:
query = query.filter(Deadline.assigned_to_user_id == assigned_to_user_id)
if assigned_to_employee_id:
query = query.filter(Deadline.assigned_to_employee_id == assigned_to_employee_id)
if deadline_type:
query = query.filter(Deadline.deadline_type == deadline_type)
if priority:
query = query.filter(Deadline.priority == priority)
if status:
query = query.filter(Deadline.status == status)
deadlines = query.order_by(Deadline.deadline_date.asc()).limit(limit).all()
# Convert to response format with computed properties
responses = []
for deadline in deadlines:
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
responses.append(response)
return responses
@router.get("/{deadline_id}", response_model=DeadlineResponse)
async def get_deadline(
deadline_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get a specific deadline by ID"""
deadline = db.query(Deadline).filter(Deadline.id == deadline_id).first()
if not deadline:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Deadline not found"
)
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
return response
@router.put("/{deadline_id}", response_model=DeadlineResponse)
async def update_deadline(
deadline_id: int,
request: DeadlineUpdateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update a deadline"""
try:
service = DeadlineService(db)
# Only update provided fields
updates = {k: v for k, v in request.model_dump(exclude_unset=True).items() if v is not None}
deadline = service.update_deadline(
deadline_id=deadline_id,
user_id=current_user.id,
**updates
)
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
return response
except DeadlineManagementError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.delete("/{deadline_id}")
async def delete_deadline(
deadline_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Delete a deadline"""
deadline = db.query(Deadline).filter(Deadline.id == deadline_id).first()
if not deadline:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Deadline not found"
)
db.delete(deadline)
db.commit()
return {"message": "Deadline deleted successfully"}
# Deadline action endpoints
@router.post("/{deadline_id}/complete", response_model=DeadlineResponse)
async def complete_deadline(
deadline_id: int,
request: DeadlineCompleteRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Mark a deadline as completed"""
try:
service = DeadlineService(db)
deadline = service.complete_deadline(
deadline_id=deadline_id,
user_id=current_user.id,
completion_notes=request.completion_notes
)
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
return response
except DeadlineManagementError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/{deadline_id}/extend", response_model=DeadlineResponse)
async def extend_deadline(
deadline_id: int,
request: DeadlineExtendRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Extend a deadline to a new date"""
try:
service = DeadlineService(db)
deadline = service.extend_deadline(
deadline_id=deadline_id,
new_deadline_date=request.new_deadline_date,
user_id=current_user.id,
extension_reason=request.extension_reason,
extension_granted_by=request.extension_granted_by
)
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
return response
except DeadlineManagementError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/{deadline_id}/cancel", response_model=DeadlineResponse)
async def cancel_deadline(
deadline_id: int,
request: DeadlineCancelRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Cancel a deadline"""
try:
service = DeadlineService(db)
deadline = service.cancel_deadline(
deadline_id=deadline_id,
user_id=current_user.id,
cancellation_reason=request.cancellation_reason
)
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
return response
except DeadlineManagementError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Deadline template endpoints
@router.post("/templates/", response_model=DeadlineTemplateResponse)
async def create_deadline_template(
request: DeadlineTemplateCreateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create a new deadline template"""
try:
service = DeadlineTemplateService(db)
template = service.create_template(
name=request.name,
description=request.description,
deadline_type=request.deadline_type,
priority=request.priority,
default_title_template=request.default_title_template,
default_description_template=request.default_description_template,
default_advance_notice_days=request.default_advance_notice_days,
default_notification_frequency=request.default_notification_frequency,
days_from_file_open=request.days_from_file_open,
days_from_event=request.days_from_event,
user_id=current_user.id
)
return DeadlineTemplateResponse.model_validate(template)
except DeadlineManagementError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.get("/templates/", response_model=List[DeadlineTemplateResponse])
async def get_deadline_templates(
deadline_type: Optional[DeadlineType] = Query(None, description="Filter by deadline type"),
active_only: bool = Query(True, description="Return only active templates"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get deadline templates"""
service = DeadlineTemplateService(db)
if active_only:
templates = service.get_active_templates(deadline_type=deadline_type)
else:
query = db.query(DeadlineTemplate)
if deadline_type:
query = query.filter(DeadlineTemplate.deadline_type == deadline_type)
templates = query.order_by(DeadlineTemplate.name).all()
return [DeadlineTemplateResponse.model_validate(t) for t in templates]
@router.post("/from-template/", response_model=DeadlineResponse)
async def create_deadline_from_template(
request: DeadlineFromTemplateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create a deadline from a template"""
try:
service = DeadlineService(db)
# Build overrides from request
overrides = {}
if request.title:
overrides['title'] = request.title
if request.description:
overrides['description'] = request.description
if request.priority:
overrides['priority'] = request.priority
if request.assigned_to_user_id:
overrides['assigned_to_user_id'] = request.assigned_to_user_id
if request.assigned_to_employee_id:
overrides['assigned_to_employee_id'] = request.assigned_to_employee_id
deadline = service.create_deadline_from_template(
template_id=request.template_id,
user_id=current_user.id,
file_no=request.file_no,
client_id=request.client_id,
deadline_date=request.deadline_date,
**overrides
)
response = DeadlineResponse.model_validate(deadline)
response.is_overdue = deadline.is_overdue
response.days_until_deadline = deadline.days_until_deadline
return response
except DeadlineManagementError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Statistics and reporting endpoints
@router.get("/statistics/", response_model=DeadlineStatisticsResponse)
async def get_deadline_statistics(
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
start_date: Optional[date] = Query(None, description="Start date for filtering"),
end_date: Optional[date] = Query(None, description="End date for filtering"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get deadline statistics for reporting"""
service = DeadlineService(db)
stats = service.get_deadline_statistics(
user_id=user_id,
employee_id=employee_id,
start_date=start_date,
end_date=end_date
)
return DeadlineStatisticsResponse(**stats)
@router.get("/{deadline_id}/history/")
async def get_deadline_history(
deadline_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get history of changes for a deadline"""
# Verify deadline exists
deadline = db.query(Deadline).filter(Deadline.id == deadline_id).first()
if not deadline:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Deadline not found"
)
history = db.query(DeadlineHistory).filter(
DeadlineHistory.deadline_id == deadline_id
).order_by(DeadlineHistory.change_date.desc()).all()
return [
{
"id": h.id,
"change_type": h.change_type,
"field_changed": h.field_changed,
"old_value": h.old_value,
"new_value": h.new_value,
"change_reason": h.change_reason,
"user_id": h.user_id,
"change_date": h.change_date
}
for h in history
]
# Utility endpoints
@router.get("/types/")
async def get_deadline_types():
"""Get available deadline types"""
return [{"value": dt.value, "name": dt.value.replace("_", " ").title()} for dt in DeadlineType]
@router.get("/priorities/")
async def get_deadline_priorities():
"""Get available deadline priorities"""
return [{"value": dp.value, "name": dp.value.replace("_", " ").title()} for dp in DeadlinePriority]
@router.get("/statuses/")
async def get_deadline_statuses():
"""Get available deadline statuses"""
return [{"value": ds.value, "name": ds.value.replace("_", " ").title()} for ds in DeadlineStatus]
@router.get("/notification-frequencies/")
async def get_notification_frequencies():
"""Get available notification frequencies"""
return [{"value": nf.value, "name": nf.value.replace("_", " ").title()} for nf in NotificationFrequency]
# Notification and alert endpoints
@router.get("/alerts/urgent/")
async def get_urgent_alerts(
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get urgent deadline alerts that need immediate attention"""
notification_service = DeadlineNotificationService(db)
# If no filters provided, default to current user
if not user_id and not employee_id:
user_id = current_user.id
alerts = notification_service.get_urgent_alerts(
user_id=user_id,
employee_id=employee_id
)
return alerts
@router.get("/dashboard/summary/")
async def get_dashboard_summary(
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get deadline summary for dashboard display"""
notification_service = DeadlineNotificationService(db)
# If no filters provided, default to current user
if not user_id and not employee_id:
user_id = current_user.id
summary = notification_service.get_dashboard_summary(
user_id=user_id,
employee_id=employee_id
)
return summary
class AdhocReminderRequest(BaseModel):
"""Request to create an ad-hoc reminder"""
recipient_user_id: int = Field(..., description="User to receive the reminder")
reminder_date: date = Field(..., description="When to send the reminder")
custom_message: Optional[str] = Field(None, description="Custom reminder message")
@router.post("/{deadline_id}/reminders/", response_model=dict)
async def create_adhoc_reminder(
deadline_id: int,
request: AdhocReminderRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create an ad-hoc reminder for a deadline"""
try:
notification_service = DeadlineNotificationService(db)
reminder = notification_service.create_adhoc_reminder(
deadline_id=deadline_id,
recipient_user_id=request.recipient_user_id,
reminder_date=request.reminder_date,
custom_message=request.custom_message
)
return {
"message": "Ad-hoc reminder created successfully",
"reminder_id": reminder.id,
"recipient_user_id": reminder.recipient_user_id,
"reminder_date": reminder.reminder_date
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
class CourtDateRemindersRequest(BaseModel):
"""Request to schedule court date reminders"""
court_date: date = Field(..., description="Court appearance date")
preparation_days: int = Field(7, ge=1, le=30, description="Days needed for preparation")
@router.post("/{deadline_id}/court-reminders/")
async def schedule_court_date_reminders(
deadline_id: int,
request: CourtDateRemindersRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Schedule special reminders for court dates with preparation milestones"""
try:
notification_service = DeadlineNotificationService(db)
notification_service.schedule_court_date_reminders(
deadline_id=deadline_id,
court_date=request.court_date,
preparation_days=request.preparation_days
)
return {
"message": "Court date reminders scheduled successfully",
"court_date": request.court_date,
"preparation_days": request.preparation_days
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.get("/notifications/preferences/")
async def get_notification_preferences(
user_id: Optional[int] = Query(None, description="User ID (defaults to current user)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get user's notification preferences"""
notification_service = DeadlineNotificationService(db)
target_user_id = user_id or current_user.id
preferences = notification_service.get_notification_preferences(target_user_id)
return preferences
@router.post("/alerts/process-daily/")
async def process_daily_alerts(
process_date: Optional[date] = Query(None, description="Date to process (defaults to today)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Process daily deadline alerts and reminders (admin function)"""
try:
alert_manager = DeadlineAlertManager(db)
results = alert_manager.run_daily_alert_processing(process_date)
return results
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to process daily alerts: {str(e)}"
)
@router.get("/alerts/overdue-escalations/")
async def get_overdue_escalations(
escalation_days: int = Query(1, ge=1, le=30, description="Days overdue before escalation"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get deadlines that need escalation due to being overdue"""
alert_manager = DeadlineAlertManager(db)
escalations = alert_manager.escalate_overdue_deadlines(escalation_days)
return {
"escalation_days": escalation_days,
"total_escalations": len(escalations),
"escalations": escalations
}
# Reporting endpoints
@router.get("/reports/upcoming/")
async def get_upcoming_deadlines_report(
start_date: Optional[date] = Query(None, description="Start date for report"),
end_date: Optional[date] = Query(None, description="End date for report"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
user_id: Optional[int] = Query(None, description="Filter by user ID"),
deadline_type: Optional[DeadlineType] = Query(None, description="Filter by deadline type"),
priority: Optional[DeadlinePriority] = Query(None, description="Filter by priority"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Generate upcoming deadlines report"""
report_service = DeadlineReportService(db)
report = report_service.generate_upcoming_deadlines_report(
start_date=start_date,
end_date=end_date,
employee_id=employee_id,
user_id=user_id,
deadline_type=deadline_type,
priority=priority
)
return report
@router.get("/reports/overdue/")
async def get_overdue_report(
cutoff_date: Optional[date] = Query(None, description="Cutoff date (defaults to today)"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
user_id: Optional[int] = Query(None, description="Filter by user ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Generate overdue deadlines report"""
report_service = DeadlineReportService(db)
report = report_service.generate_overdue_report(
cutoff_date=cutoff_date,
employee_id=employee_id,
user_id=user_id
)
return report
@router.get("/reports/completion/")
async def get_completion_report(
start_date: date = Query(..., description="Start date for report period"),
end_date: date = Query(..., description="End date for report period"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
user_id: Optional[int] = Query(None, description="Filter by user ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Generate deadline completion performance report"""
report_service = DeadlineReportService(db)
report = report_service.generate_completion_report(
start_date=start_date,
end_date=end_date,
employee_id=employee_id,
user_id=user_id
)
return report
@router.get("/reports/workload/")
async def get_workload_report(
target_date: Optional[date] = Query(None, description="Target date (defaults to today)"),
days_ahead: int = Query(30, ge=1, le=365, description="Number of days to look ahead"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Generate workload distribution report by assignee"""
report_service = DeadlineReportService(db)
report = report_service.generate_workload_report(
target_date=target_date,
days_ahead=days_ahead
)
return report
@router.get("/reports/trends/")
async def get_trends_report(
start_date: date = Query(..., description="Start date for trend analysis"),
end_date: date = Query(..., description="End date for trend analysis"),
granularity: str = Query("month", regex="^(week|month|quarter)$", description="Time granularity"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Generate deadline trends and analytics over time"""
report_service = DeadlineReportService(db)
report = report_service.generate_trends_report(
start_date=start_date,
end_date=end_date,
granularity=granularity
)
return report
# Dashboard endpoints
@router.get("/dashboard/widgets/")
async def get_dashboard_widgets(
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all dashboard widgets for deadline management"""
dashboard_service = DeadlineDashboardService(db)
# Default to current user if no filters provided
if not user_id and not employee_id:
user_id = current_user.id
widgets = dashboard_service.get_dashboard_widgets(
user_id=user_id,
employee_id=employee_id
)
return widgets
# Calendar endpoints
@router.get("/calendar/monthly/{year}/{month}/")
async def get_monthly_calendar(
year: int,
month: int,
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
show_completed: bool = Query(False, description="Include completed deadlines"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get monthly calendar view with deadlines"""
calendar_service = DeadlineCalendarService(db)
# Default to current user if no filters provided
if not user_id and not employee_id:
user_id = current_user.id
calendar = calendar_service.get_monthly_calendar(
year=year,
month=month,
user_id=user_id,
employee_id=employee_id,
show_completed=show_completed
)
return calendar
@router.get("/calendar/weekly/{year}/{week}/")
async def get_weekly_calendar(
year: int,
week: int,
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
show_completed: bool = Query(False, description="Include completed deadlines"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get weekly calendar view with detailed scheduling"""
calendar_service = DeadlineCalendarService(db)
# Default to current user if no filters provided
if not user_id and not employee_id:
user_id = current_user.id
calendar = calendar_service.get_weekly_calendar(
year=year,
week=week,
user_id=user_id,
employee_id=employee_id,
show_completed=show_completed
)
return calendar
@router.get("/calendar/daily/{target_date}/")
async def get_daily_schedule(
target_date: date,
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
show_completed: bool = Query(False, description="Include completed deadlines"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get detailed daily schedule with time slots"""
calendar_service = DeadlineCalendarService(db)
# Default to current user if no filters provided
if not user_id and not employee_id:
user_id = current_user.id
schedule = calendar_service.get_daily_schedule(
target_date=target_date,
user_id=user_id,
employee_id=employee_id,
show_completed=show_completed
)
return schedule
@router.get("/calendar/available-slots/")
async def find_available_slots(
start_date: date = Query(..., description="Start date for search"),
end_date: date = Query(..., description="End date for search"),
duration_minutes: int = Query(60, ge=15, le=480, description="Required duration in minutes"),
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
business_hours_only: bool = Query(True, description="Limit to business hours"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Find available time slots for scheduling new deadlines"""
calendar_service = DeadlineCalendarService(db)
# Default to current user if no filters provided
if not user_id and not employee_id:
user_id = current_user.id
slots = calendar_service.find_available_slots(
start_date=start_date,
end_date=end_date,
duration_minutes=duration_minutes,
user_id=user_id,
employee_id=employee_id,
business_hours_only=business_hours_only
)
return {
"search_criteria": {
"start_date": start_date,
"end_date": end_date,
"duration_minutes": duration_minutes,
"business_hours_only": business_hours_only
},
"available_slots": slots,
"total_slots": len(slots)
}
class ConflictAnalysisRequest(BaseModel):
"""Request for conflict analysis"""
proposed_datetime: datetime = Field(..., description="Proposed deadline date and time")
duration_minutes: int = Field(60, ge=15, le=480, description="Expected duration in minutes")
user_id: Optional[int] = Field(None, description="Check conflicts for specific user")
employee_id: Optional[str] = Field(None, description="Check conflicts for specific employee")
@router.post("/calendar/conflict-analysis/")
async def analyze_scheduling_conflicts(
request: ConflictAnalysisRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Analyze potential conflicts for a proposed deadline time"""
calendar_service = DeadlineCalendarService(db)
# Default to current user if no filters provided
user_id = request.user_id or current_user.id
analysis = calendar_service.get_conflict_analysis(
proposed_datetime=request.proposed_datetime,
duration_minutes=request.duration_minutes,
user_id=user_id,
employee_id=request.employee_id
)
return analysis
# Calendar export endpoints
@router.get("/calendar/export/ical/")
async def export_calendar_ical(
start_date: date = Query(..., description="Start date for export"),
end_date: date = Query(..., description="End date for export"),
user_id: Optional[int] = Query(None, description="Filter by user ID"),
employee_id: Optional[str] = Query(None, description="Filter by employee ID"),
deadline_types: Optional[str] = Query(None, description="Comma-separated deadline types"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Export deadlines to iCalendar format"""
from fastapi.responses import Response
export_service = CalendarExportService(db)
# Parse deadline types if provided
parsed_types = None
if deadline_types:
type_names = [t.strip() for t in deadline_types.split(',')]
parsed_types = []
for type_name in type_names:
try:
parsed_types.append(DeadlineType(type_name.lower()))
except ValueError:
pass # Skip invalid types
# Default to current user if no filters provided
if not user_id and not employee_id:
user_id = current_user.id
ical_content = export_service.export_to_ical(
start_date=start_date,
end_date=end_date,
user_id=user_id,
employee_id=employee_id,
deadline_types=parsed_types
)
filename = f"deadlines_{start_date}_{end_date}.ics"
return Response(
content=ical_content,
media_type="text/calendar",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)