'use client' import { useState, useEffect } from 'react' import { useTheme } from '@/lib/ThemeContext' import { useLanguage } from '@/lib/LanguageContext' import { Sidebar } from '@/components/Sidebar' // ============================================ // CONFIGURATION // ============================================ // API calls now go through Next.js rewrites (see next.config.js) // This avoids mixed-content issues when accessing via HTTPS const getBackendUrl = () => { // Return empty string to use relative URLs that go through Next.js proxy return '' } const getJitsiUrl = () => { if (typeof window === 'undefined') return 'http://localhost:8443' const { hostname, protocol } = window.location // Use /jitsi/ path on same origin to avoid SSL certificate issues with separate ports return hostname === 'localhost' ? 'http://localhost:8443' : `${protocol}//${hostname}/jitsi` } // ============================================ // TYPES // ============================================ type TabType = 'dashboard' | 'breakout' | 'recordings' interface MeetingStats { active: number scheduled: number recordings: number participants: number } interface Meeting { room_name: string title: string type: string scheduled_at?: string duration: number participants?: number started_at?: string } interface MeetingConfig { enable_lobby: boolean enable_recording: boolean start_with_audio_muted: boolean start_with_video_muted: boolean } interface Recording { id: string meeting_id: string title: string recorded_at: string duration_seconds: number file_size_bytes: number status: string transcription_status?: string } interface BreakoutRoom { id: string name: string participants: string[] } // ============================================ // ICONS // ============================================ const Icons = { video: ( ), calendar: ( ), users: ( ), graduation: ( ), record: ( ), clock: ( ), plus: ( ), copy: ( ), external: ( ), close: ( ), grid: ( ), play: ( ), download: ( ), trash: ( ), document: ( ), refresh: ( ), } // ============================================ // MAIN COMPONENT // ============================================ export default function MeetPage() { const { isDark } = useTheme() const { t } = useLanguage() // Tab state const [activeTab, setActiveTab] = useState('dashboard') const [stats, setStats] = useState({ active: 0, scheduled: 0, recordings: 0, participants: 0 }) const [scheduledMeetings, setScheduledMeetings] = useState([]) const [activeMeetings, setActiveMeetings] = useState([]) const [recordings, setRecordings] = useState([]) const [recordingsFilter, setRecordingsFilter] = useState('all') const [loading, setLoading] = useState(true) const [showNewMeetingModal, setShowNewMeetingModal] = useState(false) const [showJoinModal, setShowJoinModal] = useState(false) const [showTranscriptModal, setShowTranscriptModal] = useState(false) const [currentMeetingUrl, setCurrentMeetingUrl] = useState('') const [currentMeetingTitle, setCurrentMeetingTitle] = useState('') const [currentRecording, setCurrentRecording] = useState(null) const [transcriptText, setTranscriptText] = useState('') const [transcriptLoading, setTranscriptLoading] = useState(false) // Breakout rooms state const [breakoutRooms, setBreakoutRooms] = useState([ { id: '1', name: 'Raum 1', participants: [] }, { id: '2', name: 'Raum 2', participants: [] }, { id: '3', name: 'Raum 3', participants: [] }, ]) const [breakoutAssignment, setBreakoutAssignment] = useState('equal') const [breakoutTimer, setBreakoutTimer] = useState(15) const [hasActiveMeeting, setHasActiveMeeting] = useState(false) // Form state const [meetingType, setMeetingType] = useState('quick') const [meetingTitle, setMeetingTitle] = useState('') const [meetingDuration, setMeetingDuration] = useState(60) const [meetingDateTime, setMeetingDateTime] = useState('') const [enableLobby, setEnableLobby] = useState(true) const [enableRecording, setEnableRecording] = useState(false) const [muteOnStart, setMuteOnStart] = useState(true) const [creating, setCreating] = useState(false) const [errorMessage, setErrorMessage] = useState(null) // ============================================ // DATA FETCHING // ============================================ useEffect(() => { fetchData() }, []) const fetchData = async () => { setLoading(true) try { const [statsRes, scheduledRes, activeRes, recordingsRes] = await Promise.all([ fetch(`${getBackendUrl()}/api/meetings/stats`).catch(() => null), fetch(`${getBackendUrl()}/api/meetings/scheduled`).catch(() => null), fetch(`${getBackendUrl()}/api/meetings/active`).catch(() => null), fetch(`${getBackendUrl()}/api/recordings`).catch(() => null), ]) if (statsRes?.ok) { const statsData = await statsRes.json() setStats(statsData) } if (scheduledRes?.ok) { const scheduledData = await scheduledRes.json() setScheduledMeetings(scheduledData) } if (activeRes?.ok) { const activeData = await activeRes.json() setActiveMeetings(activeData) setHasActiveMeeting(activeData.length > 0) } if (recordingsRes?.ok) { const recordingsData = await recordingsRes.json() setRecordings(recordingsData.recordings || recordingsData || []) } } catch (error) { console.error('Failed to fetch meeting data:', error) } finally { setLoading(false) } } // ============================================ // ACTIONS // ============================================ const createMeeting = async () => { setCreating(true) setErrorMessage(null) try { const response = await fetch(`${getBackendUrl()}/api/meetings/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: meetingType, title: meetingTitle || 'Neues Meeting', duration: meetingDuration, scheduled_at: meetingType !== 'quick' ? meetingDateTime : null, config: { enable_lobby: enableLobby, enable_recording: enableRecording, start_with_audio_muted: muteOnStart, start_with_video_muted: false, }, }), }) if (response.ok) { const meeting = await response.json() setShowNewMeetingModal(false) // If quick meeting, join immediately if (meetingType === 'quick') { joinMeeting(meeting.room_name, meetingTitle || 'Neues Meeting') } else { fetchData() // Refresh the list } // Reset form setMeetingTitle('') setMeetingType('quick') } else { const errorData = await response.json().catch(() => ({})) const errorMsg = errorData.detail || `Server-Fehler: ${response.status}` setErrorMessage(errorMsg) console.error('Failed to create meeting:', response.status, errorData) } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Netzwerkfehler - Backend nicht erreichbar' setErrorMessage(errorMsg) console.error('Failed to create meeting:', error) } finally { setCreating(false) } } const startQuickMeeting = async () => { setCreating(true) setErrorMessage(null) try { const response = await fetch(`${getBackendUrl()}/api/meetings/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'quick', title: 'Sofort-Meeting', duration: 60, config: { enable_lobby: false, enable_recording: false, start_with_audio_muted: true, start_with_video_muted: false, }, }), }) if (response.ok) { const meeting = await response.json() joinMeeting(meeting.room_name, 'Sofort-Meeting') } else { const errorData = await response.json().catch(() => ({})) const errorMsg = errorData.detail || `Server-Fehler: ${response.status}` setErrorMessage(errorMsg) console.error('Failed to create meeting:', response.status, errorData) } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Netzwerkfehler - Backend nicht erreichbar' setErrorMessage(errorMsg) console.error('Failed to start quick meeting:', error) } finally { setCreating(false) } } const joinMeeting = (roomName: string, title: string) => { const url = `${getJitsiUrl()}/${roomName}#config.prejoinPageEnabled=false&config.defaultLanguage=de&interfaceConfig.SHOW_JITSI_WATERMARK=false` setCurrentMeetingUrl(url) setCurrentMeetingTitle(title) setShowJoinModal(true) } const openInNewTab = () => { window.open(currentMeetingUrl, '_blank') setShowJoinModal(false) } const copyMeetingLink = async (roomName: string) => { const url = `${getJitsiUrl()}/${roomName}` await navigator.clipboard.writeText(url) } const formatTime = (dateString: string) => { const date = new Date(dateString) return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) } const formatDate = (dateString: string) => { const date = new Date(dateString) const today = new Date() const tomorrow = new Date(today) tomorrow.setDate(tomorrow.getDate() + 1) if (date.toDateString() === today.toDateString()) return 'Heute' if (date.toDateString() === tomorrow.toDateString()) return 'Morgen' return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) } const formatDuration = (seconds: number) => { const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) const s = seconds % 60 if (h > 0) { return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` } return `${m}:${String(s).padStart(2, '0')}` } const formatFileSize = (bytes: number) => { if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(1) + ' KB' } return (bytes / (1024 * 1024)).toFixed(1) + ' MB' } // Recording actions const playRecording = (recordingId: string) => { window.open(`${getBackendUrl()}/meetings/recordings/${recordingId}/play`, '_blank') } const viewTranscript = async (recording: Recording) => { setCurrentRecording(recording) setShowTranscriptModal(true) setTranscriptLoading(true) setTranscriptText('') try { const response = await fetch(`${getBackendUrl()}/api/recordings/${recording.id}/transcription/text`) if (response.ok) { const data = await response.json() setTranscriptText(data.text || 'Kein Transkript verfuegbar') } else if (response.status === 404) { setTranscriptText('PENDING') } else { setTranscriptText('Fehler beim Laden des Transkripts') } } catch { setTranscriptText('Fehler beim Laden des Transkripts') } finally { setTranscriptLoading(false) } } const startTranscription = async (recordingId: string) => { try { const response = await fetch(`${getBackendUrl()}/api/recordings/${recordingId}/transcribe`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ language: 'de', model: 'large-v3' }), }) if (response.ok) { alert('Transkription gestartet! Dies kann einige Minuten dauern.') setShowTranscriptModal(false) fetchData() } } catch { alert('Fehler beim Starten der Transkription') } } const downloadRecording = (recordingId: string) => { window.location.href = `${getBackendUrl()}/api/recordings/${recordingId}/download` } const deleteRecording = async (recordingId: string) => { const reason = prompt('Grund fuer die Loeschung (DSGVO-Dokumentation):') if (!reason) return try { const response = await fetch(`${getBackendUrl()}/api/recordings/${recordingId}?reason=${encodeURIComponent(reason)}`, { method: 'DELETE', }) if (response.ok) { fetchData() } } catch { alert('Fehler beim Loeschen') } } const filteredRecordings = recordings.filter((r) => { if (recordingsFilter === 'all') return true return r.status === recordingsFilter }) const totalStorageBytes = recordings.reduce((sum, r) => sum + (r.file_size_bytes || 0), 0) const maxStorageGB = 10 const storagePercent = ((totalStorageBytes / (maxStorageGB * 1024 * 1024 * 1024)) * 100).toFixed(1) // Breakout room actions const addBreakoutRoom = () => { const newId = String(breakoutRooms.length + 1) setBreakoutRooms([...breakoutRooms, { id: newId, name: `Raum ${newId}`, participants: [] }]) } const removeBreakoutRoom = (id: string) => { setBreakoutRooms(breakoutRooms.filter((r) => r.id !== id)) } // Stats data const statsData = [ { label: 'Aktive Meetings', value: loading ? '-' : String(stats.active), icon: Icons.video, color: 'from-green-400 to-emerald-600' }, { label: 'Geplante Termine', value: loading ? '-' : String(stats.scheduled), icon: Icons.calendar, color: 'from-blue-400 to-blue-600' }, { label: 'Aufzeichnungen', value: loading ? '-' : String(stats.recordings), icon: Icons.record, color: 'from-red-400 to-rose-600' }, { label: 'Teilnehmer', value: loading ? '-' : String(stats.participants), icon: Icons.users, color: 'from-amber-400 to-orange-600' }, ] // ============================================ // RENDER // ============================================ return (
{/* Animated Background Blobs */}
{/* Layout with Sidebar */}
{/* Sidebar */} {/* Main Content */}
{/* Header */}

BreakPilot Meet

Videokonferenzen, Schulungen und Elterngespraeche verwalten

{/* Tabs */}
{[ { id: 'dashboard', label: 'Dashboard' }, { id: 'breakout', label: 'Breakout-Rooms' }, { id: 'recordings', label: 'Aufzeichnungen' }, ].map((tab) => ( ))}
{/* Dashboard Tab */} {activeTab === 'dashboard' && ( <> {/* Error Banner */} {errorMessage && (
{errorMessage}
)} {/* Quick Actions */}
{/* Statistics */}
{statsData.map((stat, index) => (
{stat.icon}

{stat.label}

{stat.value}

))}
{/* Active Meetings */} {activeMeetings.length > 0 && (

Aktive Meetings

{activeMeetings.length} Live
{activeMeetings.map((meeting) => (
{Icons.video}
{meeting.title}
{Icons.users} {meeting.participants || 0} Teilnehmer {meeting.started_at && ( {Icons.clock} Gestartet {formatTime(meeting.started_at)} )}
))}
)} {/* Scheduled Meetings */}

Naechste Meetings

{loading ? (
Laet...
) : scheduledMeetings.length === 0 ? (
{Icons.calendar}

Keine geplanten Meetings

) : (
{scheduledMeetings.slice(0, 5).map((meeting) => (
{meeting.scheduled_at ? formatTime(meeting.scheduled_at) : '--:--'}
{meeting.scheduled_at ? formatDate(meeting.scheduled_at) : ''}
{meeting.title}
{Icons.clock} {meeting.duration} Min {meeting.type}
Geplant
))}
)}
)} {/* Breakout Rooms Tab */} {activeTab === 'breakout' && ( <> {/* Active Meeting Warning */} {!hasActiveMeeting && (
{Icons.video}
Kein aktives Meeting
Breakout-Rooms koennen nur waehrend eines aktiven Meetings erstellt werden.
)} {/* How it works */}

So funktionieren Breakout-Rooms

{[ { icon: Icons.grid, color: 'from-blue-400 to-blue-600', title: '1. Raeume erstellen', desc: 'Erstellen Sie mehrere Breakout-Rooms fuer Gruppenarbeit.' }, { icon: Icons.users, color: 'from-purple-400 to-purple-600', title: '2. Teilnehmer zuweisen', desc: 'Weisen Sie Teilnehmer manuell oder automatisch zu.' }, { icon: Icons.play, color: 'from-green-400 to-emerald-600', title: '3. Sessions starten', desc: 'Starten Sie alle Raeume gleichzeitig oder einzeln.' }, { icon: Icons.clock, color: 'from-amber-400 to-orange-600', title: '4. Timer setzen', desc: 'Setzen Sie einen Timer fuer automatisches Beenden.' }, ].map((step, index) => (
{step.icon}

{step.title}

{step.desc}

))}
{/* Breakout Configuration */}

Breakout-Konfiguration

{/* Breakout Rooms Grid */}
{breakoutRooms.map((room) => (
{room.name} {room.participants.length} Teilnehmer
{room.participants.length > 0 ? room.participants.join(', ') : 'Keine Teilnehmer'}
{breakoutRooms.length > 1 && ( )}
))}
setBreakoutTimer(Number(e.target.value))} disabled={!hasActiveMeeting} className={`w-full px-4 py-3 rounded-xl transition-all focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed ${ isDark ? 'bg-white/10 border border-white/20 text-white focus:ring-white/30' : 'bg-white border border-slate-300 text-slate-900 focus:ring-blue-300' }`} />
)} {/* Recordings Tab */} {activeTab === 'recordings' && ( <> {/* Filter Tabs */}
{[ { key: 'all', label: 'Alle' }, { key: 'uploaded', label: 'Neu' }, { key: 'processing', label: 'In Verarbeitung' }, { key: 'ready', label: 'Fertig' }, ].map((filter) => ( ))}
{/* Recordings List */} {loading ? (
Lade Aufzeichnungen...
) : filteredRecordings.length === 0 ? (
{Icons.record}

Keine Aufzeichnungen

Starten Sie eine Aufzeichnung in einem Meeting, um sie hier zu sehen.

) : (
{filteredRecordings.map((recording) => (
{Icons.record}
{recording.title || `Aufzeichnung ${recording.meeting_id}`} {recording.status === 'processing' && ( Verarbeitung )} {recording.transcription_status === 'pending' && ( Transkript ausstehend )} {recording.transcription_status === 'completed' && ( Transkript bereit )}
{new Date(recording.recorded_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', })},{' '} {new Date(recording.recorded_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', })}{' '} | {formatDuration(recording.duration_seconds || 0)} |{' '} {formatFileSize(recording.file_size_bytes || 0)}
))}
)} {/* Storage Info */}

Speicherplatz

{(totalStorageBytes / (1024 * 1024)).toFixed(1)} MB von {maxStorageGB} GB verwendet {storagePercent}%

{recordings.length} Aufzeichnungen

)}
{/* New Meeting Modal */} {showNewMeetingModal && (
setShowNewMeetingModal(false)} />

Neues Meeting erstellen

setMeetingTitle(e.target.value)} placeholder="Meeting-Titel eingeben" className={`w-full px-4 py-3 rounded-xl transition-all focus:outline-none focus:ring-2 ${ isDark ? 'bg-white/10 border border-white/20 text-white placeholder-white/40 focus:ring-white/30' : 'bg-slate-50 border border-slate-300 text-slate-900 placeholder-slate-400 focus:ring-blue-300' }`} />
{meetingType !== 'quick' && (
setMeetingDateTime(e.target.value)} className={`w-full px-4 py-3 rounded-xl transition-all focus:outline-none focus:ring-2 ${ isDark ? 'bg-white/10 border border-white/20 text-white focus:ring-white/30' : 'bg-slate-50 border border-slate-300 text-slate-900 focus:ring-blue-300' }`} />
)}
)} {/* Join Meeting Modal (Embedded Jitsi) */} {showJoinModal && (
BP
{currentMeetingTitle}
BreakPilot Meet