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:
105
backend/frontend/meetings/__init__.py
Normal file
105
backend/frontend/meetings/__init__.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Meetings Module
|
||||
|
||||
Modular structure for the Meetings frontend.
|
||||
Jitsi Meet Integration for video conferences, trainings, and parent-teacher meetings.
|
||||
|
||||
Modular Refactoring (2026-02-03):
|
||||
- Split into sub-modules for maintainability
|
||||
- Original file: meetings.py (2,639 lines)
|
||||
- Now split into: styles.py, templates.py, pages/
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from .pages import (
|
||||
meetings_dashboard,
|
||||
meeting_room,
|
||||
active_meetings,
|
||||
schedule_meetings,
|
||||
trainings_page,
|
||||
recordings_page,
|
||||
play_recording,
|
||||
view_transcript,
|
||||
breakout_rooms_page,
|
||||
quick_meeting,
|
||||
parent_teacher_meeting,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================
|
||||
# API Routes
|
||||
# ============================================
|
||||
|
||||
@router.get("/meetings", response_class=HTMLResponse)
|
||||
def get_meetings_dashboard():
|
||||
"""Main meetings dashboard"""
|
||||
return meetings_dashboard()
|
||||
|
||||
|
||||
@router.get("/meetings/room/{room_name}", response_class=HTMLResponse)
|
||||
def get_meeting_room(room_name: str):
|
||||
"""Meeting room with embedded Jitsi"""
|
||||
return meeting_room(room_name)
|
||||
|
||||
|
||||
@router.get("/meetings/active", response_class=HTMLResponse)
|
||||
def get_active_meetings():
|
||||
"""Active meetings list"""
|
||||
return active_meetings()
|
||||
|
||||
|
||||
@router.get("/meetings/schedule", response_class=HTMLResponse)
|
||||
def get_schedule_meetings():
|
||||
"""Schedule and manage upcoming meetings"""
|
||||
return schedule_meetings()
|
||||
|
||||
|
||||
@router.get("/meetings/trainings", response_class=HTMLResponse)
|
||||
def get_trainings_page():
|
||||
"""Training sessions management"""
|
||||
return trainings_page()
|
||||
|
||||
|
||||
@router.get("/meetings/recordings", response_class=HTMLResponse)
|
||||
def get_recordings_page():
|
||||
"""Recordings and transcripts management"""
|
||||
return recordings_page()
|
||||
|
||||
|
||||
@router.get("/meetings/breakout", response_class=HTMLResponse)
|
||||
def get_breakout_rooms_page():
|
||||
"""Breakout rooms management"""
|
||||
return breakout_rooms_page()
|
||||
|
||||
|
||||
@router.get("/meetings/quick", response_class=HTMLResponse)
|
||||
def get_quick_meeting():
|
||||
"""Start a quick meeting immediately"""
|
||||
return quick_meeting()
|
||||
|
||||
|
||||
@router.get("/meetings/parent-teacher", response_class=HTMLResponse)
|
||||
def get_parent_teacher_meeting():
|
||||
"""Create a parent-teacher meeting"""
|
||||
return parent_teacher_meeting()
|
||||
|
||||
|
||||
@router.get("/meetings/recordings/{recording_id}/play", response_class=HTMLResponse)
|
||||
def get_play_recording(recording_id: str):
|
||||
"""Play a recording"""
|
||||
return play_recording(recording_id)
|
||||
|
||||
|
||||
@router.get("/meetings/recordings/{recording_id}/transcript", response_class=HTMLResponse)
|
||||
def get_view_transcript(recording_id: str):
|
||||
"""View recording transcript"""
|
||||
return view_transcript(recording_id)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
]
|
||||
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")
|
||||
918
backend/frontend/meetings/styles.py
Normal file
918
backend/frontend/meetings/styles.py
Normal file
@@ -0,0 +1,918 @@
|
||||
"""
|
||||
Meetings Module - CSS Styles
|
||||
BreakPilot Design System for the Meetings frontend
|
||||
"""
|
||||
|
||||
BREAKPILOT_STYLES = """
|
||||
:root {
|
||||
--bp-primary: #6C1B1B;
|
||||
--bp-primary-soft: rgba(108, 27, 27, 0.1);
|
||||
--bp-bg: #F8F8F8;
|
||||
--bp-surface: #FFFFFF;
|
||||
--bp-surface-elevated: #FFFFFF;
|
||||
--bp-border: #E0E0E0;
|
||||
--bp-border-subtle: rgba(108, 27, 27, 0.15);
|
||||
--bp-accent: #5ABF60;
|
||||
--bp-accent-soft: rgba(90, 191, 96, 0.15);
|
||||
--bp-text: #4A4A4A;
|
||||
--bp-text-muted: #6B6B6B;
|
||||
--bp-danger: #ef4444;
|
||||
--bp-warning: #F1C40F;
|
||||
--bp-info: #3b82f6;
|
||||
--bp-gold: #F1C40F;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Manrope', system-ui, -apple-system, sans-serif;
|
||||
background: var(--bp-bg);
|
||||
color: var(--bp-text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.app-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: var(--bp-surface);
|
||||
border-right: 1px solid var(--bp-border);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--bp-primary);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: var(--bp-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* Dashboard Grid */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-icon.primary { background: var(--bp-primary-soft); color: var(--bp-primary); }
|
||||
.card-icon.accent { background: var(--bp-accent-soft); color: var(--bp-accent); }
|
||||
.card-icon.warning { background: rgba(241, 196, 15, 0.15); color: var(--bp-warning); }
|
||||
.card-icon.info { background: rgba(59, 130, 246, 0.15); color: var(--bp-info); }
|
||||
.card-icon.danger { background: rgba(239, 68, 68, 0.15); color: var(--bp-danger); }
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--bp-text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a1717;
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background: var(--bp-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background: #4aa850;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bp-bg);
|
||||
color: var(--bp-text);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bp-border);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--bp-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--bp-border);
|
||||
cursor: pointer;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Meeting List */
|
||||
.meeting-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.meeting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.meeting-item:hover {
|
||||
border-color: var(--bp-primary);
|
||||
box-shadow: 0 2px 8px rgba(108, 27, 27, 0.1);
|
||||
}
|
||||
|
||||
.meeting-time {
|
||||
text-align: center;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.meeting-time-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.meeting-time-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.meeting-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.meeting-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.meeting-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.meeting-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.meeting-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.meeting-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-live {
|
||||
background: var(--bp-danger);
|
||||
color: white;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.badge-scheduled {
|
||||
background: var(--bp-info);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-ended {
|
||||
background: var(--bp-border);
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* Video Container */
|
||||
.video-container {
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.video-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.video-placeholder svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Meeting Controls */
|
||||
.meeting-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.control-btn.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.control-btn.inactive {
|
||||
background: var(--bp-bg);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.control-btn.danger {
|
||||
background: var(--bp-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Participants Panel */
|
||||
.participants-panel {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
padding: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.participant-item:hover {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--bp-primary-soft);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.participant-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.participant-role {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.participant-status {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-indicator.mic-on { background: var(--bp-accent); }
|
||||
.status-indicator.mic-off { background: var(--bp-danger); }
|
||||
.status-indicator.video-on { background: var(--bp-accent); }
|
||||
.status-indicator.video-off { background: var(--bp-danger); }
|
||||
|
||||
/* Chat Panel */
|
||||
.chat-panel {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chat-message-header {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.chat-message-sender {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-message-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.chat-message-content {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-input:focus {
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
/* Breakout Rooms */
|
||||
.breakout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.breakout-room {
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.breakout-room-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breakout-room-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.breakout-room-count {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bp-text-muted);
|
||||
background: var(--bp-bg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.breakout-participants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.breakout-participant {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bp-bg);
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Recording Panel */
|
||||
.recording-panel {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.recording-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.recording-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--bp-danger);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.recording-time {
|
||||
font-family: monospace;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.recording-list {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.recording-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.recording-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.recording-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.recording-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.recording-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bp-surface);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--bp-text-muted);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 8px 8px 0 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--bp-text-muted);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--bp-text);
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--bp-primary);
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
/* Training Card */
|
||||
.training-card {
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.training-card-header {
|
||||
padding: 1rem 1.5rem;
|
||||
background: linear-gradient(135deg, var(--bp-primary), #8B2E2E);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.training-card-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.training-card-subtitle {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.training-card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.training-card-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Quick Action Buttons */
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.quick-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.quick-action:hover {
|
||||
border-color: var(--bp-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(108, 27, 27, 0.1);
|
||||
}
|
||||
|
||||
.quick-action-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-action-icon svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.quick-action-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.meeting-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
"""
|
||||
116
backend/frontend/meetings/templates.py
Normal file
116
backend/frontend/meetings/templates.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Meetings Module - Templates and Icons
|
||||
Base templates and SVG icons for the Meetings frontend
|
||||
"""
|
||||
|
||||
from .styles import BREAKPILOT_STYLES
|
||||
|
||||
# ============================================
|
||||
# SVG Icons
|
||||
# ============================================
|
||||
|
||||
ICONS = {
|
||||
"video": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>',
|
||||
"video_off": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"/><line x1="1" y1="1" x2="23" y2="23"/></svg>',
|
||||
"mic": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',
|
||||
"mic_off": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="1" y1="1" x2="23" y2="23"/><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',
|
||||
"screen_share": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3"/><polyline points="8 21 12 17 16 21"/><line x1="12" y1="12" x2="12" y2="17"/><path d="M17 8V3h5"/><path d="M22 3l-7 7"/></svg>',
|
||||
"chat": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
||||
"users": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
|
||||
"calendar": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
||||
"record": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>',
|
||||
"phone_off": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"/><line x1="23" y1="1" x2="1" y2="23"/></svg>',
|
||||
"settings": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
||||
"grid": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',
|
||||
"plus": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
|
||||
"download": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
|
||||
"play": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>',
|
||||
"trash": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
|
||||
"link": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
|
||||
"copy": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
|
||||
"clock": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
|
||||
"home": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
|
||||
"graduation": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c0 2 2 3 6 3s6-1 6-3v-5"/></svg>',
|
||||
"external": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
|
||||
"file_text": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
|
||||
}
|
||||
|
||||
|
||||
# ============================================
|
||||
# Sidebar Component
|
||||
# ============================================
|
||||
|
||||
def render_sidebar(active_page: str = "dashboard") -> str:
|
||||
"""Render the meetings sidebar navigation"""
|
||||
nav_items = [
|
||||
{"id": "dashboard", "label": "Dashboard", "icon": "home", "href": "/meetings"},
|
||||
{"id": "active", "label": "Aktive Meetings", "icon": "video", "href": "/meetings/active"},
|
||||
{"id": "schedule", "label": "Termine", "icon": "calendar", "href": "/meetings/schedule"},
|
||||
{"id": "trainings", "label": "Schulungen", "icon": "graduation", "href": "/meetings/trainings"},
|
||||
{"id": "recordings", "label": "Aufzeichnungen", "icon": "record", "href": "/meetings/recordings"},
|
||||
{"id": "breakout", "label": "Breakout-Rooms", "icon": "grid", "href": "/meetings/breakout"},
|
||||
]
|
||||
|
||||
nav_html = ""
|
||||
for item in nav_items:
|
||||
active_class = "active" if item["id"] == active_page else ""
|
||||
nav_html += f'''
|
||||
<a href="{item['href']}" class="nav-item {active_class}">
|
||||
{ICONS[item['icon']]}
|
||||
<span>{item['label']}</span>
|
||||
</a>
|
||||
'''
|
||||
|
||||
return f'''
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">BP</div>
|
||||
<span class="logo-text">BreakPilot Meet</span>
|
||||
</div>
|
||||
|
||||
<nav class="nav-section">
|
||||
<div class="nav-section-title">Navigation</div>
|
||||
{nav_html}
|
||||
</nav>
|
||||
|
||||
<nav class="nav-section" style="margin-top: auto;">
|
||||
<div class="nav-section-title">Links</div>
|
||||
<a href="/studio" class="nav-item">
|
||||
{ICONS['external']}
|
||||
<span>Zurück zum Studio</span>
|
||||
</a>
|
||||
<a href="/school" class="nav-item">
|
||||
{ICONS['users']}
|
||||
<span>Schulverwaltung</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
'''
|
||||
|
||||
|
||||
# ============================================
|
||||
# Page Templates
|
||||
# ============================================
|
||||
|
||||
def render_base_page(title: str, content: str, active_page: str = "dashboard") -> str:
|
||||
"""Render the base page template"""
|
||||
return f'''
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>BreakPilot Meet – {title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>{BREAKPILOT_STYLES}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
{render_sidebar(active_page)}
|
||||
<main class="main-content">
|
||||
{content}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
Reference in New Issue
Block a user