398 lines
13 KiB
Python
398 lines
13 KiB
Python
"""Chore Completion Log API endpoints."""
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, and_
|
|
from typing import List, Optional
|
|
from datetime import datetime, timedelta
|
|
|
|
from app.core.database import get_db
|
|
from app.api.v1.auth import get_current_user
|
|
from app.models.user import User
|
|
from app.models.chore import Chore
|
|
from app.models.chore_assignment import ChoreAssignment
|
|
from app.models.chore_completion_log import ChoreCompletionLog
|
|
from app.schemas import chore_completion_log as log_schemas
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def enrich_completion_log(db: Session, log: ChoreCompletionLog) -> dict:
|
|
"""Add related information to completion log."""
|
|
# Get chore info
|
|
chore = db.query(Chore).filter(Chore.id == log.chore_id).first()
|
|
|
|
# Get user info
|
|
user = db.query(User).filter(User.id == log.user_id).first()
|
|
|
|
# Get verified_by info if exists
|
|
verified_by_name = None
|
|
if log.verified_by_user_id:
|
|
verified_by = db.query(User).filter(User.id == log.verified_by_user_id).first()
|
|
if verified_by:
|
|
verified_by_name = verified_by.full_name or verified_by.username
|
|
|
|
return {
|
|
"id": log.id,
|
|
"chore_id": log.chore_id,
|
|
"user_id": log.user_id,
|
|
"completed_at": log.completed_at,
|
|
"notes": log.notes,
|
|
"verified_by_user_id": log.verified_by_user_id,
|
|
"created_at": log.created_at,
|
|
"chore_title": chore.title if chore else None,
|
|
"user_name": user.full_name or user.username if user else None,
|
|
"user_avatar": user.avatar_url if user else None,
|
|
"verified_by_name": verified_by_name
|
|
}
|
|
|
|
|
|
@router.post("/{chore_id}/complete", response_model=log_schemas.ChoreCompletionLog, status_code=status.HTTP_201_CREATED)
|
|
def complete_chore(
|
|
chore_id: int,
|
|
notes: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Log a chore completion.
|
|
|
|
Creates a completion log entry and updates the chore assignment.
|
|
This is the primary endpoint for completing chores.
|
|
"""
|
|
# Check if chore exists
|
|
chore = db.query(Chore).filter(Chore.id == chore_id).first()
|
|
if not chore:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Chore not found"
|
|
)
|
|
|
|
# Check if user is assigned to this chore
|
|
assignment = db.query(ChoreAssignment).filter(
|
|
ChoreAssignment.chore_id == chore_id,
|
|
ChoreAssignment.user_id == current_user.id
|
|
).first()
|
|
|
|
if not assignment:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You are not assigned to this chore"
|
|
)
|
|
|
|
# Create completion log
|
|
completion_log = ChoreCompletionLog(
|
|
chore_id=chore_id,
|
|
user_id=current_user.id,
|
|
completed_at=datetime.utcnow(),
|
|
notes=notes
|
|
)
|
|
|
|
db.add(completion_log)
|
|
|
|
# Update assignment completed_at
|
|
assignment.completed_at = datetime.utcnow()
|
|
|
|
# Check if all assignments are completed
|
|
all_assignments = db.query(ChoreAssignment).filter(
|
|
ChoreAssignment.chore_id == chore_id
|
|
).all()
|
|
|
|
if all(a.completed_at is not None for a in all_assignments):
|
|
chore.completed_at = datetime.utcnow()
|
|
chore.status = "completed"
|
|
|
|
db.commit()
|
|
db.refresh(completion_log)
|
|
|
|
return enrich_completion_log(db, completion_log)
|
|
|
|
|
|
@router.get("/completions", response_model=List[log_schemas.ChoreCompletionLog])
|
|
def get_completion_logs(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
chore_id: Optional[int] = Query(None, description="Filter by chore ID"),
|
|
user_id: Optional[int] = Query(None, description="Filter by user ID"),
|
|
start_date: Optional[datetime] = Query(None, description="Filter completions after this date"),
|
|
end_date: Optional[datetime] = Query(None, description="Filter completions before this date"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get chore completion logs with optional filters.
|
|
|
|
- **chore_id**: Filter by specific chore
|
|
- **user_id**: Filter by specific user
|
|
- **start_date**: Filter completions after this date
|
|
- **end_date**: Filter completions before this date
|
|
"""
|
|
query = db.query(ChoreCompletionLog)
|
|
|
|
# Apply filters
|
|
if chore_id:
|
|
query = query.filter(ChoreCompletionLog.chore_id == chore_id)
|
|
|
|
if user_id:
|
|
query = query.filter(ChoreCompletionLog.user_id == user_id)
|
|
|
|
if start_date:
|
|
query = query.filter(ChoreCompletionLog.completed_at >= start_date)
|
|
|
|
if end_date:
|
|
query = query.filter(ChoreCompletionLog.completed_at <= end_date)
|
|
|
|
# Order by most recent first
|
|
query = query.order_by(ChoreCompletionLog.completed_at.desc())
|
|
|
|
logs = query.offset(skip).limit(limit).all()
|
|
|
|
# Enrich with related data
|
|
return [enrich_completion_log(db, log) for log in logs]
|
|
|
|
|
|
@router.get("/reports/weekly", response_model=log_schemas.WeeklyChoreReport)
|
|
def get_weekly_report(
|
|
user_id: Optional[int] = Query(None, description="Get report for specific user (omit for family-wide)"),
|
|
weeks_ago: int = Query(0, description="Number of weeks ago (0 = current week)"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Generate a weekly chore completion report.
|
|
|
|
- **user_id**: Optional - get report for specific user
|
|
- **weeks_ago**: Which week to report on (0 = current week, 1 = last week, etc.)
|
|
|
|
Returns comprehensive statistics including:
|
|
- Total completions
|
|
- Completions by user
|
|
- Completions by chore
|
|
- Completions by day
|
|
- Top performers
|
|
- Recent completions
|
|
"""
|
|
# Calculate week boundaries
|
|
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
start_of_week = today - timedelta(days=today.weekday()) - timedelta(weeks=weeks_ago)
|
|
end_of_week = start_of_week + timedelta(days=7)
|
|
|
|
# Base query
|
|
query = db.query(ChoreCompletionLog).filter(
|
|
and_(
|
|
ChoreCompletionLog.completed_at >= start_of_week,
|
|
ChoreCompletionLog.completed_at < end_of_week
|
|
)
|
|
)
|
|
|
|
# Apply user filter if specified
|
|
if user_id:
|
|
query = query.filter(ChoreCompletionLog.user_id == user_id)
|
|
|
|
logs = query.all()
|
|
|
|
# Calculate statistics
|
|
total_completions = len(logs)
|
|
|
|
# Completions by user
|
|
completions_by_user = {}
|
|
for log in logs:
|
|
user = db.query(User).filter(User.id == log.user_id).first()
|
|
if user:
|
|
username = user.full_name or user.username
|
|
completions_by_user[username] = completions_by_user.get(username, 0) + 1
|
|
|
|
# Completions by chore
|
|
completions_by_chore = {}
|
|
for log in logs:
|
|
chore = db.query(Chore).filter(Chore.id == log.chore_id).first()
|
|
if chore:
|
|
completions_by_chore[chore.title] = completions_by_chore.get(chore.title, 0) + 1
|
|
|
|
# Completions by day
|
|
completions_by_day = {}
|
|
for log in logs:
|
|
day_name = log.completed_at.strftime("%A")
|
|
completions_by_day[day_name] = completions_by_day.get(day_name, 0) + 1
|
|
|
|
# Top performers
|
|
user_stats = []
|
|
for user_name, count in completions_by_user.items():
|
|
user = db.query(User).filter(
|
|
(User.full_name == user_name) | (User.username == user_name)
|
|
).first()
|
|
|
|
user_stats.append({
|
|
"username": user_name,
|
|
"count": count,
|
|
"avatar_url": user.avatar_url if user else None
|
|
})
|
|
|
|
user_stats.sort(key=lambda x: x["count"], reverse=True)
|
|
top_performers = user_stats[:5] # Top 5 performers
|
|
|
|
# Recent completions (last 10)
|
|
recent_logs = sorted(logs, key=lambda x: x.completed_at, reverse=True)[:10]
|
|
recent_completions = [enrich_completion_log(db, log) for log in recent_logs]
|
|
|
|
return {
|
|
"start_date": start_of_week,
|
|
"end_date": end_of_week,
|
|
"total_completions": total_completions,
|
|
"completions_by_user": completions_by_user,
|
|
"completions_by_chore": completions_by_chore,
|
|
"completions_by_day": completions_by_day,
|
|
"top_performers": top_performers,
|
|
"recent_completions": recent_completions
|
|
}
|
|
|
|
|
|
@router.get("/reports/user/{user_id}", response_model=log_schemas.UserChoreStats)
|
|
def get_user_stats(
|
|
user_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get comprehensive statistics for a specific user.
|
|
|
|
Returns:
|
|
- Total completions (all time)
|
|
- Completions this week
|
|
- Completions this month
|
|
- Favorite chore (most completed)
|
|
- Recent completions
|
|
"""
|
|
# Check if user exists
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="User not found"
|
|
)
|
|
|
|
# Total completions
|
|
total_completions = db.query(ChoreCompletionLog).filter(
|
|
ChoreCompletionLog.user_id == user_id
|
|
).count()
|
|
|
|
# This week
|
|
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
start_of_week = today - timedelta(days=today.weekday())
|
|
completions_this_week = db.query(ChoreCompletionLog).filter(
|
|
and_(
|
|
ChoreCompletionLog.user_id == user_id,
|
|
ChoreCompletionLog.completed_at >= start_of_week
|
|
)
|
|
).count()
|
|
|
|
# This month
|
|
start_of_month = today.replace(day=1)
|
|
completions_this_month = db.query(ChoreCompletionLog).filter(
|
|
and_(
|
|
ChoreCompletionLog.user_id == user_id,
|
|
ChoreCompletionLog.completed_at >= start_of_month
|
|
)
|
|
).count()
|
|
|
|
# Favorite chore (most completed)
|
|
favorite_chore = None
|
|
chore_counts = db.query(
|
|
ChoreCompletionLog.chore_id,
|
|
func.count(ChoreCompletionLog.id).label('count')
|
|
).filter(
|
|
ChoreCompletionLog.user_id == user_id
|
|
).group_by(
|
|
ChoreCompletionLog.chore_id
|
|
).order_by(
|
|
func.count(ChoreCompletionLog.id).desc()
|
|
).first()
|
|
|
|
if chore_counts:
|
|
chore = db.query(Chore).filter(Chore.id == chore_counts[0]).first()
|
|
if chore:
|
|
favorite_chore = chore.title
|
|
|
|
# Recent completions
|
|
recent_logs = db.query(ChoreCompletionLog).filter(
|
|
ChoreCompletionLog.user_id == user_id
|
|
).order_by(
|
|
ChoreCompletionLog.completed_at.desc()
|
|
).limit(10).all()
|
|
|
|
recent_completions = [enrich_completion_log(db, log) for log in recent_logs]
|
|
|
|
return {
|
|
"user_id": user.id,
|
|
"username": user.username,
|
|
"full_name": user.full_name,
|
|
"avatar_url": user.avatar_url,
|
|
"total_completions": total_completions,
|
|
"completions_this_week": completions_this_week,
|
|
"completions_this_month": completions_this_month,
|
|
"favorite_chore": favorite_chore,
|
|
"recent_completions": recent_completions
|
|
}
|
|
|
|
|
|
@router.delete("/completions/{log_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_completion_log(
|
|
log_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Delete a completion log entry (admin only or log owner).
|
|
|
|
Useful for correcting mistakes or removing accidental completions.
|
|
"""
|
|
log = db.query(ChoreCompletionLog).filter(ChoreCompletionLog.id == log_id).first()
|
|
if not log:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Completion log not found"
|
|
)
|
|
|
|
# Only admin or the user who completed can delete
|
|
if not current_user.is_admin and log.user_id != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Not authorized to delete this completion log"
|
|
)
|
|
|
|
db.delete(log)
|
|
db.commit()
|
|
return None
|
|
|
|
|
|
@router.post("/completions/{log_id}/verify", response_model=log_schemas.ChoreCompletionLog)
|
|
def verify_completion(
|
|
log_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Verify a chore completion (requires different user than completer).
|
|
|
|
Useful for parents verifying kids' chores, or quality checks.
|
|
"""
|
|
log = db.query(ChoreCompletionLog).filter(ChoreCompletionLog.id == log_id).first()
|
|
if not log:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Completion log not found"
|
|
)
|
|
|
|
# Can't verify your own completion
|
|
if log.user_id == current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="You cannot verify your own completion"
|
|
)
|
|
|
|
log.verified_by_user_id = current_user.id
|
|
db.commit()
|
|
db.refresh(log)
|
|
|
|
return enrich_completion_log(db, log)
|