242 lines
9 KiB
TypeScript
242 lines
9 KiB
TypeScript
/**
|
|
* CommCoach Dossier View
|
|
*
|
|
* Shows context detail: sessions timeline, tasks checklist, scores, insights.
|
|
*/
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { useCommcoach } from '../../../hooks/useCommcoach';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import styles from './CommcoachDossierView.module.css';
|
|
|
|
export const CommcoachDossierView: React.FC = () => {
|
|
const coach = useCommcoach();
|
|
const [newTaskTitle, setNewTaskTitle] = useState('');
|
|
const [activeTab, setActiveTab] = useState<'sessions' | 'tasks' | 'scores'>('tasks');
|
|
|
|
useEffect(() => {
|
|
if (!coach.selectedContextId && coach.contexts.length > 0) {
|
|
coach.selectContext(coach.contexts[0].id);
|
|
}
|
|
}, [coach.contexts, coach.selectedContextId, coach.selectContext]);
|
|
|
|
const handleAddTask = useCallback(async () => {
|
|
if (!newTaskTitle.trim()) return;
|
|
await coach.addTask(newTaskTitle);
|
|
setNewTaskTitle('');
|
|
}, [newTaskTitle, coach]);
|
|
|
|
if (coach.loadingContexts) {
|
|
return <div className={styles.empty}><p>Lade...</p></div>;
|
|
}
|
|
|
|
if (coach.contexts.length === 0) {
|
|
return (
|
|
<div className={styles.empty}>
|
|
<p>Noch keine Coaching-Themen vorhanden. Erstelle zuerst eines im Coaching-Tab.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.dossier}>
|
|
{/* Context Selector */}
|
|
<div className={styles.contextSelector}>
|
|
{coach.contexts.map(ctx => (
|
|
<button
|
|
key={ctx.id}
|
|
className={`${styles.contextChip} ${ctx.id === coach.selectedContextId ? styles.contextChipActive : ''}`}
|
|
onClick={() => coach.selectContext(ctx.id)}
|
|
>
|
|
{ctx.title}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{!coach.selectedContextId ? (
|
|
<div className={styles.empty}><p>Waehle ein Coaching-Thema.</p></div>
|
|
) : (<>
|
|
{/* Context Header */}
|
|
<div className={styles.header}>
|
|
<div>
|
|
<h2 className={styles.title}>{coach.selectedContext?.title}</h2>
|
|
{coach.selectedContext?.description && (
|
|
<p className={styles.description}>{coach.selectedContext.description}</p>
|
|
)}
|
|
</div>
|
|
<div className={styles.headerActions}>
|
|
<button className={styles.btnArchive} onClick={() => coach.archiveContext(coach.selectedContextId!)}>
|
|
Archivieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Navigation */}
|
|
<div className={styles.tabs}>
|
|
<button
|
|
className={`${styles.tab} ${activeTab === 'tasks' ? styles.tabActive : ''}`}
|
|
onClick={() => setActiveTab('tasks')}
|
|
>
|
|
Aufgaben ({coach.tasks.length})
|
|
</button>
|
|
<button
|
|
className={`${styles.tab} ${activeTab === 'sessions' ? styles.tabActive : ''}`}
|
|
onClick={() => setActiveTab('sessions')}
|
|
>
|
|
Sessions ({coach.sessions.length})
|
|
</button>
|
|
<button
|
|
className={`${styles.tab} ${activeTab === 'scores' ? styles.tabActive : ''}`}
|
|
onClick={() => setActiveTab('scores')}
|
|
>
|
|
Bewertungen ({coach.scores.length})
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tasks Tab */}
|
|
{activeTab === 'tasks' && (
|
|
<div className={styles.tabContent}>
|
|
<div className={styles.addTaskRow}>
|
|
<input
|
|
className={styles.addTaskInput}
|
|
placeholder="Neue Aufgabe..."
|
|
value={newTaskTitle}
|
|
onChange={e => setNewTaskTitle(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && handleAddTask()}
|
|
/>
|
|
<button className={styles.addTaskBtn} onClick={handleAddTask} disabled={!newTaskTitle.trim()}>
|
|
Hinzufuegen
|
|
</button>
|
|
</div>
|
|
{coach.tasks.length === 0 ? (
|
|
<div className={styles.emptyTab}>Noch keine Aufgaben. Der Coach schlaegt waehrend Sessions Aufgaben vor.</div>
|
|
) : (
|
|
<div className={styles.taskList}>
|
|
{coach.tasks.map(task => (
|
|
<div key={task.id} className={`${styles.taskItem} ${task.status === 'done' ? styles.taskDone : ''}`}>
|
|
<button
|
|
className={styles.taskCheck}
|
|
onClick={() => coach.toggleTaskStatus(task.id, task.status)}
|
|
>
|
|
{task.status === 'done' ? '\u2713' : '\u25CB'}
|
|
</button>
|
|
<div className={styles.taskContent}>
|
|
<div className={styles.taskTitle}>{task.title}</div>
|
|
{task.description && <div className={styles.taskDesc}>{task.description}</div>}
|
|
</div>
|
|
<div className={styles.taskMeta}>
|
|
<span className={`${styles.taskPriority} ${styles[`priority_${task.priority}`]}`}>
|
|
{task.priority}
|
|
</span>
|
|
</div>
|
|
<button className={styles.taskDelete} onClick={() => coach.removeTask(task.id)}>
|
|
x
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Sessions Tab */}
|
|
{activeTab === 'sessions' && (
|
|
<div className={styles.tabContent}>
|
|
{coach.sessions.length === 0 ? (
|
|
<div className={styles.emptyTab}>Noch keine abgeschlossenen Sessions.</div>
|
|
) : (
|
|
<div className={styles.sessionTimeline}>
|
|
{coach.sessions.map(s => (
|
|
<div key={s.id} className={styles.sessionItem}>
|
|
<div className={styles.sessionItemHeader}>
|
|
<span className={`${styles.sessionStatus} ${styles[`status_${s.status}`]}`}>
|
|
{s.status === 'completed' ? 'Abgeschlossen' : s.status === 'active' ? 'Aktiv' : 'Abgebrochen'}
|
|
</span>
|
|
<span className={styles.sessionDate}>
|
|
{s.startedAt ? new Date(s.startedAt).toLocaleDateString('de-CH') : ''}
|
|
</span>
|
|
{s.competenceScore != null && (
|
|
<span className={styles.sessionScore}>Score: {Math.round(s.competenceScore)}</span>
|
|
)}
|
|
</div>
|
|
{s.summary && (
|
|
<div className={styles.sessionSummary}>
|
|
<ReactMarkdown>{s.summary}</ReactMarkdown>
|
|
</div>
|
|
)}
|
|
<div className={styles.sessionMeta}>
|
|
{s.messageCount} Nachrichten | {Math.round(s.durationSeconds / 60)} Min.
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Scores Tab */}
|
|
{activeTab === 'scores' && (
|
|
<div className={styles.tabContent}>
|
|
{coach.scores.length === 0 ? (
|
|
<div className={styles.emptyTab}>Noch keine Bewertungen. Schliesse eine Session ab, um Scores zu erhalten.</div>
|
|
) : (
|
|
<div className={styles.scoreList}>
|
|
{_groupScoresByDimension(coach.scores).map(group => (
|
|
<div key={group.dimension} className={styles.scoreGroup}>
|
|
<div className={styles.scoreDimension}>
|
|
<span className={styles.scoreDimensionLabel}>{_dimensionLabel(group.dimension)}</span>
|
|
<span className={styles.scoreLatest}>{Math.round(group.latest.score)}/100</span>
|
|
<span className={`${styles.scoreTrend} ${styles[`trend_${group.latest.trend}`]}`}>
|
|
{group.latest.trend === 'improving' ? 'steigend' : group.latest.trend === 'declining' ? 'sinkend' : 'stabil'}
|
|
</span>
|
|
</div>
|
|
<div className={styles.scoreBar}>
|
|
<div className={styles.scoreBarFill} style={{ width: `${group.latest.score}%` }} />
|
|
</div>
|
|
{group.latest.evidence && (
|
|
<div className={styles.scoreEvidence}>{group.latest.evidence}</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface ScoreGroup {
|
|
dimension: string;
|
|
latest: { score: number; trend: string; evidence?: string; createdAt?: string };
|
|
history: Array<{ score: number; createdAt?: string }>;
|
|
}
|
|
|
|
function _groupScoresByDimension(scores: any[]): ScoreGroup[] {
|
|
const groups: Record<string, ScoreGroup> = {};
|
|
for (const s of scores) {
|
|
const dim = s.dimension;
|
|
if (!groups[dim]) {
|
|
groups[dim] = { dimension: dim, latest: s, history: [] };
|
|
}
|
|
groups[dim].history.push({ score: s.score, createdAt: s.createdAt });
|
|
if (s.createdAt > (groups[dim].latest.createdAt || '')) {
|
|
groups[dim].latest = s;
|
|
}
|
|
}
|
|
return Object.values(groups);
|
|
}
|
|
|
|
function _dimensionLabel(dim: string): string {
|
|
const labels: Record<string, string> = {
|
|
empathy: 'Einfuehlungsvermoegen',
|
|
clarity: 'Klarheit',
|
|
assertiveness: 'Durchsetzung',
|
|
listening: 'Zuhoeren',
|
|
selfReflection: 'Selbstreflexion',
|
|
};
|
|
return labels[dim] || dim;
|
|
}
|
|
|
|
export default CommcoachDossierView;
|