/** * TeamsBot Modules View * * CRUD list of MeetingModules with expandable session lists per module. */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import * as teamsbotApi from '../../../api/teamsbotApi'; import type { MeetingModule, TeamsbotSession, MediaFileInfo } from '../../../api/teamsbotApi'; import { useLanguage } from '../../../providers/language/LanguageContext'; import { useFileContext } from '../../../contexts/FileContext'; import { FaSpinner } from 'react-icons/fa'; import styles from './Teamsbot.module.css'; const SERIES_TYPE_LABELS: Record = { weekly: 'Wöchentlich', biweekly: 'Zweiwöchentlich', monthly: 'Monatlich', adhoc: 'Adhoc', project: 'Projekt', }; const STATUS_LABELS: Record = { active: 'Aktiv', archived: 'Archiviert', completed: 'Abgeschlossen', }; export const TeamsbotModulesView: React.FC = () => { const { t } = useLanguage(); const { instance, mandateId } = useCurrentInstance(); const instanceId = instance?.id || ''; const navigate = useNavigate(); const [searchParams] = useSearchParams(); const focusModuleId = searchParams.get('moduleId') || ''; const moduleRowRefs = useRef>({}); const [modules, setModules] = useState([]); const [loading, setLoading] = useState(true); const [expandedId, setExpandedId] = useState(null); const [moduleSessions, setModuleSessions] = useState>({}); const [deleteConfirm, setDeleteConfirm] = useState(null); const [editingModule, setEditingModule] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [createTitle, setCreateTitle] = useState(''); const [createSeriesType, setCreateSeriesType] = useState('adhoc'); const [createDefaultLink, setCreateDefaultLink] = useState(''); const [createDefaultBotName, setCreateDefaultBotName] = useState(''); const [createGoals, setCreateGoals] = useState(''); const [createDefaultAvatarFileId, setCreateDefaultAvatarFileId] = useState(''); const [createSaving, setCreateSaving] = useState(false); const [mediaFiles, setMediaFiles] = useState([]); const fileCtx = useFileContext(); const avatarInputRef = useRef(null); const [avatarUploading, setAvatarUploading] = useState(false); const [avatarTarget, setAvatarTarget] = useState<'create' | 'edit'>('create'); const _refreshMediaFiles = useCallback(async () => { const result = await teamsbotApi.listMediaFiles().catch(() => [] as MediaFileInfo[]); setMediaFiles(result); }, []); const _handleAvatarUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !fileCtx?.handleFileUpload) return; setAvatarUploading(true); try { const result = await fileCtx.handleFileUpload(file); if (result?.success) { const data: any = (result.fileData as any)?.file || result.fileData; const id = data?.id || (result.fileData as any)?.id; if (id) { if (avatarTarget === 'create') { setCreateDefaultAvatarFileId(id); } else if (editingModule) { setEditingModule({ ...editingModule, defaultAvatarFileId: id }); } const refreshed = await teamsbotApi.listMediaFiles().catch(() => [] as MediaFileInfo[]); setMediaFiles(refreshed); } } } catch { // upload error handled by FileContext } finally { setAvatarUploading(false); if (avatarInputRef.current) avatarInputRef.current.value = ''; } }; const _loadModules = useCallback(async () => { if (!instanceId) return; setLoading(true); try { const result = await teamsbotApi.listModules(instanceId); setModules(result || []); } catch (err) { console.error('Failed to load modules:', err); } finally { setLoading(false); } }, [instanceId]); useEffect(() => { _loadModules(); }, [_loadModules]); useEffect(() => { teamsbotApi.listMediaFiles().then(setMediaFiles).catch(() => {}); }, []); const _loadModuleSessions = useCallback(async (moduleId: string) => { if (!instanceId) return; try { const detail = await teamsbotApi.getModuleDetail(instanceId, moduleId); setModuleSessions(prev => ({ ...prev, [moduleId]: detail?.sessions || [] })); } catch (err) { console.error('Failed to load module sessions:', err); } }, [instanceId]); useEffect(() => { if (!focusModuleId || modules.length === 0) return; if (!modules.some((m) => m.id === focusModuleId)) return; setExpandedId(focusModuleId); _loadModuleSessions(focusModuleId); }, [focusModuleId, modules, _loadModuleSessions]); useEffect(() => { if (!focusModuleId || expandedId !== focusModuleId) return; requestAnimationFrame(() => { moduleRowRefs.current[focusModuleId]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }); }, [focusModuleId, expandedId]); const _toggleExpand = (moduleId: string) => { if (expandedId === moduleId) { setExpandedId(null); } else { setExpandedId(moduleId); if (!moduleSessions[moduleId]) _loadModuleSessions(moduleId); } }; const _handleDelete = async (moduleId: string) => { try { await teamsbotApi.deleteModule(instanceId, moduleId); setDeleteConfirm(null); _loadModules(); } catch (err) { console.error('Delete failed:', err); } }; const _handleUpdate = async (moduleId: string, updates: Partial) => { try { await teamsbotApi.updateModule(instanceId, moduleId, updates); setEditingModule(null); _loadModules(); } catch (err) { console.error('Update failed:', err); } }; const _handleCreateModule = async () => { if (!createTitle.trim()) return; setCreateSaving(true); try { await teamsbotApi.createModule(instanceId, { title: createTitle.trim(), seriesType: createSeriesType, defaultMeetingLink: createDefaultLink.trim() || undefined, defaultBotName: createDefaultBotName.trim() || undefined, defaultAvatarFileId: createDefaultAvatarFileId || undefined, goals: createGoals.trim() || undefined, }); setCreateOpen(false); setCreateTitle(''); setCreateSeriesType('adhoc'); setCreateDefaultLink(''); setCreateDefaultBotName(''); setCreateDefaultAvatarFileId(''); setCreateGoals(''); _loadModules(); } catch (err) { console.error('Create module failed:', err); } finally { setCreateSaving(false); } }; const _formatSessionDateTime = (ts?: string | number): string => { if (ts == null) return '-'; const ms = typeof ts === 'number' ? ts * 1000 : Date.parse(String(ts)); if (Number.isNaN(ms)) return '-'; const d = new Date(ms); const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; }; const _calcDurationMin = (startedAt?: string | number, endedAt?: string | number): string => { if (!startedAt) return '-'; const startMs = typeof startedAt === 'number' ? startedAt * 1000 : Date.parse(String(startedAt)); if (Number.isNaN(startMs)) return '-'; const endMs = endedAt ? (typeof endedAt === 'number' ? endedAt * 1000 : Date.parse(String(endedAt))) : Date.now(); if (Number.isNaN(endMs)) return '-'; const mins = Math.round((endMs - startMs) / 60000); return `${mins} min`; }; const _sortedSessions = (sessions: TeamsbotSession[]): TeamsbotSession[] => [...sessions].sort((a, b) => { const ta = a.startedAt ? (typeof a.startedAt === 'number' ? a.startedAt * 1000 : Date.parse(String(a.startedAt))) : 0; const tb = b.startedAt ? (typeof b.startedAt === 'number' ? b.startedAt * 1000 : Date.parse(String(b.startedAt))) : 0; return tb - ta; }); return (

{t('Meeting-Module')}

{loading &&
{t('Laden...')}
}
{modules.map(mod => (
{ moduleRowRefs.current[mod.id] = el; }} className={`${styles.moduleCard} ${expandedId === mod.id ? styles.moduleExpanded : ''} ${focusModuleId === mod.id ? styles.moduleRowFocused : ''}`} >
_toggleExpand(mod.id)}> {t(SERIES_TYPE_LABELS[mod.seriesType] || mod.seriesType)} {mod.title} {t(STATUS_LABELS[mod.status] || mod.status || 'Aktiv')}
{expandedId === mod.id && (
{(moduleSessions[mod.id] || []).length === 0 ? (

{t('Keine Sitzungen')}

) : ( {_sortedSessions(moduleSessions[mod.id] || []).map((sess) => ( navigate(`/mandates/${mandateId}/teamsbot/${instanceId}/sessions?sessionId=${sess.id}`)} > ))}
{t('Datum')} {t('Dauer')} {t('Status')}
{_formatSessionDateTime(sess.startedAt)} {_calcDurationMin(sess.startedAt, sess.endedAt)} {sess.status}
)}
)}
))}
{createOpen && (

{t('Modul anlegen')}

setCreateTitle(e.target.value)} placeholder={t('z.B. Weekly Standup')} autoFocus /> setCreateDefaultLink(e.target.value)} placeholder="https://teams.microsoft.com/..." /> setCreateDefaultBotName(e.target.value)} />