Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import {
|
|
type TabType,
|
|
type MeetingStats,
|
|
type Meeting,
|
|
type Recording,
|
|
type BreakoutRoom,
|
|
getBackendUrl,
|
|
getJitsiUrl,
|
|
} from './types'
|
|
|
|
export function useMeetPage() {
|
|
// Tab state
|
|
const [activeTab, setActiveTab] = useState<TabType>('dashboard')
|
|
|
|
const [stats, setStats] = useState<MeetingStats>({ active: 0, scheduled: 0, recordings: 0, participants: 0 })
|
|
const [scheduledMeetings, setScheduledMeetings] = useState<Meeting[]>([])
|
|
const [activeMeetings, setActiveMeetings] = useState<Meeting[]>([])
|
|
const [recordings, setRecordings] = useState<Recording[]>([])
|
|
const [recordingsFilter, setRecordingsFilter] = useState<string>('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<Recording | null>(null)
|
|
const [transcriptText, setTranscriptText] = useState('')
|
|
const [transcriptLoading, setTranscriptLoading] = useState(false)
|
|
|
|
// Breakout rooms state
|
|
const [breakoutRooms, setBreakoutRooms] = useState<BreakoutRoom[]>([
|
|
{ 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<string | null>(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 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 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 (meetingType === 'quick') {
|
|
joinMeeting(meeting.room_name, meetingTitle || 'Neues Meeting')
|
|
} else {
|
|
fetchData()
|
|
}
|
|
|
|
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 openInNewTab = () => {
|
|
window.open(currentMeetingUrl, '_blank')
|
|
setShowJoinModal(false)
|
|
}
|
|
|
|
const copyMeetingLink = async (roomName: string) => {
|
|
const url = `${getJitsiUrl()}/${roomName}`
|
|
await navigator.clipboard.writeText(url)
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
|
|
return {
|
|
// Tab
|
|
activeTab, setActiveTab,
|
|
// Data
|
|
stats, scheduledMeetings, activeMeetings, recordings, loading,
|
|
// Modals
|
|
showNewMeetingModal, setShowNewMeetingModal,
|
|
showJoinModal, setShowJoinModal,
|
|
showTranscriptModal, setShowTranscriptModal,
|
|
currentMeetingUrl, currentMeetingTitle,
|
|
currentRecording, transcriptText, transcriptLoading,
|
|
// Recordings
|
|
recordingsFilter, setRecordingsFilter,
|
|
filteredRecordings, totalStorageBytes, maxStorageGB, storagePercent,
|
|
// Breakout
|
|
breakoutRooms, breakoutAssignment, setBreakoutAssignment,
|
|
breakoutTimer, setBreakoutTimer, hasActiveMeeting,
|
|
addBreakoutRoom, removeBreakoutRoom,
|
|
// Form
|
|
meetingType, setMeetingType,
|
|
meetingTitle, setMeetingTitle,
|
|
meetingDuration, setMeetingDuration,
|
|
meetingDateTime, setMeetingDateTime,
|
|
enableLobby, setEnableLobby,
|
|
enableRecording, setEnableRecording,
|
|
muteOnStart, setMuteOnStart,
|
|
creating, errorMessage, setErrorMessage,
|
|
// Actions
|
|
fetchData, createMeeting, startQuickMeeting,
|
|
joinMeeting, openInNewTab, copyMeetingLink,
|
|
playRecording, viewTranscript, startTranscription,
|
|
downloadRecording, deleteRecording,
|
|
}
|
|
}
|