Files
breakpilot-lehrer/studio-v2/app/schulkalender/_components/DayDetail.tsx
T
Benjamin Admin 33409352ee
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 28s
CI / test-go-edu-search (push) Successful in 28s
CI / test-python-klausur (push) Failing after 2m38s
CI / test-python-agent-core (push) Successful in 20s
CI / test-nodejs-website (push) Successful in 26s
Phase 9b: Schul-Events CRUD + Schuljahres-Rollover
Backend (school-service):
  - calendar_events.go — Create/List/Delete on cal_school_event with
    UUID[] handling for affected_class_ids. Default lead-days [7,1]
    if caller omits the array.
  - calendar_rollover.go — single-transaction promotion: graduating
    classes (grade >= 13) get deleted first so the +1 update doesn't
    bump them to invalid grade 14. defaultSchoolYearDates() picks the
    next Aug-Jul pair when the caller doesn't specify.
  - Handlers + routes: GET/POST /calendar/events,
    DELETE /calendar/events/:id, POST /calendar/school-year-rollover.

Frontend (studio-v2):
  - EventModal: form with Title / Typ / Datum/Zeit / unterrichtsfrei /
    Beschreibung / Sichtbarkeit + Notification-Checkboxen. Per-Type
    Farb-Mapping in types.ts.
  - DayDetail: Modal das beim Klick auf einen Kalender-Tag aufgeht und
    Feiertage + Schulferien + Schul-Events fuer diesen Tag listet,
    inkl. Loeschen-Button pro Event.
  - RolloverWizard: zwei-Schritt-Dialog mit Datums-Auswahl + Tipp-
    Bestaetigung ("SCHULJAHR WECHSELN") gegen versehentliche Auslo-
    sung, danach Ergebnis-Card mit promoted/graduated-Counts.
  - MonthView gewinnt onDayClick + onAddEvent + onRollover Props,
    rendert farb-codierte Punkte fuer School-Events am Tagesrand.
  - Page laed Events parallel mit Holidays und reicht alle Handler
    nach unten.

Tests:
  - Go: 3 neue Tests fuer defaultSchoolYearDates + parseClassIDs.
    Validator-Test fuer CreateSchoolEventRequest existiert bereits.
    80 Subtests gesamt, alle gruen.
  - Playwright: mockCalendarApi gewinnt Routes fuer events GET/POST/
    DELETE und school-year-rollover. 6 neue Tests (EventModal open,
    submit, DayDetail open, Rollover-Trigger, Confirm-Schutz,
    Ergebnis-Anzeige).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:32:33 +02:00

103 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useTheme } from '@/lib/ThemeContext'
import { calendarApi } from '@/lib/schulkalender/api'
import type { PublicEvent, SchoolEvent } from '@/app/schulkalender/types'
import { EVENT_TYPE_COLOR, EVENT_TYPE_LABEL } from '@/app/schulkalender/types'
interface DayDetailProps {
iso: string
holidays: PublicEvent[]
events: SchoolEvent[]
onClose: () => void
onDeleted: () => void
}
export function DayDetail({ iso, holidays, events, onClose, onDeleted }: DayDetailProps) {
const { isDark } = useTheme()
const handleDelete = async (id: string) => {
if (!confirm('Termin wirklich loeschen?')) return
try {
await calendarApi.deleteEvent(id)
onDeleted()
} catch {
// best-effort
}
}
const cardClass = isDark ? 'bg-slate-900/95 border-white/20 text-white' : 'bg-white border-black/10 text-slate-900'
const dayHolidays = holidays.filter(h => iso >= h.start_date && iso <= h.end_date)
const dayEvents = events.filter(e => iso >= e.start_date && iso <= e.end_date)
const formattedDate = new Date(iso).toLocaleDateString('de-DE', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
})
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur" data-testid="day-detail">
<div className={`w-full max-w-lg rounded-2xl border p-6 space-y-4 max-h-[90vh] overflow-y-auto ${cardClass}`}>
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">{formattedDate}</h2>
<button onClick={onClose} className="opacity-60 hover:opacity-100"></button>
</div>
{dayHolidays.length === 0 && dayEvents.length === 0 && (
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
Keine Eintraege fuer diesen Tag.
</p>
)}
{dayHolidays.length > 0 && (
<section className="space-y-2">
<h3 className="text-sm font-medium opacity-80">Bundesweite Eintraege</h3>
{dayHolidays.map(h => (
<div key={h.id} className={`p-2 rounded-lg text-sm ${h.event_type === 'public_holiday' ? (isDark ? 'bg-rose-500/20' : 'bg-rose-50') : (isDark ? 'bg-amber-500/20' : 'bg-amber-50')}`}>
<div className="font-medium">{h.name_de}</div>
<div className="text-xs opacity-70">{h.event_type === 'public_holiday' ? 'Feiertag' : 'Schulferien'} · {h.start_date}{h.start_date !== h.end_date ? ` ${h.end_date}` : ''}</div>
</div>
))}
</section>
)}
{dayEvents.length > 0 && (
<section className="space-y-2">
<h3 className="text-sm font-medium opacity-80">Schul-Termine</h3>
{dayEvents.map(e => (
<div
key={e.id}
className={`p-3 rounded-lg ${isDark ? 'bg-white/10' : 'bg-slate-50'}`}
style={{ borderLeft: `4px solid ${EVENT_TYPE_COLOR[e.event_type]}` }}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="font-medium">{e.title}</div>
<div className="text-xs opacity-70 mt-0.5">
{EVENT_TYPE_LABEL[e.event_type]}
{e.start_time && ` · ${e.start_time}${e.end_time ? `${e.end_time}` : ''}`}
{e.is_school_free && ' · unterrichtsfrei'}
</div>
{e.description && <div className="text-sm opacity-90 mt-1">{e.description}</div>}
<div className="text-xs opacity-60 mt-1.5">
{e.visible_to_parents && '👨‍👩‍👧 sichtbar fuer Eltern'}
{e.notify_parents && ' · 📧 Eltern erinnern'}
{e.notify_students && ' · 💬 Schueler erinnern'}
</div>
</div>
<button
onClick={() => handleDelete(e.id)}
className="text-xs text-red-400 hover:text-red-300"
>
Loeschen
</button>
</div>
</div>
))}
</section>
)}
</div>
</div>
)
}