feat(admin-v2): Major SDK/Compliance overhaul and new modules

SDK modules added/enhanced:
- compliance-hub, compliance-scope, consent-management, notfallplan
- audit-report, workflow, source-policy, dsms
- advisory-board documentation section
- TOM dashboard components, TOM generator SDM mapping
- DSFA: mitigation library, risk catalog, threshold analysis, source attribution
- VVT: baseline catalog, profiling engine, types
- Loeschfristen: baseline catalog, compliance engine, export, profiling, types
- Compliance scope: engine, profiling, golden tests, types

Existing SDK pages updated:
- dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality
- SDKSidebar, StepHeader — new navigation items and layout
- SDK layout, context, types — expanded type system

Other admin-v2 changes:
- AI agents page, RAG pipeline DSFA integration
- GridOverlay component updates
- Companion feature (development + education)
- Compliance advisor SOUL definition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-10 00:01:04 +01:00
parent 53219e3eaf
commit dff2ef796b
94 changed files with 29706 additions and 1039 deletions

View File

@@ -0,0 +1,153 @@
'use client'
import { useState } from 'react'
import { Plus, Trash2, BookOpen, Calendar } from 'lucide-react'
import { Homework } from '@/lib/companion/types'
interface HomeworkSectionProps {
homeworkList: Homework[]
onAdd: (title: string, dueDate: string) => void
onRemove: (id: string) => void
}
export function HomeworkSection({ homeworkList, onAdd, onRemove }: HomeworkSectionProps) {
const [newTitle, setNewTitle] = useState('')
const [newDueDate, setNewDueDate] = useState('')
const [isAdding, setIsAdding] = useState(false)
// Default due date to next week
const getDefaultDueDate = () => {
const date = new Date()
date.setDate(date.getDate() + 7)
return date.toISOString().split('T')[0]
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!newTitle.trim()) return
onAdd(newTitle.trim(), newDueDate || getDefaultDueDate())
setNewTitle('')
setNewDueDate('')
setIsAdding(false)
}
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
return date.toLocaleDateString('de-DE', {
weekday: 'short',
day: 'numeric',
month: 'short',
})
}
return (
<div className="bg-white border border-slate-200 rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-900 flex items-center gap-2">
<BookOpen className="w-5 h-5 text-slate-400" />
Hausaufgaben
</h3>
{!isAdding && (
<button
onClick={() => setIsAdding(true)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
<Plus className="w-4 h-4" />
Hinzufuegen
</button>
)}
</div>
{/* Add Form */}
{isAdding && (
<form onSubmit={handleSubmit} className="mb-4 p-4 bg-blue-50 rounded-xl">
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Aufgabe
</label>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="z.B. Aufgabe 1-5 auf S. 42..."
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Faellig am
</label>
<input
type="date"
value={newDueDate}
onChange={(e) => setNewDueDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="flex gap-2">
<button
type="submit"
disabled={!newTitle.trim()}
className="flex-1 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Speichern
</button>
<button
type="button"
onClick={() => {
setIsAdding(false)
setNewTitle('')
setNewDueDate('')
}}
className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg"
>
Abbrechen
</button>
</div>
</div>
</form>
)}
{/* Homework List */}
{homeworkList.length === 0 ? (
<div className="text-center py-8">
<BookOpen className="w-10 h-10 text-slate-300 mx-auto mb-2" />
<p className="text-slate-500">Keine Hausaufgaben eingetragen</p>
<p className="text-sm text-slate-400 mt-1">
Fuegen Sie Hausaufgaben hinzu, um sie zu dokumentieren
</p>
</div>
) : (
<div className="space-y-3">
{homeworkList.map((hw) => (
<div
key={hw.id}
className="flex items-start gap-3 p-4 bg-slate-50 rounded-xl group"
>
<div className="flex-1">
<p className="font-medium text-slate-900">{hw.title}</p>
<div className="flex items-center gap-2 mt-1 text-sm text-slate-500">
<Calendar className="w-3 h-3" />
<span>Faellig: {formatDate(hw.dueDate)}</span>
</div>
</div>
<button
onClick={() => onRemove(hw.id)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg opacity-0 group-hover:opacity-100 transition-all"
title="Entfernen"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,172 @@
'use client'
import { BookOpen, Clock, Users } from 'lucide-react'
import { LessonSession } from '@/lib/companion/types'
import { VisualPieTimer } from './VisualPieTimer'
import { QuickActionsBar } from './QuickActionsBar'
import { PhaseTimelineDetailed } from '../companion-mode/PhaseTimeline'
import {
PHASE_COLORS,
PHASE_DISPLAY_NAMES,
formatTime,
getTimerColorStatus,
} from '@/lib/companion/constants'
interface LessonActiveViewProps {
session: LessonSession
onPauseToggle: () => void
onExtendTime: (minutes: number) => void
onSkipPhase: () => void
onEndLesson: () => void
}
export function LessonActiveView({
session,
onPauseToggle,
onExtendTime,
onSkipPhase,
onEndLesson,
}: LessonActiveViewProps) {
const currentPhase = session.phases[session.currentPhaseIndex]
const phaseId = currentPhase?.phase || 'einstieg'
const phaseColor = PHASE_COLORS[phaseId].hex
const phaseName = PHASE_DISPLAY_NAMES[phaseId]
// Calculate timer values
const phaseDurationSeconds = (currentPhase?.duration || 0) * 60
const elapsedInPhase = currentPhase?.actualTime || 0
const remainingSeconds = phaseDurationSeconds - elapsedInPhase
const progress = Math.min(elapsedInPhase / phaseDurationSeconds, 1)
const isOvertime = remainingSeconds < 0
const colorStatus = getTimerColorStatus(remainingSeconds, isOvertime)
const isLastPhase = session.currentPhaseIndex === session.phases.length - 1
// Calculate total elapsed
const totalElapsedMinutes = Math.floor(session.elapsedTime / 60)
return (
<div className="space-y-6">
{/* Header with Session Info */}
<div
className="bg-gradient-to-r rounded-xl p-6 text-white"
style={{
background: `linear-gradient(135deg, ${phaseColor}, ${phaseColor}dd)`,
}}
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2 text-white/80 text-sm mb-1">
<Users className="w-4 h-4" />
<span>{session.className}</span>
<span className="mx-2">|</span>
<BookOpen className="w-4 h-4" />
<span>{session.subject}</span>
</div>
<h2 className="text-2xl font-bold">
{session.topic || phaseName}
</h2>
</div>
<div className="text-right">
<div className="text-white/80 text-sm">Gesamtzeit</div>
<div className="text-xl font-mono font-bold">
{formatTime(session.elapsedTime)}
</div>
</div>
</div>
</div>
{/* Main Timer Section */}
<div className="bg-white border border-slate-200 rounded-xl p-8">
<div className="flex flex-col items-center">
{/* Visual Pie Timer */}
<VisualPieTimer
progress={progress}
remainingSeconds={remainingSeconds}
totalSeconds={phaseDurationSeconds}
colorStatus={colorStatus}
isPaused={session.isPaused}
currentPhaseName={phaseName}
phaseColor={phaseColor}
onTogglePause={onPauseToggle}
size="lg"
/>
{/* Quick Actions */}
<div className="mt-8 w-full max-w-md">
<QuickActionsBar
onExtend={onExtendTime}
onPause={onPauseToggle}
onResume={onPauseToggle}
onSkip={onSkipPhase}
onEnd={onEndLesson}
isPaused={session.isPaused}
isLastPhase={isLastPhase}
/>
</div>
</div>
</div>
{/* Phase Timeline */}
<PhaseTimelineDetailed
phases={session.phases.map((p, i) => ({
id: p.phase,
shortName: p.phase[0].toUpperCase(),
displayName: PHASE_DISPLAY_NAMES[p.phase],
duration: p.duration,
status: p.status === 'active' ? 'active' : p.status === 'completed' ? 'completed' : 'planned',
actualTime: p.actualTime,
color: PHASE_COLORS[p.phase].hex,
}))}
currentPhaseIndex={session.currentPhaseIndex}
onPhaseClick={(index) => {
// Optional: Allow clicking to navigate to a phase
}}
/>
{/* Lesson Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-white border border-slate-200 rounded-xl p-4 text-center">
<Clock className="w-5 h-5 text-slate-400 mx-auto mb-2" />
<div className="text-2xl font-bold text-slate-900">{totalElapsedMinutes}</div>
<div className="text-sm text-slate-500">Minuten vergangen</div>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-4 text-center">
<div
className="w-5 h-5 rounded-full mx-auto mb-2"
style={{ backgroundColor: phaseColor }}
/>
<div className="text-2xl font-bold text-slate-900">
{session.currentPhaseIndex + 1}/{session.phases.length}
</div>
<div className="text-sm text-slate-500">Phase</div>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-4 text-center">
<Clock className="w-5 h-5 text-slate-400 mx-auto mb-2" />
<div className="text-2xl font-bold text-slate-900">
{session.totalPlannedDuration - totalElapsedMinutes}
</div>
<div className="text-sm text-slate-500">Minuten verbleibend</div>
</div>
</div>
{/* Keyboard Shortcuts Hint */}
<div className="text-center text-sm text-slate-400">
<span className="inline-flex items-center gap-4">
<span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs">Leertaste</kbd> Pause
</span>
<span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs">E</kbd> +5 Min
</span>
<span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs">N</kbd> Weiter
</span>
</span>
</div>
</div>
)
}

View File

@@ -0,0 +1,80 @@
'use client'
import { LessonSession, LessonStatus } from '@/lib/companion/types'
import { LessonStartForm } from './LessonStartForm'
import { LessonActiveView } from './LessonActiveView'
import { LessonEndedView } from './LessonEndedView'
interface LessonContainerProps {
session: LessonSession | null
onStartLesson: (data: { classId: string; subject: string; topic?: string; templateId?: string }) => void
onEndLesson: () => void
onPauseToggle: () => void
onExtendTime: (minutes: number) => void
onSkipPhase: () => void
onSaveReflection: (rating: number, notes: string, nextSteps: string) => void
onAddHomework: (title: string, dueDate: string) => void
onRemoveHomework: (id: string) => void
loading?: boolean
}
export function LessonContainer({
session,
onStartLesson,
onEndLesson,
onPauseToggle,
onExtendTime,
onSkipPhase,
onSaveReflection,
onAddHomework,
onRemoveHomework,
loading,
}: LessonContainerProps) {
// Determine which view to show based on session state
const getView = (): 'start' | 'active' | 'ended' => {
if (!session) return 'start'
const status = session.status
if (status === 'completed') return 'ended'
if (status === 'not_started') return 'start'
return 'active'
}
const view = getView()
if (view === 'start') {
return (
<LessonStartForm
onStart={onStartLesson}
loading={loading}
/>
)
}
if (view === 'ended' && session) {
return (
<LessonEndedView
session={session}
onSaveReflection={onSaveReflection}
onAddHomework={onAddHomework}
onRemoveHomework={onRemoveHomework}
onStartNew={() => onEndLesson()} // This will clear the session and show start form
/>
)
}
if (session) {
return (
<LessonActiveView
session={session}
onPauseToggle={onPauseToggle}
onExtendTime={onExtendTime}
onSkipPhase={onSkipPhase}
onEndLesson={onEndLesson}
/>
)
}
return null
}

View File

@@ -0,0 +1,209 @@
'use client'
import { useState } from 'react'
import { CheckCircle, Clock, BarChart3, Plus, RefreshCw } from 'lucide-react'
import { LessonSession } from '@/lib/companion/types'
import { HomeworkSection } from './HomeworkSection'
import { ReflectionSection } from './ReflectionSection'
import {
PHASE_COLORS,
PHASE_DISPLAY_NAMES,
formatTime,
formatMinutes,
} from '@/lib/companion/constants'
interface LessonEndedViewProps {
session: LessonSession
onSaveReflection: (rating: number, notes: string, nextSteps: string) => void
onAddHomework: (title: string, dueDate: string) => void
onRemoveHomework: (id: string) => void
onStartNew: () => void
}
export function LessonEndedView({
session,
onSaveReflection,
onAddHomework,
onRemoveHomework,
onStartNew,
}: LessonEndedViewProps) {
const [activeTab, setActiveTab] = useState<'summary' | 'homework' | 'reflection'>('summary')
// Calculate analytics
const totalPlannedSeconds = session.totalPlannedDuration * 60
const totalActualSeconds = session.elapsedTime
const timeDiff = totalActualSeconds - totalPlannedSeconds
const timeDiffMinutes = Math.round(timeDiff / 60)
const startTime = new Date(session.startTime)
const endTime = session.endTime ? new Date(session.endTime) : new Date()
return (
<div className="space-y-6">
{/* Success Header */}
<div className="bg-gradient-to-r from-green-500 to-green-600 rounded-xl p-6 text-white">
<div className="flex items-center gap-4">
<div className="p-3 bg-white/20 rounded-full">
<CheckCircle className="w-8 h-8" />
</div>
<div>
<h2 className="text-2xl font-bold">Stunde beendet!</h2>
<p className="text-green-100">
{session.className} - {session.subject}
{session.topic && ` - ${session.topic}`}
</p>
</div>
</div>
</div>
{/* Tab Navigation */}
<div className="bg-white border border-slate-200 rounded-xl p-1 flex">
{[
{ id: 'summary', label: 'Zusammenfassung', icon: BarChart3 },
{ id: 'homework', label: 'Hausaufgaben', icon: Plus },
{ id: 'reflection', label: 'Reflexion', icon: RefreshCw },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`
flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-lg
font-medium transition-all duration-200
${activeTab === tab.id
? 'bg-slate-900 text-white'
: 'text-slate-600 hover:bg-slate-100'
}
`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'summary' && (
<div className="space-y-6">
{/* Time Overview */}
<div className="bg-white border border-slate-200 rounded-xl p-6">
<h3 className="font-semibold text-slate-900 mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-slate-400" />
Zeitauswertung
</h3>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="text-center p-4 bg-slate-50 rounded-xl">
<div className="text-2xl font-bold text-slate-900">
{formatTime(totalActualSeconds)}
</div>
<div className="text-sm text-slate-500">Tatsaechlich</div>
</div>
<div className="text-center p-4 bg-slate-50 rounded-xl">
<div className="text-2xl font-bold text-slate-900">
{formatMinutes(session.totalPlannedDuration)}
</div>
<div className="text-sm text-slate-500">Geplant</div>
</div>
<div className={`text-center p-4 rounded-xl ${timeDiff > 0 ? 'bg-amber-50' : 'bg-green-50'}`}>
<div className={`text-2xl font-bold ${timeDiff > 0 ? 'text-amber-600' : 'text-green-600'}`}>
{timeDiffMinutes > 0 ? '+' : ''}{timeDiffMinutes} Min
</div>
<div className={`text-sm ${timeDiff > 0 ? 'text-amber-500' : 'text-green-500'}`}>
Differenz
</div>
</div>
</div>
{/* Session Times */}
<div className="flex items-center justify-between text-sm text-slate-500 border-t border-slate-100 pt-4">
<span>Start: {startTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</span>
<span>Ende: {endTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</span>
</div>
</div>
{/* Phase Breakdown */}
<div className="bg-white border border-slate-200 rounded-xl p-6">
<h3 className="font-semibold text-slate-900 mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-slate-400" />
Phasen-Analyse
</h3>
<div className="space-y-4">
{session.phases.map((phase) => {
const plannedSeconds = phase.duration * 60
const actualSeconds = phase.actualTime
const diff = actualSeconds - plannedSeconds
const diffMinutes = Math.round(diff / 60)
const percentage = Math.min((actualSeconds / plannedSeconds) * 100, 150)
return (
<div key={phase.phase} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: PHASE_COLORS[phase.phase].hex }}
/>
<span className="font-medium text-slate-700">
{PHASE_DISPLAY_NAMES[phase.phase]}
</span>
</div>
<div className="flex items-center gap-3 text-slate-500">
<span>{Math.round(actualSeconds / 60)} / {phase.duration} Min</span>
<span className={`
px-2 py-0.5 rounded text-xs font-medium
${diff > 60 ? 'bg-amber-100 text-amber-700' : diff < -60 ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'}
`}>
{diffMinutes > 0 ? '+' : ''}{diffMinutes} Min
</span>
</div>
</div>
{/* Progress Bar */}
<div className="h-3 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${Math.min(percentage, 100)}%`,
backgroundColor: percentage > 100
? '#f59e0b' // amber for overtime
: PHASE_COLORS[phase.phase].hex,
}}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)}
{activeTab === 'homework' && (
<HomeworkSection
homeworkList={session.homeworkList}
onAdd={onAddHomework}
onRemove={onRemoveHomework}
/>
)}
{activeTab === 'reflection' && (
<ReflectionSection
reflection={session.reflection}
onSave={onSaveReflection}
/>
)}
{/* Start New Lesson Button */}
<div className="pt-4">
<button
onClick={onStartNew}
className="w-full py-4 px-6 bg-slate-900 text-white rounded-xl font-semibold hover:bg-slate-800 transition-colors flex items-center justify-center gap-2"
>
<RefreshCw className="w-5 h-5" />
Neue Stunde starten
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,269 @@
'use client'
import { useState } from 'react'
import { Play, Clock, BookOpen, Users, ChevronDown, Info } from 'lucide-react'
import { LessonTemplate, PhaseDurations, Class } from '@/lib/companion/types'
import {
SYSTEM_TEMPLATES,
DEFAULT_PHASE_DURATIONS,
PHASE_DISPLAY_NAMES,
PHASE_ORDER,
calculateTotalDuration,
formatMinutes,
} from '@/lib/companion/constants'
interface LessonStartFormProps {
onStart: (data: {
classId: string
subject: string
topic?: string
templateId?: string
}) => void
loading?: boolean
availableClasses?: Class[]
}
// Mock classes for development
const MOCK_CLASSES: Class[] = [
{ id: 'c1', name: '9a', grade: '9', studentCount: 28 },
{ id: 'c2', name: '9b', grade: '9', studentCount: 26 },
{ id: 'c3', name: '10a', grade: '10', studentCount: 24 },
{ id: 'c4', name: 'Deutsch LK', grade: 'Q1', studentCount: 18 },
{ id: 'c5', name: 'Mathe GK', grade: 'Q2', studentCount: 22 },
]
const SUBJECTS = [
'Deutsch',
'Mathematik',
'Englisch',
'Biologie',
'Physik',
'Chemie',
'Geschichte',
'Geographie',
'Politik',
'Kunst',
'Musik',
'Sport',
'Informatik',
'Sonstiges',
]
export function LessonStartForm({
onStart,
loading,
availableClasses = MOCK_CLASSES,
}: LessonStartFormProps) {
const [selectedClass, setSelectedClass] = useState('')
const [selectedSubject, setSelectedSubject] = useState('')
const [topic, setTopic] = useState('')
const [selectedTemplate, setSelectedTemplate] = useState<LessonTemplate | null>(
SYSTEM_TEMPLATES[0] as LessonTemplate
)
const [showTemplateDetails, setShowTemplateDetails] = useState(false)
const totalDuration = selectedTemplate
? calculateTotalDuration(selectedTemplate.durations)
: calculateTotalDuration(DEFAULT_PHASE_DURATIONS)
const canStart = selectedClass && selectedSubject
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!canStart) return
onStart({
classId: selectedClass,
subject: selectedSubject,
topic: topic || undefined,
templateId: selectedTemplate?.templateId,
})
}
return (
<div className="bg-white border border-slate-200 rounded-xl p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-3 bg-blue-100 rounded-xl">
<Play className="w-6 h-6 text-blue-600" />
</div>
<div>
<h2 className="text-xl font-semibold text-slate-900">Neue Stunde starten</h2>
<p className="text-sm text-slate-500">
Waehlen Sie Klasse, Fach und Template
</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Class Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
<Users className="w-4 h-4 inline mr-2" />
Klasse *
</label>
<select
value={selectedClass}
onChange={(e) => setSelectedClass(e.target.value)}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
required
>
<option value="">Klasse auswaehlen...</option>
{availableClasses.map((cls) => (
<option key={cls.id} value={cls.id}>
{cls.name} ({cls.studentCount} Schueler)
</option>
))}
</select>
</div>
{/* Subject Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
<BookOpen className="w-4 h-4 inline mr-2" />
Fach *
</label>
<select
value={selectedSubject}
onChange={(e) => setSelectedSubject(e.target.value)}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
required
>
<option value="">Fach auswaehlen...</option>
{SUBJECTS.map((subject) => (
<option key={subject} value={subject}>
{subject}
</option>
))}
</select>
</div>
{/* Topic (Optional) */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Thema (optional)
</label>
<input
type="text"
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="z.B. Quadratische Funktionen, Gedichtanalyse..."
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Template Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
<Clock className="w-4 h-4 inline mr-2" />
Template
</label>
<div className="space-y-2">
{SYSTEM_TEMPLATES.map((template) => {
const tpl = template as LessonTemplate
const isSelected = selectedTemplate?.templateId === tpl.templateId
const total = calculateTotalDuration(tpl.durations)
return (
<button
key={tpl.templateId}
type="button"
onClick={() => setSelectedTemplate(tpl)}
className={`
w-full p-4 rounded-xl border text-left transition-all
${isSelected
? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500/20'
: 'border-slate-200 hover:border-slate-300 hover:bg-slate-50'
}
`}
>
<div className="flex items-center justify-between">
<div>
<p className={`font-medium ${isSelected ? 'text-blue-900' : 'text-slate-900'}`}>
{tpl.name}
</p>
<p className="text-sm text-slate-500">{tpl.description}</p>
</div>
<span className={`text-sm font-medium ${isSelected ? 'text-blue-600' : 'text-slate-500'}`}>
{formatMinutes(total)}
</span>
</div>
</button>
)
})}
</div>
{/* Template Details Toggle */}
{selectedTemplate && (
<button
type="button"
onClick={() => setShowTemplateDetails(!showTemplateDetails)}
className="mt-3 flex items-center gap-2 text-sm text-slate-600 hover:text-slate-900"
>
<Info className="w-4 h-4" />
Phasendauern anzeigen
<ChevronDown className={`w-4 h-4 transition-transform ${showTemplateDetails ? 'rotate-180' : ''}`} />
</button>
)}
{/* Template Details */}
{showTemplateDetails && selectedTemplate && (
<div className="mt-3 p-4 bg-slate-50 rounded-xl">
<div className="grid grid-cols-5 gap-2">
{PHASE_ORDER.map((phaseId) => (
<div key={phaseId} className="text-center">
<p className="text-xs text-slate-500">{PHASE_DISPLAY_NAMES[phaseId]}</p>
<p className="font-medium text-slate-900">
{selectedTemplate.durations[phaseId]} Min
</p>
</div>
))}
</div>
</div>
)}
</div>
{/* Summary & Start Button */}
<div className="pt-4 border-t border-slate-200">
<div className="flex items-center justify-between mb-4">
<div className="text-sm text-slate-600">
Gesamtdauer: <span className="font-semibold">{formatMinutes(totalDuration)}</span>
</div>
{selectedClass && (
<div className="text-sm text-slate-600">
Klasse: <span className="font-semibold">
{availableClasses.find((c) => c.id === selectedClass)?.name}
</span>
</div>
)}
</div>
<button
type="submit"
disabled={!canStart || loading}
className={`
w-full py-4 px-6 rounded-xl font-semibold text-lg
flex items-center justify-center gap-3
transition-all duration-200
${canStart && !loading
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-lg shadow-blue-500/25'
: 'bg-slate-200 text-slate-500 cursor-not-allowed'
}
`}
>
{loading ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Stunde wird gestartet...
</>
) : (
<>
<Play className="w-5 h-5" />
Stunde starten
</>
)}
</button>
</div>
</form>
</div>
)
}

View File

@@ -0,0 +1,194 @@
'use client'
import { Plus, Pause, Play, SkipForward, Square, Clock } from 'lucide-react'
interface QuickActionsBarProps {
onExtend: (minutes: number) => void
onPause: () => void
onResume: () => void
onSkip: () => void
onEnd: () => void
isPaused: boolean
isLastPhase: boolean
disabled?: boolean
}
export function QuickActionsBar({
onExtend,
onPause,
onResume,
onSkip,
onEnd,
isPaused,
isLastPhase,
disabled,
}: QuickActionsBarProps) {
return (
<div
className="flex items-center justify-center gap-3 p-4 bg-white border border-slate-200 rounded-xl"
role="toolbar"
aria-label="Steuerung"
>
{/* Extend +5 Min */}
<button
onClick={() => onExtend(5)}
disabled={disabled || isPaused}
className={`
flex items-center gap-2 px-4 py-3 rounded-xl
font-medium transition-all duration-200
min-w-[52px] justify-center
${disabled || isPaused
? 'bg-slate-100 text-slate-400 cursor-not-allowed'
: 'bg-blue-50 text-blue-700 hover:bg-blue-100 active:scale-95'
}
`}
title="+5 Minuten (E)"
aria-keyshortcuts="e"
aria-label="5 Minuten verlaengern"
>
<Plus className="w-5 h-5" />
<span>5 Min</span>
</button>
{/* Pause / Resume */}
<button
onClick={isPaused ? onResume : onPause}
disabled={disabled}
className={`
flex items-center gap-2 px-6 py-3 rounded-xl
font-semibold transition-all duration-200
min-w-[52px] min-h-[52px] justify-center
${disabled
? 'bg-slate-100 text-slate-400 cursor-not-allowed'
: isPaused
? 'bg-green-600 text-white hover:bg-green-700 shadow-lg shadow-green-500/25 active:scale-95'
: 'bg-amber-500 text-white hover:bg-amber-600 shadow-lg shadow-amber-500/25 active:scale-95'
}
`}
title={isPaused ? 'Fortsetzen (Leertaste)' : 'Pausieren (Leertaste)'}
aria-keyshortcuts="Space"
aria-label={isPaused ? 'Stunde fortsetzen' : 'Stunde pausieren'}
>
{isPaused ? (
<>
<Play className="w-5 h-5" />
<span>Fortsetzen</span>
</>
) : (
<>
<Pause className="w-5 h-5" />
<span>Pause</span>
</>
)}
</button>
{/* Skip Phase / End Lesson */}
{isLastPhase ? (
<button
onClick={onEnd}
disabled={disabled}
className={`
flex items-center gap-2 px-4 py-3 rounded-xl
font-medium transition-all duration-200
min-w-[52px] justify-center
${disabled
? 'bg-slate-100 text-slate-400 cursor-not-allowed'
: 'bg-red-50 text-red-700 hover:bg-red-100 active:scale-95'
}
`}
title="Stunde beenden"
aria-label="Stunde beenden"
>
<Square className="w-5 h-5" />
<span>Beenden</span>
</button>
) : (
<button
onClick={onSkip}
disabled={disabled || isPaused}
className={`
flex items-center gap-2 px-4 py-3 rounded-xl
font-medium transition-all duration-200
min-w-[52px] justify-center
${disabled || isPaused
? 'bg-slate-100 text-slate-400 cursor-not-allowed'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200 active:scale-95'
}
`}
title="Naechste Phase (N)"
aria-keyshortcuts="n"
aria-label="Zur naechsten Phase springen"
>
<SkipForward className="w-5 h-5" />
<span>Weiter</span>
</button>
)}
</div>
)
}
/**
* Compact version for mobile or sidebar
*/
export function QuickActionsCompact({
onExtend,
onPause,
onResume,
onSkip,
isPaused,
isLastPhase,
disabled,
}: Omit<QuickActionsBarProps, 'onEnd'>) {
return (
<div className="flex items-center gap-2">
<button
onClick={() => onExtend(5)}
disabled={disabled || isPaused}
className={`
p-2 rounded-lg transition-all
${disabled || isPaused
? 'text-slate-300'
: 'text-blue-600 hover:bg-blue-50'
}
`}
title="+5 Min"
>
<Clock className="w-5 h-5" />
</button>
<button
onClick={isPaused ? onResume : onPause}
disabled={disabled}
className={`
p-2 rounded-lg transition-all
${disabled
? 'text-slate-300'
: isPaused
? 'text-green-600 hover:bg-green-50'
: 'text-amber-600 hover:bg-amber-50'
}
`}
title={isPaused ? 'Fortsetzen' : 'Pausieren'}
>
{isPaused ? <Play className="w-5 h-5" /> : <Pause className="w-5 h-5" />}
</button>
{!isLastPhase && (
<button
onClick={onSkip}
disabled={disabled || isPaused}
className={`
p-2 rounded-lg transition-all
${disabled || isPaused
? 'text-slate-300'
: 'text-slate-600 hover:bg-slate-50'
}
`}
title="Naechste Phase"
>
<SkipForward className="w-5 h-5" />
</button>
)}
</div>
)
}

View File

@@ -0,0 +1,146 @@
'use client'
import { useState, useEffect } from 'react'
import { Star, Save, CheckCircle } from 'lucide-react'
import { LessonReflection } from '@/lib/companion/types'
interface ReflectionSectionProps {
reflection?: LessonReflection
onSave: (rating: number, notes: string, nextSteps: string) => void
}
export function ReflectionSection({ reflection, onSave }: ReflectionSectionProps) {
const [rating, setRating] = useState(reflection?.rating || 0)
const [notes, setNotes] = useState(reflection?.notes || '')
const [nextSteps, setNextSteps] = useState(reflection?.nextSteps || '')
const [hoverRating, setHoverRating] = useState(0)
const [saved, setSaved] = useState(false)
useEffect(() => {
if (reflection) {
setRating(reflection.rating)
setNotes(reflection.notes)
setNextSteps(reflection.nextSteps)
}
}, [reflection])
const handleSave = () => {
if (rating === 0) return
onSave(rating, notes, nextSteps)
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
const ratingLabels = [
'', // 0
'Verbesserungsbedarf',
'Okay',
'Gut',
'Sehr gut',
'Ausgezeichnet',
]
return (
<div className="bg-white border border-slate-200 rounded-xl p-6 space-y-6">
{/* Star Rating */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">
Wie lief die Stunde?
</label>
<div className="flex items-center gap-2">
{[1, 2, 3, 4, 5].map((star) => {
const isFilled = star <= (hoverRating || rating)
return (
<button
key={star}
type="button"
onClick={() => setRating(star)}
onMouseEnter={() => setHoverRating(star)}
onMouseLeave={() => setHoverRating(0)}
className="p-1 transition-transform hover:scale-110"
aria-label={`${star} Stern${star > 1 ? 'e' : ''}`}
>
<Star
className={`w-8 h-8 ${
isFilled
? 'fill-amber-400 text-amber-400'
: 'text-slate-300'
}`}
/>
</button>
)
})}
{(hoverRating || rating) > 0 && (
<span className="ml-3 text-sm text-slate-600">
{ratingLabels[hoverRating || rating]}
</span>
)}
</div>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Notizen zur Stunde
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Was lief gut? Was koennte besser laufen? Besondere Vorkommnisse..."
rows={4}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
/>
</div>
{/* Next Steps */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Naechste Schritte
</label>
<textarea
value={nextSteps}
onChange={(e) => setNextSteps(e.target.value)}
placeholder="Was muss fuer die naechste Stunde vorbereitet werden? Follow-ups..."
rows={3}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
/>
</div>
{/* Save Button */}
<button
onClick={handleSave}
disabled={rating === 0}
className={`
w-full py-3 px-6 rounded-xl font-semibold
flex items-center justify-center gap-2
transition-all duration-200
${saved
? 'bg-green-600 text-white'
: rating === 0
? 'bg-slate-100 text-slate-400 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}
`}
>
{saved ? (
<>
<CheckCircle className="w-5 h-5" />
Gespeichert!
</>
) : (
<>
<Save className="w-5 h-5" />
Reflexion speichern
</>
)}
</button>
{/* Previous Reflection Info */}
{reflection?.savedAt && (
<p className="text-center text-sm text-slate-400">
Zuletzt gespeichert: {new Date(reflection.savedAt).toLocaleString('de-DE')}
</p>
)}
</div>
)
}

View File

@@ -0,0 +1,220 @@
'use client'
import { Pause, Play } from 'lucide-react'
import { TimerColorStatus } from '@/lib/companion/types'
import {
PIE_TIMER_RADIUS,
PIE_TIMER_CIRCUMFERENCE,
PIE_TIMER_STROKE_WIDTH,
PIE_TIMER_SIZE,
TIMER_COLOR_CLASSES,
TIMER_BG_COLORS,
formatTime,
} from '@/lib/companion/constants'
interface VisualPieTimerProps {
progress: number // 0-1 (how much time has elapsed)
remainingSeconds: number
totalSeconds: number
colorStatus: TimerColorStatus
isPaused: boolean
currentPhaseName: string
phaseColor: string
onTogglePause?: () => void
size?: 'sm' | 'md' | 'lg'
}
const sizeConfig = {
sm: { outer: 120, viewBox: 100, radius: 38, stroke: 6, fontSize: 'text-lg' },
md: { outer: 180, viewBox: 100, radius: 40, stroke: 7, fontSize: 'text-2xl' },
lg: { outer: 240, viewBox: 100, radius: 42, stroke: 8, fontSize: 'text-4xl' },
}
export function VisualPieTimer({
progress,
remainingSeconds,
totalSeconds,
colorStatus,
isPaused,
currentPhaseName,
phaseColor,
onTogglePause,
size = 'lg',
}: VisualPieTimerProps) {
const config = sizeConfig[size]
const circumference = 2 * Math.PI * config.radius
// Calculate stroke-dashoffset for progress
// Progress goes from 0 (full) to 1 (empty), so offset decreases as time passes
const strokeDashoffset = circumference * (1 - progress)
// For overtime, show a pulsing full circle
const isOvertime = colorStatus === 'overtime'
const displayTime = formatTime(remainingSeconds)
// Get color classes based on status
const colorClasses = TIMER_COLOR_CLASSES[colorStatus]
const bgColorClass = TIMER_BG_COLORS[colorStatus]
return (
<div className="flex flex-col items-center">
{/* Timer Circle */}
<div
className={`relative ${bgColorClass} rounded-full p-4 transition-colors duration-300`}
style={{ width: config.outer, height: config.outer }}
>
<svg
width="100%"
height="100%"
viewBox={`0 0 ${config.viewBox} ${config.viewBox}`}
className="transform -rotate-90"
>
{/* Background circle */}
<circle
cx={config.viewBox / 2}
cy={config.viewBox / 2}
r={config.radius}
fill="none"
stroke="currentColor"
strokeWidth={config.stroke}
className="text-slate-200"
/>
{/* Progress circle */}
<circle
cx={config.viewBox / 2}
cy={config.viewBox / 2}
r={config.radius}
fill="none"
stroke={isOvertime ? '#dc2626' : phaseColor}
strokeWidth={config.stroke}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={isOvertime ? 0 : strokeDashoffset}
className={`transition-all duration-100 ${isOvertime ? 'animate-pulse' : ''}`}
/>
</svg>
{/* Center Content */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
{/* Time Display */}
<span
className={`
font-mono font-bold ${config.fontSize}
${isOvertime ? 'text-red-600 animate-pulse' : colorStatus === 'critical' ? 'text-red-500' : colorStatus === 'warning' ? 'text-amber-500' : 'text-slate-900'}
`}
>
{displayTime}
</span>
{/* Phase Name */}
<span className="text-sm text-slate-500 mt-1">
{currentPhaseName}
</span>
{/* Paused Indicator */}
{isPaused && (
<span className="text-xs text-amber-600 font-medium mt-1 flex items-center gap-1">
<Pause className="w-3 h-3" />
Pausiert
</span>
)}
{/* Overtime Badge */}
{isOvertime && (
<span className="absolute -bottom-2 px-2 py-0.5 bg-red-600 text-white text-xs font-bold rounded-full">
+{Math.abs(Math.floor(remainingSeconds / 60))} Min
</span>
)}
</div>
{/* Pause/Play Button (overlay) */}
{onTogglePause && (
<button
onClick={onTogglePause}
className={`
absolute inset-0 rounded-full
flex items-center justify-center
opacity-0 hover:opacity-100
bg-black/20 backdrop-blur-sm
transition-opacity duration-200
`}
aria-label={isPaused ? 'Fortsetzen' : 'Pausieren'}
>
{isPaused ? (
<Play className="w-12 h-12 text-white" />
) : (
<Pause className="w-12 h-12 text-white" />
)}
</button>
)}
</div>
{/* Status Text */}
<div className="mt-4 text-center">
{isOvertime ? (
<p className="text-red-600 font-semibold animate-pulse">
Ueberzogen - Zeit fuer die naechste Phase!
</p>
) : colorStatus === 'critical' ? (
<p className="text-red-500 font-medium">
Weniger als 2 Minuten verbleibend
</p>
) : colorStatus === 'warning' ? (
<p className="text-amber-500">
Weniger als 5 Minuten verbleibend
</p>
) : null}
</div>
</div>
)
}
/**
* Compact timer for header/toolbar
*/
export function CompactTimer({
remainingSeconds,
colorStatus,
isPaused,
phaseName,
phaseColor,
}: {
remainingSeconds: number
colorStatus: TimerColorStatus
isPaused: boolean
phaseName: string
phaseColor: string
}) {
const isOvertime = colorStatus === 'overtime'
return (
<div className="flex items-center gap-3 px-4 py-2 bg-white border border-slate-200 rounded-xl">
{/* Phase indicator */}
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: phaseColor }}
/>
{/* Phase name */}
<span className="text-sm font-medium text-slate-600">{phaseName}</span>
{/* Time */}
<span
className={`
font-mono font-bold
${isOvertime ? 'text-red-600 animate-pulse' : colorStatus === 'critical' ? 'text-red-500' : colorStatus === 'warning' ? 'text-amber-500' : 'text-slate-900'}
`}
>
{formatTime(remainingSeconds)}
</span>
{/* Paused badge */}
{isPaused && (
<span className="px-2 py-0.5 bg-amber-100 text-amber-700 text-xs font-medium rounded">
Pausiert
</span>
)}
</div>
)
}