feat(companion): Migrate Companion from admin-v2 to studio-v2 as pure lesson tool
Remove Companion module entirely from admin-v2. Rebuild in studio-v2 as a focused lesson timer (no dashboard mode). Direct flow: start → active → ended. Fix timer bug where lastTickRef reset prevented countdown. Add companion link to Sidebar and i18n translations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,39 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
||||||
import { getModuleByHref } from '@/lib/navigation'
|
|
||||||
import { GraduationCap, Construction } from 'lucide-react'
|
|
||||||
|
|
||||||
export default function CompanionPage() {
|
|
||||||
const moduleInfo = getModuleByHref('/development/companion')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{moduleInfo && (
|
|
||||||
<PagePurpose
|
|
||||||
title={moduleInfo.module.name}
|
|
||||||
purpose={moduleInfo.module.purpose}
|
|
||||||
audience={moduleInfo.module.audience}
|
|
||||||
collapsible={true}
|
|
||||||
defaultCollapsed={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-8 text-center">
|
|
||||||
<div className="flex justify-center mb-4">
|
|
||||||
<div className="p-4 bg-slate-100 rounded-full">
|
|
||||||
<GraduationCap className="w-12 h-12 text-slate-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-xl font-semibold text-slate-800 mb-2">Companion Dev</h2>
|
|
||||||
<p className="text-slate-600 mb-4">
|
|
||||||
Lesson-Modus Entwicklung fuer strukturiertes Lernen.
|
|
||||||
</p>
|
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-amber-50 border border-amber-200 rounded-lg text-amber-700">
|
|
||||||
<Construction className="w-4 h-4" />
|
|
||||||
<span className="text-sm font-medium">In Entwicklung</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Suspense } from 'react'
|
|
||||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
||||||
import { getModuleByHref } from '@/lib/navigation'
|
|
||||||
import { CompanionDashboard } from '@/components/companion/CompanionDashboard'
|
|
||||||
import { GraduationCap } from 'lucide-react'
|
|
||||||
|
|
||||||
function LoadingFallback() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header Skeleton */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="h-12 w-80 bg-slate-200 rounded-xl animate-pulse" />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{[1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="h-10 w-10 bg-slate-200 rounded-lg animate-pulse" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Phase Timeline Skeleton */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<div className="h-4 w-24 bg-slate-200 rounded mb-4 animate-pulse" />
|
|
||||||
<div className="flex gap-4">
|
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
|
||||||
<div key={i} className="flex items-center gap-2">
|
|
||||||
<div className="w-10 h-10 bg-slate-200 rounded-full animate-pulse" />
|
|
||||||
{i < 5 && <div className="w-8 h-1 bg-slate-200 animate-pulse" />}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Skeleton */}
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
{[1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<div className="h-4 w-16 bg-slate-200 rounded mb-2 animate-pulse" />
|
|
||||||
<div className="h-8 w-12 bg-slate-200 rounded animate-pulse" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Skeleton */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6 h-64 animate-pulse" />
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6 h-64 animate-pulse" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CompanionPage() {
|
|
||||||
const moduleInfo = getModuleByHref('/education/companion')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Page Purpose Header */}
|
|
||||||
{moduleInfo && (
|
|
||||||
<PagePurpose
|
|
||||||
title={moduleInfo.module.name}
|
|
||||||
purpose={moduleInfo.module.purpose}
|
|
||||||
audience={moduleInfo.module.audience}
|
|
||||||
collapsible={true}
|
|
||||||
defaultCollapsed={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Companion Dashboard */}
|
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
|
||||||
<CompanionDashboard />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/admin/companion
|
|
||||||
* Proxy to backend /api/state/dashboard for companion dashboard data
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
|
||||||
|
|
||||||
// TODO: Replace with actual backend call when endpoint is available
|
|
||||||
// const response = await fetch(`${backendUrl}/api/state/dashboard`, {
|
|
||||||
// method: 'GET',
|
|
||||||
// headers: {
|
|
||||||
// 'Content-Type': 'application/json',
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// if (!response.ok) {
|
|
||||||
// throw new Error(`Backend responded with ${response.status}`)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const data = await response.json()
|
|
||||||
// return NextResponse.json(data)
|
|
||||||
|
|
||||||
// Mock response for development
|
|
||||||
const mockData = {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
context: {
|
|
||||||
currentPhase: 'erarbeitung',
|
|
||||||
phaseDisplayName: 'Erarbeitung',
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
classesCount: 4,
|
|
||||||
studentsCount: 96,
|
|
||||||
learningUnitsCreated: 23,
|
|
||||||
gradesEntered: 156,
|
|
||||||
},
|
|
||||||
phases: [
|
|
||||||
{ id: 'einstieg', shortName: 'E', displayName: 'Einstieg', duration: 8, status: 'completed', color: '#4A90E2' },
|
|
||||||
{ id: 'erarbeitung', shortName: 'A', displayName: 'Erarbeitung', duration: 20, status: 'active', color: '#F5A623' },
|
|
||||||
{ id: 'sicherung', shortName: 'S', displayName: 'Sicherung', duration: 10, status: 'planned', color: '#7ED321' },
|
|
||||||
{ id: 'transfer', shortName: 'T', displayName: 'Transfer', duration: 7, status: 'planned', color: '#9013FE' },
|
|
||||||
{ id: 'reflexion', shortName: 'R', displayName: 'Reflexion', duration: 5, status: 'planned', color: '#6B7280' },
|
|
||||||
],
|
|
||||||
progress: {
|
|
||||||
percentage: 65,
|
|
||||||
completed: 13,
|
|
||||||
total: 20,
|
|
||||||
},
|
|
||||||
suggestions: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Klausuren korrigieren',
|
|
||||||
description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten',
|
|
||||||
priority: 'urgent',
|
|
||||||
icon: 'ClipboardCheck',
|
|
||||||
actionTarget: '/ai/klausur-korrektur',
|
|
||||||
estimatedTime: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Elternsprechtag vorbereiten',
|
|
||||||
description: 'Notenuebersicht fuer 8b erstellen',
|
|
||||||
priority: 'high',
|
|
||||||
icon: 'Users',
|
|
||||||
actionTarget: '/education/grades',
|
|
||||||
estimatedTime: 30,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
upcomingEvents: [
|
|
||||||
{
|
|
||||||
id: 'e1',
|
|
||||||
title: 'Mathe-Test 9b',
|
|
||||||
date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
type: 'exam',
|
|
||||||
inDays: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'e2',
|
|
||||||
title: 'Elternsprechtag',
|
|
||||||
date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
type: 'parent_meeting',
|
|
||||||
inDays: 5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(mockData)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Companion dashboard error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
|
||||||
import { Settings, MessageSquare, HelpCircle, RefreshCw } from 'lucide-react'
|
|
||||||
import { CompanionMode, TeacherSettings, FeedbackType } from '@/lib/companion/types'
|
|
||||||
import { DEFAULT_TEACHER_SETTINGS, STORAGE_KEYS } from '@/lib/companion/constants'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import { ModeToggle } from './ModeToggle'
|
|
||||||
import { PhaseTimeline } from './companion-mode/PhaseTimeline'
|
|
||||||
import { StatsGrid } from './companion-mode/StatsGrid'
|
|
||||||
import { SuggestionList } from './companion-mode/SuggestionList'
|
|
||||||
import { EventsCard } from './companion-mode/EventsCard'
|
|
||||||
import { LessonContainer } from './lesson-mode/LessonContainer'
|
|
||||||
import { SettingsModal } from './modals/SettingsModal'
|
|
||||||
import { FeedbackModal } from './modals/FeedbackModal'
|
|
||||||
import { OnboardingModal } from './modals/OnboardingModal'
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
import { useCompanionData } from '@/hooks/companion/useCompanionData'
|
|
||||||
import { useLessonSession } from '@/hooks/companion/useLessonSession'
|
|
||||||
import { useKeyboardShortcuts } from '@/hooks/companion/useKeyboardShortcuts'
|
|
||||||
|
|
||||||
export function CompanionDashboard() {
|
|
||||||
// Mode state
|
|
||||||
const [mode, setMode] = useState<CompanionMode>('companion')
|
|
||||||
|
|
||||||
// Modal states
|
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
|
||||||
const [showFeedback, setShowFeedback] = useState(false)
|
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
const [settings, setSettings] = useState<TeacherSettings>(DEFAULT_TEACHER_SETTINGS)
|
|
||||||
|
|
||||||
// Load settings from localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEYS.SETTINGS)
|
|
||||||
if (stored) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(stored)
|
|
||||||
setSettings({ ...DEFAULT_TEACHER_SETTINGS, ...parsed })
|
|
||||||
} catch {
|
|
||||||
// Invalid stored settings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if onboarding needed
|
|
||||||
const onboardingStored = localStorage.getItem(STORAGE_KEYS.ONBOARDING_STATE)
|
|
||||||
if (!onboardingStored) {
|
|
||||||
setShowOnboarding(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore last mode
|
|
||||||
const lastMode = localStorage.getItem(STORAGE_KEYS.LAST_MODE) as CompanionMode
|
|
||||||
if (lastMode && ['companion', 'lesson', 'classic'].includes(lastMode)) {
|
|
||||||
setMode(lastMode)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Save mode to localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem(STORAGE_KEYS.LAST_MODE, mode)
|
|
||||||
}, [mode])
|
|
||||||
|
|
||||||
// Companion data hook
|
|
||||||
const { data: companionData, loading: companionLoading, refresh } = useCompanionData()
|
|
||||||
|
|
||||||
// Lesson session hook
|
|
||||||
const {
|
|
||||||
session,
|
|
||||||
startLesson,
|
|
||||||
endLesson,
|
|
||||||
pauseLesson,
|
|
||||||
resumeLesson,
|
|
||||||
extendTime,
|
|
||||||
skipPhase,
|
|
||||||
saveReflection,
|
|
||||||
addHomework,
|
|
||||||
removeHomework,
|
|
||||||
isPaused,
|
|
||||||
} = useLessonSession({
|
|
||||||
onOvertimeStart: () => {
|
|
||||||
// Play sound if enabled
|
|
||||||
if (settings.soundNotifications) {
|
|
||||||
// TODO: Play notification sound
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle pause/resume toggle
|
|
||||||
const handlePauseToggle = useCallback(() => {
|
|
||||||
if (isPaused) {
|
|
||||||
resumeLesson()
|
|
||||||
} else {
|
|
||||||
pauseLesson()
|
|
||||||
}
|
|
||||||
}, [isPaused, pauseLesson, resumeLesson])
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
useKeyboardShortcuts({
|
|
||||||
onPauseResume: mode === 'lesson' && session ? handlePauseToggle : undefined,
|
|
||||||
onExtend: mode === 'lesson' && session && !isPaused ? () => extendTime(5) : undefined,
|
|
||||||
onNextPhase: mode === 'lesson' && session && !isPaused ? skipPhase : undefined,
|
|
||||||
onCloseModal: () => {
|
|
||||||
setShowSettings(false)
|
|
||||||
setShowFeedback(false)
|
|
||||||
setShowOnboarding(false)
|
|
||||||
},
|
|
||||||
enabled: settings.showKeyboardShortcuts,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle settings save
|
|
||||||
const handleSaveSettings = (newSettings: TeacherSettings) => {
|
|
||||||
setSettings(newSettings)
|
|
||||||
localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(newSettings))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle feedback submit
|
|
||||||
const handleFeedbackSubmit = async (type: FeedbackType, title: string, description: string) => {
|
|
||||||
const response = await fetch('/api/admin/companion/feedback', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
type,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
sessionId: session?.sessionId,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to submit feedback')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle onboarding complete
|
|
||||||
const handleOnboardingComplete = (data: { state?: string; schoolType?: string }) => {
|
|
||||||
localStorage.setItem(STORAGE_KEYS.ONBOARDING_STATE, JSON.stringify({
|
|
||||||
...data,
|
|
||||||
completed: true,
|
|
||||||
completedAt: new Date().toISOString(),
|
|
||||||
}))
|
|
||||||
setShowOnboarding(false)
|
|
||||||
setSettings({ ...settings, onboardingCompleted: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle lesson start
|
|
||||||
const handleStartLesson = (data: { classId: string; subject: string; topic?: string; templateId?: string }) => {
|
|
||||||
startLesson(data)
|
|
||||||
setMode('lesson')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`min-h-[calc(100vh-200px)] ${settings.highContrastMode ? 'high-contrast' : ''}`}>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<ModeToggle
|
|
||||||
currentMode={mode}
|
|
||||||
onModeChange={setMode}
|
|
||||||
disabled={!!session && session.status === 'in_progress'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Refresh Button */}
|
|
||||||
{mode === 'companion' && (
|
|
||||||
<button
|
|
||||||
onClick={refresh}
|
|
||||||
disabled={companionLoading}
|
|
||||||
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
|
||||||
title="Aktualisieren"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`w-5 h-5 ${companionLoading ? 'animate-spin' : ''}`} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Feedback Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setShowFeedback(true)}
|
|
||||||
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
|
||||||
title="Feedback"
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Settings Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setShowSettings(true)}
|
|
||||||
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
|
||||||
title="Einstellungen"
|
|
||||||
>
|
|
||||||
<Settings className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Help Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setShowOnboarding(true)}
|
|
||||||
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
|
||||||
title="Hilfe"
|
|
||||||
>
|
|
||||||
<HelpCircle className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
{mode === 'companion' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Phase Timeline */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<h3 className="text-sm font-medium text-slate-500 mb-4">Aktuelle Phase</h3>
|
|
||||||
{companionData ? (
|
|
||||||
<PhaseTimeline
|
|
||||||
phases={companionData.phases}
|
|
||||||
currentPhaseIndex={companionData.phases.findIndex(p => p.status === 'active')}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-10 bg-slate-100 rounded animate-pulse" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<StatsGrid
|
|
||||||
stats={companionData?.stats || { classesCount: 0, studentsCount: 0, learningUnitsCreated: 0, gradesEntered: 0 }}
|
|
||||||
loading={companionLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Two Column Layout */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Suggestions */}
|
|
||||||
<SuggestionList
|
|
||||||
suggestions={companionData?.suggestions || []}
|
|
||||||
loading={companionLoading}
|
|
||||||
onSuggestionClick={(suggestion) => {
|
|
||||||
// Navigate to action target
|
|
||||||
window.location.href = suggestion.actionTarget
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Events */}
|
|
||||||
<EventsCard
|
|
||||||
events={companionData?.upcomingEvents || []}
|
|
||||||
loading={companionLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Start Lesson Button */}
|
|
||||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl p-6 text-white">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-1">Bereit fuer die naechste Stunde?</h3>
|
|
||||||
<p className="text-blue-100">Starten Sie den Lesson-Modus fuer strukturierten Unterricht.</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setMode('lesson')}
|
|
||||||
className="px-6 py-3 bg-white text-blue-600 rounded-xl font-semibold hover:bg-blue-50 transition-colors"
|
|
||||||
>
|
|
||||||
Stunde starten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === 'lesson' && (
|
|
||||||
<LessonContainer
|
|
||||||
session={session}
|
|
||||||
onStartLesson={handleStartLesson}
|
|
||||||
onEndLesson={endLesson}
|
|
||||||
onPauseToggle={handlePauseToggle}
|
|
||||||
onExtendTime={extendTime}
|
|
||||||
onSkipPhase={skipPhase}
|
|
||||||
onSaveReflection={saveReflection}
|
|
||||||
onAddHomework={addHomework}
|
|
||||||
onRemoveHomework={removeHomework}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === 'classic' && (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-8 text-center">
|
|
||||||
<h2 className="text-xl font-semibold text-slate-800 mb-2">Classic Mode</h2>
|
|
||||||
<p className="text-slate-600 mb-4">
|
|
||||||
Die klassische Ansicht ohne Timer und Phasenstruktur.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-500">
|
|
||||||
Dieser Modus ist fuer flexible Unterrichtsgestaltung gedacht.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Modals */}
|
|
||||||
<SettingsModal
|
|
||||||
isOpen={showSettings}
|
|
||||||
onClose={() => setShowSettings(false)}
|
|
||||||
settings={settings}
|
|
||||||
onSave={handleSaveSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FeedbackModal
|
|
||||||
isOpen={showFeedback}
|
|
||||||
onClose={() => setShowFeedback(false)}
|
|
||||||
onSubmit={handleFeedbackSubmit}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OnboardingModal
|
|
||||||
isOpen={showOnboarding}
|
|
||||||
onClose={() => setShowOnboarding(false)}
|
|
||||||
onComplete={handleOnboardingComplete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { GraduationCap, Timer, Layout } from 'lucide-react'
|
|
||||||
import { CompanionMode } from '@/lib/companion/types'
|
|
||||||
|
|
||||||
interface ModeToggleProps {
|
|
||||||
currentMode: CompanionMode
|
|
||||||
onModeChange: (mode: CompanionMode) => void
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const modes: { id: CompanionMode; label: string; icon: React.ReactNode; description: string }[] = [
|
|
||||||
{
|
|
||||||
id: 'companion',
|
|
||||||
label: 'Companion',
|
|
||||||
icon: <GraduationCap className="w-4 h-4" />,
|
|
||||||
description: 'Dashboard mit Vorschlaegen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lesson',
|
|
||||||
label: 'Lesson',
|
|
||||||
icon: <Timer className="w-4 h-4" />,
|
|
||||||
description: 'Timer und Phasen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'classic',
|
|
||||||
label: 'Classic',
|
|
||||||
icon: <Layout className="w-4 h-4" />,
|
|
||||||
description: 'Klassische Ansicht',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function ModeToggle({ currentMode, onModeChange, disabled }: ModeToggleProps) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-1 inline-flex gap-1">
|
|
||||||
{modes.map((mode) => {
|
|
||||||
const isActive = currentMode === mode.id
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={mode.id}
|
|
||||||
onClick={() => onModeChange(mode.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`
|
|
||||||
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium
|
|
||||||
transition-all duration-200
|
|
||||||
${isActive
|
|
||||||
? 'bg-slate-900 text-white shadow-sm'
|
|
||||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
|
||||||
}
|
|
||||||
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
|
|
||||||
`}
|
|
||||||
title={mode.description}
|
|
||||||
>
|
|
||||||
{mode.icon}
|
|
||||||
<span>{mode.label}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Calendar, FileQuestion, Users, Clock, ChevronRight } from 'lucide-react'
|
|
||||||
import { UpcomingEvent, EventType } from '@/lib/companion/types'
|
|
||||||
import { EVENT_TYPE_CONFIG } from '@/lib/companion/constants'
|
|
||||||
|
|
||||||
interface EventsCardProps {
|
|
||||||
events: UpcomingEvent[]
|
|
||||||
onEventClick?: (event: UpcomingEvent) => void
|
|
||||||
loading?: boolean
|
|
||||||
maxItems?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
||||||
FileQuestion,
|
|
||||||
Users,
|
|
||||||
Clock,
|
|
||||||
Calendar,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEventIcon(type: EventType) {
|
|
||||||
const config = EVENT_TYPE_CONFIG[type]
|
|
||||||
const Icon = iconMap[config.icon] || Calendar
|
|
||||||
return { Icon, ...config }
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatEventDate(dateStr: string, inDays: number): string {
|
|
||||||
if (inDays === 0) return 'Heute'
|
|
||||||
if (inDays === 1) return 'Morgen'
|
|
||||||
if (inDays < 7) return `In ${inDays} Tagen`
|
|
||||||
|
|
||||||
const date = new Date(dateStr)
|
|
||||||
return date.toLocaleDateString('de-DE', {
|
|
||||||
weekday: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventItemProps {
|
|
||||||
event: UpcomingEvent
|
|
||||||
onClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function EventItem({ event, onClick }: EventItemProps) {
|
|
||||||
const { Icon, color, bg } = getEventIcon(event.type)
|
|
||||||
const isUrgent = event.inDays <= 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`
|
|
||||||
w-full flex items-center gap-3 p-3 rounded-lg
|
|
||||||
transition-all duration-200
|
|
||||||
hover:bg-slate-50
|
|
||||||
${isUrgent ? 'bg-red-50/50' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className={`p-2 rounded-lg ${bg}`}>
|
|
||||||
<Icon className={`w-4 h-4 ${color}`} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 text-left min-w-0">
|
|
||||||
<p className="font-medium text-slate-900 truncate">{event.title}</p>
|
|
||||||
<p className={`text-sm ${isUrgent ? 'text-red-600 font-medium' : 'text-slate-500'}`}>
|
|
||||||
{formatEventDate(event.date, event.inDays)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ChevronRight className="w-4 h-4 text-slate-400 flex-shrink-0" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EventsCard({
|
|
||||||
events,
|
|
||||||
onEventClick,
|
|
||||||
loading,
|
|
||||||
maxItems = 5,
|
|
||||||
}: EventsCardProps) {
|
|
||||||
const displayEvents = events.slice(0, maxItems)
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<Calendar className="w-5 h-5 text-blue-500" />
|
|
||||||
<h3 className="font-semibold text-slate-900">Termine</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="h-14 bg-slate-100 rounded-lg animate-pulse" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<Calendar className="w-5 h-5 text-blue-500" />
|
|
||||||
<h3 className="font-semibold text-slate-900">Termine</h3>
|
|
||||||
</div>
|
|
||||||
<div className="text-center py-6">
|
|
||||||
<Calendar className="w-10 h-10 text-slate-300 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-slate-500">Keine anstehenden Termine</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Calendar className="w-5 h-5 text-blue-500" />
|
|
||||||
<h3 className="font-semibold text-slate-900">Termine</h3>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-slate-500">
|
|
||||||
{events.length} Termin{events.length !== 1 ? 'e' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
{displayEvents.map((event) => (
|
|
||||||
<EventItem
|
|
||||||
key={event.id}
|
|
||||||
event={event}
|
|
||||||
onClick={() => onEventClick?.(event)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{events.length > maxItems && (
|
|
||||||
<button className="w-full mt-3 py-2 text-sm text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-colors">
|
|
||||||
Alle {events.length} anzeigen
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compact inline version for header/toolbar
|
|
||||||
*/
|
|
||||||
export function EventsInline({ events }: { events: UpcomingEvent[] }) {
|
|
||||||
const nextEvent = events[0]
|
|
||||||
|
|
||||||
if (!nextEvent) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
|
||||||
<Calendar className="w-4 h-4" />
|
|
||||||
<span>Keine Termine</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Icon, color } = getEventIcon(nextEvent.type)
|
|
||||||
const isUrgent = nextEvent.inDays <= 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex items-center gap-2 text-sm ${isUrgent ? 'text-red-600' : 'text-slate-600'}`}>
|
|
||||||
<Icon className={`w-4 h-4 ${color}`} />
|
|
||||||
<span className="truncate max-w-[150px]">{nextEvent.title}</span>
|
|
||||||
<span className="text-slate-400">-</span>
|
|
||||||
<span className={isUrgent ? 'font-medium' : ''}>
|
|
||||||
{formatEventDate(nextEvent.date, nextEvent.inDays)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Users, GraduationCap, BookOpen, FileCheck } from 'lucide-react'
|
|
||||||
import { CompanionStats } from '@/lib/companion/types'
|
|
||||||
|
|
||||||
interface StatsGridProps {
|
|
||||||
stats: CompanionStats
|
|
||||||
loading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StatCardProps {
|
|
||||||
label: string
|
|
||||||
value: number
|
|
||||||
icon: React.ReactNode
|
|
||||||
color: string
|
|
||||||
loading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatCard({ label, value, icon, color, loading }: StatCardProps) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4 hover:shadow-md transition-shadow">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-slate-500 mb-1">{label}</p>
|
|
||||||
{loading ? (
|
|
||||||
<div className="h-8 w-16 bg-slate-200 rounded animate-pulse" />
|
|
||||||
) : (
|
|
||||||
<p className="text-2xl font-bold text-slate-900">{value}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={`p-2 rounded-lg ${color}`}>
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatsGrid({ stats, loading }: StatsGridProps) {
|
|
||||||
const statCards = [
|
|
||||||
{
|
|
||||||
label: 'Klassen',
|
|
||||||
value: stats.classesCount,
|
|
||||||
icon: <Users className="w-5 h-5 text-blue-600" />,
|
|
||||||
color: 'bg-blue-100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Schueler',
|
|
||||||
value: stats.studentsCount,
|
|
||||||
icon: <GraduationCap className="w-5 h-5 text-green-600" />,
|
|
||||||
color: 'bg-green-100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Lerneinheiten',
|
|
||||||
value: stats.learningUnitsCreated,
|
|
||||||
icon: <BookOpen className="w-5 h-5 text-purple-600" />,
|
|
||||||
color: 'bg-purple-100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Noten',
|
|
||||||
value: stats.gradesEntered,
|
|
||||||
icon: <FileCheck className="w-5 h-5 text-amber-600" />,
|
|
||||||
color: 'bg-amber-100',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
{statCards.map((card) => (
|
|
||||||
<StatCard
|
|
||||||
key={card.label}
|
|
||||||
label={card.label}
|
|
||||||
value={card.value}
|
|
||||||
icon={card.icon}
|
|
||||||
color={card.color}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compact version of StatsGrid for sidebar or smaller spaces
|
|
||||||
*/
|
|
||||||
export function StatsGridCompact({ stats, loading }: StatsGridProps) {
|
|
||||||
const items = [
|
|
||||||
{ label: 'Klassen', value: stats.classesCount, icon: <Users className="w-4 h-4" /> },
|
|
||||||
{ label: 'Schueler', value: stats.studentsCount, icon: <GraduationCap className="w-4 h-4" /> },
|
|
||||||
{ label: 'Einheiten', value: stats.learningUnitsCreated, icon: <BookOpen className="w-4 h-4" /> },
|
|
||||||
{ label: 'Noten', value: stats.gradesEntered, icon: <FileCheck className="w-4 h-4" /> },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h3 className="text-sm font-medium text-slate-500 mb-3">Statistiken</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{items.map((item) => (
|
|
||||||
<div key={item.label} className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2 text-slate-600">
|
|
||||||
{item.icon}
|
|
||||||
<span className="text-sm">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
{loading ? (
|
|
||||||
<div className="h-5 w-8 bg-slate-200 rounded animate-pulse" />
|
|
||||||
) : (
|
|
||||||
<span className="font-semibold text-slate-900">{item.value}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { ChevronRight, Clock, Lightbulb, ClipboardCheck, BookOpen, Calendar, Users, MessageSquare, FileText } from 'lucide-react'
|
|
||||||
import { Suggestion, SuggestionPriority } from '@/lib/companion/types'
|
|
||||||
import { PRIORITY_COLORS } from '@/lib/companion/constants'
|
|
||||||
|
|
||||||
interface SuggestionListProps {
|
|
||||||
suggestions: Suggestion[]
|
|
||||||
onSuggestionClick?: (suggestion: Suggestion) => void
|
|
||||||
loading?: boolean
|
|
||||||
maxItems?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
||||||
ClipboardCheck,
|
|
||||||
BookOpen,
|
|
||||||
Calendar,
|
|
||||||
Users,
|
|
||||||
Clock,
|
|
||||||
MessageSquare,
|
|
||||||
FileText,
|
|
||||||
Lightbulb,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIcon(iconName: string) {
|
|
||||||
const Icon = iconMap[iconName] || Lightbulb
|
|
||||||
return Icon
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SuggestionCardProps {
|
|
||||||
suggestion: Suggestion
|
|
||||||
onClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function SuggestionCard({ suggestion, onClick }: SuggestionCardProps) {
|
|
||||||
const priorityStyles = PRIORITY_COLORS[suggestion.priority]
|
|
||||||
const Icon = getIcon(suggestion.icon)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`
|
|
||||||
w-full p-4 rounded-xl border text-left
|
|
||||||
transition-all duration-200
|
|
||||||
hover:shadow-md hover:scale-[1.01]
|
|
||||||
${priorityStyles.bg} ${priorityStyles.border}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
{/* Priority Dot & Icon */}
|
|
||||||
<div className="flex-shrink-0 relative">
|
|
||||||
<div className={`p-2 rounded-lg bg-white shadow-sm`}>
|
|
||||||
<Icon className={`w-5 h-5 ${priorityStyles.text}`} />
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`absolute -top-1 -right-1 w-3 h-3 rounded-full ${priorityStyles.dot}`}
|
|
||||||
title={`Prioritaet: ${suggestion.priority}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h4 className={`font-medium ${priorityStyles.text} mb-1`}>
|
|
||||||
{suggestion.title}
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-slate-600 line-clamp-2">
|
|
||||||
{suggestion.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Meta */}
|
|
||||||
<div className="flex items-center gap-3 mt-2">
|
|
||||||
<span className="inline-flex items-center gap-1 text-xs text-slate-500">
|
|
||||||
<Clock className="w-3 h-3" />
|
|
||||||
~{suggestion.estimatedTime} Min
|
|
||||||
</span>
|
|
||||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${priorityStyles.bg} ${priorityStyles.text}`}>
|
|
||||||
{suggestion.priority}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Arrow */}
|
|
||||||
<ChevronRight className="w-5 h-5 text-slate-400 flex-shrink-0" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SuggestionList({
|
|
||||||
suggestions,
|
|
||||||
onSuggestionClick,
|
|
||||||
loading,
|
|
||||||
maxItems = 5,
|
|
||||||
}: SuggestionListProps) {
|
|
||||||
// Sort by priority: urgent > high > medium > low
|
|
||||||
const priorityOrder: Record<SuggestionPriority, number> = {
|
|
||||||
urgent: 0,
|
|
||||||
high: 1,
|
|
||||||
medium: 2,
|
|
||||||
low: 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedSuggestions = [...suggestions]
|
|
||||||
.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority])
|
|
||||||
.slice(0, maxItems)
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<Lightbulb className="w-5 h-5 text-amber-500" />
|
|
||||||
<h3 className="font-semibold text-slate-900">Vorschlaege</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="h-24 bg-slate-100 rounded-xl animate-pulse" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (suggestions.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<Lightbulb className="w-5 h-5 text-amber-500" />
|
|
||||||
<h3 className="font-semibold text-slate-900">Vorschlaege</h3>
|
|
||||||
</div>
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<ClipboardCheck className="w-6 h-6 text-green-600" />
|
|
||||||
</div>
|
|
||||||
<p className="text-slate-600">Alles erledigt!</p>
|
|
||||||
<p className="text-sm text-slate-500 mt-1">Keine offenen Aufgaben</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Lightbulb className="w-5 h-5 text-amber-500" />
|
|
||||||
<h3 className="font-semibold text-slate-900">Vorschlaege</h3>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-slate-500">
|
|
||||||
{suggestions.length} Aufgabe{suggestions.length !== 1 ? 'n' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{sortedSuggestions.map((suggestion) => (
|
|
||||||
<SuggestionCard
|
|
||||||
key={suggestion.id}
|
|
||||||
suggestion={suggestion}
|
|
||||||
onClick={() => onSuggestionClick?.(suggestion)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{suggestions.length > maxItems && (
|
|
||||||
<button className="w-full mt-4 py-2 text-sm text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-colors">
|
|
||||||
Alle {suggestions.length} anzeigen
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Main components
|
|
||||||
export { CompanionDashboard } from './CompanionDashboard'
|
|
||||||
export { ModeToggle } from './ModeToggle'
|
|
||||||
|
|
||||||
// Companion Mode components
|
|
||||||
export { PhaseTimeline, PhaseTimelineDetailed } from './companion-mode/PhaseTimeline'
|
|
||||||
export { StatsGrid, StatsGridCompact } from './companion-mode/StatsGrid'
|
|
||||||
export { SuggestionList } from './companion-mode/SuggestionList'
|
|
||||||
export { EventsCard, EventsInline } from './companion-mode/EventsCard'
|
|
||||||
|
|
||||||
// Lesson Mode components
|
|
||||||
export { LessonContainer } from './lesson-mode/LessonContainer'
|
|
||||||
export { LessonStartForm } from './lesson-mode/LessonStartForm'
|
|
||||||
export { LessonActiveView } from './lesson-mode/LessonActiveView'
|
|
||||||
export { LessonEndedView } from './lesson-mode/LessonEndedView'
|
|
||||||
export { VisualPieTimer, CompactTimer } from './lesson-mode/VisualPieTimer'
|
|
||||||
export { QuickActionsBar, QuickActionsCompact } from './lesson-mode/QuickActionsBar'
|
|
||||||
export { HomeworkSection } from './lesson-mode/HomeworkSection'
|
|
||||||
export { ReflectionSection } from './lesson-mode/ReflectionSection'
|
|
||||||
|
|
||||||
// Modals
|
|
||||||
export { SettingsModal } from './modals/SettingsModal'
|
|
||||||
export { FeedbackModal } from './modals/FeedbackModal'
|
|
||||||
export { OnboardingModal } from './modals/OnboardingModal'
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
'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
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
|
||||||
import { CompanionData } from '@/lib/companion/types'
|
|
||||||
import { createDefaultPhases } from '@/lib/companion/constants'
|
|
||||||
|
|
||||||
interface UseCompanionDataOptions {
|
|
||||||
pollingInterval?: number // ms, default 30000
|
|
||||||
autoRefresh?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseCompanionDataReturn {
|
|
||||||
data: CompanionData | null
|
|
||||||
loading: boolean
|
|
||||||
error: string | null
|
|
||||||
refresh: () => Promise<void>
|
|
||||||
lastUpdated: Date | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock data for development - will be replaced with actual API calls
|
|
||||||
function getMockData(): CompanionData {
|
|
||||||
return {
|
|
||||||
context: {
|
|
||||||
currentPhase: 'erarbeitung',
|
|
||||||
phaseDisplayName: 'Erarbeitung',
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
classesCount: 4,
|
|
||||||
studentsCount: 96,
|
|
||||||
learningUnitsCreated: 23,
|
|
||||||
gradesEntered: 156,
|
|
||||||
},
|
|
||||||
phases: createDefaultPhases(),
|
|
||||||
progress: {
|
|
||||||
percentage: 65,
|
|
||||||
completed: 13,
|
|
||||||
total: 20,
|
|
||||||
},
|
|
||||||
suggestions: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Klausuren korrigieren',
|
|
||||||
description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten',
|
|
||||||
priority: 'urgent',
|
|
||||||
icon: 'ClipboardCheck',
|
|
||||||
actionTarget: '/ai/klausur-korrektur',
|
|
||||||
estimatedTime: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Elternsprechtag vorbereiten',
|
|
||||||
description: 'Notenuebersicht fuer 8b erstellen',
|
|
||||||
priority: 'high',
|
|
||||||
icon: 'Users',
|
|
||||||
actionTarget: '/education/grades',
|
|
||||||
estimatedTime: 30,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'Material hochladen',
|
|
||||||
description: 'Arbeitsblatt fuer naechste Woche bereitstellen',
|
|
||||||
priority: 'medium',
|
|
||||||
icon: 'FileText',
|
|
||||||
actionTarget: '/development/content',
|
|
||||||
estimatedTime: 15,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
title: 'Lernstandserhebung planen',
|
|
||||||
description: 'Mathe 7a - Naechster Test in 2 Wochen',
|
|
||||||
priority: 'low',
|
|
||||||
icon: 'Calendar',
|
|
||||||
actionTarget: '/education/planning',
|
|
||||||
estimatedTime: 45,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
upcomingEvents: [
|
|
||||||
{
|
|
||||||
id: 'e1',
|
|
||||||
title: 'Mathe-Test 9b',
|
|
||||||
date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
type: 'exam',
|
|
||||||
inDays: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'e2',
|
|
||||||
title: 'Elternsprechtag',
|
|
||||||
date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
type: 'parent_meeting',
|
|
||||||
inDays: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'e3',
|
|
||||||
title: 'Notenschluss Q1',
|
|
||||||
date: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
type: 'deadline',
|
|
||||||
inDays: 14,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCompanionData(options: UseCompanionDataOptions = {}): UseCompanionDataReturn {
|
|
||||||
const { pollingInterval = 30000, autoRefresh = true } = options
|
|
||||||
|
|
||||||
const [data, setData] = useState<CompanionData | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
// TODO: Replace with actual API call
|
|
||||||
// const response = await fetch('/api/admin/companion')
|
|
||||||
// if (!response.ok) throw new Error('Failed to fetch companion data')
|
|
||||||
// const result = await response.json()
|
|
||||||
// setData(result.data)
|
|
||||||
|
|
||||||
// For now, use mock data with a small delay to simulate network
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
||||||
setData(getMockData())
|
|
||||||
setLastUpdated(new Date())
|
|
||||||
setError(null)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
await fetchData()
|
|
||||||
}, [fetchData])
|
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData()
|
|
||||||
}, [fetchData])
|
|
||||||
|
|
||||||
// Polling
|
|
||||||
useEffect(() => {
|
|
||||||
if (!autoRefresh || pollingInterval <= 0) return
|
|
||||||
|
|
||||||
const interval = setInterval(fetchData, pollingInterval)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [autoRefresh, pollingInterval, fetchData])
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh,
|
|
||||||
lastUpdated,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/admin/companion/feedback
|
* POST /api/companion/feedback
|
||||||
* Submit feedback (bug report, feature request, general feedback)
|
* Submit feedback (bug report, feature request, general feedback)
|
||||||
* Proxy to backend /api/feedback
|
* Proxy to backend /api/feedback
|
||||||
*/
|
*/
|
||||||
@@ -95,7 +95,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/admin/companion/feedback
|
* GET /api/companion/feedback
|
||||||
* Get feedback history (admin only)
|
* Get feedback history (admin only)
|
||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/admin/companion/lesson
|
* POST /api/companion/lesson
|
||||||
* Start a new lesson session
|
* Start a new lesson session
|
||||||
* Proxy to backend /api/classroom/sessions
|
* Proxy to backend /api/classroom/sessions
|
||||||
*/
|
*/
|
||||||
@@ -71,7 +71,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/admin/companion/lesson
|
* GET /api/companion/lesson
|
||||||
* Get current lesson session or list of recent sessions
|
* Get current lesson session or list of recent sessions
|
||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@@ -116,7 +116,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PATCH /api/admin/companion/lesson
|
* PATCH /api/companion/lesson
|
||||||
* Update lesson session (timer state, phase changes, etc.)
|
* Update lesson session (timer state, phase changes, etc.)
|
||||||
*/
|
*/
|
||||||
export async function PATCH(request: NextRequest) {
|
export async function PATCH(request: NextRequest) {
|
||||||
@@ -160,7 +160,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/admin/companion/lesson
|
* DELETE /api/companion/lesson
|
||||||
* End/delete a lesson session
|
* End/delete a lesson session
|
||||||
*/
|
*/
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
@@ -17,7 +17,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/admin/companion/settings
|
* GET /api/companion/settings
|
||||||
* Get teacher settings
|
* Get teacher settings
|
||||||
* Proxy to backend /api/teacher/settings
|
* Proxy to backend /api/teacher/settings
|
||||||
*/
|
*/
|
||||||
@@ -58,7 +58,7 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/admin/companion/settings
|
* PUT /api/companion/settings
|
||||||
* Update teacher settings
|
* Update teacher settings
|
||||||
*/
|
*/
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
@@ -110,7 +110,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PATCH /api/admin/companion/settings
|
* PATCH /api/companion/settings
|
||||||
* Partially update teacher settings
|
* Partially update teacher settings
|
||||||
*/
|
*/
|
||||||
export async function PATCH(request: NextRequest) {
|
export async function PATCH(request: NextRequest) {
|
||||||
51
studio-v2/app/companion/page.tsx
Normal file
51
studio-v2/app/companion/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useTheme } from '@/lib/ThemeContext'
|
||||||
|
import { Sidebar } from '@/components/Sidebar'
|
||||||
|
import { CompanionDashboard } from '@/components/companion/CompanionDashboard'
|
||||||
|
import { ThemeToggle } from '@/components/ThemeToggle'
|
||||||
|
import { LanguageDropdown } from '@/components/LanguageDropdown'
|
||||||
|
|
||||||
|
export default function CompanionPage() {
|
||||||
|
const { isDark } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen flex relative overflow-hidden ${
|
||||||
|
isDark
|
||||||
|
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
|
||||||
|
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
|
||||||
|
}`}>
|
||||||
|
{/* Animated Background Blobs */}
|
||||||
|
<div className={`absolute -top-40 -right-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${
|
||||||
|
isDark ? 'bg-purple-500 opacity-30' : 'bg-purple-300 opacity-40'
|
||||||
|
}`} />
|
||||||
|
<div className={`absolute top-1/2 -left-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${
|
||||||
|
isDark ? 'bg-pink-500 opacity-30' : 'bg-pink-300 opacity-40'
|
||||||
|
}`} />
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="relative z-10 p-4">
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 flex flex-col relative z-10 p-4 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h1 className={`text-2xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||||||
|
Companion
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
<LanguageDropdown />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Companion Dashboard */}
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<CompanionDashboard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -81,6 +81,12 @@ export function Sidebar({ selectedTab = 'dashboard', onTabChange }: SidebarProps
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
)},
|
)},
|
||||||
|
{ id: 'companion', labelKey: 'nav_companion', href: '/companion', icon: (
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<circle cx="12" cy="12" r="9" strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6v6l4 2" />
|
||||||
|
</svg>
|
||||||
|
)},
|
||||||
{ id: 'meet', labelKey: 'nav_meet', href: '/meet', icon: (
|
{ id: 'meet', labelKey: 'nav_meet', href: '/meet', icon: (
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
@@ -111,6 +117,7 @@ export function Sidebar({ selectedTab = 'dashboard', onTabChange }: SidebarProps
|
|||||||
|
|
||||||
// Determine active item based on pathname or selectedTab
|
// Determine active item based on pathname or selectedTab
|
||||||
const getActiveItem = () => {
|
const getActiveItem = () => {
|
||||||
|
if (pathname === '/companion') return 'companion'
|
||||||
if (pathname === '/meet') return 'meet'
|
if (pathname === '/meet') return 'meet'
|
||||||
if (pathname === '/vocab-worksheet') return 'vokabeln'
|
if (pathname === '/vocab-worksheet') return 'vokabeln'
|
||||||
if (pathname === '/worksheet-editor') return 'worksheet-editor'
|
if (pathname === '/worksheet-editor') return 'worksheet-editor'
|
||||||
|
|||||||
222
studio-v2/components/companion/CompanionDashboard.tsx
Normal file
222
studio-v2/components/companion/CompanionDashboard.tsx
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { Settings, MessageSquare, HelpCircle, Timer } from 'lucide-react'
|
||||||
|
import { TeacherSettings, FeedbackType } from '@/lib/companion/types'
|
||||||
|
import { DEFAULT_TEACHER_SETTINGS, STORAGE_KEYS } from '@/lib/companion/constants'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { LessonStartForm } from './lesson-mode/LessonStartForm'
|
||||||
|
import { LessonActiveView } from './lesson-mode/LessonActiveView'
|
||||||
|
import { LessonEndedView } from './lesson-mode/LessonEndedView'
|
||||||
|
import { SettingsModal } from './modals/SettingsModal'
|
||||||
|
import { FeedbackModal } from './modals/FeedbackModal'
|
||||||
|
import { OnboardingModal } from './modals/OnboardingModal'
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import { useLessonSession } from '@/hooks/companion/useLessonSession'
|
||||||
|
import { useKeyboardShortcuts } from '@/hooks/companion/useKeyboardShortcuts'
|
||||||
|
|
||||||
|
export function CompanionDashboard() {
|
||||||
|
// Modal states
|
||||||
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
|
const [showFeedback, setShowFeedback] = useState(false)
|
||||||
|
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
const [settings, setSettings] = useState<TeacherSettings>(DEFAULT_TEACHER_SETTINGS)
|
||||||
|
|
||||||
|
// Load settings from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.SETTINGS)
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored)
|
||||||
|
setSettings({ ...DEFAULT_TEACHER_SETTINGS, ...parsed })
|
||||||
|
} catch {
|
||||||
|
// Invalid stored settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if onboarding needed
|
||||||
|
const onboardingStored = localStorage.getItem(STORAGE_KEYS.ONBOARDING_STATE)
|
||||||
|
if (!onboardingStored) {
|
||||||
|
setShowOnboarding(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Lesson session hook
|
||||||
|
const {
|
||||||
|
session,
|
||||||
|
startLesson,
|
||||||
|
endLesson,
|
||||||
|
clearSession,
|
||||||
|
pauseLesson,
|
||||||
|
resumeLesson,
|
||||||
|
extendTime,
|
||||||
|
skipPhase,
|
||||||
|
saveReflection,
|
||||||
|
addHomework,
|
||||||
|
removeHomework,
|
||||||
|
isPaused,
|
||||||
|
} = useLessonSession({
|
||||||
|
onOvertimeStart: () => {
|
||||||
|
if (settings.soundNotifications) {
|
||||||
|
// TODO: Play notification sound
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle pause/resume toggle
|
||||||
|
const handlePauseToggle = useCallback(() => {
|
||||||
|
if (isPaused) {
|
||||||
|
resumeLesson()
|
||||||
|
} else {
|
||||||
|
pauseLesson()
|
||||||
|
}
|
||||||
|
}, [isPaused, pauseLesson, resumeLesson])
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
useKeyboardShortcuts({
|
||||||
|
onPauseResume: session ? handlePauseToggle : undefined,
|
||||||
|
onExtend: session && !isPaused ? () => extendTime(5) : undefined,
|
||||||
|
onNextPhase: session && !isPaused ? skipPhase : undefined,
|
||||||
|
onCloseModal: () => {
|
||||||
|
setShowSettings(false)
|
||||||
|
setShowFeedback(false)
|
||||||
|
setShowOnboarding(false)
|
||||||
|
},
|
||||||
|
enabled: settings.showKeyboardShortcuts,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle settings save
|
||||||
|
const handleSaveSettings = (newSettings: TeacherSettings) => {
|
||||||
|
setSettings(newSettings)
|
||||||
|
localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(newSettings))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle feedback submit
|
||||||
|
const handleFeedbackSubmit = async (type: FeedbackType, title: string, description: string) => {
|
||||||
|
const response = await fetch('/api/companion/feedback', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
sessionId: session?.sessionId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to submit feedback')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle onboarding complete
|
||||||
|
const handleOnboardingComplete = (data: { state?: string; schoolType?: string }) => {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.ONBOARDING_STATE, JSON.stringify({
|
||||||
|
...data,
|
||||||
|
completed: true,
|
||||||
|
completedAt: new Date().toISOString(),
|
||||||
|
}))
|
||||||
|
setShowOnboarding(false)
|
||||||
|
setSettings({ ...settings, onboardingCompleted: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine current view based on session status
|
||||||
|
const renderContent = () => {
|
||||||
|
if (!session) {
|
||||||
|
return <LessonStartForm onStart={startLesson} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.status === 'completed') {
|
||||||
|
return (
|
||||||
|
<LessonEndedView
|
||||||
|
session={session}
|
||||||
|
onSaveReflection={saveReflection}
|
||||||
|
onAddHomework={addHomework}
|
||||||
|
onRemoveHomework={removeHomework}
|
||||||
|
onStartNew={clearSession}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// in_progress or paused
|
||||||
|
return (
|
||||||
|
<LessonActiveView
|
||||||
|
session={session}
|
||||||
|
onPauseToggle={handlePauseToggle}
|
||||||
|
onExtendTime={extendTime}
|
||||||
|
onSkipPhase={skipPhase}
|
||||||
|
onEndLesson={endLesson}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-[calc(100vh-200px)] ${settings.highContrastMode ? 'high-contrast' : ''}`}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-blue-100 rounded-lg">
|
||||||
|
<Timer className="w-5 h-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-semibold text-slate-900">Unterrichtsstunde</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Feedback Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFeedback(true)}
|
||||||
|
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
||||||
|
title="Feedback"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Settings Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSettings(true)}
|
||||||
|
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
||||||
|
title="Einstellungen"
|
||||||
|
>
|
||||||
|
<Settings className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Help Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowOnboarding(true)}
|
||||||
|
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
||||||
|
title="Hilfe"
|
||||||
|
>
|
||||||
|
<HelpCircle className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
{renderContent()}
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
|
<SettingsModal
|
||||||
|
isOpen={showSettings}
|
||||||
|
onClose={() => setShowSettings(false)}
|
||||||
|
settings={settings}
|
||||||
|
onSave={handleSaveSettings}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FeedbackModal
|
||||||
|
isOpen={showFeedback}
|
||||||
|
onClose={() => setShowFeedback(false)}
|
||||||
|
onSubmit={handleFeedbackSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OnboardingModal
|
||||||
|
isOpen={showOnboarding}
|
||||||
|
onClose={() => setShowOnboarding(false)}
|
||||||
|
onComplete={handleOnboardingComplete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { BookOpen, Clock, Users } from 'lucide-react'
|
|||||||
import { LessonSession } from '@/lib/companion/types'
|
import { LessonSession } from '@/lib/companion/types'
|
||||||
import { VisualPieTimer } from './VisualPieTimer'
|
import { VisualPieTimer } from './VisualPieTimer'
|
||||||
import { QuickActionsBar } from './QuickActionsBar'
|
import { QuickActionsBar } from './QuickActionsBar'
|
||||||
import { PhaseTimelineDetailed } from '../companion-mode/PhaseTimeline'
|
import { PhaseTimelineDetailed } from './PhaseTimeline'
|
||||||
import {
|
import {
|
||||||
PHASE_COLORS,
|
PHASE_COLORS,
|
||||||
PHASE_DISPLAY_NAMES,
|
PHASE_DISPLAY_NAMES,
|
||||||
@@ -1,103 +1,29 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Check } from 'lucide-react'
|
import { Check } from 'lucide-react'
|
||||||
import { Phase } from '@/lib/companion/types'
|
import { formatMinutes } from '@/lib/companion/constants'
|
||||||
import { PHASE_COLORS, formatMinutes } from '@/lib/companion/constants'
|
|
||||||
|
|
||||||
interface PhaseTimelineProps {
|
interface PhaseTimelinePhase {
|
||||||
phases: Phase[]
|
id: string
|
||||||
|
shortName: string
|
||||||
|
displayName: string
|
||||||
|
duration: number
|
||||||
|
status: string
|
||||||
|
actualTime?: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PhaseTimelineDetailedProps {
|
||||||
|
phases: PhaseTimelinePhase[]
|
||||||
currentPhaseIndex: number
|
currentPhaseIndex: number
|
||||||
onPhaseClick?: (index: number) => void
|
onPhaseClick?: (index: number) => void
|
||||||
compact?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PhaseTimeline({
|
|
||||||
phases,
|
|
||||||
currentPhaseIndex,
|
|
||||||
onPhaseClick,
|
|
||||||
compact = false,
|
|
||||||
}: PhaseTimelineProps) {
|
|
||||||
return (
|
|
||||||
<div className={`flex items-center ${compact ? 'gap-2' : 'gap-3'}`}>
|
|
||||||
{phases.map((phase, index) => {
|
|
||||||
const isActive = index === currentPhaseIndex
|
|
||||||
const isCompleted = phase.status === 'completed'
|
|
||||||
const isPast = index < currentPhaseIndex
|
|
||||||
const colors = PHASE_COLORS[phase.id]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={phase.id} className="flex items-center">
|
|
||||||
{/* Phase Dot/Circle */}
|
|
||||||
<button
|
|
||||||
onClick={() => onPhaseClick?.(index)}
|
|
||||||
disabled={!onPhaseClick}
|
|
||||||
className={`
|
|
||||||
relative flex items-center justify-center
|
|
||||||
${compact ? 'w-8 h-8' : 'w-10 h-10'}
|
|
||||||
rounded-full font-semibold text-sm
|
|
||||||
transition-all duration-300
|
|
||||||
${onPhaseClick ? 'cursor-pointer hover:scale-110' : 'cursor-default'}
|
|
||||||
${isActive
|
|
||||||
? `ring-4 ring-offset-2 ${colors.tailwind} text-white`
|
|
||||||
: isCompleted || isPast
|
|
||||||
? `${colors.tailwind} text-white opacity-80`
|
|
||||||
: 'bg-slate-200 text-slate-500'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: isActive || isCompleted || isPast ? colors.hex : undefined,
|
|
||||||
// Use CSS custom property for ring color with Tailwind
|
|
||||||
'--tw-ring-color': isActive ? colors.hex : undefined,
|
|
||||||
} as React.CSSProperties}
|
|
||||||
title={`${phase.displayName} (${formatMinutes(phase.duration)})`}
|
|
||||||
>
|
|
||||||
{isCompleted ? (
|
|
||||||
<Check className={compact ? 'w-4 h-4' : 'w-5 h-5'} />
|
|
||||||
) : (
|
|
||||||
phase.shortName
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Active indicator pulse */}
|
|
||||||
{isActive && (
|
|
||||||
<span
|
|
||||||
className="absolute inset-0 rounded-full animate-ping opacity-30"
|
|
||||||
style={{ backgroundColor: colors.hex }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Connector Line */}
|
|
||||||
{index < phases.length - 1 && (
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
${compact ? 'w-4' : 'w-8'} h-1 mx-1
|
|
||||||
${isPast || isCompleted
|
|
||||||
? 'bg-gradient-to-r'
|
|
||||||
: 'bg-slate-200'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
style={{
|
|
||||||
background: isPast || isCompleted
|
|
||||||
? `linear-gradient(to right, ${colors.hex}, ${PHASE_COLORS[phases[index + 1].id].hex})`
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detailed Phase Timeline with labels and durations
|
|
||||||
*/
|
|
||||||
export function PhaseTimelineDetailed({
|
export function PhaseTimelineDetailed({
|
||||||
phases,
|
phases,
|
||||||
currentPhaseIndex,
|
currentPhaseIndex,
|
||||||
onPhaseClick,
|
onPhaseClick,
|
||||||
}: PhaseTimelineProps) {
|
}: PhaseTimelineDetailedProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
<div className="bg-white border border-slate-200 rounded-xl p-6">
|
||||||
<h3 className="text-sm font-medium text-slate-500 mb-4">Unterrichtsphasen</h3>
|
<h3 className="text-sm font-medium text-slate-500 mb-4">Unterrichtsphasen</h3>
|
||||||
@@ -107,7 +33,6 @@ export function PhaseTimelineDetailed({
|
|||||||
const isActive = index === currentPhaseIndex
|
const isActive = index === currentPhaseIndex
|
||||||
const isCompleted = phase.status === 'completed'
|
const isCompleted = phase.status === 'completed'
|
||||||
const isPast = index < currentPhaseIndex
|
const isPast = index < currentPhaseIndex
|
||||||
const colors = PHASE_COLORS[phase.id]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={phase.id} className="flex flex-col items-center flex-1">
|
<div key={phase.id} className="flex flex-col items-center flex-1">
|
||||||
@@ -118,7 +43,7 @@ export function PhaseTimelineDetailed({
|
|||||||
className="flex-1 h-1"
|
className="flex-1 h-1"
|
||||||
style={{
|
style={{
|
||||||
background: isPast || isCompleted
|
background: isPast || isCompleted
|
||||||
? PHASE_COLORS[phases[index - 1].id].hex
|
? phases[index - 1].color
|
||||||
: '#e2e8f0',
|
: '#e2e8f0',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -138,9 +63,9 @@ export function PhaseTimelineDetailed({
|
|||||||
${isActive ? 'ring-4 ring-offset-2 shadow-lg' : ''}
|
${isActive ? 'ring-4 ring-offset-2 shadow-lg' : ''}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isActive || isCompleted || isPast ? colors.hex : '#e2e8f0',
|
backgroundColor: isActive || isCompleted || isPast ? phase.color : '#e2e8f0',
|
||||||
color: isActive || isCompleted || isPast ? 'white' : '#64748b',
|
color: isActive || isCompleted || isPast ? 'white' : '#64748b',
|
||||||
'--tw-ring-color': isActive ? `${colors.hex}40` : undefined,
|
'--tw-ring-color': isActive ? `${phase.color}40` : undefined,
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties}
|
||||||
>
|
>
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
@@ -152,7 +77,7 @@ export function PhaseTimelineDetailed({
|
|||||||
{isActive && (
|
{isActive && (
|
||||||
<span
|
<span
|
||||||
className="absolute inset-0 rounded-full animate-ping opacity-20"
|
className="absolute inset-0 rounded-full animate-ping opacity-20"
|
||||||
style={{ backgroundColor: colors.hex }}
|
style={{ backgroundColor: phase.color }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -161,7 +86,7 @@ export function PhaseTimelineDetailed({
|
|||||||
<div
|
<div
|
||||||
className="flex-1 h-1"
|
className="flex-1 h-1"
|
||||||
style={{
|
style={{
|
||||||
background: isCompleted ? colors.hex : '#e2e8f0',
|
background: isCompleted ? phase.color : '#e2e8f0',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export { useCompanionData } from './useCompanionData'
|
|
||||||
export { useLessonSession } from './useLessonSession'
|
export { useLessonSession } from './useLessonSession'
|
||||||
export { useKeyboardShortcuts, useKeyboardShortcutHints } from './useKeyboardShortcuts'
|
export { useKeyboardShortcuts, useKeyboardShortcutHints } from './useKeyboardShortcuts'
|
||||||
@@ -29,6 +29,7 @@ interface UseLessonSessionReturn {
|
|||||||
templateId?: string
|
templateId?: string
|
||||||
}) => void
|
}) => void
|
||||||
endLesson: () => void
|
endLesson: () => void
|
||||||
|
clearSession: () => void
|
||||||
pauseLesson: () => void
|
pauseLesson: () => void
|
||||||
resumeLesson: () => void
|
resumeLesson: () => void
|
||||||
extendTime: (minutes: number) => void
|
extendTime: (minutes: number) => void
|
||||||
@@ -96,10 +97,11 @@ export function useLessonSession(
|
|||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const delta = Math.floor((now - lastTickRef.current) / 1000)
|
const delta = Math.floor((now - lastTickRef.current) / 1000)
|
||||||
lastTickRef.current = now
|
|
||||||
|
|
||||||
if (delta <= 0) return
|
if (delta <= 0) return
|
||||||
|
|
||||||
|
lastTickRef.current = now
|
||||||
|
|
||||||
setSession((prev) => {
|
setSession((prev) => {
|
||||||
if (!prev) return null
|
if (!prev) return null
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ export function useLessonSession(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session?.status === 'in_progress' && !session.isPaused) {
|
if (session?.status === 'in_progress' && !session.isPaused) {
|
||||||
lastTickRef.current = Date.now()
|
lastTickRef.current = Date.now()
|
||||||
timerRef.current = setInterval(tick, 100) // Update every 100ms for smooth animation
|
timerRef.current = setInterval(tick, 100)
|
||||||
} else {
|
} else {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearInterval(timerRef.current)
|
clearInterval(timerRef.current)
|
||||||
@@ -175,12 +177,10 @@ export function useLessonSession(
|
|||||||
if (stored) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored) as LessonSession
|
const parsed = JSON.parse(stored) as LessonSession
|
||||||
// Only restore if session is not completed and not too old (< 24h)
|
|
||||||
const sessionTime = new Date(parsed.startTime).getTime()
|
const sessionTime = new Date(parsed.startTime).getTime()
|
||||||
const isRecent = Date.now() - sessionTime < 24 * 60 * 60 * 1000
|
const isRecent = Date.now() - sessionTime < 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
if (parsed.status !== 'completed' && isRecent) {
|
if (parsed.status !== 'completed' && isRecent) {
|
||||||
// Pause the restored session
|
|
||||||
setSession({ ...parsed, isPaused: true })
|
setSession({ ...parsed, isPaused: true })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -197,7 +197,6 @@ export function useLessonSession(
|
|||||||
topic?: string
|
topic?: string
|
||||||
templateId?: string
|
templateId?: string
|
||||||
}) => {
|
}) => {
|
||||||
// Find template durations
|
|
||||||
let durations = DEFAULT_PHASE_DURATIONS
|
let durations = DEFAULT_PHASE_DURATIONS
|
||||||
if (data.templateId) {
|
if (data.templateId) {
|
||||||
const template = SYSTEM_TEMPLATES.find((t) => t.templateId === data.templateId)
|
const template = SYSTEM_TEMPLATES.find((t) => t.templateId === data.templateId)
|
||||||
@@ -253,6 +252,11 @@ export function useLessonSession(
|
|||||||
onLessonComplete?.(completedSession)
|
onLessonComplete?.(completedSession)
|
||||||
}, [session, onLessonComplete])
|
}, [session, onLessonComplete])
|
||||||
|
|
||||||
|
const clearSession = useCallback(() => {
|
||||||
|
setSession(null)
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.CURRENT_SESSION)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const pauseLesson = useCallback(() => {
|
const pauseLesson = useCallback(() => {
|
||||||
if (!session || session.isPaused) return
|
if (!session || session.isPaused) return
|
||||||
|
|
||||||
@@ -302,7 +306,6 @@ export function useLessonSession(
|
|||||||
|
|
||||||
currentPhase.duration += minutes
|
currentPhase.duration += minutes
|
||||||
|
|
||||||
// Reset overtime trigger if we've added time
|
|
||||||
if (hasTriggeredOvertimeRef.current) {
|
if (hasTriggeredOvertimeRef.current) {
|
||||||
const phaseDurationSeconds = currentPhase.duration * 60
|
const phaseDurationSeconds = currentPhase.duration * 60
|
||||||
if (currentPhase.actualTime < phaseDurationSeconds) {
|
if (currentPhase.actualTime < phaseDurationSeconds) {
|
||||||
@@ -325,7 +328,6 @@ export function useLessonSession(
|
|||||||
|
|
||||||
const nextPhaseIndex = session.currentPhaseIndex + 1
|
const nextPhaseIndex = session.currentPhaseIndex + 1
|
||||||
|
|
||||||
// Check if this was the last phase
|
|
||||||
if (nextPhaseIndex >= session.phases.length) {
|
if (nextPhaseIndex >= session.phases.length) {
|
||||||
endLesson()
|
endLesson()
|
||||||
return
|
return
|
||||||
@@ -336,14 +338,12 @@ export function useLessonSession(
|
|||||||
|
|
||||||
const updatedPhases = [...prev.phases]
|
const updatedPhases = [...prev.phases]
|
||||||
|
|
||||||
// Complete current phase
|
|
||||||
updatedPhases[prev.currentPhaseIndex] = {
|
updatedPhases[prev.currentPhaseIndex] = {
|
||||||
...updatedPhases[prev.currentPhaseIndex],
|
...updatedPhases[prev.currentPhaseIndex],
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
completedAt: new Date().toISOString(),
|
completedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start next phase
|
|
||||||
updatedPhases[nextPhaseIndex] = {
|
updatedPhases[nextPhaseIndex] = {
|
||||||
...updatedPhases[nextPhaseIndex],
|
...updatedPhases[nextPhaseIndex],
|
||||||
status: 'active',
|
status: 'active',
|
||||||
@@ -422,17 +422,12 @@ export function useLessonSession(
|
|||||||
[session]
|
[session]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clear session (for starting new)
|
|
||||||
const clearSession = useCallback(() => {
|
|
||||||
setSession(null)
|
|
||||||
localStorage.removeItem(STORAGE_KEYS.CURRENT_SESSION)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
timerState,
|
timerState,
|
||||||
startLesson,
|
startLesson,
|
||||||
endLesson: session?.status === 'completed' ? clearSession : endLesson,
|
endLesson,
|
||||||
|
clearSession,
|
||||||
pauseLesson,
|
pauseLesson,
|
||||||
resumeLesson,
|
resumeLesson,
|
||||||
extendTime,
|
extendTime,
|
||||||
@@ -37,7 +37,7 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
nav_worksheet_editor: 'Arbeitsblätter',
|
nav_worksheet_editor: 'Arbeitsblätter',
|
||||||
nav_worksheet_cleanup: 'Bereinigung',
|
nav_worksheet_cleanup: 'Bereinigung',
|
||||||
nav_korrektur: 'Korrekturen',
|
nav_korrektur: 'Korrekturen',
|
||||||
nav_compliance_pipeline: 'Compliance Pipeline',
|
nav_companion: 'Companion', nav_compliance_pipeline: 'Compliance Pipeline',
|
||||||
nav_meet: 'Meet',
|
nav_meet: 'Meet',
|
||||||
nav_alerts: 'Alerts',
|
nav_alerts: 'Alerts',
|
||||||
nav_alerts_b2b: 'B2B Alerts',
|
nav_alerts_b2b: 'B2B Alerts',
|
||||||
@@ -98,7 +98,7 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
nav_worksheet_editor: 'Worksheets',
|
nav_worksheet_editor: 'Worksheets',
|
||||||
nav_worksheet_cleanup: 'Cleanup',
|
nav_worksheet_cleanup: 'Cleanup',
|
||||||
nav_korrektur: 'Corrections',
|
nav_korrektur: 'Corrections',
|
||||||
nav_compliance_pipeline: 'Compliance Pipeline',
|
nav_companion: 'Companion', nav_compliance_pipeline: 'Compliance Pipeline',
|
||||||
nav_meet: 'Meet',
|
nav_meet: 'Meet',
|
||||||
nav_alerts: 'Alerts',
|
nav_alerts: 'Alerts',
|
||||||
nav_alerts_b2b: 'B2B Alerts',
|
nav_alerts_b2b: 'B2B Alerts',
|
||||||
@@ -145,7 +145,7 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
nav_worksheet_editor: 'Çalışma Sayfaları',
|
nav_worksheet_editor: 'Çalışma Sayfaları',
|
||||||
nav_worksheet_cleanup: 'Temizleme',
|
nav_worksheet_cleanup: 'Temizleme',
|
||||||
nav_korrektur: 'Düzeltmeler',
|
nav_korrektur: 'Düzeltmeler',
|
||||||
nav_compliance_pipeline: 'Uyum Boru Hattı',
|
nav_companion: 'Companion', nav_compliance_pipeline: 'Uyum Boru Hattı',
|
||||||
nav_meet: 'Meet',
|
nav_meet: 'Meet',
|
||||||
nav_alerts: 'Uyarılar',
|
nav_alerts: 'Uyarılar',
|
||||||
nav_alerts_b2b: 'B2B Uyarılar',
|
nav_alerts_b2b: 'B2B Uyarılar',
|
||||||
@@ -192,7 +192,7 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
nav_worksheet_editor: 'أوراق العمل',
|
nav_worksheet_editor: 'أوراق العمل',
|
||||||
nav_worksheet_cleanup: 'تنظيف',
|
nav_worksheet_cleanup: 'تنظيف',
|
||||||
nav_korrektur: 'التصحيحات',
|
nav_korrektur: 'التصحيحات',
|
||||||
nav_compliance_pipeline: 'خط أنابيب الامتثال',
|
nav_companion: 'المرافق', nav_compliance_pipeline: 'خط أنابيب الامتثال',
|
||||||
nav_meet: 'اجتماع',
|
nav_meet: 'اجتماع',
|
||||||
nav_alerts: 'تنبيهات',
|
nav_alerts: 'تنبيهات',
|
||||||
nav_alerts_b2b: 'تنبيهات الشركات',
|
nav_alerts_b2b: 'تنبيهات الشركات',
|
||||||
@@ -239,7 +239,7 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
nav_worksheet_editor: 'Рабочие листы',
|
nav_worksheet_editor: 'Рабочие листы',
|
||||||
nav_worksheet_cleanup: 'Очистка',
|
nav_worksheet_cleanup: 'Очистка',
|
||||||
nav_korrektur: 'Проверки',
|
nav_korrektur: 'Проверки',
|
||||||
nav_compliance_pipeline: 'Пайплайн соответствия',
|
nav_companion: 'Компаньон', nav_compliance_pipeline: 'Пайплайн соответствия',
|
||||||
nav_meet: 'Встреча',
|
nav_meet: 'Встреча',
|
||||||
nav_alerts: 'Оповещения',
|
nav_alerts: 'Оповещения',
|
||||||
nav_alerts_b2b: 'B2B оповещения',
|
nav_alerts_b2b: 'B2B оповещения',
|
||||||
@@ -286,7 +286,7 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
nav_worksheet_editor: 'Робочі аркуші',
|
nav_worksheet_editor: 'Робочі аркуші',
|
||||||
nav_worksheet_cleanup: 'Очищення',
|
nav_worksheet_cleanup: 'Очищення',
|
||||||
nav_korrektur: 'Перевірки',
|
nav_korrektur: 'Перевірки',
|
||||||
nav_compliance_pipeline: 'Пайплайн відповідності',
|
nav_companion: 'Компаньйон', nav_compliance_pipeline: 'Пайплайн відповідності',
|
||||||
nav_meet: 'Зустріч',
|
nav_meet: 'Зустріч',
|
||||||
nav_alerts: 'Сповіщення',
|
nav_alerts: 'Сповіщення',
|
||||||
nav_alerts_b2b: 'B2B сповіщення',
|
nav_alerts_b2b: 'B2B сповіщення',
|
||||||
@@ -333,7 +333,7 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
nav_worksheet_editor: 'Arkusze robocze',
|
nav_worksheet_editor: 'Arkusze robocze',
|
||||||
nav_worksheet_cleanup: 'Czyszczenie',
|
nav_worksheet_cleanup: 'Czyszczenie',
|
||||||
nav_korrektur: 'Korekty',
|
nav_korrektur: 'Korekty',
|
||||||
nav_compliance_pipeline: 'Pipeline zgodności',
|
nav_companion: 'Companion', nav_compliance_pipeline: 'Pipeline zgodności',
|
||||||
nav_meet: 'Spotkanie',
|
nav_meet: 'Spotkanie',
|
||||||
nav_alerts: 'Powiadomienia',
|
nav_alerts: 'Powiadomienia',
|
||||||
nav_alerts_b2b: 'Powiadomienia B2B',
|
nav_alerts_b2b: 'Powiadomienia B2B',
|
||||||
|
|||||||
Reference in New Issue
Block a user