+
Erinnerungen
+
+ {rows.map((r, i) => (
+
+ {STATUS_ICON[r.status]} {r.lead_days === 0 ? 'Heute' : r.lead_days === 1 ? '1 Tag' : `${r.lead_days} Tage`}
+ {' · '}{r.audience === 'parents' ? 'Eltern' : 'Schueler'}
+ {' · '}{r.channel}
+
+ ))}
+
+
+ )
+}
diff --git a/studio-v2/app/schulkalender/types.ts b/studio-v2/app/schulkalender/types.ts
index b9cbfe7..b6c8d9d 100644
--- a/studio-v2/app/schulkalender/types.ts
+++ b/studio-v2/app/schulkalender/types.ts
@@ -159,3 +159,25 @@ export interface InviteParentResponse {
magic_url: string
expires_at: string
}
+
+// ---------- Notifications (Phase 9d) ----------
+
+export type NotificationStatus = 'sent' | 'failed' | 'skipped'
+
+export interface NotificationLogRow {
+ lead_days: number
+ audience: 'parents' | 'students'
+ channel: 'matrix' | 'email'
+ status: NotificationStatus
+ error_message?: string
+ run_date: string
+ created_at: string
+}
+
+export interface NotificationRunResult {
+ date: string
+ sent: number
+ failed: number
+ skipped: number
+ already_logged: number
+}
diff --git a/studio-v2/e2e/schulkalender.spec.ts b/studio-v2/e2e/schulkalender.spec.ts
index db4c650..6473399 100644
--- a/studio-v2/e2e/schulkalender.spec.ts
+++ b/studio-v2/e2e/schulkalender.spec.ts
@@ -10,6 +10,7 @@ interface MockOpts {
config?: { user_id: string; bundesland: string } | null
holidays?: unknown[]
events?: unknown[]
+ notificationLog?: unknown[]
}
async function mockCalendarApi(page: Page, opts: MockOpts = {}) {
@@ -83,6 +84,16 @@ async function mockCalendarApi(page: Page, opts: MockOpts = {}) {
}),
})
})
+
+ // Phase 9d: per-event notification_log + manual trigger. NotificationStatus
+ // component fetches the log when an event has notify_parents/students.
+ await page.route(/\/api\/school\/calendar\/events\/[^/]+\/notifications$/, async (route) => {
+ return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(opts.notificationLog ?? []) })
+ })
+ await page.route(/\/api\/school\/calendar\/notifications\/run-now.*/, async (route) => {
+ return route.fulfill({ status: 200, contentType: 'application/json',
+ body: '{"date":"2026-05-22","sent":0,"failed":0,"skipped":0,"already_logged":0}' })
+ })
}
test.describe('Schulkalender — Bundesland Wizard', () => {
@@ -249,3 +260,58 @@ test.describe('Schulkalender — Schuljahres-Rollover', () => {
await expect(page.getByText('2 Abschlussklassen entfernt')).toBeVisible()
})
})
+
+// ==========================================================================
+// Phase 9d — Notification-Status im DayDetail
+// ==========================================================================
+
+test.describe('Schulkalender — Notification-Status', () => {
+ test('shows sent badge for delivered reminders', async ({ page }) => {
+ const todayIso = new Date().toISOString().slice(0, 10)
+ await mockCalendarApi(page, {
+ config: { user_id: 'dev', bundesland: 'DE-NI' },
+ events: [{
+ id: 'e1', created_by_user_id: 'dev',
+ title: 'Pruefe Test-Event', event_type: 'projekttag',
+ is_school_free: false,
+ start_date: todayIso, end_date: todayIso,
+ affected_class_ids: [], visible_to_parents: true,
+ notify_parents: true, notify_students: false,
+ notification_lead_days: [7, 1],
+ }],
+ notificationLog: [
+ { lead_days: 7, audience: 'parents', channel: 'email', status: 'sent', run_date: '2026-05-15', created_at: '2026-05-15T06:00:00Z' },
+ { lead_days: 1, audience: 'parents', channel: 'email', status: 'skipped', run_date: '2026-05-21', created_at: '2026-05-21T06:00:00Z' },
+ ],
+ })
+ await page.goto('/schulkalender')
+ await page.waitForLoadState('networkidle')
+ await page.getByTestId(`day-${todayIso}`).click()
+ await expect(page.getByTestId('day-detail')).toBeVisible()
+
+ const status = page.getByTestId('notif-status-e1')
+ await expect(status).toBeVisible()
+ await expect(status.getByText(/7 Tage.*Eltern.*email/)).toBeVisible()
+ await expect(status.getByText(/1 Tag.*Eltern.*email/)).toBeVisible()
+ })
+
+ test('hides notification status when notifications are off', async ({ page }) => {
+ const todayIso = new Date().toISOString().slice(0, 10)
+ await mockCalendarApi(page, {
+ config: { user_id: 'dev', bundesland: 'DE-NI' },
+ events: [{
+ id: 'e2', created_by_user_id: 'dev',
+ title: 'Stilles Event', event_type: 'andere',
+ is_school_free: false,
+ start_date: todayIso, end_date: todayIso,
+ affected_class_ids: [], visible_to_parents: true,
+ notify_parents: false, notify_students: false,
+ notification_lead_days: [],
+ }],
+ })
+ await page.goto('/schulkalender')
+ await page.waitForLoadState('networkidle')
+ await page.getByTestId(`day-${todayIso}`).click()
+ await expect(page.getByTestId('notif-status-e2')).toHaveCount(0)
+ })
+})
diff --git a/studio-v2/lib/schulkalender/api.ts b/studio-v2/lib/schulkalender/api.ts
index 1c54bbf..389a427 100644
--- a/studio-v2/lib/schulkalender/api.ts
+++ b/studio-v2/lib/schulkalender/api.ts
@@ -8,6 +8,7 @@ import type {
PublicEvent, SchoolCalendarConfig, UpsertSchoolCalendarConfig,
SchoolEvent, CreateSchoolEvent, SchoolYearRolloverResult,
ParentInviteListItem, InviteParentRequest, InviteParentResponse,
+ NotificationLogRow, NotificationRunResult,
} from '@/app/schulkalender/types'
async function apiFetch