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>
335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'
|
|
|
|
// Types
|
|
export type AlertImportance = 'KRITISCH' | 'DRINGEND' | 'WICHTIG' | 'PRUEFEN' | 'INFO'
|
|
|
|
export interface AlertSource {
|
|
title: string
|
|
url: string
|
|
domain: string
|
|
}
|
|
|
|
export interface Alert {
|
|
id: string
|
|
title: string
|
|
summary: string // LLM-generierte Zusammenfassung
|
|
source: string
|
|
importance: AlertImportance
|
|
timestamp: Date
|
|
isRead: boolean
|
|
sources: AlertSource[]
|
|
topicId?: string
|
|
}
|
|
|
|
export interface Topic {
|
|
id: string
|
|
name: string
|
|
keywords: string[]
|
|
googleAlertUrl?: string
|
|
rssFeedUrl?: string
|
|
isActive: boolean
|
|
icon?: string
|
|
}
|
|
|
|
export interface AlertsSettings {
|
|
notificationFrequency: 'realtime' | 'hourly' | 'daily'
|
|
minImportance: AlertImportance
|
|
wizardCompleted: boolean
|
|
}
|
|
|
|
interface AlertsContextType {
|
|
alerts: Alert[]
|
|
unreadCount: number
|
|
isLoading: boolean
|
|
error: string | null
|
|
fetchAlerts: () => Promise<void>
|
|
markAsRead: (id: string) => void
|
|
markAllAsRead: () => void
|
|
topics: Topic[]
|
|
addTopic: (topic: Topic) => void
|
|
updateTopic: (id: string, updates: Partial<Topic>) => void
|
|
removeTopic: (id: string) => void
|
|
settings: AlertsSettings
|
|
updateSettings: (settings: Partial<AlertsSettings>) => void
|
|
}
|
|
|
|
const AlertsContext = createContext<AlertsContextType | null>(null)
|
|
|
|
// LocalStorage Keys
|
|
const ALERTS_KEY = 'bp_alerts'
|
|
const TOPICS_KEY = 'bp_alerts_topics'
|
|
const SETTINGS_KEY = 'bp_alerts_settings'
|
|
|
|
// Default settings
|
|
const defaultSettings: AlertsSettings = {
|
|
notificationFrequency: 'daily',
|
|
minImportance: 'INFO',
|
|
wizardCompleted: false
|
|
}
|
|
|
|
// Vordefinierte Themen fuer Lehrer
|
|
export const lehrerThemen: Omit<Topic, 'id'>[] = [
|
|
{ name: 'Bildungspolitik', icon: '📜', keywords: ['Kultusministerium', 'Schulreform', 'Lehrplan', 'Bildungsminister'], isActive: false },
|
|
{ name: 'Digitale Bildung', icon: '💻', keywords: ['iPad Schule', 'digitale Medien', 'E-Learning', 'Tablet Unterricht'], isActive: false },
|
|
{ name: 'Inklusion', icon: '🤝', keywords: ['Förderschule', 'Integration', 'barrierefreies Lernen', 'Inklusion Schule'], isActive: false },
|
|
{ name: 'Abitur & Prüfungen', icon: '📝', keywords: ['Abitur', 'Zentralabitur', 'Prüfungsordnung', 'Abiturprüfung'], isActive: false },
|
|
{ name: 'Lehrerberuf', icon: '👩🏫', keywords: ['Lehrkräftemangel', 'Besoldung', 'Quereinsteiger', 'Lehrergehalt'], isActive: false },
|
|
{ name: 'KI in der Schule', icon: '🤖', keywords: ['ChatGPT Schule', 'KI Unterricht', 'künstliche Intelligenz Bildung'], isActive: false },
|
|
]
|
|
|
|
// Mock-Alerts für Demo (werden später durch Backend ersetzt)
|
|
const mockAlerts: Alert[] = [
|
|
{
|
|
id: 'alert-1',
|
|
title: 'Niedersachsen plant Digitalisierungsoffensive an Schulen',
|
|
summary: 'Das Kultusministerium Niedersachsen kündigt ein 50-Millionen-Programm zur Digitalisierung an. Alle weiterführenden Schulen sollen bis 2027 mit interaktiven Displays ausgestattet werden. Fortbildungen für Lehrkräfte sind Teil des Pakets.',
|
|
source: 'Hannoversche Allgemeine',
|
|
importance: 'WICHTIG',
|
|
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
|
isRead: false,
|
|
sources: [
|
|
{ title: 'Digitalisierung in Niedersachsens Schulen', url: 'https://example.com/1', domain: 'haz.de' }
|
|
],
|
|
topicId: 'digitale-bildung'
|
|
},
|
|
{
|
|
id: 'alert-2',
|
|
title: 'Neue Richtlinien für ChatGPT im Unterricht veröffentlicht',
|
|
summary: 'Die KMK hat Empfehlungen zum Umgang mit KI-Tools im Unterricht herausgegeben. Lehrkräfte sollen KI als Werkzeug nutzen dürfen, Prüfungsleistungen müssen jedoch eigenständig erbracht werden.',
|
|
source: 'Der Spiegel',
|
|
importance: 'DRINGEND',
|
|
timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000),
|
|
isRead: false,
|
|
sources: [
|
|
{ title: 'KMK-Richtlinien zu KI', url: 'https://example.com/2', domain: 'spiegel.de' }
|
|
],
|
|
topicId: 'ki-schule'
|
|
},
|
|
{
|
|
id: 'alert-3',
|
|
title: 'Lehrerverband fordert bessere Besoldung',
|
|
summary: 'Der Deutsche Lehrerverband kritisiert die aktuelle Besoldungssituation. Besonders Grundschullehrkräfte seien im Vergleich zu anderen Bundesländern benachteiligt.',
|
|
source: 'Zeit Online',
|
|
importance: 'PRUEFEN',
|
|
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
|
isRead: true,
|
|
sources: [
|
|
{ title: 'Lehrergehälter im Vergleich', url: 'https://example.com/3', domain: 'zeit.de' }
|
|
],
|
|
topicId: 'lehrerberuf'
|
|
}
|
|
]
|
|
|
|
export function AlertsProvider({ children }: { children: ReactNode }) {
|
|
const [alerts, setAlerts] = useState<Alert[]>([])
|
|
const [topics, setTopics] = useState<Topic[]>([])
|
|
const [settings, setSettings] = useState<AlertsSettings>(defaultSettings)
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
// Nach dem ersten Render: Daten aus localStorage laden
|
|
useEffect(() => {
|
|
const storedAlerts = localStorage.getItem(ALERTS_KEY)
|
|
const storedTopics = localStorage.getItem(TOPICS_KEY)
|
|
const storedSettings = localStorage.getItem(SETTINGS_KEY)
|
|
|
|
if (storedAlerts) {
|
|
try {
|
|
const parsed = JSON.parse(storedAlerts)
|
|
// Konvertiere timestamp strings zu Date objects
|
|
setAlerts(parsed.map((a: any) => ({ ...a, timestamp: new Date(a.timestamp) })))
|
|
} catch (e) {
|
|
console.error('Error parsing stored alerts:', e)
|
|
setAlerts(mockAlerts)
|
|
}
|
|
} else {
|
|
// Demo-Alerts laden
|
|
setAlerts(mockAlerts)
|
|
}
|
|
|
|
if (storedTopics) {
|
|
try {
|
|
setTopics(JSON.parse(storedTopics))
|
|
} catch (e) {
|
|
console.error('Error parsing stored topics:', e)
|
|
}
|
|
}
|
|
|
|
if (storedSettings) {
|
|
try {
|
|
setSettings({ ...defaultSettings, ...JSON.parse(storedSettings) })
|
|
} catch (e) {
|
|
console.error('Error parsing stored settings:', e)
|
|
}
|
|
}
|
|
|
|
setMounted(true)
|
|
}, [])
|
|
|
|
// Alerts in localStorage speichern
|
|
useEffect(() => {
|
|
if (mounted && alerts.length > 0) {
|
|
localStorage.setItem(ALERTS_KEY, JSON.stringify(alerts))
|
|
}
|
|
}, [alerts, mounted])
|
|
|
|
// Topics in localStorage speichern
|
|
useEffect(() => {
|
|
if (mounted) {
|
|
localStorage.setItem(TOPICS_KEY, JSON.stringify(topics))
|
|
}
|
|
}, [topics, mounted])
|
|
|
|
// Settings in localStorage speichern
|
|
useEffect(() => {
|
|
if (mounted) {
|
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
|
|
}
|
|
}, [settings, mounted])
|
|
|
|
// Alerts vom Backend abrufen
|
|
const fetchAlerts = useCallback(async () => {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
try {
|
|
// TODO: Backend-Integration
|
|
// const response = await fetch('http://macmini:8000/api/alerts/')
|
|
// const data = await response.json()
|
|
// setAlerts(data)
|
|
|
|
// Für jetzt: Mock-Daten verwenden
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
// Alerts sind bereits geladen
|
|
} catch (err) {
|
|
setError('Fehler beim Laden der Alerts')
|
|
console.error('Error fetching alerts:', err)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
// Alert als gelesen markieren
|
|
const markAsRead = useCallback((id: string) => {
|
|
setAlerts(prev => prev.map(alert =>
|
|
alert.id === id ? { ...alert, isRead: true } : alert
|
|
))
|
|
}, [])
|
|
|
|
// Alle Alerts als gelesen markieren
|
|
const markAllAsRead = useCallback(() => {
|
|
setAlerts(prev => prev.map(alert => ({ ...alert, isRead: true })))
|
|
}, [])
|
|
|
|
// Topic hinzufügen
|
|
const addTopic = useCallback((topic: Topic) => {
|
|
setTopics(prev => [...prev, topic])
|
|
}, [])
|
|
|
|
// Topic aktualisieren
|
|
const updateTopic = useCallback((id: string, updates: Partial<Topic>) => {
|
|
setTopics(prev => prev.map(topic =>
|
|
topic.id === id ? { ...topic, ...updates } : topic
|
|
))
|
|
}, [])
|
|
|
|
// Topic entfernen
|
|
const removeTopic = useCallback((id: string) => {
|
|
setTopics(prev => prev.filter(topic => topic.id !== id))
|
|
}, [])
|
|
|
|
// Settings aktualisieren
|
|
const updateSettings = useCallback((updates: Partial<AlertsSettings>) => {
|
|
setSettings(prev => ({ ...prev, ...updates }))
|
|
}, [])
|
|
|
|
// Ungelesene Alerts zählen
|
|
const unreadCount = alerts.filter(a => !a.isRead).length
|
|
|
|
// Während SSR: Default-Werte anzeigen
|
|
if (!mounted) {
|
|
return (
|
|
<AlertsContext.Provider
|
|
value={{
|
|
alerts: [],
|
|
unreadCount: 0,
|
|
isLoading: false,
|
|
error: null,
|
|
fetchAlerts: async () => {},
|
|
markAsRead: () => {},
|
|
markAllAsRead: () => {},
|
|
topics: [],
|
|
addTopic: () => {},
|
|
updateTopic: () => {},
|
|
removeTopic: () => {},
|
|
settings: defaultSettings,
|
|
updateSettings: () => {},
|
|
}}
|
|
>
|
|
{children}
|
|
</AlertsContext.Provider>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AlertsContext.Provider
|
|
value={{
|
|
alerts,
|
|
unreadCount,
|
|
isLoading,
|
|
error,
|
|
fetchAlerts,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
topics,
|
|
addTopic,
|
|
updateTopic,
|
|
removeTopic,
|
|
settings,
|
|
updateSettings,
|
|
}}
|
|
>
|
|
{children}
|
|
</AlertsContext.Provider>
|
|
)
|
|
}
|
|
|
|
// Hook fuer einfache Verwendung
|
|
export function useAlerts() {
|
|
const context = useContext(AlertsContext)
|
|
if (!context) {
|
|
throw new Error('useAlerts must be used within an AlertsProvider')
|
|
}
|
|
return context
|
|
}
|
|
|
|
// Hilfsfunktion: Importance-Farben
|
|
export function getImportanceColor(importance: AlertImportance, isDark: boolean): string {
|
|
const colors = {
|
|
'KRITISCH': isDark ? 'bg-red-500/20 text-red-300 border-red-500/30' : 'bg-red-100 text-red-700 border-red-200',
|
|
'DRINGEND': isDark ? 'bg-orange-500/20 text-orange-300 border-orange-500/30' : 'bg-orange-100 text-orange-700 border-orange-200',
|
|
'WICHTIG': isDark ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' : 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
|
'PRUEFEN': isDark ? 'bg-blue-500/20 text-blue-300 border-blue-500/30' : 'bg-blue-100 text-blue-700 border-blue-200',
|
|
'INFO': isDark ? 'bg-slate-500/20 text-slate-300 border-slate-500/30' : 'bg-slate-100 text-slate-600 border-slate-200',
|
|
}
|
|
return colors[importance]
|
|
}
|
|
|
|
// Hilfsfunktion: Relative Zeitangabe
|
|
export function getRelativeTime(date: Date): string {
|
|
const now = new Date()
|
|
const diffMs = now.getTime() - date.getTime()
|
|
const diffMins = Math.floor(diffMs / (1000 * 60))
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
|
|
if (diffMins < 1) return 'Gerade eben'
|
|
if (diffMins < 60) return `vor ${diffMins} Min.`
|
|
if (diffHours < 24) return `vor ${diffHours} Std.`
|
|
if (diffDays === 1) return 'Gestern'
|
|
if (diffDays < 7) return `vor ${diffDays} Tagen`
|
|
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })
|
|
}
|