Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
688 lines
23 KiB
Python
688 lines
23 KiB
Python
"""
|
|
Classroom API - Teacher Context Endpoints (v1 API).
|
|
|
|
Endpoints fuer Teacher Context, Events, Routines und Antizipations-Engine.
|
|
"""
|
|
|
|
from typing import Dict, List, Optional, Any
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Depends
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.orm import Session as DBSession
|
|
|
|
from .shared import init_db_if_needed, DB_ENABLED, logger
|
|
|
|
try:
|
|
from classroom_engine.database import get_db, SessionLocal
|
|
from classroom_engine.repository import (
|
|
TeacherContextRepository, SchoolyearEventRepository, RecurringRoutineRepository
|
|
)
|
|
from classroom_engine.context_models import (
|
|
MacroPhaseEnum, EventTypeEnum, EventStatusEnum,
|
|
RoutineTypeEnum, RecurrencePatternEnum,
|
|
FEDERAL_STATES, SCHOOL_TYPES
|
|
)
|
|
from classroom_engine.antizipation import SuggestionGenerator
|
|
except ImportError:
|
|
FEDERAL_STATES = {}
|
|
SCHOOL_TYPES = {}
|
|
|
|
router = APIRouter(prefix="/v1", tags=["Teacher Context"])
|
|
|
|
|
|
# === Pydantic Models ===
|
|
|
|
class SchoolInfo(BaseModel):
|
|
federal_state: str
|
|
federal_state_name: str
|
|
school_type: str
|
|
school_type_name: str
|
|
|
|
|
|
class SchoolYearInfo(BaseModel):
|
|
id: str
|
|
start: Optional[str]
|
|
current_week: int
|
|
|
|
|
|
class MacroPhaseInfo(BaseModel):
|
|
id: str
|
|
label: str
|
|
confidence: float
|
|
|
|
|
|
class CoreCounts(BaseModel):
|
|
classes: int = 0
|
|
exams_scheduled: int = 0
|
|
corrections_pending: int = 0
|
|
|
|
|
|
class ContextFlags(BaseModel):
|
|
onboarding_completed: bool = False
|
|
has_classes: bool = False
|
|
has_schedule: bool = False
|
|
is_exam_period: bool = False
|
|
is_before_holidays: bool = False
|
|
|
|
|
|
class TeacherContextResponse(BaseModel):
|
|
schema_version: str = "1.0"
|
|
teacher_id: str
|
|
school: SchoolInfo
|
|
school_year: SchoolYearInfo
|
|
macro_phase: MacroPhaseInfo
|
|
core_counts: CoreCounts
|
|
flags: ContextFlags
|
|
|
|
|
|
class UpdateContextRequest(BaseModel):
|
|
federal_state: Optional[str] = None
|
|
school_type: Optional[str] = None
|
|
schoolyear: Optional[str] = None
|
|
schoolyear_start: Optional[str] = None
|
|
macro_phase: Optional[str] = None
|
|
current_week: Optional[int] = None
|
|
|
|
|
|
class CreateEventRequest(BaseModel):
|
|
title: str = Field(..., max_length=300)
|
|
event_type: str = "other"
|
|
start_date: str
|
|
end_date: Optional[str] = None
|
|
class_id: Optional[str] = None
|
|
subject: Optional[str] = None
|
|
description: str = ""
|
|
needs_preparation: bool = False
|
|
reminder_days_before: int = 3
|
|
|
|
|
|
class EventResponse(BaseModel):
|
|
id: str
|
|
teacher_id: str
|
|
event_type: str
|
|
title: str
|
|
description: str
|
|
start_date: str
|
|
end_date: Optional[str]
|
|
class_id: Optional[str]
|
|
subject: Optional[str]
|
|
status: str
|
|
needs_preparation: bool
|
|
preparation_done: bool
|
|
reminder_days_before: int
|
|
|
|
|
|
class CreateRoutineRequest(BaseModel):
|
|
title: str
|
|
routine_type: str = "other"
|
|
recurrence_pattern: str = "weekly"
|
|
day_of_week: Optional[int] = None
|
|
day_of_month: Optional[int] = None
|
|
time_of_day: Optional[str] = None
|
|
duration_minutes: int = 60
|
|
description: str = ""
|
|
|
|
|
|
class RoutineResponse(BaseModel):
|
|
id: str
|
|
teacher_id: str
|
|
routine_type: str
|
|
title: str
|
|
description: str
|
|
recurrence_pattern: str
|
|
day_of_week: Optional[int]
|
|
day_of_month: Optional[int]
|
|
time_of_day: Optional[str]
|
|
duration_minutes: int
|
|
is_active: bool
|
|
|
|
|
|
# === Helper Functions ===
|
|
|
|
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)
|
|
|
|
|
|
def get_default_context_response(teacher_id: str) -> TeacherContextResponse:
|
|
"""Gibt eine Default-Context-Response zurueck."""
|
|
return TeacherContextResponse(
|
|
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(),
|
|
)
|
|
|
|
|
|
# === Context Endpoints ===
|
|
|
|
@router.get("/context", response_model=TeacherContextResponse)
|
|
async def get_teacher_context(teacher_id: str = Query(...)):
|
|
"""Liefert den aktuellen Makro-Kontext eines Lehrers."""
|
|
if not DB_ENABLED:
|
|
return get_default_context_response(teacher_id)
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.get_or_create(teacher_id)
|
|
|
|
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"])
|
|
|
|
result = TeacherContextResponse(
|
|
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,
|
|
),
|
|
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,
|
|
),
|
|
)
|
|
db.close()
|
|
return result
|
|
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}")
|
|
|
|
|
|
@router.put("/context", response_model=TeacherContextResponse)
|
|
async def update_teacher_context(teacher_id: str, request: UpdateContextRequest):
|
|
"""Aktualisiert den Kontext eines Lehrers."""
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TeacherContextRepository(db)
|
|
|
|
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}")
|
|
|
|
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,
|
|
)
|
|
db.close()
|
|
|
|
return await get_teacher_context(teacher_id)
|
|
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("/context/complete-onboarding")
|
|
async def complete_onboarding(teacher_id: str = Query(...)):
|
|
"""Markiert das Onboarding als abgeschlossen."""
|
|
if not DB_ENABLED:
|
|
return {"success": True, "macro_phase": "schuljahresstart", "note": "DB not available"}
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.complete_onboarding(teacher_id)
|
|
db.close()
|
|
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("/context/reset-onboarding")
|
|
async def reset_onboarding(teacher_id: str = Query(...)):
|
|
"""Setzt das Onboarding zurueck (fuer Tests)."""
|
|
if not DB_ENABLED:
|
|
return {"success": True, "macro_phase": "onboarding", "note": "DB not available"}
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.get_or_create(teacher_id)
|
|
context.onboarding_completed = False
|
|
context.macro_phase = MacroPhaseEnum.ONBOARDING
|
|
db.commit()
|
|
db.close()
|
|
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("/events")
|
|
async def get_events(
|
|
teacher_id: str = Query(...),
|
|
status: Optional[str] = None,
|
|
event_type: Optional[str] = None,
|
|
limit: int = 50
|
|
):
|
|
"""Holt Events eines Lehrers."""
|
|
if not DB_ENABLED:
|
|
return {"events": [], "count": 0}
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = SchoolyearEventRepository(db)
|
|
events = repo.get_by_teacher(teacher_id, status=status, event_type=event_type, limit=limit)
|
|
result = {"events": [repo.to_dict(e) for e in events], "count": len(events)}
|
|
db.close()
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to get events: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.get("/events/upcoming")
|
|
async def get_upcoming_events(teacher_id: str = Query(...), days: int = 30, limit: int = 10):
|
|
"""Holt anstehende Events der naechsten X Tage."""
|
|
if not DB_ENABLED:
|
|
return {"events": [], "count": 0}
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = SchoolyearEventRepository(db)
|
|
events = repo.get_upcoming(teacher_id, days=days, limit=limit)
|
|
result = {"events": [repo.to_dict(e) for e in events], "count": len(events)}
|
|
db.close()
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to get upcoming events: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.post("/events", response_model=EventResponse)
|
|
async def create_event(teacher_id: str, request: CreateEventRequest):
|
|
"""Erstellt ein neues Schuljahr-Event."""
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = SchoolyearEventRepository(db)
|
|
start_date = datetime.fromisoformat(request.start_date.replace('Z', '+00:00'))
|
|
end_date = datetime.fromisoformat(request.end_date.replace('Z', '+00:00')) if request.end_date else None
|
|
|
|
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,
|
|
)
|
|
|
|
result = 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,
|
|
)
|
|
db.close()
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to create event: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.delete("/events/{event_id}")
|
|
async def delete_event(event_id: str):
|
|
"""Loescht ein Event."""
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = SchoolyearEventRepository(db)
|
|
if repo.delete(event_id):
|
|
db.close()
|
|
return {"success": True, "deleted_id": event_id}
|
|
db.close()
|
|
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("/routines")
|
|
async def get_routines(
|
|
teacher_id: str = Query(...),
|
|
is_active: bool = True,
|
|
routine_type: Optional[str] = None
|
|
):
|
|
"""Holt Routinen eines Lehrers."""
|
|
if not DB_ENABLED:
|
|
return {"routines": [], "count": 0}
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = RecurringRoutineRepository(db)
|
|
routines = repo.get_by_teacher(teacher_id, is_active=is_active, routine_type=routine_type)
|
|
result = {"routines": [repo.to_dict(r) for r in routines], "count": len(routines)}
|
|
db.close()
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to get routines: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.get("/routines/today")
|
|
async def get_today_routines(teacher_id: str = Query(...)):
|
|
"""Holt Routinen die heute stattfinden."""
|
|
if not DB_ENABLED:
|
|
return {"routines": [], "count": 0}
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = RecurringRoutineRepository(db)
|
|
routines = repo.get_today(teacher_id)
|
|
result = {"routines": [repo.to_dict(r) for r in routines], "count": len(routines)}
|
|
db.close()
|
|
return result
|
|
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("/routines", response_model=RoutineResponse)
|
|
async def create_routine(teacher_id: str, request: CreateRoutineRequest):
|
|
"""Erstellt eine neue wiederkehrende Routine."""
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
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,
|
|
)
|
|
|
|
result = 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,
|
|
)
|
|
db.close()
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to create routine: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
@router.delete("/routines/{routine_id}")
|
|
async def delete_routine(routine_id: str):
|
|
"""Loescht eine Routine."""
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = RecurringRoutineRepository(db)
|
|
if repo.delete(routine_id):
|
|
db.close()
|
|
return {"success": True, "deleted_id": routine_id}
|
|
db.close()
|
|
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("/federal-states")
|
|
async def get_federal_states_list():
|
|
"""Gibt alle Bundeslaender zurueck."""
|
|
return {"federal_states": [{"id": k, "name": v} for k, v in FEDERAL_STATES.items()]}
|
|
|
|
|
|
@router.get("/school-types")
|
|
async def get_school_types_list():
|
|
"""Gibt alle Schularten zurueck."""
|
|
return {"school_types": [{"id": k, "name": v} for k, v in SCHOOL_TYPES.items()]}
|
|
|
|
|
|
@router.get("/macro-phases")
|
|
async def get_macro_phases_list():
|
|
"""Gibt alle Makro-Phasen zurueck."""
|
|
return {
|
|
"macro_phases": [
|
|
{"id": "onboarding", "label": "Einrichtung", "order": 1},
|
|
{"id": "schuljahresstart", "label": "Schuljahresstart", "order": 2},
|
|
{"id": "unterrichtsaufbau", "label": "Unterrichtsaufbau", "order": 3},
|
|
{"id": "leistungsphase_1", "label": "Leistungsphase 1", "order": 4},
|
|
{"id": "halbjahresabschluss", "label": "Halbjahresabschluss", "order": 5},
|
|
{"id": "leistungsphase_2", "label": "Leistungsphase 2", "order": 6},
|
|
{"id": "jahresabschluss", "label": "Jahresabschluss", "order": 7},
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/event-types")
|
|
async def get_event_types_list():
|
|
"""Gibt alle Event-Typen zurueck."""
|
|
return {
|
|
"event_types": [
|
|
{"id": "exam", "label": "Klassenarbeit/Klausur"},
|
|
{"id": "parent_evening", "label": "Elternabend"},
|
|
{"id": "trip", "label": "Klassenfahrt/Ausflug"},
|
|
{"id": "project", "label": "Projektwoche"},
|
|
{"id": "other", "label": "Sonstiges"},
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/routine-types")
|
|
async def get_routine_types_list():
|
|
"""Gibt alle Routine-Typen zurueck."""
|
|
return {
|
|
"routine_types": [
|
|
{"id": "teacher_conference", "label": "Lehrerkonferenz"},
|
|
{"id": "subject_conference", "label": "Fachkonferenz"},
|
|
{"id": "office_hours", "label": "Sprechstunde"},
|
|
{"id": "correction_time", "label": "Korrekturzeit"},
|
|
{"id": "other", "label": "Sonstiges"},
|
|
]
|
|
}
|
|
|
|
|
|
# === Antizipations-Engine ===
|
|
|
|
@router.get("/suggestions")
|
|
async def get_suggestions(teacher_id: str = Query(...), limit: int = Query(5, ge=1, le=20)):
|
|
"""Generiert kontextbasierte Vorschlaege fuer einen Lehrer."""
|
|
if not DB_ENABLED:
|
|
return {
|
|
"active_contexts": [],
|
|
"suggestions": [],
|
|
"signals_summary": {"macro_phase": "onboarding"},
|
|
"total_suggestions": 0,
|
|
}
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
generator = SuggestionGenerator(db)
|
|
result = generator.generate(teacher_id, limit=limit)
|
|
db.close()
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate suggestions: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
|
|
|
|
|
|
# === Sidebar ===
|
|
|
|
@router.get("/sidebar")
|
|
async def get_sidebar(teacher_id: str = Query(...), mode: str = Query("companion")):
|
|
"""Generiert das dynamische Sidebar-Model."""
|
|
if mode == "companion":
|
|
return {
|
|
"mode": "companion",
|
|
"sections": [
|
|
{"id": "SEARCH", "type": "search_bar", "placeholder": "Suchen..."},
|
|
{"id": "NOW_RELEVANT", "type": "list", "title": "Jetzt relevant", "items": []},
|
|
{
|
|
"id": "ALL_MODULES",
|
|
"type": "folder",
|
|
"label": "Alle Module",
|
|
"collapsed": True,
|
|
"items": [
|
|
{"id": "lesson", "label": "Stundenmodus", "icon": "timer"},
|
|
{"id": "classes", "label": "Klassen", "icon": "groups"},
|
|
{"id": "exams", "label": "Klausuren", "icon": "quiz"},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
return {
|
|
"mode": "classic",
|
|
"sections": [
|
|
{
|
|
"id": "NAVIGATION",
|
|
"type": "tree",
|
|
"items": [
|
|
{"id": "dashboard", "label": "Dashboard", "icon": "dashboard"},
|
|
{"id": "lesson", "label": "Stundenmodus", "icon": "timer"},
|
|
{"id": "classes", "label": "Klassen", "icon": "groups"},
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
# === Schuljahres-Pfad ===
|
|
|
|
@router.get("/path")
|
|
async def get_schoolyear_path(teacher_id: str = Query(...)):
|
|
"""Generiert den Schuljahres-Pfad mit Meilensteinen."""
|
|
current_phase = "onboarding"
|
|
if DB_ENABLED:
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TeacherContextRepository(db)
|
|
context = repo.get_or_create(teacher_id)
|
|
current_phase = context.macro_phase.value
|
|
db.close()
|
|
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"},
|
|
{"id": "MS_SETUP", "label": "Einrichtung", "phase": "schuljahresstart"},
|
|
{"id": "MS_ROUTINE", "label": "Routinen", "phase": "unterrichtsaufbau"},
|
|
{"id": "MS_EXAM_1", "label": "Klausuren", "phase": "leistungsphase_1"},
|
|
{"id": "MS_HALFYEAR", "label": "Halbjahr", "phase": "halbjahresabschluss"},
|
|
{"id": "MS_EXAM_2", "label": "Pruefungen", "phase": "leistungsphase_2"},
|
|
{"id": "MS_END", "label": "Abschluss", "phase": "jahresabschluss"},
|
|
]
|
|
|
|
for i, milestone in enumerate(milestones):
|
|
phase_index = phase_order.index(milestone["phase"])
|
|
if phase_index < current_index:
|
|
milestone["status"] = "done"
|
|
elif phase_index == current_index:
|
|
milestone["status"] = "current"
|
|
else:
|
|
milestone["status"] = "upcoming"
|
|
|
|
return {
|
|
"milestones": milestones,
|
|
"current_milestone_id": milestones[current_index]["id"],
|
|
"progress_percent": int((current_index / (len(phase_order) - 1)) * 100),
|
|
}
|