""" Daily Helper API endpoints for Phase 1 enhancements Includes user profiles, routines, notifications, and habit tracking """ from fastapi import APIRouter, HTTPException, Depends, status, Query from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime, date, timedelta from app.db.database import get_db from app.models.models import ( UserProfile, DailyRoutine, RoutineExecution, Notification, DailyChecklist, HabitTracker ) from app.schemas.schemas import ( UserProfileCreate, UserProfileUpdate, UserProfileResponse, DailyRoutineCreate, DailyRoutineUpdate, DailyRoutineResponse, RoutineExecutionResponse, NotificationCreate, NotificationResponse, NotificationListResponse, DailyChecklistCreate, DailyChecklistUpdate, DailyChecklistResponse, HabitTrackerCreate, HabitTrackerResponse, HabitCompletionRequest, ChecklistItem ) router = APIRouter(prefix="/api/daily-helper", tags=["Daily Helper"]) # ============================================================================ # USER PROFILE ENDPOINTS # ============================================================================ @router.post("/profile", response_model=UserProfileResponse, status_code=status.HTTP_201_CREATED) async def create_user_profile( profile: UserProfileCreate, db: Session = Depends(get_db) ): """Create a new user profile""" # Check if email already exists if profile.email: existing = db.query(UserProfile).filter(UserProfile.email == profile.email).first() if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) db_profile = UserProfile(**profile.dict()) db.add(db_profile) db.commit() db.refresh(db_profile) return db_profile @router.get("/profile", response_model=UserProfileResponse) async def get_user_profile(db: Session = Depends(get_db)): """Get user profile (current implementation returns first profile)""" profile = db.query(UserProfile).first() if not profile: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User profile not found" ) return profile @router.put("/profile", response_model=UserProfileResponse) async def update_user_profile( profile_update: UserProfileUpdate, db: Session = Depends(get_db) ): """Update user profile""" profile = db.query(UserProfile).first() if not profile: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User profile not found" ) update_data = profile_update.dict(exclude_unset=True) for key, value in update_data.items(): setattr(profile, key, value) profile.updated_at = datetime.utcnow() db.commit() db.refresh(profile) return profile @router.delete("/profile", status_code=status.HTTP_204_NO_CONTENT) async def delete_user_profile(db: Session = Depends(get_db)): """Delete user profile""" profile = db.query(UserProfile).first() if not profile: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User profile not found" ) db.delete(profile) db.commit() # ============================================================================ # DAILY ROUTINE ENDPOINTS # ============================================================================ @router.post("/routines", response_model=DailyRoutineResponse, status_code=status.HTTP_201_CREATED) async def create_routine( routine: DailyRoutineCreate, db: Session = Depends(get_db) ): """Create a new daily routine""" db_routine = DailyRoutine(**routine.dict()) db.add(db_routine) db.commit() db.refresh(db_routine) return db_routine @router.get("/routines", response_model=List[DailyRoutineResponse]) async def list_routines( routine_type: Optional[str] = Query(None), enabled: Optional[bool] = Query(None), db: Session = Depends(get_db) ): """List daily routines with optional filters""" query = db.query(DailyRoutine) if routine_type: query = query.filter(DailyRoutine.routine_type == routine_type) if enabled is not None: query = query.filter(DailyRoutine.enabled == enabled) return query.all() @router.get("/routines/{routine_id}", response_model=DailyRoutineResponse) async def get_routine(routine_id: int, db: Session = Depends(get_db)): """Get a specific routine""" routine = db.query(DailyRoutine).filter(DailyRoutine.id == routine_id).first() if not routine: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Routine not found" ) return routine @router.put("/routines/{routine_id}", response_model=DailyRoutineResponse) async def update_routine( routine_id: int, routine_update: DailyRoutineUpdate, db: Session = Depends(get_db) ): """Update a routine""" routine = db.query(DailyRoutine).filter(DailyRoutine.id == routine_id).first() if not routine: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Routine not found" ) update_data = routine_update.dict(exclude_unset=True) for key, value in update_data.items(): setattr(routine, key, value) routine.updated_at = datetime.utcnow() db.commit() db.refresh(routine) return routine @router.delete("/routines/{routine_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_routine(routine_id: int, db: Session = Depends(get_db)): """Delete a routine""" routine = db.query(DailyRoutine).filter(DailyRoutine.id == routine_id).first() if not routine: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Routine not found" ) db.delete(routine) db.commit() @router.post("/routines/{routine_id}/execute", response_model=RoutineExecutionResponse, status_code=status.HTTP_201_CREATED) async def execute_routine( routine_id: int, db: Session = Depends(get_db) ): """Execute a routine (log execution)""" routine = db.query(DailyRoutine).filter(DailyRoutine.id == routine_id).first() if not routine: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Routine not found" ) # Create execution record execution = RoutineExecution( routine_id=routine_id, completion_status="completed", tasks_completed=routine.tasks ) db.add(execution) db.commit() db.refresh(execution) return execution @router.get("/routines/{routine_id}/executions", response_model=List[RoutineExecutionResponse]) async def get_routine_executions( routine_id: int, limit: int = Query(10, ge=1, le=100), db: Session = Depends(get_db) ): """Get execution history for a routine""" executions = db.query(RoutineExecution).filter( RoutineExecution.routine_id == routine_id ).order_by(RoutineExecution.executed_at.desc()).limit(limit).all() return executions # ============================================================================ # NOTIFICATION ENDPOINTS # ============================================================================ @router.post("/notifications", response_model=NotificationResponse, status_code=status.HTTP_201_CREATED) async def create_notification( notification: NotificationCreate, db: Session = Depends(get_db) ): """Create a new notification""" db_notification = Notification(**notification.dict()) db.add(db_notification) db.commit() db.refresh(db_notification) return db_notification @router.get("/notifications", response_model=NotificationListResponse) async def get_notifications( notification_type: Optional[str] = Query(None), read: Optional[bool] = Query(None), limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0), db: Session = Depends(get_db) ): """Get notifications with optional filters""" query = db.query(Notification) if notification_type: query = query.filter(Notification.notification_type == notification_type) if read is not None: query = query.filter(Notification.read == read) total = query.count() unread = query.filter(Notification.read == False).count() notifications = query.order_by( Notification.created_at.desc() ).limit(limit).offset(offset).all() return NotificationListResponse( notifications=notifications, unread_count=unread, total_count=total ) @router.get("/notifications/{notification_id}", response_model=NotificationResponse) async def get_notification(notification_id: int, db: Session = Depends(get_db)): """Get a specific notification""" notification = db.query(Notification).filter( Notification.id == notification_id ).first() if not notification: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found" ) return notification @router.put("/notifications/{notification_id}/read", response_model=NotificationResponse) async def mark_notification_read( notification_id: int, db: Session = Depends(get_db) ): """Mark notification as read""" notification = db.query(Notification).filter( Notification.id == notification_id ).first() if not notification: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found" ) notification.read = True notification.read_at = datetime.utcnow() db.commit() db.refresh(notification) return notification @router.post("/notifications/mark-all-read") async def mark_all_notifications_read(db: Session = Depends(get_db)): """Mark all notifications as read""" db.query(Notification).filter(Notification.read == False).update({ Notification.read: True, Notification.read_at: datetime.utcnow() }) db.commit() return {"message": "All notifications marked as read"} @router.delete("/notifications/{notification_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_notification(notification_id: int, db: Session = Depends(get_db)): """Delete a notification""" notification = db.query(Notification).filter( Notification.id == notification_id ).first() if not notification: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found" ) db.delete(notification) db.commit() # ============================================================================ # DAILY CHECKLIST ENDPOINTS # ============================================================================ @router.post("/checklists", response_model=DailyChecklistResponse, status_code=status.HTTP_201_CREATED) async def create_checklist( checklist: DailyChecklistCreate, db: Session = Depends(get_db) ): """Create a new daily checklist""" db_checklist = DailyChecklist(**checklist.dict()) db.add(db_checklist) db.commit() db.refresh(db_checklist) return db_checklist @router.get("/checklists/today", response_model=Optional[DailyChecklistResponse]) async def get_today_checklist( checklist_type: Optional[str] = Query(None), db: Session = Depends(get_db) ): """Get today's checklist""" query = db.query(DailyChecklist).filter( DailyChecklist.checklist_date == date.today() ) if checklist_type: query = query.filter(DailyChecklist.checklist_type == checklist_type) return query.first() @router.get("/checklists", response_model=List[DailyChecklistResponse]) async def list_checklists( checklist_type: Optional[str] = Query(None), start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), limit: int = Query(30, ge=1, le=365), db: Session = Depends(get_db) ): """List checklists with optional filters""" query = db.query(DailyChecklist) if checklist_type: query = query.filter(DailyChecklist.checklist_type == checklist_type) if start_date: try: start = datetime.fromisoformat(start_date).date() query = query.filter(DailyChecklist.checklist_date >= start) except ValueError: raise HTTPException(status_code=400, detail="Invalid start_date format") if end_date: try: end = datetime.fromisoformat(end_date).date() query = query.filter(DailyChecklist.checklist_date <= end) except ValueError: raise HTTPException(status_code=400, detail="Invalid end_date format") return query.order_by(DailyChecklist.checklist_date.desc()).limit(limit).all() @router.get("/checklists/{checklist_id}", response_model=DailyChecklistResponse) async def get_checklist(checklist_id: int, db: Session = Depends(get_db)): """Get a specific checklist""" checklist = db.query(DailyChecklist).filter( DailyChecklist.id == checklist_id ).first() if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) return checklist @router.put("/checklists/{checklist_id}", response_model=DailyChecklistResponse) async def update_checklist( checklist_id: int, checklist_update: DailyChecklistUpdate, db: Session = Depends(get_db) ): """Update a checklist""" checklist = db.query(DailyChecklist).filter( DailyChecklist.id == checklist_id ).first() if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) update_data = checklist_update.dict(exclude_unset=True) # Calculate completion percentage if items are provided if "items" in update_data: items = update_data["items"] if items: completed = sum(1 for item in items if item.get("completed", False)) completion_pct = (completed / len(items)) * 100 update_data["completion_percentage"] = completion_pct for key, value in update_data.items(): setattr(checklist, key, value) checklist.updated_at = datetime.utcnow() db.commit() db.refresh(checklist) return checklist @router.put("/checklists/{checklist_id}/items/{item_id}", response_model=DailyChecklistResponse) async def update_checklist_item( checklist_id: int, item_id: str, completed: bool, db: Session = Depends(get_db) ): """Update a specific checklist item""" checklist = db.query(DailyChecklist).filter( DailyChecklist.id == checklist_id ).first() if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) # Find and update item items = checklist.items or [] for item in items: if item.get("id") == item_id: item["completed"] = completed if completed: item["completed_at"] = datetime.utcnow().isoformat() break # Recalculate completion percentage if items: completed_count = sum(1 for item in items if item.get("completed", False)) checklist.completion_percentage = (completed_count / len(items)) * 100 checklist.items = items checklist.updated_at = datetime.utcnow() db.commit() db.refresh(checklist) return checklist @router.delete("/checklists/{checklist_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_checklist(checklist_id: int, db: Session = Depends(get_db)): """Delete a checklist""" checklist = db.query(DailyChecklist).filter( DailyChecklist.id == checklist_id ).first() if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) db.delete(checklist) db.commit() # ============================================================================ # HABIT TRACKER ENDPOINTS # ============================================================================ @router.post("/habits", response_model=HabitTrackerResponse, status_code=status.HTTP_201_CREATED) async def create_habit( habit: HabitTrackerCreate, db: Session = Depends(get_db) ): """Create a new habit to track""" db_habit = HabitTracker(**habit.dict()) db.add(db_habit) db.commit() db.refresh(db_habit) return db_habit @router.get("/habits", response_model=List[HabitTrackerResponse]) async def list_habits( habit_name: Optional[str] = Query(None), db: Session = Depends(get_db) ): """List all habits""" query = db.query(HabitTracker) if habit_name: query = query.filter(HabitTracker.habit_name.ilike(f"%{habit_name}%")) return query.all() @router.get("/habits/{habit_id}", response_model=HabitTrackerResponse) async def get_habit(habit_id: int, db: Session = Depends(get_db)): """Get a specific habit""" habit = db.query(HabitTracker).filter(HabitTracker.id == habit_id).first() if not habit: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Habit not found" ) return habit @router.post("/habits/{habit_id}/log", response_model=HabitTrackerResponse) async def log_habit_completion( habit_id: int, request: HabitCompletionRequest, db: Session = Depends(get_db) ): """Log habit completion""" habit = db.query(HabitTracker).filter(HabitTracker.id == habit_id).first() if not habit: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Habit not found" ) completion_date = request.completion_date or date.today().isoformat() completion_dates = habit.completion_dates or [] # Avoid duplicates if completion_date not in completion_dates: completion_dates.append(completion_date) habit.completion_dates = completion_dates habit.total_completions = len(completion_dates) # Calculate streaks sorted_dates = sorted(completion_dates) current_streak = 0 for i in range(len(sorted_dates) - 1, -1, -1): current_date = datetime.fromisoformat(sorted_dates[i]).date() expected_date = date.today() - timedelta(days=len(sorted_dates) - 1 - i) if i == len(sorted_dates) - 1: if current_date >= date.today() - timedelta(days=1): current_streak = 1 else: break else: next_date = datetime.fromisoformat(sorted_dates[i + 1]).date() if next_date - current_date == timedelta(days=1): current_streak += 1 else: break habit.current_streak = current_streak habit.longest_streak = max(habit.longest_streak or 0, current_streak) habit.updated_at = datetime.utcnow() db.commit() db.refresh(habit) return habit @router.delete("/habits/{habit_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_habit(habit_id: int, db: Session = Depends(get_db)): """Delete a habit""" habit = db.query(HabitTracker).filter(HabitTracker.id == habit_id).first() if not habit: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Habit not found" ) db.delete(habit) db.commit() # ============================================================================ # SUMMARY ENDPOINTS # ============================================================================ @router.get("/dashboard") async def get_dashboard_summary(db: Session = Depends(get_db)): """Get daily helper dashboard summary""" today = date.today() # Get today's checklists checklists = db.query(DailyChecklist).filter( DailyChecklist.checklist_date == today ).all() # Get unread notifications unread_notifications = db.query(Notification).filter( Notification.read == False ).count() # Get habits habits = db.query(HabitTracker).all() habit_summary = { "total": len(habits), "completed_today": sum( 1 for h in habits if h.completion_dates and today.isoformat() in h.completion_dates ) } # Get user profile profile = db.query(UserProfile).first() return { "user_profile": profile if profile else None, "today_date": today.isoformat(), "checklists": checklists, "checklists_count": len(checklists), "unread_notifications": unread_notifications, "habits_summary": habit_summary, "pending_routines": db.query(DailyRoutine).filter( DailyRoutine.enabled == True ).count() }