fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
backend/frontend/meetings/pages/__init__.py
Normal file
27
backend/frontend/meetings/pages/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Meetings Module - Pages
|
||||
Route handlers for the Meetings frontend
|
||||
"""
|
||||
|
||||
from .dashboard import meetings_dashboard
|
||||
from .meeting_room import meeting_room
|
||||
from .active import active_meetings
|
||||
from .schedule import schedule_meetings
|
||||
from .trainings import trainings_page
|
||||
from .recordings import recordings_page, play_recording, view_transcript
|
||||
from .breakout import breakout_rooms_page
|
||||
from .quick_actions import quick_meeting, parent_teacher_meeting
|
||||
|
||||
__all__ = [
|
||||
"meetings_dashboard",
|
||||
"meeting_room",
|
||||
"active_meetings",
|
||||
"schedule_meetings",
|
||||
"trainings_page",
|
||||
"recordings_page",
|
||||
"play_recording",
|
||||
"view_transcript",
|
||||
"breakout_rooms_page",
|
||||
"quick_meeting",
|
||||
"parent_teacher_meeting",
|
||||
]
|
||||
76
backend/frontend/meetings/pages/active.py
Normal file
76
backend/frontend/meetings/pages/active.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Meetings Module - Active Meetings Page
|
||||
List of currently active meetings
|
||||
"""
|
||||
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def active_meetings() -> str:
|
||||
"""Active meetings list"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Aktive Meetings</h1>
|
||||
<p class="page-subtitle">Laufende Videokonferenzen und Schulungen</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="window.location.href='/meetings/quick'">
|
||||
{ICONS['plus']} Neues Meeting starten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Active Meetings List -->
|
||||
<div class="meeting-list" id="activeMeetingsList">
|
||||
<div style="text-align: center; padding: 3rem; color: var(--bp-text-muted);">
|
||||
<p>Keine aktiven Meetings</p>
|
||||
<p style="font-size: 0.875rem; margin-top: 0.5rem;">
|
||||
Starten Sie ein neues Meeting oder warten Sie, bis ein geplantes Meeting beginnt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadActiveMeetings() {{
|
||||
try {{
|
||||
const response = await fetch('/api/meetings/active');
|
||||
if (response.ok) {{
|
||||
const meetings = await response.json();
|
||||
renderMeetings(meetings);
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error loading active meetings:', error);
|
||||
}}
|
||||
}}
|
||||
|
||||
function renderMeetings(meetings) {{
|
||||
const container = document.getElementById('activeMeetingsList');
|
||||
|
||||
if (!meetings || meetings.length === 0) {{
|
||||
return;
|
||||
}}
|
||||
|
||||
container.innerHTML = meetings.map(meeting => `
|
||||
<div class="meeting-item">
|
||||
<div class="meeting-time">
|
||||
<div class="meeting-time-value">${{meeting.participants || 0}}</div>
|
||||
<div class="meeting-time-date">Teilnehmer</div>
|
||||
</div>
|
||||
<div class="meeting-info">
|
||||
<div class="meeting-title">${{meeting.title}}</div>
|
||||
<div class="meeting-meta">
|
||||
<span>{ICONS['clock']} Seit ${{meeting.started_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="meeting-badge badge-live">LIVE</span>
|
||||
<div class="meeting-actions">
|
||||
<button class="btn btn-primary" onclick="window.location.href='/meetings/room/${{meeting.room_name}}'">Beitreten</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}}
|
||||
|
||||
loadActiveMeetings();
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Aktive Meetings", content, "active")
|
||||
136
backend/frontend/meetings/pages/breakout.py
Normal file
136
backend/frontend/meetings/pages/breakout.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Meetings Module - Breakout Rooms Page
|
||||
Breakout rooms management
|
||||
"""
|
||||
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def breakout_rooms_page() -> str:
|
||||
"""Breakout rooms management"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Breakout-Rooms</h1>
|
||||
<p class="page-subtitle">Gruppenräume für Workshops und Übungen verwalten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Meeting Warning -->
|
||||
<div class="card" style="background: var(--bp-primary-soft); border-color: var(--bp-primary); margin-bottom: 2rem;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div class="card-icon primary">{ICONS['video']}</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600;">Kein aktives Meeting</div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
Breakout-Rooms können nur während eines aktiven Meetings erstellt werden.
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="window.location.href='/meetings/quick'">
|
||||
Meeting starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- How it works -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">So funktionieren Breakout-Rooms</span>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-top: 1rem;">
|
||||
<div style="text-align: center;">
|
||||
<div class="card-icon primary" style="margin: 0 auto 1rem;">{ICONS['grid']}</div>
|
||||
<h4 style="margin-bottom: 0.5rem;">1. Räume erstellen</h4>
|
||||
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
Erstellen Sie mehrere Breakout-Rooms für Gruppenarbeit.
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div class="card-icon info" style="margin: 0 auto 1rem;">{ICONS['users']}</div>
|
||||
<h4 style="margin-bottom: 0.5rem;">2. Teilnehmer zuweisen</h4>
|
||||
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
Weisen Sie Teilnehmer manuell oder automatisch zu.
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div class="card-icon accent" style="margin: 0 auto 1rem;">{ICONS['play']}</div>
|
||||
<h4 style="margin-bottom: 0.5rem;">3. Sessions starten</h4>
|
||||
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
Starten Sie alle Räume gleichzeitig oder einzeln.
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div class="card-icon warning" style="margin: 0 auto 1rem;">{ICONS['clock']}</div>
|
||||
<h4 style="margin-bottom: 0.5rem;">4. Timer setzen</h4>
|
||||
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
Setzen Sie einen Timer für automatisches Beenden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Breakout Configuration Preview -->
|
||||
<div class="card" style="margin-top: 2rem;">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Breakout-Konfiguration (Vorschau)</span>
|
||||
<button class="btn btn-secondary" disabled>
|
||||
{ICONS['plus']} Raum hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="breakout-grid" style="margin-top: 1rem;">
|
||||
<div class="breakout-room" style="opacity: 0.5;">
|
||||
<div class="breakout-room-header">
|
||||
<span class="breakout-room-title">Raum 1</span>
|
||||
<span class="breakout-room-count">0 Teilnehmer</span>
|
||||
</div>
|
||||
<div class="breakout-participants">
|
||||
<span style="color: var(--bp-text-muted); font-size: 0.875rem;">Keine Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="breakout-room" style="opacity: 0.5;">
|
||||
<div class="breakout-room-header">
|
||||
<span class="breakout-room-title">Raum 2</span>
|
||||
<span class="breakout-room-count">0 Teilnehmer</span>
|
||||
</div>
|
||||
<div class="breakout-participants">
|
||||
<span style="color: var(--bp-text-muted); font-size: 0.875rem;">Keine Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="breakout-room" style="opacity: 0.5;">
|
||||
<div class="breakout-room-header">
|
||||
<span class="breakout-room-title">Raum 3</span>
|
||||
<span class="breakout-room-count">0 Teilnehmer</span>
|
||||
</div>
|
||||
<div class="breakout-participants">
|
||||
<span style="color: var(--bp-text-muted); font-size: 0.875rem;">Keine Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--bp-border);">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Automatische Zuweisung</label>
|
||||
<select class="form-select" disabled>
|
||||
<option>Gleichmäßig verteilen</option>
|
||||
<option>Zufällig zuweisen</option>
|
||||
<option>Manuell zuweisen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Timer (Minuten)</label>
|
||||
<input type="number" class="form-input" value="15" disabled>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" disabled>Breakout-Sessions starten</button>
|
||||
<button class="btn btn-secondary" disabled>Alle zurückholen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
return render_base_page("Breakout-Rooms", content, "breakout")
|
||||
298
backend/frontend/meetings/pages/dashboard.py
Normal file
298
backend/frontend/meetings/pages/dashboard.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
Meetings Module - Dashboard Page
|
||||
Main meetings dashboard with statistics and quick actions
|
||||
"""
|
||||
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def meetings_dashboard() -> str:
|
||||
"""Main meetings dashboard"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Meeting Dashboard</h1>
|
||||
<p class="page-subtitle">Videokonferenzen, Schulungen und Elterngespräche verwalten</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="openModal('newMeeting')">
|
||||
{ICONS['plus']} Neues Meeting
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="quick-actions">
|
||||
<a href="/meetings/quick" class="quick-action">
|
||||
<div class="quick-action-icon card-icon primary">{ICONS['video']}</div>
|
||||
<span class="quick-action-label">Sofort-Meeting starten</span>
|
||||
</a>
|
||||
<a href="/meetings/schedule/new" class="quick-action">
|
||||
<div class="quick-action-icon card-icon info">{ICONS['calendar']}</div>
|
||||
<span class="quick-action-label">Meeting planen</span>
|
||||
</a>
|
||||
<a href="/meetings/trainings/new" class="quick-action">
|
||||
<div class="quick-action-icon card-icon accent">{ICONS['graduation']}</div>
|
||||
<span class="quick-action-label">Schulung erstellen</span>
|
||||
</a>
|
||||
<a href="/meetings/parent-teacher" class="quick-action">
|
||||
<div class="quick-action-icon card-icon warning">{ICONS['users']}</div>
|
||||
<span class="quick-action-label">Elterngespräch</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Aktive Meetings</span>
|
||||
<div class="card-icon primary">{ICONS['video']}</div>
|
||||
</div>
|
||||
<div class="stat-value" id="activeMeetings">0</div>
|
||||
<div class="stat-label">Jetzt live</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Geplante Termine</span>
|
||||
<div class="card-icon info">{ICONS['calendar']}</div>
|
||||
</div>
|
||||
<div class="stat-value" id="scheduledMeetings">0</div>
|
||||
<div class="stat-label">Diese Woche</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Aufzeichnungen</span>
|
||||
<div class="card-icon accent">{ICONS['record']}</div>
|
||||
</div>
|
||||
<div class="stat-value" id="recordings">0</div>
|
||||
<div class="stat-label">Verfügbar</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Teilnehmer</span>
|
||||
<div class="card-icon warning">{ICONS['users']}</div>
|
||||
</div>
|
||||
<div class="stat-value" id="totalParticipants">0</div>
|
||||
<div class="stat-label">Diese Woche</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming Meetings -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Nächste Meetings</span>
|
||||
<a href="/meetings/schedule" class="btn btn-secondary">Alle anzeigen</a>
|
||||
</div>
|
||||
<div class="meeting-list" id="upcomingMeetings">
|
||||
<div class="meeting-item">
|
||||
<div class="meeting-time">
|
||||
<div class="meeting-time-value">14:00</div>
|
||||
<div class="meeting-time-date">Heute</div>
|
||||
</div>
|
||||
<div class="meeting-info">
|
||||
<div class="meeting-title">Elterngespräch - Max Müller</div>
|
||||
<div class="meeting-meta">
|
||||
<span>{ICONS['clock']} 30 Min</span>
|
||||
<span>{ICONS['users']} 2 Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="meeting-badge badge-scheduled">Geplant</span>
|
||||
<div class="meeting-actions">
|
||||
<button class="btn btn-primary" onclick="joinMeeting('parent-123')">Beitreten</button>
|
||||
<button class="btn-icon" onclick="copyMeetingLink('parent-123')">{ICONS['copy']}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meeting-item">
|
||||
<div class="meeting-time">
|
||||
<div class="meeting-time-value">15:30</div>
|
||||
<div class="meeting-time-date">Heute</div>
|
||||
</div>
|
||||
<div class="meeting-info">
|
||||
<div class="meeting-title">Go Grundlagen Schulung</div>
|
||||
<div class="meeting-meta">
|
||||
<span>{ICONS['clock']} 120 Min</span>
|
||||
<span>{ICONS['users']} 12 Teilnehmer</span>
|
||||
<span>{ICONS['record']} Aufzeichnung</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="meeting-badge badge-scheduled">Geplant</span>
|
||||
<div class="meeting-actions">
|
||||
<button class="btn btn-primary" onclick="joinMeeting('training-456')">Beitreten</button>
|
||||
<button class="btn-icon" onclick="copyMeetingLink('training-456')">{ICONS['copy']}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Meeting Modal -->
|
||||
<div class="modal-overlay" id="newMeetingModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Neues Meeting erstellen</h2>
|
||||
<button class="modal-close" onclick="closeModal('newMeeting')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Meeting-Typ</label>
|
||||
<select class="form-select" id="meetingType" onchange="updateMeetingForm()">
|
||||
<option value="quick">Sofort-Meeting</option>
|
||||
<option value="scheduled">Geplantes Meeting</option>
|
||||
<option value="training">Schulung</option>
|
||||
<option value="parent">Elterngespräch</option>
|
||||
<option value="class">Klassenkonferenz</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Titel</label>
|
||||
<input type="text" class="form-input" id="meetingTitle" placeholder="Meeting-Titel eingeben">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="dateTimeGroup" style="display: none;">
|
||||
<label class="form-label">Datum & Uhrzeit</label>
|
||||
<input type="datetime-local" class="form-input" id="meetingDateTime">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="durationGroup">
|
||||
<label class="form-label">Dauer (Minuten)</label>
|
||||
<select class="form-select" id="meetingDuration">
|
||||
<option value="30">30 Minuten</option>
|
||||
<option value="60" selected>60 Minuten</option>
|
||||
<option value="90">90 Minuten</option>
|
||||
<option value="120">120 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="enableLobby" checked> Warteraum aktivieren
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="enableRecording"> Aufzeichnung erlauben
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="muteOnStart" checked> Teilnehmer stummschalten bei Beitritt
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('newMeeting')">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="createMeeting()">Meeting erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Modal Functions
|
||||
function openModal(type) {{
|
||||
document.getElementById(type + 'Modal').classList.add('active');
|
||||
}}
|
||||
|
||||
function closeModal(type) {{
|
||||
document.getElementById(type + 'Modal').classList.remove('active');
|
||||
}}
|
||||
|
||||
function updateMeetingForm() {{
|
||||
const type = document.getElementById('meetingType').value;
|
||||
const dateTimeGroup = document.getElementById('dateTimeGroup');
|
||||
|
||||
if (type === 'quick') {{
|
||||
dateTimeGroup.style.display = 'none';
|
||||
}} else {{
|
||||
dateTimeGroup.style.display = 'block';
|
||||
}}
|
||||
}}
|
||||
|
||||
// Meeting Functions
|
||||
async function createMeeting() {{
|
||||
const type = document.getElementById('meetingType').value;
|
||||
const title = document.getElementById('meetingTitle').value || 'Neues Meeting';
|
||||
const duration = document.getElementById('meetingDuration').value;
|
||||
const enableLobby = document.getElementById('enableLobby').checked;
|
||||
const enableRecording = document.getElementById('enableRecording').checked;
|
||||
const muteOnStart = document.getElementById('muteOnStart').checked;
|
||||
|
||||
const payload = {{
|
||||
type: type,
|
||||
title: title,
|
||||
duration: parseInt(duration),
|
||||
config: {{
|
||||
enable_lobby: enableLobby,
|
||||
enable_recording: enableRecording,
|
||||
start_with_audio_muted: muteOnStart
|
||||
}}
|
||||
}};
|
||||
|
||||
try {{
|
||||
const response = await fetch('/api/meetings/create', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify(payload)
|
||||
}});
|
||||
|
||||
if (response.ok) {{
|
||||
const data = await response.json();
|
||||
closeModal('newMeeting');
|
||||
if (type === 'quick') {{
|
||||
window.location.href = '/meetings/room/' + data.room_name;
|
||||
}} else {{
|
||||
alert('Meeting erfolgreich erstellt!\\nLink: ' + data.join_url);
|
||||
loadUpcomingMeetings();
|
||||
}}
|
||||
}} else {{
|
||||
alert('Fehler beim Erstellen des Meetings');
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error:', error);
|
||||
alert('Fehler beim Erstellen des Meetings');
|
||||
}}
|
||||
}}
|
||||
|
||||
function joinMeeting(roomId) {{
|
||||
window.location.href = '/meetings/room/' + roomId;
|
||||
}}
|
||||
|
||||
function copyMeetingLink(roomId) {{
|
||||
const link = window.location.origin + '/meetings/room/' + roomId;
|
||||
navigator.clipboard.writeText(link).then(() => {{
|
||||
alert('Link kopiert!');
|
||||
}});
|
||||
}}
|
||||
|
||||
// Load Data
|
||||
async function loadDashboardData() {{
|
||||
try {{
|
||||
const response = await fetch('/api/meetings/stats');
|
||||
if (response.ok) {{
|
||||
const data = await response.json();
|
||||
document.getElementById('activeMeetings').textContent = data.active || 0;
|
||||
document.getElementById('scheduledMeetings').textContent = data.scheduled || 0;
|
||||
document.getElementById('recordings').textContent = data.recordings || 0;
|
||||
document.getElementById('totalParticipants').textContent = data.participants || 0;
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error loading stats:', error);
|
||||
}}
|
||||
}}
|
||||
|
||||
async function loadUpcomingMeetings() {{
|
||||
// In production, load from API
|
||||
console.log('Loading upcoming meetings...');
|
||||
}}
|
||||
|
||||
// Initialize
|
||||
loadDashboardData();
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Dashboard", content, "dashboard")
|
||||
266
backend/frontend/meetings/pages/meeting_room.py
Normal file
266
backend/frontend/meetings/pages/meeting_room.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Meetings Module - Meeting Room Page
|
||||
Meeting room with embedded Jitsi
|
||||
"""
|
||||
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def meeting_room(room_name: str) -> str:
|
||||
"""Meeting room with embedded Jitsi"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Meeting: {room_name}</h1>
|
||||
<p class="page-subtitle">Verbunden mit BreakPilot Meet</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="toggleFullscreen()">
|
||||
Vollbild
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="leaveMeeting()">
|
||||
{ICONS['phone_off']} Verlassen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video Container -->
|
||||
<div class="video-container" id="jitsiContainer">
|
||||
<div class="video-placeholder">
|
||||
{ICONS['video']}
|
||||
<p>Meeting wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meeting Controls -->
|
||||
<div class="meeting-controls">
|
||||
<button class="control-btn active" id="micBtn" onclick="toggleMic()">
|
||||
{ICONS['mic']}
|
||||
</button>
|
||||
<button class="control-btn active" id="videoBtn" onclick="toggleVideo()">
|
||||
{ICONS['video']}
|
||||
</button>
|
||||
<button class="control-btn inactive" id="screenBtn" onclick="toggleScreenShare()">
|
||||
{ICONS['screen_share']}
|
||||
</button>
|
||||
<button class="control-btn inactive" id="chatBtn" onclick="toggleChat()">
|
||||
{ICONS['chat']}
|
||||
</button>
|
||||
<button class="control-btn inactive" id="recordBtn" onclick="toggleRecording()">
|
||||
{ICONS['record']}
|
||||
</button>
|
||||
<button class="control-btn danger" onclick="leaveMeeting()">
|
||||
{ICONS['phone_off']}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Participants and Chat -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
|
||||
<!-- Participants Panel -->
|
||||
<div class="participants-panel">
|
||||
<div style="font-weight: 600; margin-bottom: 1rem;">Teilnehmer (0)</div>
|
||||
<div id="participantsList">
|
||||
<div class="participant-item">
|
||||
<div class="participant-avatar">Sie</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">Sie (Moderator)</div>
|
||||
<div class="participant-role">Host</div>
|
||||
</div>
|
||||
<div class="participant-status">
|
||||
<span class="status-indicator mic-on" title="Mikrofon an"></span>
|
||||
<span class="status-indicator video-on" title="Kamera an"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Panel -->
|
||||
<div class="chat-panel">
|
||||
<div class="chat-header">Chat</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-message-sender">System</span>
|
||||
<span class="chat-message-time">Jetzt</span>
|
||||
</div>
|
||||
<div class="chat-message-content">Willkommen im Meeting!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-area">
|
||||
<input type="text" class="chat-input" id="chatInput" placeholder="Nachricht eingeben..." onkeypress="if(event.key==='Enter')sendMessage()">
|
||||
<button class="btn btn-primary" onclick="sendMessage()">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jitsi Integration Script -->
|
||||
<script src="https://meet.jit.si/external_api.js"></script>
|
||||
<script>
|
||||
let api = null;
|
||||
let isMuted = false;
|
||||
let isVideoOff = false;
|
||||
let isScreenSharing = false;
|
||||
let isRecording = false;
|
||||
|
||||
// Initialize Jitsi
|
||||
function initJitsi() {{
|
||||
const domain = 'meet.jit.si';
|
||||
const options = {{
|
||||
roomName: 'BreakPilot-{room_name}',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
parentNode: document.getElementById('jitsiContainer'),
|
||||
configOverwrite: {{
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false,
|
||||
enableWelcomePage: false,
|
||||
prejoinPageEnabled: false,
|
||||
}},
|
||||
interfaceConfigOverwrite: {{
|
||||
TOOLBAR_BUTTONS: [],
|
||||
SHOW_JITSI_WATERMARK: false,
|
||||
SHOW_BRAND_WATERMARK: false,
|
||||
SHOW_WATERMARK_FOR_GUESTS: false,
|
||||
DEFAULT_BACKGROUND: '#1a1a1a',
|
||||
DISABLE_VIDEO_BACKGROUND: true,
|
||||
}},
|
||||
userInfo: {{
|
||||
displayName: 'Lehrer'
|
||||
}}
|
||||
}};
|
||||
|
||||
api = new JitsiMeetExternalAPI(domain, options);
|
||||
|
||||
// Event Listeners
|
||||
api.addListener('participantJoined', (participant) => {{
|
||||
console.log('Participant joined:', participant);
|
||||
updateParticipantsList();
|
||||
}});
|
||||
|
||||
api.addListener('participantLeft', (participant) => {{
|
||||
console.log('Participant left:', participant);
|
||||
updateParticipantsList();
|
||||
}});
|
||||
|
||||
api.addListener('readyToClose', () => {{
|
||||
window.location.href = '/meetings';
|
||||
}});
|
||||
}}
|
||||
|
||||
// Control Functions
|
||||
function toggleMic() {{
|
||||
if (api) {{
|
||||
api.executeCommand('toggleAudio');
|
||||
isMuted = !isMuted;
|
||||
updateControlButton('micBtn', !isMuted);
|
||||
}}
|
||||
}}
|
||||
|
||||
function toggleVideo() {{
|
||||
if (api) {{
|
||||
api.executeCommand('toggleVideo');
|
||||
isVideoOff = !isVideoOff;
|
||||
updateControlButton('videoBtn', !isVideoOff);
|
||||
}}
|
||||
}}
|
||||
|
||||
function toggleScreenShare() {{
|
||||
if (api) {{
|
||||
api.executeCommand('toggleShareScreen');
|
||||
isScreenSharing = !isScreenSharing;
|
||||
updateControlButton('screenBtn', isScreenSharing);
|
||||
}}
|
||||
}}
|
||||
|
||||
function toggleChat() {{
|
||||
if (api) {{
|
||||
api.executeCommand('toggleChat');
|
||||
}}
|
||||
}}
|
||||
|
||||
function toggleRecording() {{
|
||||
if (api) {{
|
||||
if (!isRecording) {{
|
||||
api.executeCommand('startRecording', {{
|
||||
mode: 'file'
|
||||
}});
|
||||
}} else {{
|
||||
api.executeCommand('stopRecording', 'file');
|
||||
}}
|
||||
isRecording = !isRecording;
|
||||
updateControlButton('recordBtn', isRecording);
|
||||
}}
|
||||
}}
|
||||
|
||||
function updateControlButton(btnId, isActive) {{
|
||||
const btn = document.getElementById(btnId);
|
||||
if (isActive) {{
|
||||
btn.classList.remove('inactive');
|
||||
btn.classList.add('active');
|
||||
}} else {{
|
||||
btn.classList.remove('active');
|
||||
btn.classList.add('inactive');
|
||||
}}
|
||||
}}
|
||||
|
||||
function leaveMeeting() {{
|
||||
if (confirm('Meeting wirklich verlassen?')) {{
|
||||
if (api) {{
|
||||
api.executeCommand('hangup');
|
||||
}}
|
||||
window.location.href = '/meetings';
|
||||
}}
|
||||
}}
|
||||
|
||||
function toggleFullscreen() {{
|
||||
const container = document.getElementById('jitsiContainer');
|
||||
if (document.fullscreenElement) {{
|
||||
document.exitFullscreen();
|
||||
}} else {{
|
||||
container.requestFullscreen();
|
||||
}}
|
||||
}}
|
||||
|
||||
// Chat Functions
|
||||
function sendMessage() {{
|
||||
const input = document.getElementById('chatInput');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (message && api) {{
|
||||
api.executeCommand('sendChatMessage', message);
|
||||
addChatMessage('Sie', message);
|
||||
input.value = '';
|
||||
}}
|
||||
}}
|
||||
|
||||
function addChatMessage(sender, text) {{
|
||||
const container = document.getElementById('chatMessages');
|
||||
const now = new Date().toLocaleTimeString('de-DE', {{ hour: '2-digit', minute: '2-digit' }});
|
||||
|
||||
container.innerHTML += `
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-message-sender">${{sender}}</span>
|
||||
<span class="chat-message-time">${{now}}</span>
|
||||
</div>
|
||||
<div class="chat-message-content">${{text}}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}}
|
||||
|
||||
function updateParticipantsList() {{
|
||||
if (api) {{
|
||||
const participants = api.getParticipantsInfo();
|
||||
console.log('Participants:', participants);
|
||||
// Update UI with participants
|
||||
}}
|
||||
}}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', initJitsi);
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page(f"Meeting: {room_name}", content, "active")
|
||||
138
backend/frontend/meetings/pages/quick_actions.py
Normal file
138
backend/frontend/meetings/pages/quick_actions.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Meetings Module - Quick Actions Pages
|
||||
Quick meeting start and parent-teacher meeting creation
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def quick_meeting() -> str:
|
||||
"""Start a quick meeting immediately"""
|
||||
room_name = f"quick-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
content = f'''
|
||||
<div style="text-align: center; padding: 3rem;">
|
||||
<h1 class="page-title">Sofort-Meeting wird gestartet...</h1>
|
||||
<p class="page-subtitle">Sie werden in wenigen Sekunden weitergeleitet.</p>
|
||||
<div style="margin-top: 2rem;">
|
||||
<div class="card-icon primary" style="margin: 0 auto; width: 80px; height: 80px; font-size: 2rem;">
|
||||
{ICONS['video']}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
setTimeout(() => {{
|
||||
window.location.href = '/meetings/room/{room_name}';
|
||||
}}, 1000);
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Sofort-Meeting", content, "active")
|
||||
|
||||
|
||||
def parent_teacher_meeting() -> str:
|
||||
"""Create a parent-teacher meeting"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Elterngespräch planen</h1>
|
||||
<p class="page-subtitle">Sicheres Meeting mit Warteraum und Passwort</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Schüler/in</label>
|
||||
<input type="text" class="form-input" id="studentName" placeholder="Name des Schülers/der Schülerin">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Elternteil</label>
|
||||
<input type="text" class="form-input" id="parentName" placeholder="Name der Eltern">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">E-Mail (für Einladung)</label>
|
||||
<input type="email" class="form-input" id="parentEmail" placeholder="eltern@example.com">
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Datum</label>
|
||||
<input type="date" class="form-input" id="meetingDate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Uhrzeit</label>
|
||||
<input type="time" class="form-input" id="meetingTime">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Anlass (optional)</label>
|
||||
<textarea class="form-textarea" id="meetingReason" placeholder="Grund für das Gespräch..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="sendInvite" checked> Einladung per E-Mail senden
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" style="margin-top: 1.5rem;">
|
||||
<button class="btn btn-secondary" onclick="window.location.href='/meetings'">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="createParentTeacherMeeting()">Termin erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set minimum date to today
|
||||
document.getElementById('meetingDate').min = new Date().toISOString().split('T')[0];
|
||||
|
||||
async function createParentTeacherMeeting() {{
|
||||
const studentName = document.getElementById('studentName').value;
|
||||
const parentName = document.getElementById('parentName').value;
|
||||
const parentEmail = document.getElementById('parentEmail').value;
|
||||
const date = document.getElementById('meetingDate').value;
|
||||
const time = document.getElementById('meetingTime').value;
|
||||
const reason = document.getElementById('meetingReason').value;
|
||||
const sendInvite = document.getElementById('sendInvite').checked;
|
||||
|
||||
if (!studentName || !parentName || !date || !time) {{
|
||||
alert('Bitte füllen Sie alle Pflichtfelder aus.');
|
||||
return;
|
||||
}}
|
||||
|
||||
const payload = {{
|
||||
type: 'parent-teacher',
|
||||
student_name: studentName,
|
||||
parent_name: parentName,
|
||||
parent_email: parentEmail,
|
||||
scheduled_at: `${{date}}T${{time}}`,
|
||||
reason: reason,
|
||||
send_invite: sendInvite,
|
||||
duration: 30
|
||||
}};
|
||||
|
||||
try {{
|
||||
const response = await fetch('/api/meetings/parent-teacher', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify(payload)
|
||||
}});
|
||||
|
||||
if (response.ok) {{
|
||||
const data = await response.json();
|
||||
alert('Elterngespräch erfolgreich geplant!\\n\\nLink: ' + data.join_url + '\\nPasswort: ' + data.password);
|
||||
window.location.href = '/meetings/schedule';
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error:', error);
|
||||
alert('Fehler beim Erstellen des Termins');
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Elterngespräch", content, "schedule")
|
||||
284
backend/frontend/meetings/pages/recordings.py
Normal file
284
backend/frontend/meetings/pages/recordings.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
Meetings Module - Recordings Page
|
||||
Recordings and transcripts management
|
||||
"""
|
||||
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def recordings_page() -> str:
|
||||
"""Recordings and transcripts management"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Aufzeichnungen</h1>
|
||||
<p class="page-subtitle">Aufzeichnungen und Protokolle verwalten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="filterRecordings('all')">Alle</button>
|
||||
<button class="tab" onclick="filterRecordings('trainings')">Schulungen</button>
|
||||
<button class="tab" onclick="filterRecordings('meetings')">Meetings</button>
|
||||
</div>
|
||||
|
||||
<!-- Recordings List -->
|
||||
<div class="recording-list">
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div class="card-icon primary">{ICONS['record']}</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.25rem;">Docker Grundlagen Schulung</div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
10.12.2025, 10:00 - 11:30 | 1:30:00 | 156 MB
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="playRecording('docker-basics')">
|
||||
{ICONS['play']} Abspielen
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="viewTranscript('docker-basics')">
|
||||
{ICONS['file_text']} Protokoll
|
||||
</button>
|
||||
<button class="btn-icon" onclick="downloadRecording('docker-basics')">
|
||||
{ICONS['download']}
|
||||
</button>
|
||||
<button class="btn-icon" onclick="deleteRecording('docker-basics')">
|
||||
{ICONS['trash']}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div class="card-icon primary">{ICONS['record']}</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.25rem;">Team-Meeting KW 49</div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
06.12.2025, 14:00 - 15:00 | 1:00:00 | 98 MB
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="playRecording('team-kw49')">
|
||||
{ICONS['play']} Abspielen
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="viewTranscript('team-kw49')">
|
||||
{ICONS['file_text']} Protokoll
|
||||
</button>
|
||||
<button class="btn-icon" onclick="downloadRecording('team-kw49')">
|
||||
{ICONS['download']}
|
||||
</button>
|
||||
<button class="btn-icon" onclick="deleteRecording('team-kw49')">
|
||||
{ICONS['trash']}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div class="card-icon primary">{ICONS['record']}</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.25rem;">Elterngespräch - Max Müller</div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
02.12.2025, 16:00 - 16:30 | 0:28:00 | 42 MB
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="playRecording('parent-mueller')">
|
||||
{ICONS['play']} Abspielen
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="viewTranscript('parent-mueller')">
|
||||
{ICONS['file_text']} Protokoll
|
||||
</button>
|
||||
<button class="btn-icon" onclick="downloadRecording('parent-mueller')">
|
||||
{ICONS['download']}
|
||||
</button>
|
||||
<button class="btn-icon" onclick="deleteRecording('parent-mueller')">
|
||||
{ICONS['trash']}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Info -->
|
||||
<div class="card" style="margin-top: 2rem;">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Speicherplatz</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span>296 MB von 10 GB verwendet</span>
|
||||
<span>3%</span>
|
||||
</div>
|
||||
<div style="background: var(--bp-bg); border-radius: 4px; height: 8px; overflow: hidden;">
|
||||
<div style="background: var(--bp-primary); width: 3%; height: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
3 Aufzeichnungen | Älteste Aufzeichnung: 02.12.2025
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterRecordings(filter) {{
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
console.log('Filter:', filter);
|
||||
}}
|
||||
|
||||
function playRecording(recordingId) {{
|
||||
window.location.href = '/meetings/recordings/' + recordingId + '/play';
|
||||
}}
|
||||
|
||||
function viewTranscript(recordingId) {{
|
||||
window.location.href = '/meetings/recordings/' + recordingId + '/transcript';
|
||||
}}
|
||||
|
||||
function downloadRecording(recordingId) {{
|
||||
window.location.href = '/api/recordings/' + recordingId + '/download';
|
||||
}}
|
||||
|
||||
function deleteRecording(recordingId) {{
|
||||
if (confirm('Aufzeichnung wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {{
|
||||
fetch('/api/recordings/' + recordingId, {{ method: 'DELETE' }})
|
||||
.then(() => location.reload());
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Aufzeichnungen", content, "recordings")
|
||||
|
||||
|
||||
def play_recording(recording_id: str) -> str:
|
||||
"""Play a recording"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Aufzeichnung abspielen</h1>
|
||||
<p class="page-subtitle">{recording_id}</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="downloadRecording()">
|
||||
{ICONS['download']} Herunterladen
|
||||
</button>
|
||||
<a href="/meetings/recordings" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-container">
|
||||
<div class="video-placeholder">
|
||||
{ICONS['play']}
|
||||
<p>Aufzeichnung wird geladen...</p>
|
||||
<p style="font-size: 0.875rem;">Recording ID: {recording_id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recording Info -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Details</span>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||
<div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Datum</div>
|
||||
<div style="font-weight: 600;">10.12.2025, 10:00</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Dauer</div>
|
||||
<div style="font-weight: 600;">1:30:00</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Größe</div>
|
||||
<div style="font-weight: 600;">156 MB</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Teilnehmer</div>
|
||||
<div style="font-weight: 600;">15</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function downloadRecording() {{
|
||||
window.location.href = '/api/recordings/{recording_id}/download';
|
||||
}}
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Aufzeichnung", content, "recordings")
|
||||
|
||||
|
||||
def view_transcript(recording_id: str) -> str:
|
||||
"""View recording transcript"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Protokoll</h1>
|
||||
<p class="page-subtitle">{recording_id}</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="downloadTranscript()">
|
||||
{ICONS['download']} Als PDF exportieren
|
||||
</button>
|
||||
<a href="/meetings/recordings" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Transkript</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span style="font-weight: 600;">Max Trainer</span>
|
||||
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:00:15</span>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem;">Willkommen zur Docker Grundlagen Schulung. Heute werden wir die Basics von Containern und Images besprechen.</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span style="font-weight: 600;">Max Trainer</span>
|
||||
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:02:30</span>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem;">Docker ist eine Open-Source-Plattform, die es ermöglicht, Anwendungen in Containern zu entwickeln, zu versenden und auszuführen.</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span style="font-weight: 600;">Teilnehmer 1</span>
|
||||
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:05:45</span>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem;">Was ist der Unterschied zwischen einem Container und einer virtuellen Maschine?</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span style="font-weight: 600;">Max Trainer</span>
|
||||
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:06:00</span>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem;">Gute Frage! Container teilen sich den Kernel des Host-Systems, während VMs einen vollständigen Hypervisor und ein eigenes Betriebssystem benötigen...</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding: 2rem; color: var(--bp-text-muted);">
|
||||
<p>... Transkript wird fortgesetzt ...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function downloadTranscript() {{
|
||||
alert('PDF-Export wird vorbereitet...');
|
||||
// In production: window.location.href = '/api/recordings/{recording_id}/transcript/pdf';
|
||||
}}
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Protokoll", content, "recordings")
|
||||
206
backend/frontend/meetings/pages/schedule.py
Normal file
206
backend/frontend/meetings/pages/schedule.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Meetings Module - Schedule Page
|
||||
Schedule and manage upcoming meetings
|
||||
"""
|
||||
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def schedule_meetings() -> str:
|
||||
"""Schedule and manage upcoming meetings"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Termine</h1>
|
||||
<p class="page-subtitle">Geplante Meetings und Termine verwalten</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openScheduleModal()">
|
||||
{ICONS['plus']} Meeting planen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="filterMeetings('all')">Alle</button>
|
||||
<button class="tab" onclick="filterMeetings('today')">Heute</button>
|
||||
<button class="tab" onclick="filterMeetings('week')">Diese Woche</button>
|
||||
<button class="tab" onclick="filterMeetings('month')">Dieser Monat</button>
|
||||
</div>
|
||||
|
||||
<div class="meeting-list" id="scheduledMeetings">
|
||||
<div class="meeting-item">
|
||||
<div class="meeting-time">
|
||||
<div class="meeting-time-value">14:00</div>
|
||||
<div class="meeting-time-date">Mo, 16.12.</div>
|
||||
</div>
|
||||
<div class="meeting-info">
|
||||
<div class="meeting-title">Team-Besprechung</div>
|
||||
<div class="meeting-meta">
|
||||
<span>{ICONS['clock']} 60 Min</span>
|
||||
<span>{ICONS['users']} 5 Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="meeting-badge badge-scheduled">Geplant</span>
|
||||
<div class="meeting-actions">
|
||||
<button class="btn btn-primary" onclick="joinMeeting('team-abc')">Beitreten</button>
|
||||
<button class="btn-icon" onclick="editMeeting('team-abc')">{ICONS['settings']}</button>
|
||||
<button class="btn-icon" onclick="copyLink('team-abc')">{ICONS['link']}</button>
|
||||
<button class="btn-icon" onclick="deleteMeeting('team-abc')">{ICONS['trash']}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meeting-item">
|
||||
<div class="meeting-time">
|
||||
<div class="meeting-time-value">09:30</div>
|
||||
<div class="meeting-time-date">Di, 17.12.</div>
|
||||
</div>
|
||||
<div class="meeting-info">
|
||||
<div class="meeting-title">Elterngespräch - Anna Schmidt</div>
|
||||
<div class="meeting-meta">
|
||||
<span>{ICONS['clock']} 30 Min</span>
|
||||
<span>{ICONS['users']} 2 Teilnehmer</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="meeting-badge badge-scheduled">Geplant</span>
|
||||
<div class="meeting-actions">
|
||||
<button class="btn btn-primary" onclick="joinMeeting('parent-xyz')">Beitreten</button>
|
||||
<button class="btn-icon" onclick="editMeeting('parent-xyz')">{ICONS['settings']}</button>
|
||||
<button class="btn-icon" onclick="copyLink('parent-xyz')">{ICONS['link']}</button>
|
||||
<button class="btn-icon" onclick="deleteMeeting('parent-xyz')">{ICONS['trash']}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Modal -->
|
||||
<div class="modal-overlay" id="scheduleModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Meeting planen</h2>
|
||||
<button class="modal-close" onclick="closeScheduleModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Titel</label>
|
||||
<input type="text" class="form-input" id="scheduleTitle" placeholder="Meeting-Titel">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Datum</label>
|
||||
<input type="date" class="form-input" id="scheduleDate">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Uhrzeit</label>
|
||||
<input type="time" class="form-input" id="scheduleTime">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Dauer</label>
|
||||
<select class="form-select" id="scheduleDuration">
|
||||
<option value="30">30 Minuten</option>
|
||||
<option value="60" selected>60 Minuten</option>
|
||||
<option value="90">90 Minuten</option>
|
||||
<option value="120">120 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Beschreibung (optional)</label>
|
||||
<textarea class="form-textarea" id="scheduleDescription" placeholder="Agenda oder Beschreibung..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Teilnehmer einladen</label>
|
||||
<input type="email" class="form-input" id="scheduleInvites" placeholder="E-Mail-Adressen (kommagetrennt)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeScheduleModal()">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="scheduleMeeting()">Meeting planen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openScheduleModal() {{
|
||||
document.getElementById('scheduleModal').classList.add('active');
|
||||
// Set default date to today
|
||||
document.getElementById('scheduleDate').valueAsDate = new Date();
|
||||
}}
|
||||
|
||||
function closeScheduleModal() {{
|
||||
document.getElementById('scheduleModal').classList.remove('active');
|
||||
}}
|
||||
|
||||
async function scheduleMeeting() {{
|
||||
const title = document.getElementById('scheduleTitle').value;
|
||||
const date = document.getElementById('scheduleDate').value;
|
||||
const time = document.getElementById('scheduleTime').value;
|
||||
const duration = document.getElementById('scheduleDuration').value;
|
||||
const description = document.getElementById('scheduleDescription').value;
|
||||
const invites = document.getElementById('scheduleInvites').value;
|
||||
|
||||
if (!title || !date || !time) {{
|
||||
alert('Bitte füllen Sie alle Pflichtfelder aus.');
|
||||
return;
|
||||
}}
|
||||
|
||||
const payload = {{
|
||||
title,
|
||||
scheduled_at: `${{date}}T${{time}}`,
|
||||
duration: parseInt(duration),
|
||||
description,
|
||||
invites: invites.split(',').map(e => e.trim()).filter(e => e)
|
||||
}};
|
||||
|
||||
try {{
|
||||
const response = await fetch('/api/meetings/schedule', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify(payload)
|
||||
}});
|
||||
|
||||
if (response.ok) {{
|
||||
alert('Meeting erfolgreich geplant!');
|
||||
closeScheduleModal();
|
||||
location.reload();
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error:', error);
|
||||
}}
|
||||
}}
|
||||
|
||||
function filterMeetings(filter) {{
|
||||
// Update active tab
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Filter logic would go here
|
||||
console.log('Filter:', filter);
|
||||
}}
|
||||
|
||||
function joinMeeting(roomId) {{
|
||||
window.location.href = '/meetings/room/' + roomId;
|
||||
}}
|
||||
|
||||
function editMeeting(roomId) {{
|
||||
// Open edit modal
|
||||
console.log('Edit:', roomId);
|
||||
}}
|
||||
|
||||
function copyLink(roomId) {{
|
||||
const link = window.location.origin + '/meetings/room/' + roomId;
|
||||
navigator.clipboard.writeText(link);
|
||||
alert('Link kopiert!');
|
||||
}}
|
||||
|
||||
function deleteMeeting(roomId) {{
|
||||
if (confirm('Meeting wirklich löschen?')) {{
|
||||
// Delete logic
|
||||
console.log('Delete:', roomId);
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Termine", content, "schedule")
|
||||
267
backend/frontend/meetings/pages/trainings.py
Normal file
267
backend/frontend/meetings/pages/trainings.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
Meetings Module - Trainings Page
|
||||
Training sessions management
|
||||
"""
|
||||
|
||||
from ..templates import ICONS, render_base_page
|
||||
|
||||
|
||||
def trainings_page() -> str:
|
||||
"""Training sessions management"""
|
||||
content = f'''
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Schulungen</h1>
|
||||
<p class="page-subtitle">Schulungen und Workshops verwalten</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openTrainingModal()">
|
||||
{ICONS['plus']} Neue Schulung
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Training Cards -->
|
||||
<div class="dashboard-grid">
|
||||
<div class="training-card">
|
||||
<div class="training-card-header">
|
||||
<div class="training-card-title">Go Grundlagen Workshop</div>
|
||||
<div class="training-card-subtitle">Einführung in die Go-Programmierung</div>
|
||||
</div>
|
||||
<div class="training-card-body">
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
|
||||
{ICONS['calendar']} 18.12.2025, 14:00
|
||||
</span>
|
||||
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
|
||||
{ICONS['clock']} 120 Min
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
|
||||
<span class="meeting-badge badge-scheduled">Geplant</span>
|
||||
<span class="meeting-badge" style="background: var(--bp-accent-soft); color: var(--bp-accent);">Aufzeichnung</span>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
12 von 20 Teilnehmern angemeldet
|
||||
</p>
|
||||
</div>
|
||||
<div class="training-card-footer">
|
||||
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">Trainer: Max Mustermann</span>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="editTraining('go-basics')">{ICONS['settings']}</button>
|
||||
<button class="btn btn-primary" onclick="startTraining('go-basics')">Starten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="training-card">
|
||||
<div class="training-card-header">
|
||||
<div class="training-card-title">PWA Entwicklung</div>
|
||||
<div class="training-card-subtitle">Progressive Web Apps mit JavaScript</div>
|
||||
</div>
|
||||
<div class="training-card-body">
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
|
||||
{ICONS['calendar']} 20.12.2025, 10:00
|
||||
</span>
|
||||
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
|
||||
{ICONS['clock']} 180 Min
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
|
||||
<span class="meeting-badge badge-scheduled">Geplant</span>
|
||||
<span class="meeting-badge" style="background: var(--bp-accent-soft); color: var(--bp-accent);">Aufzeichnung</span>
|
||||
<span class="meeting-badge" style="background: var(--bp-info); color: white;">Breakout</span>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
||||
8 von 15 Teilnehmern angemeldet
|
||||
</p>
|
||||
</div>
|
||||
<div class="training-card-footer">
|
||||
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">Trainer: Lisa Schmidt</span>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="editTraining('pwa-dev')">{ICONS['settings']}</button>
|
||||
<button class="btn btn-primary" onclick="startTraining('pwa-dev')">Starten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Past Trainings -->
|
||||
<div class="card" style="margin-top: 2rem;">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Vergangene Schulungen</span>
|
||||
</div>
|
||||
<div class="meeting-list">
|
||||
<div class="meeting-item">
|
||||
<div class="meeting-time">
|
||||
<div class="meeting-time-value">10:00</div>
|
||||
<div class="meeting-time-date">10.12.</div>
|
||||
</div>
|
||||
<div class="meeting-info">
|
||||
<div class="meeting-title">Docker Grundlagen</div>
|
||||
<div class="meeting-meta">
|
||||
<span>{ICONS['clock']} 90 Min</span>
|
||||
<span>{ICONS['users']} 15 Teilnehmer</span>
|
||||
<span>{ICONS['record']} Aufzeichnung verfügbar</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="meeting-badge badge-ended">Beendet</span>
|
||||
<div class="meeting-actions">
|
||||
<button class="btn btn-secondary" onclick="viewRecording('docker-basics')">{ICONS['play']} Ansehen</button>
|
||||
<button class="btn-icon" onclick="downloadRecording('docker-basics')">{ICONS['download']}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training Modal -->
|
||||
<div class="modal-overlay" id="trainingModal">
|
||||
<div class="modal" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Neue Schulung erstellen</h2>
|
||||
<button class="modal-close" onclick="closeTrainingModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Titel</label>
|
||||
<input type="text" class="form-input" id="trainingTitle" placeholder="Schulungstitel">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Beschreibung</label>
|
||||
<textarea class="form-textarea" id="trainingDescription" placeholder="Beschreibung der Schulung..."></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Datum</label>
|
||||
<input type="date" class="form-input" id="trainingDate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Uhrzeit</label>
|
||||
<input type="time" class="form-input" id="trainingTime">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Dauer (Minuten)</label>
|
||||
<select class="form-select" id="trainingDuration">
|
||||
<option value="60">60 Minuten</option>
|
||||
<option value="90">90 Minuten</option>
|
||||
<option value="120" selected>120 Minuten</option>
|
||||
<option value="180">180 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Max. Teilnehmer</label>
|
||||
<input type="number" class="form-input" id="trainingMaxParticipants" value="20">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Trainer</label>
|
||||
<input type="text" class="form-input" id="trainingTrainer" placeholder="Name des Trainers">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="trainingRecording" checked> Aufzeichnung aktivieren
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="trainingBreakout"> Breakout-Rooms aktivieren
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="trainingLobby" checked> Warteraum aktivieren (Trainer lässt Teilnehmer ein)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeTrainingModal()">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="createTraining()">Schulung erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openTrainingModal() {{
|
||||
document.getElementById('trainingModal').classList.add('active');
|
||||
}}
|
||||
|
||||
function closeTrainingModal() {{
|
||||
document.getElementById('trainingModal').classList.remove('active');
|
||||
}}
|
||||
|
||||
async function createTraining() {{
|
||||
const title = document.getElementById('trainingTitle').value;
|
||||
const description = document.getElementById('trainingDescription').value;
|
||||
const date = document.getElementById('trainingDate').value;
|
||||
const time = document.getElementById('trainingTime').value;
|
||||
const duration = document.getElementById('trainingDuration').value;
|
||||
const maxParticipants = document.getElementById('trainingMaxParticipants').value;
|
||||
const trainer = document.getElementById('trainingTrainer').value;
|
||||
const enableRecording = document.getElementById('trainingRecording').checked;
|
||||
const enableBreakout = document.getElementById('trainingBreakout').checked;
|
||||
const enableLobby = document.getElementById('trainingLobby').checked;
|
||||
|
||||
if (!title || !date || !time || !trainer) {{
|
||||
alert('Bitte füllen Sie alle Pflichtfelder aus.');
|
||||
return;
|
||||
}}
|
||||
|
||||
const payload = {{
|
||||
title,
|
||||
description,
|
||||
scheduled_at: `${{date}}T${{time}}`,
|
||||
duration: parseInt(duration),
|
||||
max_participants: parseInt(maxParticipants),
|
||||
trainer,
|
||||
config: {{
|
||||
enable_recording: enableRecording,
|
||||
enable_breakout: enableBreakout,
|
||||
enable_lobby: enableLobby,
|
||||
start_with_audio_muted: true
|
||||
}}
|
||||
}};
|
||||
|
||||
try {{
|
||||
const response = await fetch('/api/meetings/training', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify(payload)
|
||||
}});
|
||||
|
||||
if (response.ok) {{
|
||||
alert('Schulung erfolgreich erstellt!');
|
||||
closeTrainingModal();
|
||||
location.reload();
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error:', error);
|
||||
}}
|
||||
}}
|
||||
|
||||
function startTraining(trainingId) {{
|
||||
window.location.href = '/meetings/room/training-' + trainingId;
|
||||
}}
|
||||
|
||||
function editTraining(trainingId) {{
|
||||
console.log('Edit training:', trainingId);
|
||||
}}
|
||||
|
||||
function viewRecording(trainingId) {{
|
||||
window.location.href = '/meetings/recordings/' + trainingId;
|
||||
}}
|
||||
|
||||
function downloadRecording(trainingId) {{
|
||||
console.log('Download recording:', trainingId);
|
||||
}}
|
||||
</script>
|
||||
'''
|
||||
|
||||
return render_base_page("Schulungen", content, "trainings")
|
||||
Reference in New Issue
Block a user