Phase 9a: Schulkalender — Bundesland-Auswahl + Monatsansicht mit Ferien
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 29s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Failing after 2m50s
CI / test-python-agent-core (push) Successful in 18s
CI / test-nodejs-website (push) Successful in 21s

Backend (school-service):
  - cal_public_event (region, event_type, name_de, name_en, start/end,
    UNIQUE(region, event_type, name_de, start_date)) — global snapshot.
  - cal_school_config (user_id PRIMARY KEY, bundesland, school year dates).
  - cal_school_event — Schul-eigene Termine; CRUD folgt in 9b.
  - GET /calendar/holidays?region=&from=&to= — Range-Query against
    cal_public_event, ordered by start_date.
  - GET / PUT /calendar/config — upsert Bundesland per User.
  - SeedFromSnapshot reads internal/seed/calendar_holidays.json on every
    boot; idempotent via the unique constraint. Async goroutine so the
    HTTP server starts immediately even if the seed file is large.

Data source:
  - scripts/calendar-snapshot.sh ruft openholidaysapi.org fuer alle 16
    Bundeslaender x 3 Schuljahre und schreibt
    school-service/internal/seed/calendar_holidays.json (854 Events,
    Stand Schuljahre 2026-2028).
  - Dockerfile kopiert das seed/-Verzeichnis ins Image, damit die
    Container-Datenbank beim ersten Start gefuellt wird.

Frontend (studio-v2):
  - /schulkalender Page mit Gradient + Blobs wie /stundenplan und
    /korrektur — gleicher Visual-Style.
  - BundeslandWizard: zeigt alle 16 Laender als Dropdown, speichert
    bei Klick die Config und switcht zur Monatsansicht.
  - MonthView: 6-Wochen-Grid Mo-So, Feiertage rose-toned, Schulferien
    amber-toned, heutiges Datum mit Indigo-Ring. Prev/Next/Heute
    Navigation.
  - lib/schulkalender/api.ts re-uses the stundenplan JWT helper so
    auth-mode wechselt nicht.
  - Sidebar bekommt einen Schulkalender-Eintrag (Icon mit Datum-Dots,
    Pfad /schulkalender) in allen 26 Sprachen.

Tests:
  - Go: 3 neue Validator-Tests (Bundesland len=5, EventType oneof,
    Pflichtfelder). 77 Tests gesamt, alle gruen.
  - Playwright: e2e/schulkalender.spec.ts mit Wizard, Save-Flow,
    MonthView-Render, Heute-Button, Sidebar-Link. Hermetisch via
    mockCalendarApi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-22 09:46:39 +02:00
parent 65e7ed94f6
commit 97e37837ee
18 changed files with 7951 additions and 0 deletions
@@ -0,0 +1,80 @@
package models
import (
"time"
"github.com/google/uuid"
)
// PublicEvent is a holiday or school-vacation row imported from
// OpenHolidaysAPI. Global (no owner) — same for every school per region.
type PublicEvent struct {
ID uuid.UUID `json:"id" db:"id"`
Region string `json:"region" db:"region"` // e.g. "DE-NI"
EventType string `json:"event_type" db:"event_type"` // public_holiday | school_holiday
NameDe string `json:"name_de" db:"name_de"`
NameEn string `json:"name_en,omitempty" db:"name_en"`
StartDate string `json:"start_date" db:"start_date"` // YYYY-MM-DD
EndDate string `json:"end_date" db:"end_date"`
Source string `json:"source,omitempty" db:"source"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// SchoolCalendarConfig stores the Bundesland selection for one school
// (= one Rektor account). One row per user.
type SchoolCalendarConfig struct {
UserID uuid.UUID `json:"user_id" db:"user_id"`
Bundesland string `json:"bundesland" db:"bundesland"` // DE-NI ...
SchoolYearStart *string `json:"school_year_start,omitempty" db:"school_year_start"`
SchoolYearEnd *string `json:"school_year_end,omitempty" db:"school_year_end"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// SchoolEvent is a user-managed event (Fortbildung, Schulfeier, …).
type SchoolEvent struct {
ID uuid.UUID `json:"id" db:"id"`
CreatedByUserID uuid.UUID `json:"created_by_user_id" db:"created_by_user_id"`
Title string `json:"title" db:"title"`
Description string `json:"description,omitempty" db:"description"`
EventType string `json:"event_type" db:"event_type"`
IsSchoolFree bool `json:"is_school_free" db:"is_school_free"`
StartDate string `json:"start_date" db:"start_date"`
EndDate string `json:"end_date" db:"end_date"`
StartTime *string `json:"start_time,omitempty" db:"start_time"`
EndTime *string `json:"end_time,omitempty" db:"end_time"`
AffectedClassIDs []uuid.UUID `json:"affected_class_ids" db:"affected_class_ids"`
VisibleToParents bool `json:"visible_to_parents" db:"visible_to_parents"`
NotifyParents bool `json:"notify_parents" db:"notify_parents"`
NotifyStudents bool `json:"notify_students" db:"notify_students"`
NotificationLeadDays []int `json:"notification_lead_days" db:"notification_lead_days"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Request DTOs
// UpsertSchoolCalendarConfigRequest sets or updates the Bundesland for the
// authenticated user. Both school-year dates are optional (defaults to the
// running year based on today's date).
type UpsertSchoolCalendarConfigRequest struct {
Bundesland string `json:"bundesland" binding:"required,len=5"`
SchoolYearStart *string `json:"school_year_start,omitempty"`
SchoolYearEnd *string `json:"school_year_end,omitempty"`
}
type CreateSchoolEventRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
EventType string `json:"event_type" binding:"required,oneof=fortbildung schulfeier klassenfahrt projekttag eltern_info andere"`
IsSchoolFree bool `json:"is_school_free"`
StartDate string `json:"start_date" binding:"required"`
EndDate string `json:"end_date" binding:"required"`
StartTime *string `json:"start_time,omitempty"`
EndTime *string `json:"end_time,omitempty"`
AffectedClassIDs []string `json:"affected_class_ids"`
VisibleToParents bool `json:"visible_to_parents"`
NotifyParents bool `json:"notify_parents"`
NotifyStudents bool `json:"notify_students"`
NotificationLeadDays []int `json:"notification_lead_days"`
}