293 lines
12 KiB
TypeScript
293 lines
12 KiB
TypeScript
/**
|
|
* CommCoach Modules View
|
|
*
|
|
* CRUD list of all TrainingModules, filterable by status/type.
|
|
* Each module row expands to show its sessions.
|
|
* Edit dialog includes persona multi-select for module-persona mapping.
|
|
*/
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
|
import * as commcoachApi from '../../../api/commcoachApi';
|
|
import type { CoachingPersona } from '../../../api/commcoachApi';
|
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
|
import styles from './Commcoach.module.css';
|
|
|
|
const MODULE_TYPE_LABELS: Record<string, string> = {
|
|
coaching: 'Coaching',
|
|
training: 'Training',
|
|
exam: 'Prüfung',
|
|
elearning: 'E-Learning',
|
|
};
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
active: 'Aktiv',
|
|
paused: 'Pausiert',
|
|
archived: 'Archiviert',
|
|
completed: 'Abgeschlossen',
|
|
};
|
|
|
|
export const CommcoachModulesView: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
const { instance, mandateId } = useCurrentInstance();
|
|
const instanceId = instance?.id || '';
|
|
const navigate = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const highlightModuleId = searchParams.get('moduleId');
|
|
|
|
const [modules, setModules] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [expandedId, setExpandedId] = useState<string | null>(highlightModuleId);
|
|
const [sessions, setSessions] = useState<Record<string, any[]>>({});
|
|
const [editingModule, setEditingModule] = useState<any | null>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
|
const [filterType, setFilterType] = useState<string>('');
|
|
const [filterStatus, setFilterStatus] = useState<string>('');
|
|
|
|
const [allPersonas, setAllPersonas] = useState<CoachingPersona[]>([]);
|
|
const [editPersonaIds, setEditPersonaIds] = useState<string[]>([]);
|
|
const [personasLoaded, setPersonasLoaded] = useState(false);
|
|
|
|
const _loadModules = useCallback(async () => {
|
|
if (!instanceId) return;
|
|
setLoading(true);
|
|
try {
|
|
const apiRequest = commcoachApi.getApiRequest();
|
|
const result = await commcoachApi.listModulesApi(apiRequest, instanceId);
|
|
setModules(result || []);
|
|
} catch (err) {
|
|
console.error('Failed to load modules:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [instanceId]);
|
|
|
|
useEffect(() => { _loadModules(); }, [_loadModules]);
|
|
|
|
useEffect(() => {
|
|
if (!instanceId || personasLoaded) return;
|
|
const _loadAllPersonas = async () => {
|
|
try {
|
|
const apiRequest = commcoachApi.getApiRequest();
|
|
const personas = await commcoachApi.getPersonasApi(apiRequest, instanceId);
|
|
setAllPersonas(personas);
|
|
setPersonasLoaded(true);
|
|
} catch {}
|
|
};
|
|
_loadAllPersonas();
|
|
}, [instanceId, personasLoaded]);
|
|
|
|
const _loadSessions = useCallback(async (moduleId: string) => {
|
|
if (!instanceId) return;
|
|
try {
|
|
const apiRequest = commcoachApi.getApiRequest();
|
|
const result = await commcoachApi.listSessionsApi(apiRequest, instanceId, moduleId);
|
|
setSessions(prev => ({ ...prev, [moduleId]: result || [] }));
|
|
} catch (err) {
|
|
console.error('Failed to load sessions:', err);
|
|
}
|
|
}, [instanceId]);
|
|
|
|
const _toggleExpand = (moduleId: string) => {
|
|
if (expandedId === moduleId) {
|
|
setExpandedId(null);
|
|
} else {
|
|
setExpandedId(moduleId);
|
|
if (!sessions[moduleId]) _loadSessions(moduleId);
|
|
}
|
|
};
|
|
|
|
const _handleDelete = async (moduleId: string) => {
|
|
try {
|
|
const apiRequest = commcoachApi.getApiRequest();
|
|
await commcoachApi.deleteModuleApi(apiRequest, instanceId, moduleId);
|
|
setDeleteConfirm(null);
|
|
_loadModules();
|
|
} catch (err) {
|
|
console.error('Delete failed:', err);
|
|
}
|
|
};
|
|
|
|
const _handleEdit = async (moduleId: string, updates: any) => {
|
|
try {
|
|
const apiRequest = commcoachApi.getApiRequest();
|
|
await commcoachApi.updateModuleApi(apiRequest, instanceId, moduleId, updates);
|
|
await commcoachApi.setModulePersonasApi(apiRequest, instanceId, moduleId, editPersonaIds);
|
|
setEditingModule(null);
|
|
_loadModules();
|
|
} catch (err) {
|
|
console.error('Update failed:', err);
|
|
}
|
|
};
|
|
|
|
const _openEditDialog = async (mod: any) => {
|
|
setEditingModule(mod);
|
|
try {
|
|
const apiRequest = commcoachApi.getApiRequest();
|
|
const ids = await commcoachApi.getModulePersonasApi(apiRequest, instanceId, mod.id);
|
|
setEditPersonaIds(ids);
|
|
} catch {
|
|
setEditPersonaIds([]);
|
|
}
|
|
};
|
|
|
|
const _togglePersonaId = (personaId: string) => {
|
|
setEditPersonaIds(prev =>
|
|
prev.includes(personaId)
|
|
? prev.filter(id => id !== personaId)
|
|
: [...prev, personaId]
|
|
);
|
|
};
|
|
|
|
const filteredModules = modules.filter(m => {
|
|
if (filterType && m.moduleType !== filterType) return false;
|
|
if (filterStatus && m.status !== filterStatus) return false;
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<div className={styles.modulesContainer}>
|
|
<div className={styles.modulesHeader}>
|
|
<h2>{t('Module')}</h2>
|
|
<div className={styles.modulesFilters}>
|
|
<select value={filterType} onChange={e => setFilterType(e.target.value)}>
|
|
<option value="">{t('Alle Typen')}</option>
|
|
{Object.entries(MODULE_TYPE_LABELS).map(([k, v]) => (
|
|
<option key={k} value={k}>{t(v)}</option>
|
|
))}
|
|
</select>
|
|
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
|
|
<option value="">{t('Alle Status')}</option>
|
|
{Object.entries(STATUS_LABELS).map(([k, v]) => (
|
|
<option key={k} value={k}>{t(v)}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<button
|
|
className={styles.btnPrimary}
|
|
onClick={() => navigate(`/mandates/${mandateId}/commcoach/${instanceId}/assistant`)}
|
|
>
|
|
{t('Neues Modul')}
|
|
</button>
|
|
</div>
|
|
|
|
{loading && <div className={styles.loading}>{t('Laden...')}</div>}
|
|
|
|
<div className={styles.modulesList}>
|
|
{filteredModules.map(mod => (
|
|
<div
|
|
key={mod.id}
|
|
className={`${styles.moduleCard} ${expandedId === mod.id ? styles.moduleExpanded : ''} ${highlightModuleId === mod.id ? styles.moduleHighlighted : ''}`}
|
|
>
|
|
<div className={styles.moduleRow} onClick={() => _toggleExpand(mod.id)}>
|
|
<span className={styles.moduleType}>{t(MODULE_TYPE_LABELS[mod.moduleType] || mod.moduleType)}</span>
|
|
<span className={styles.moduleTitle}>{mod.title}</span>
|
|
<span className={styles.moduleStatus}>{t(STATUS_LABELS[mod.status] || mod.status)}</span>
|
|
<span className={styles.moduleSessions}>{mod.sessionCount || 0} {t('Sessions')}</span>
|
|
<div className={styles.moduleActions}>
|
|
<button className={styles.btnPrimary} style={{ padding: '0.3rem 0.7rem', fontSize: '0.8rem' }} onClick={e => {
|
|
e.stopPropagation();
|
|
navigate(`/mandates/${mandateId}/commcoach/${instanceId}/session?moduleId=${mod.id}`);
|
|
}}>{t('Session starten')}</button>
|
|
<button className={styles.btnSmall} onClick={e => { e.stopPropagation(); _openEditDialog(mod); }}>{t('Bearbeiten')}</button>
|
|
<button className={styles.btnSmallDanger} onClick={e => { e.stopPropagation(); setDeleteConfirm(mod.id); }}>{t('Löschen')}</button>
|
|
</div>
|
|
</div>
|
|
|
|
{expandedId === mod.id && (
|
|
<div className={styles.moduleSessions}>
|
|
{(sessions[mod.id] || []).length === 0 ? (
|
|
<p className={styles.noSessions}>{t('Keine Sessions vorhanden')}</p>
|
|
) : (
|
|
<div className={styles.sessionList}>
|
|
{(sessions[mod.id] || []).map((sess: any) => (
|
|
<div key={sess.id} className={styles.sessionRow}>
|
|
<span>{sess.summary || t('Session')}</span>
|
|
<span className={styles.sessionStatus}>{sess.status}</span>
|
|
<span className={styles.sessionDate}>
|
|
{sess.startedAt ? new Date(sess.startedAt * 1000).toLocaleDateString() : '-'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{deleteConfirm && (
|
|
<div className={styles.confirmOverlay}>
|
|
<div className={styles.confirmDialog}>
|
|
<p>{t('Modul und alle zugehörigen Sessions wirklich löschen?')}</p>
|
|
<div className={styles.confirmActions}>
|
|
<button className={styles.btnSecondary} onClick={() => setDeleteConfirm(null)}>{t('Abbrechen')}</button>
|
|
<button className={styles.btnDanger} onClick={() => _handleDelete(deleteConfirm)}>{t('Löschen')}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{editingModule && (
|
|
<div className={styles.confirmOverlay}>
|
|
<div className={styles.editDialog}>
|
|
<h3>{t('Modul bearbeiten')}</h3>
|
|
<input
|
|
type="text"
|
|
defaultValue={editingModule.title}
|
|
className={styles.wizardInput}
|
|
onBlur={e => setEditingModule({ ...editingModule, title: e.target.value })}
|
|
/>
|
|
<textarea
|
|
defaultValue={editingModule.goals || ''}
|
|
className={styles.wizardTextarea}
|
|
placeholder={t('Ziele')}
|
|
rows={3}
|
|
onBlur={e => setEditingModule({ ...editingModule, goals: e.target.value })}
|
|
/>
|
|
|
|
{/* Persona Multi-Select */}
|
|
<div style={{ marginTop: '1rem' }}>
|
|
<label className={styles.wizardLabel} style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600 }}>
|
|
{t('Verfuegbare Gespraechspartner')}
|
|
</label>
|
|
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', margin: '0 0 0.5rem' }}>
|
|
{t('Waehle, welche Gespraechspartner in Sessions dieses Moduls zur Verfuegung stehen. Ohne Auswahl sind alle verfuegbar.')}
|
|
</p>
|
|
<div style={{
|
|
maxHeight: 200, overflowY: 'auto', border: '1px solid var(--border-color, #ddd)',
|
|
borderRadius: 6, padding: '0.5rem',
|
|
}}>
|
|
{allPersonas.filter(p => p.isActive).map(p => (
|
|
<label key={p.id} style={{
|
|
display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.3rem 0',
|
|
cursor: 'pointer', fontSize: '0.85rem',
|
|
}}>
|
|
<input
|
|
type="checkbox"
|
|
checked={editPersonaIds.includes(p.id)}
|
|
onChange={() => _togglePersonaId(p.id)}
|
|
/>
|
|
<span>{p.label}</span>
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #888)' }}>
|
|
({p.category === 'builtin' ? t('System') : t('Eigene')})
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.confirmActions}>
|
|
<button className={styles.btnSecondary} onClick={() => setEditingModule(null)}>{t('Abbrechen')}</button>
|
|
<button className={styles.btnPrimary} onClick={() => _handleEdit(editingModule.id, {
|
|
title: editingModule.title,
|
|
goals: editingModule.goals,
|
|
})}>{t('Speichern')}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|