website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
178 lines
7.6 KiB
TypeScript
178 lines
7.6 KiB
TypeScript
import { ActiveMeeting, RecentRoom, CommunicationStats } from './types'
|
|
import { formatDuration, formatTimeAgo, getRoomTypeBadge } from './helpers'
|
|
|
|
export function ActiveMeetingsSection({
|
|
activeMeetings,
|
|
loading,
|
|
onRefresh,
|
|
}: {
|
|
activeMeetings: ActiveMeeting[]
|
|
loading: boolean
|
|
onRefresh: () => void
|
|
}) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="font-semibold text-slate-900">Aktive Meetings</h3>
|
|
<button
|
|
onClick={onRefresh}
|
|
disabled={loading}
|
|
className="px-3 py-1.5 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-50"
|
|
>
|
|
{loading ? 'Lade...' : 'Aktualisieren'}
|
|
</button>
|
|
</div>
|
|
|
|
{activeMeetings.length === 0 ? (
|
|
<div className="text-center py-8 text-slate-500">
|
|
<svg className="w-12 h-12 mx-auto mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
<p>Keine aktiven Meetings</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="text-left text-xs text-slate-500 uppercase border-b border-slate-200">
|
|
<th className="pb-3 pr-4">Meeting</th>
|
|
<th className="pb-3 pr-4">Teilnehmer</th>
|
|
<th className="pb-3 pr-4">Gestartet</th>
|
|
<th className="pb-3">Dauer</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{activeMeetings.map((meeting, idx) => (
|
|
<tr key={idx} className="text-sm">
|
|
<td className="py-3 pr-4">
|
|
<div className="font-medium text-slate-900">{meeting.display_name}</div>
|
|
<div className="text-xs text-slate-500">{meeting.room_name}</div>
|
|
</td>
|
|
<td className="py-3 pr-4">
|
|
<span className="inline-flex items-center gap-1">
|
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
{meeting.participants}
|
|
</span>
|
|
</td>
|
|
<td className="py-3 pr-4 text-slate-500">{formatTimeAgo(meeting.started_at)}</td>
|
|
<td className="py-3 font-medium">{formatDuration(meeting.duration_minutes)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ChatRoomsAndUsage({
|
|
recentRooms,
|
|
stats,
|
|
}: {
|
|
recentRooms: RecentRoom[]
|
|
stats: CommunicationStats | null
|
|
}) {
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Aktive Chat-Räume</h3>
|
|
|
|
{recentRooms.length === 0 ? (
|
|
<div className="text-center py-6 text-slate-500">
|
|
<p>Keine aktiven Räume</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{recentRooms.slice(0, 5).map((room, idx) => (
|
|
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-slate-200 rounded-lg flex items-center justify-center">
|
|
<svg className="w-4 h-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-slate-900 text-sm">{room.name}</div>
|
|
<div className="text-xs text-slate-500">{room.member_count} Mitglieder</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className={getRoomTypeBadge(room.room_type)}>{room.room_type}</span>
|
|
<span className="text-xs text-slate-400">{formatTimeAgo(room.last_activity)}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Usage Statistics */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Nutzungsstatistiken</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-slate-600">Call-Minuten heute</span>
|
|
<span className="font-semibold">{stats?.jitsi.total_minutes_today || 0} Min.</span>
|
|
</div>
|
|
<div className="w-full bg-slate-100 rounded-full h-2">
|
|
<div
|
|
className="bg-blue-600 h-2 rounded-full transition-all"
|
|
style={{ width: `${Math.min((stats?.jitsi.total_minutes_today || 0) / 500 * 100, 100)}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-slate-600">Aktive Chat-Räume</span>
|
|
<span className="font-semibold">{stats?.matrix.active_rooms || 0} / {stats?.matrix.total_rooms || 0}</span>
|
|
</div>
|
|
<div className="w-full bg-slate-100 rounded-full h-2">
|
|
<div
|
|
className="bg-purple-600 h-2 rounded-full transition-all"
|
|
style={{ width: `${stats?.matrix.total_rooms ? ((stats.matrix.active_rooms / stats.matrix.total_rooms) * 100) : 0}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-slate-600">Aktive Nutzer</span>
|
|
<span className="font-semibold">{stats?.matrix.active_users || 0} / {stats?.matrix.total_users || 0}</span>
|
|
</div>
|
|
<div className="w-full bg-slate-100 rounded-full h-2">
|
|
<div
|
|
className="bg-green-600 h-2 rounded-full transition-all"
|
|
style={{ width: `${stats?.matrix.total_users ? ((stats.matrix.active_users / stats.matrix.total_users) * 100) : 0}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="mt-6 pt-4 border-t border-slate-100">
|
|
<h4 className="text-sm font-medium text-slate-700 mb-3">Schnellaktionen</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
<a
|
|
href="http://localhost:8448/_synapse/admin"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-3 py-1.5 text-sm bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
|
|
>
|
|
Synapse Admin
|
|
</a>
|
|
<a
|
|
href="http://localhost:8443"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
|
|
>
|
|
Jitsi Meet
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|