Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
727 lines
26 KiB
Python
727 lines
26 KiB
Python
"""
|
|
Classroom API - Context Routes
|
|
|
|
School year context, events, routines, and suggestions endpoints (Phase 8).
|
|
"""
|
|
|
|
from typing import Dict, Any, Optional
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Depends
|
|
|
|
from classroom_engine import (
|
|
FEDERAL_STATES,
|
|
SCHOOL_TYPES,
|
|
MacroPhaseEnum,
|
|
)
|
|
|
|
from ..models import (
|
|
TeacherContextResponse,
|
|
SchoolInfo,
|
|
SchoolYearInfo,
|
|
MacroPhaseInfo,
|
|
CoreCounts,
|
|
ContextFlags,
|
|
UpdateContextRequest,
|
|
CreateEventRequest,
|
|
EventResponse,
|
|
CreateRoutineRequest,
|
|
RoutineResponse,
|
|
)
|
|
from ..services.persistence import (
|
|
init_db_if_needed,
|
|
DB_ENABLED,
|
|
SessionLocal,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(tags=["Context"])
|
|
|
|
|
|
def get_db():
|
|
"""Database session dependency."""
|
|
if DB_ENABLED and SessionLocal:
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
else:
|
|
yield None
|
|
|
|
|
|
def _get_macro_phase_label(phase) -> str:
|
|
"""Gibt den Anzeigenamen einer Makro-Phase zurueck."""
|
|
labels = {
|
|
"onboarding": "Einrichtung",
|
|
"schuljahresstart": "Schuljahresstart",
|
|
"unterrichtsaufbau": "Unterrichtsaufbau",
|
|
"leistungsphase_1": "Leistungsphase 1",
|
|
"halbjahresabschluss": "Halbjahresabschluss",
|
|
"leistungsphase_2": "Leistungsphase 2",
|
|
"jahresabschluss": "Jahresabschluss",
|
|
}
|
|
phase_value = phase.value if hasattr(phase, 'value') else str(phase)
|
|
return labels.get(phase_value, phase_value)
|
|
|
|
|
|
# === Context Endpoints ===
|
|
|
|
@router.get("/v1/context", response_model=TeacherContextResponse)
|
|
async def get_teacher_context(
|
|
teacher_id: str = Query(..., description="Teacher ID"),
|
|
db=Depends(get_db)
|
|
):
|
|
"""
|
|
Liefert den aktuellen Makro-Kontext eines Lehrers.
|
|
|
|
Der Kontext beinhaltet:
|
|
- Schul-Informationen (Bundesland, Schulart)
|
|
- Schuljahr-Daten (aktuelles Jahr, Woche)
|
|
- Makro-Phase (ONBOARDING bis JAHRESABSCHLUSS)
|
|
- Zaehler (Klassen, geplante Klausuren, etc.)
|
|
- Status-Flags (Onboarding abgeschlossen, etc.)
|
|
"""
|
|
if DB_ENABLED and db:
|
|
try:
|
|
from classroom_engine.repository import TeacherContextRepository, SchoolyearEventRepository
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.get_or_create(teacher_id)
|
|
|
|
# Zaehler berechnen
|
|
event_repo = SchoolyearEventRepository(db)
|
|
upcoming_exams = event_repo.get_upcoming(teacher_id, days=30)
|
|
exams_count = len([e for e in upcoming_exams if e.event_type.value == "exam"])
|
|
|
|
return TeacherContextResponse(
|
|
schema_version="1.0",
|
|
teacher_id=teacher_id,
|
|
school=SchoolInfo(
|
|
federal_state=context.federal_state or "BY",
|
|
federal_state_name=FEDERAL_STATES.get(context.federal_state, ""),
|
|
school_type=context.school_type or "gymnasium",
|
|
school_type_name=SCHOOL_TYPES.get(context.school_type, ""),
|
|
),
|
|
school_year=SchoolYearInfo(
|
|
id=context.schoolyear or "2024-2025",
|
|
start=context.schoolyear_start.isoformat() if context.schoolyear_start else None,
|
|
current_week=context.current_week or 1,
|
|
),
|
|
macro_phase=MacroPhaseInfo(
|
|
id=context.macro_phase.value,
|
|
label=_get_macro_phase_label(context.macro_phase),
|
|
confidence=1.0,
|
|
),
|
|
core_counts=CoreCounts(
|
|
classes=1 if context.has_classes else 0,
|
|
exams_scheduled=exams_count,
|
|
corrections_pending=0,
|
|
),
|
|
flags=ContextFlags(
|
|
onboarding_completed=context.onboarding_completed,
|
|
has_classes=context.has_classes,
|
|
has_schedule=context.has_schedule,
|
|
is_exam_period=context.is_exam_period,
|
|
is_before_holidays=context.is_before_holidays,
|
|
),
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to get teacher context: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler beim Laden des Kontexts: {e}")
|
|
|
|
# Fallback ohne DB
|
|
return TeacherContextResponse(
|
|
schema_version="1.0",
|
|
teacher_id=teacher_id,
|
|
school=SchoolInfo(
|
|
federal_state="BY",
|
|
federal_state_name="Bayern",
|
|
school_type="gymnasium",
|
|
school_type_name="Gymnasium",
|
|
),
|
|
school_year=SchoolYearInfo(
|
|
id="2024-2025",
|
|
start=None,
|
|
current_week=1,
|
|
),
|
|
macro_phase=MacroPhaseInfo(
|
|
id="onboarding",
|
|
label="Einrichtung",
|
|
confidence=1.0,
|
|
),
|
|
core_counts=CoreCounts(),
|
|
flags=ContextFlags(),
|
|
)
|
|
|
|
|
|
@router.put("/v1/context", response_model=TeacherContextResponse)
|
|
async def update_teacher_context(
|
|
teacher_id: str,
|
|
request: UpdateContextRequest,
|
|
db=Depends(get_db)
|
|
):
|
|
"""
|
|
Aktualisiert den Kontext eines Lehrers.
|
|
"""
|
|
if not DB_ENABLED or not db:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
from classroom_engine.repository import TeacherContextRepository
|
|
repo = TeacherContextRepository(db)
|
|
|
|
# Validierung
|
|
if request.federal_state and request.federal_state not in FEDERAL_STATES:
|
|
raise HTTPException(status_code=400, detail=f"Ungueltiges Bundesland: {request.federal_state}")
|
|
if request.school_type and request.school_type not in SCHOOL_TYPES:
|
|
raise HTTPException(status_code=400, detail=f"Ungueltige Schulart: {request.school_type}")
|
|
|
|
# Parse datetime if provided
|
|
schoolyear_start = None
|
|
if request.schoolyear_start:
|
|
schoolyear_start = datetime.fromisoformat(request.schoolyear_start.replace('Z', '+00:00'))
|
|
|
|
repo.update_context(
|
|
teacher_id=teacher_id,
|
|
federal_state=request.federal_state,
|
|
school_type=request.school_type,
|
|
schoolyear=request.schoolyear,
|
|
schoolyear_start=schoolyear_start,
|
|
macro_phase=request.macro_phase,
|
|
current_week=request.current_week,
|
|
)
|
|
|
|
return await get_teacher_context(teacher_id, db)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to update teacher context: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler beim Aktualisieren: {e}")
|
|
|
|
|
|
@router.post("/v1/context/complete-onboarding")
|
|
async def complete_onboarding(
|
|
teacher_id: str = Query(...),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Markiert das Onboarding als abgeschlossen."""
|
|
if not DB_ENABLED or not db:
|
|
return {"success": True, "macro_phase": "schuljahresstart", "note": "DB not available"}
|
|
|
|
try:
|
|
from classroom_engine.repository import TeacherContextRepository
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.complete_onboarding(teacher_id)
|
|
return {
|
|
"success": True,
|
|
"macro_phase": context.macro_phase.value,
|
|
"teacher_id": teacher_id,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to complete onboarding: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.post("/v1/context/reset-onboarding")
|
|
async def reset_onboarding(
|
|
teacher_id: str = Query(...),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Setzt das Onboarding zurueck (fuer Tests)."""
|
|
if not DB_ENABLED or not db:
|
|
return {"success": True, "macro_phase": "onboarding", "note": "DB not available"}
|
|
|
|
try:
|
|
from classroom_engine.repository import TeacherContextRepository
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.get_or_create(teacher_id)
|
|
context.onboarding_completed = False
|
|
context.macro_phase = MacroPhaseEnum.ONBOARDING
|
|
db.commit()
|
|
db.refresh(context)
|
|
return {
|
|
"success": True,
|
|
"macro_phase": "onboarding",
|
|
"teacher_id": teacher_id,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to reset onboarding: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
# === Events Endpoints ===
|
|
|
|
@router.get("/v1/events")
|
|
async def get_events(
|
|
teacher_id: str = Query(...),
|
|
status: Optional[str] = None,
|
|
event_type: Optional[str] = None,
|
|
limit: int = 50,
|
|
db=Depends(get_db)
|
|
):
|
|
"""Holt Events eines Lehrers."""
|
|
if not DB_ENABLED or not db:
|
|
return {"events": [], "count": 0}
|
|
|
|
try:
|
|
from classroom_engine.repository import SchoolyearEventRepository
|
|
repo = SchoolyearEventRepository(db)
|
|
events = repo.get_by_teacher(teacher_id, status=status, event_type=event_type, limit=limit)
|
|
return {
|
|
"events": [repo.to_dict(e) for e in events],
|
|
"count": len(events),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get events: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.get("/v1/events/upcoming")
|
|
async def get_upcoming_events(
|
|
teacher_id: str = Query(...),
|
|
days: int = 30,
|
|
limit: int = 10,
|
|
db=Depends(get_db)
|
|
):
|
|
"""Holt anstehende Events der naechsten X Tage."""
|
|
if not DB_ENABLED or not db:
|
|
return {"events": [], "count": 0}
|
|
|
|
try:
|
|
from classroom_engine.repository import SchoolyearEventRepository
|
|
repo = SchoolyearEventRepository(db)
|
|
events = repo.get_upcoming(teacher_id, days=days, limit=limit)
|
|
return {
|
|
"events": [repo.to_dict(e) for e in events],
|
|
"count": len(events),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get upcoming events: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.post("/v1/events", response_model=EventResponse)
|
|
async def create_event(
|
|
teacher_id: str,
|
|
request: CreateEventRequest,
|
|
db=Depends(get_db)
|
|
):
|
|
"""Erstellt ein neues Schuljahr-Event."""
|
|
if not DB_ENABLED or not db:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
from classroom_engine.repository import SchoolyearEventRepository
|
|
repo = SchoolyearEventRepository(db)
|
|
start_date = datetime.fromisoformat(request.start_date.replace('Z', '+00:00'))
|
|
end_date = None
|
|
if request.end_date:
|
|
end_date = datetime.fromisoformat(request.end_date.replace('Z', '+00:00'))
|
|
|
|
event = repo.create(
|
|
teacher_id=teacher_id,
|
|
title=request.title,
|
|
event_type=request.event_type,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
class_id=request.class_id,
|
|
subject=request.subject,
|
|
description=request.description,
|
|
needs_preparation=request.needs_preparation,
|
|
reminder_days_before=request.reminder_days_before,
|
|
)
|
|
|
|
return EventResponse(
|
|
id=event.id,
|
|
teacher_id=event.teacher_id,
|
|
event_type=event.event_type.value,
|
|
title=event.title,
|
|
description=event.description,
|
|
start_date=event.start_date.isoformat(),
|
|
end_date=event.end_date.isoformat() if event.end_date else None,
|
|
class_id=event.class_id,
|
|
subject=event.subject,
|
|
status=event.status.value,
|
|
needs_preparation=event.needs_preparation,
|
|
preparation_done=event.preparation_done,
|
|
reminder_days_before=event.reminder_days_before,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create event: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.delete("/v1/events/{event_id}")
|
|
async def delete_event(event_id: str, db=Depends(get_db)):
|
|
"""Loescht ein Event."""
|
|
if not DB_ENABLED or not db:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
from classroom_engine.repository import SchoolyearEventRepository
|
|
repo = SchoolyearEventRepository(db)
|
|
if repo.delete(event_id):
|
|
return {"success": True, "deleted_id": event_id}
|
|
raise HTTPException(status_code=404, detail="Event nicht gefunden")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete event: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
# === Routines Endpoints ===
|
|
|
|
@router.get("/v1/routines")
|
|
async def get_routines(
|
|
teacher_id: str = Query(...),
|
|
is_active: bool = True,
|
|
routine_type: Optional[str] = None,
|
|
db=Depends(get_db)
|
|
):
|
|
"""Holt Routinen eines Lehrers."""
|
|
if not DB_ENABLED or not db:
|
|
return {"routines": [], "count": 0}
|
|
|
|
try:
|
|
from classroom_engine.repository import RecurringRoutineRepository
|
|
repo = RecurringRoutineRepository(db)
|
|
routines = repo.get_by_teacher(teacher_id, is_active=is_active, routine_type=routine_type)
|
|
return {
|
|
"routines": [repo.to_dict(r) for r in routines],
|
|
"count": len(routines),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get routines: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.get("/v1/routines/today")
|
|
async def get_today_routines(teacher_id: str = Query(...), db=Depends(get_db)):
|
|
"""Holt Routinen die heute stattfinden."""
|
|
if not DB_ENABLED or not db:
|
|
return {"routines": [], "count": 0}
|
|
|
|
try:
|
|
from classroom_engine.repository import RecurringRoutineRepository
|
|
repo = RecurringRoutineRepository(db)
|
|
routines = repo.get_today(teacher_id)
|
|
return {
|
|
"routines": [repo.to_dict(r) for r in routines],
|
|
"count": len(routines),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get today's routines: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.post("/v1/routines", response_model=RoutineResponse)
|
|
async def create_routine(
|
|
teacher_id: str,
|
|
request: CreateRoutineRequest,
|
|
db=Depends(get_db)
|
|
):
|
|
"""Erstellt eine neue wiederkehrende Routine."""
|
|
if not DB_ENABLED or not db:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
from classroom_engine.repository import RecurringRoutineRepository
|
|
repo = RecurringRoutineRepository(db)
|
|
routine = repo.create(
|
|
teacher_id=teacher_id,
|
|
title=request.title,
|
|
routine_type=request.routine_type,
|
|
recurrence_pattern=request.recurrence_pattern,
|
|
day_of_week=request.day_of_week,
|
|
day_of_month=request.day_of_month,
|
|
time_of_day=request.time_of_day,
|
|
duration_minutes=request.duration_minutes,
|
|
description=request.description,
|
|
)
|
|
|
|
return RoutineResponse(
|
|
id=routine.id,
|
|
teacher_id=routine.teacher_id,
|
|
routine_type=routine.routine_type.value,
|
|
title=routine.title,
|
|
description=routine.description,
|
|
recurrence_pattern=routine.recurrence_pattern.value,
|
|
day_of_week=routine.day_of_week,
|
|
day_of_month=routine.day_of_month,
|
|
time_of_day=routine.time_of_day.isoformat() if routine.time_of_day else None,
|
|
duration_minutes=routine.duration_minutes,
|
|
is_active=routine.is_active,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create routine: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.delete("/v1/routines/{routine_id}")
|
|
async def delete_routine(routine_id: str, db=Depends(get_db)):
|
|
"""Loescht eine Routine."""
|
|
if not DB_ENABLED or not db:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
from classroom_engine.repository import RecurringRoutineRepository
|
|
repo = RecurringRoutineRepository(db)
|
|
if repo.delete(routine_id):
|
|
return {"success": True, "deleted_id": routine_id}
|
|
raise HTTPException(status_code=404, detail="Routine nicht gefunden")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete routine: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
# === Static Data Endpoints ===
|
|
|
|
@router.get("/v1/federal-states")
|
|
async def get_federal_states():
|
|
"""Gibt alle Bundeslaender zurueck."""
|
|
return {
|
|
"federal_states": [{"id": k, "name": v} for k, v in FEDERAL_STATES.items()]
|
|
}
|
|
|
|
|
|
@router.get("/v1/school-types")
|
|
async def get_school_types():
|
|
"""Gibt alle Schularten zurueck."""
|
|
return {
|
|
"school_types": [{"id": k, "name": v} for k, v in SCHOOL_TYPES.items()]
|
|
}
|
|
|
|
|
|
@router.get("/v1/macro-phases")
|
|
async def get_macro_phases():
|
|
"""Gibt alle Makro-Phasen mit Beschreibungen zurueck."""
|
|
phases = [
|
|
{"id": "onboarding", "label": "Einrichtung", "description": "Ersteinrichtung (Klassen, Stundenplan)", "order": 1},
|
|
{"id": "schuljahresstart", "label": "Schuljahresstart", "description": "Erste 2-3 Wochen des Schuljahres", "order": 2},
|
|
{"id": "unterrichtsaufbau", "label": "Unterrichtsaufbau", "description": "Routinen etablieren, erste Bewertungen", "order": 3},
|
|
{"id": "leistungsphase_1", "label": "Leistungsphase 1", "description": "Erste Klassenarbeiten und Klausuren", "order": 4},
|
|
{"id": "halbjahresabschluss", "label": "Halbjahresabschluss", "description": "Notenschluss, Zeugnisse, Konferenzen", "order": 5},
|
|
{"id": "leistungsphase_2", "label": "Leistungsphase 2", "description": "Zweites Halbjahr, Pruefungsvorbereitung", "order": 6},
|
|
{"id": "jahresabschluss", "label": "Jahresabschluss", "description": "Finale Noten, Versetzung, Schuljahresende", "order": 7},
|
|
]
|
|
return {"macro_phases": phases}
|
|
|
|
|
|
@router.get("/v1/event-types")
|
|
async def get_event_types():
|
|
"""Gibt alle Event-Typen zurueck."""
|
|
types = [
|
|
{"id": "exam", "label": "Klassenarbeit/Klausur"},
|
|
{"id": "parent_evening", "label": "Elternabend"},
|
|
{"id": "trip", "label": "Klassenfahrt/Ausflug"},
|
|
{"id": "project", "label": "Projektwoche"},
|
|
{"id": "internship", "label": "Praktikum"},
|
|
{"id": "presentation", "label": "Referate/Praesentationen"},
|
|
{"id": "sports_day", "label": "Sporttag"},
|
|
{"id": "school_festival", "label": "Schulfest"},
|
|
{"id": "parent_consultation", "label": "Elternsprechtag"},
|
|
{"id": "grade_deadline", "label": "Notenschluss"},
|
|
{"id": "report_cards", "label": "Zeugnisausgabe"},
|
|
{"id": "holiday_start", "label": "Ferienbeginn"},
|
|
{"id": "holiday_end", "label": "Ferienende"},
|
|
{"id": "other", "label": "Sonstiges"},
|
|
]
|
|
return {"event_types": types}
|
|
|
|
|
|
@router.get("/v1/routine-types")
|
|
async def get_routine_types():
|
|
"""Gibt alle Routine-Typen zurueck."""
|
|
types = [
|
|
{"id": "teacher_conference", "label": "Lehrerkonferenz"},
|
|
{"id": "subject_conference", "label": "Fachkonferenz"},
|
|
{"id": "office_hours", "label": "Sprechstunde"},
|
|
{"id": "team_meeting", "label": "Teamsitzung"},
|
|
{"id": "supervision", "label": "Pausenaufsicht"},
|
|
{"id": "correction_time", "label": "Korrekturzeit"},
|
|
{"id": "prep_time", "label": "Vorbereitungszeit"},
|
|
{"id": "other", "label": "Sonstiges"},
|
|
]
|
|
return {"routine_types": types}
|
|
|
|
|
|
# === Suggestions & Sidebar ===
|
|
|
|
@router.get("/v1/suggestions")
|
|
async def get_suggestions(
|
|
teacher_id: str = Query(...),
|
|
limit: int = Query(5, ge=1, le=20),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Generiert kontextbasierte Vorschlaege fuer einen Lehrer."""
|
|
if DB_ENABLED and db:
|
|
try:
|
|
from classroom_engine.suggestions import SuggestionGenerator
|
|
generator = SuggestionGenerator(db)
|
|
result = generator.generate(teacher_id, limit=limit)
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate suggestions: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
return {
|
|
"active_contexts": [],
|
|
"suggestions": [],
|
|
"signals_summary": {
|
|
"macro_phase": "onboarding",
|
|
"current_week": 1,
|
|
"has_classes": False,
|
|
"exams_soon": 0,
|
|
"routines_today": 0,
|
|
},
|
|
"total_suggestions": 0,
|
|
}
|
|
|
|
|
|
@router.get("/v1/sidebar")
|
|
async def get_sidebar(
|
|
teacher_id: str = Query(...),
|
|
mode: str = Query("companion"),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Generiert das dynamische Sidebar-Model."""
|
|
if mode == "companion":
|
|
now_relevant = []
|
|
if DB_ENABLED and db:
|
|
try:
|
|
from classroom_engine.suggestions import SuggestionGenerator
|
|
generator = SuggestionGenerator(db)
|
|
result = generator.generate(teacher_id, limit=5)
|
|
now_relevant = [
|
|
{
|
|
"id": s["id"],
|
|
"label": s["title"],
|
|
"state": "recommended" if s["priority"] > 70 else "default",
|
|
"badge": s.get("badge"),
|
|
"icon": s.get("icon", "lightbulb"),
|
|
"action_url": s.get("action_url"),
|
|
}
|
|
for s in result.get("suggestions", [])
|
|
]
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get suggestions for sidebar: {e}")
|
|
|
|
return {
|
|
"mode": "companion",
|
|
"sections": [
|
|
{"id": "SEARCH", "type": "search_bar", "placeholder": "Suchen..."},
|
|
{
|
|
"id": "NOW_RELEVANT",
|
|
"type": "list",
|
|
"title": "Jetzt relevant",
|
|
"items": now_relevant if now_relevant else [
|
|
{"id": "no_suggestions", "label": "Keine Vorschlaege", "state": "default", "icon": "check_circle"}
|
|
],
|
|
},
|
|
{
|
|
"id": "ALL_MODULES",
|
|
"type": "folder",
|
|
"label": "Alle Module",
|
|
"icon": "folder",
|
|
"collapsed": True,
|
|
"items": [
|
|
{"id": "lesson", "label": "Stundenmodus", "icon": "timer"},
|
|
{"id": "classes", "label": "Klassen", "icon": "groups"},
|
|
{"id": "exams", "label": "Klausuren", "icon": "quiz"},
|
|
{"id": "grades", "label": "Noten", "icon": "calculate"},
|
|
{"id": "calendar", "label": "Kalender", "icon": "calendar_month"},
|
|
{"id": "materials", "label": "Materialien", "icon": "folder_open"},
|
|
],
|
|
},
|
|
{
|
|
"id": "QUICK_ACTIONS",
|
|
"type": "actions",
|
|
"title": "Kurzaktionen",
|
|
"items": [
|
|
{"id": "scan", "label": "Scan hochladen", "icon": "upload_file"},
|
|
{"id": "note", "label": "Notiz erstellen", "icon": "note_add"},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
else:
|
|
return {
|
|
"mode": "classic",
|
|
"sections": [
|
|
{
|
|
"id": "NAVIGATION",
|
|
"type": "tree",
|
|
"items": [
|
|
{"id": "dashboard", "label": "Dashboard", "icon": "dashboard", "url": "/dashboard"},
|
|
{"id": "lesson", "label": "Stundenmodus", "icon": "timer", "url": "/lesson"},
|
|
{"id": "classes", "label": "Klassen", "icon": "groups", "url": "/classes"},
|
|
{"id": "exams", "label": "Klausuren", "icon": "quiz", "url": "/exams"},
|
|
{"id": "grades", "label": "Noten", "icon": "calculate", "url": "/grades"},
|
|
{"id": "calendar", "label": "Kalender", "icon": "calendar_month", "url": "/calendar"},
|
|
{"id": "materials", "label": "Materialien", "icon": "folder_open", "url": "/materials"},
|
|
{"id": "settings", "label": "Einstellungen", "icon": "settings", "url": "/settings"},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
@router.get("/v1/path")
|
|
async def get_schoolyear_path(teacher_id: str = Query(...), db=Depends(get_db)):
|
|
"""Generiert den Schuljahres-Pfad mit Meilensteinen."""
|
|
current_phase = "onboarding"
|
|
if DB_ENABLED and db:
|
|
try:
|
|
from classroom_engine.repository import TeacherContextRepository
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.get_or_create(teacher_id)
|
|
current_phase = context.macro_phase.value
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get context for path: {e}")
|
|
|
|
phase_order = [
|
|
"onboarding", "schuljahresstart", "unterrichtsaufbau",
|
|
"leistungsphase_1", "halbjahresabschluss", "leistungsphase_2", "jahresabschluss",
|
|
]
|
|
|
|
current_index = phase_order.index(current_phase) if current_phase in phase_order else 0
|
|
|
|
milestones = [
|
|
{"id": "MS_START", "label": "Start", "phase": "onboarding", "icon": "flag"},
|
|
{"id": "MS_SETUP", "label": "Einrichtung", "phase": "schuljahresstart", "icon": "tune"},
|
|
{"id": "MS_ROUTINE", "label": "Routinen", "phase": "unterrichtsaufbau", "icon": "repeat"},
|
|
{"id": "MS_EXAM_1", "label": "Klausuren", "phase": "leistungsphase_1", "icon": "quiz"},
|
|
{"id": "MS_HALFYEAR", "label": "Halbjahr", "phase": "halbjahresabschluss", "icon": "event"},
|
|
{"id": "MS_EXAM_2", "label": "Pruefungen", "phase": "leistungsphase_2", "icon": "school"},
|
|
{"id": "MS_END", "label": "Abschluss", "phase": "jahresabschluss", "icon": "celebration"},
|
|
]
|
|
|
|
for milestone in milestones:
|
|
phase = milestone["phase"]
|
|
phase_index = phase_order.index(phase) if phase in phase_order else 999
|
|
if phase_index < current_index:
|
|
milestone["status"] = "done"
|
|
elif phase_index == current_index:
|
|
milestone["status"] = "current"
|
|
else:
|
|
milestone["status"] = "upcoming"
|
|
|
|
current_milestone_id = next(
|
|
(m["id"] for m in milestones if m["status"] == "current"),
|
|
milestones[0]["id"]
|
|
)
|
|
|
|
progress = int((current_index / (len(phase_order) - 1)) * 100) if len(phase_order) > 1 else 0
|
|
|
|
return {
|
|
"milestones": milestones,
|
|
"current_milestone_id": current_milestone_id,
|
|
"progress_percent": progress,
|
|
"current_phase": current_phase,
|
|
}
|