# app/api/schedule.py """ User schedule management endpoints. Allows users to set their daily availability for study time. Endpoints: GET /api/schedule — get current schedule POST /api/schedule — save/update schedule """ import logging from typing import Dict, List from fastapi import APIRouter, Depends from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import Session from app.core.database import get_db from app.core.security import get_current_user from app.models.user import User logger = logging.getLogger("hale.api.schedule") router = APIRouter(prefix="/api", tags=["Schedule"]) # ── Schemas ──────────────────────────────────────────────────────────────── class TimeBlock(BaseModel): start: str = Field(..., description="Start time in HH:MM format") end: str = Field(..., description="End time in HH:MM format") label: str = Field("Study", description="Label for this block (Study, Exercise, etc.)") @field_validator("start", "end") @classmethod def validate_time(cls, v: str) -> str: import re if not re.match(r"^\d{2}:\d{2}$", v): raise ValueError("Time must be in HH:MM format") h, m = map(int, v.split(":")) if not (0 <= h <= 23 and 0 <= m <= 59): raise ValueError("Invalid time value") return v class ScheduleRequest(BaseModel): lifestyle: str = Field( "student", description="Lifestyle type: student, job, college, freelancer, custom" ) sleep_time: str = Field("23:00", description="Bedtime in HH:MM") wake_time: str = Field("07:00", description="Wake-up time in HH:MM") blocks: Dict[str, List[TimeBlock]] = Field( default_factory=dict, description="Available blocks per weekday: {'monday': [{start, end, label}], ...}" ) @field_validator("lifestyle") @classmethod def validate_lifestyle(cls, v: str) -> str: valid = {"student", "job", "college", "freelancer", "custom"} if v not in valid: raise ValueError(f"lifestyle must be one of: {', '.join(valid)}") return v # ── Preset templates per lifestyle ──────────────────────────────────────── LIFESTYLE_PRESETS: Dict[str, Dict] = { "student": { "blocks": { "monday": [{"start": "18:00", "end": "20:30", "label": "Study"}], "tuesday": [{"start": "18:00", "end": "20:30", "label": "Study"}], "wednesday": [{"start": "18:00", "end": "20:30", "label": "Study"}], "thursday": [{"start": "18:00", "end": "20:30", "label": "Study"}], "friday": [{"start": "17:00", "end": "19:30", "label": "Study"}], "saturday": [{"start": "10:00", "end": "13:00", "label": "Study"}], "sunday": [{"start": "10:00", "end": "12:00", "label": "Study"}], } }, "job": { "blocks": { "monday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], "tuesday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], "wednesday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], "thursday": [{"start": "07:00", "end": "08:00", "label": "Study"}, {"start": "21:00", "end": "22:30", "label": "Study"}], "friday": [{"start": "21:00", "end": "22:30", "label": "Study"}], "saturday": [{"start": "09:00", "end": "12:00", "label": "Study"}], "sunday": [{"start": "10:00", "end": "12:00", "label": "Study"}], } }, "college": { "blocks": { "monday": [{"start": "16:00", "end": "19:00", "label": "Study"}], "tuesday": [{"start": "16:00", "end": "19:00", "label": "Study"}], "wednesday": [{"start": "16:00", "end": "19:00", "label": "Study"}], "thursday": [{"start": "16:00", "end": "19:00", "label": "Study"}], "friday": [{"start": "15:00", "end": "18:00", "label": "Study"}], "saturday": [{"start": "10:00", "end": "14:00", "label": "Study"}], "sunday": [{"start": "11:00", "end": "14:00", "label": "Study"}], } }, "freelancer": { "blocks": { "monday": [{"start": "09:00", "end": "11:00", "label": "Study"}], "tuesday": [{"start": "09:00", "end": "11:00", "label": "Study"}], "wednesday": [{"start": "09:00", "end": "11:00", "label": "Study"}], "thursday": [{"start": "09:00", "end": "11:00", "label": "Study"}], "friday": [{"start": "09:00", "end": "11:00", "label": "Study"}], "saturday": [{"start": "10:00", "end": "13:00", "label": "Study"}], "sunday": [{"start": "10:00", "end": "12:00", "label": "Study"}], } }, } def _compute_daily_minutes(blocks_dict: Dict) -> Dict[str, int]: """Compute total study minutes per day.""" result = {} for day, blocks in blocks_dict.items(): total = 0 for b in blocks: try: sh, sm = map(int, b["start"].split(":")) eh, em = map(int, b["end"].split(":")) total += (eh * 60 + em) - (sh * 60 + sm) except Exception: pass result[day] = max(0, total) return result # ── Endpoints ────────────────────────────────────────────────────────────── @router.get("/schedule", summary="Get user's current schedule") def get_schedule( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): schedule = current_user.schedule_json or {} if not schedule: # Return default student preset preset = LIFESTYLE_PRESETS["student"].copy() preset["lifestyle"] = "student" preset["sleep_time"] = "23:00" preset["wake_time"] = "07:00" preset["daily_minutes"] = _compute_daily_minutes(preset["blocks"]) return {"schedule": preset, "is_default": True} daily_mins = _compute_daily_minutes(schedule.get("blocks", {})) return {"schedule": {**schedule, "daily_minutes": daily_mins}, "is_default": False} @router.post("/schedule", summary="Save user's study schedule") def save_schedule( payload: ScheduleRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): # Apply preset as base if blocks were not explicitly provided if "blocks" not in payload.model_fields_set and payload.lifestyle in LIFESTYLE_PRESETS: blocks = LIFESTYLE_PRESETS[payload.lifestyle]["blocks"] else: blocks = {day: [b.model_dump() for b in block_list] for day, block_list in payload.blocks.items()} schedule_data = { "lifestyle": payload.lifestyle, "sleep_time": payload.sleep_time, "wake_time": payload.wake_time, "blocks": blocks, } current_user.schedule_json = schedule_data db.commit() daily_mins = _compute_daily_minutes(blocks) logger.info("Schedule saved for %s: %s (%d days configured)", current_user.user_id, payload.lifestyle, len(blocks)) return { "message": "Schedule saved!", "schedule": {**schedule_data, "daily_minutes": daily_mins}, "avg_daily_min": sum(daily_mins.values()) // max(len(daily_mins), 1), } @router.get("/schedule/presets", summary="Get lifestyle schedule presets") def get_presets(): result = {} for lifestyle, data in LIFESTYLE_PRESETS.items(): daily_mins = _compute_daily_minutes(data["blocks"]) result[lifestyle] = { "blocks": data["blocks"], "daily_minutes": daily_mins, "avg_daily_min": sum(daily_mins.values()) // 7, } return {"presets": result}