'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { useToast } from '@/components/ui/use-toast' import { getSocket } from '@/lib/socket' import { TableModule } from '@/components/journal/modules/table-module' import type { CockpitModule } from '@/lib/modules' import { GraduationCap, Plus, Target, Check, CircleDashed, CircleSlash, MinusCircle, Trash2, ClipboardCheck, Loader2, Blocks, } from 'lucide-react' type GoalStatus = 'OPEN' | 'REACHED' | 'PARTIAL' | 'MISSED' interface ExerciseGoal { id: string text: string status: GoalStatus note: string | null sortOrder: number } interface ExerciseCockpitProps { projectId: string | null projectTitle: string canEdit: boolean initialEvaluation?: string onEvaluationSaved?: (value: string) => void } const STATUS_CONFIG: Record = { OPEN: { label: 'Offen', icon: CircleDashed, cls: 'text-muted-foreground', activeCls: 'bg-muted text-foreground border-muted-foreground/40', }, REACHED: { label: 'Erreicht', icon: Check, cls: 'text-green-600', activeCls: 'bg-green-100 text-green-700 border-green-500 dark:bg-green-950/40 dark:text-green-300', }, PARTIAL: { label: 'Teilweise', icon: MinusCircle, cls: 'text-amber-600', activeCls: 'bg-amber-100 text-amber-700 border-amber-500 dark:bg-amber-950/40 dark:text-amber-300', }, MISSED: { label: 'Nicht erreicht', icon: CircleSlash, cls: 'text-red-600', activeCls: 'bg-red-100 text-red-700 border-red-500 dark:bg-red-950/40 dark:text-red-300', }, } const STATUS_ORDER: GoalStatus[] = ['OPEN', 'REACHED', 'PARTIAL', 'MISSED'] export function ExerciseCockpit({ projectId, projectTitle, canEdit, initialEvaluation, onEvaluationSaved }: ExerciseCockpitProps) { const [goals, setGoals] = useState([]) const [isLoading, setIsLoading] = useState(false) const [newGoalText, setNewGoalText] = useState('') const [isAdding, setIsAdding] = useState(false) const [evaluation, setEvaluation] = useState(initialEvaluation || '') const [evalSaving, setEvalSaving] = useState(false) const evalDebounce = useRef(null) const { toast } = useToast() // Modul-Baukasten: dieselben eigenen Tabellen-Module wie im Einsatz const [tableModules, setTableModules] = useState([]) useEffect(() => { fetch('/api/tenant/modules') .then(r => r.ok ? r.json() : null) .then(data => { if (data?.modules) { setTableModules(data.modules.filter((m: CockpitModule) => m.enabled && m.type === 'table' && m.columns?.length)) } }) .catch(() => {}) }, []) const notifyModuleChanged = useCallback(() => { if (!projectId) return try { getSocket().emit('journal-updated', { projectId }) } catch { /* offline */ } }, [projectId]) // Ziele laden const loadGoals = useCallback(async () => { if (!projectId) { setGoals([]) return } setIsLoading(true) try { const res = await fetch(`/api/projects/${projectId}/exercise-goals`) if (res.ok) { const data = await res.json() setGoals(data.goals || []) } } catch (e) { console.warn('Fehler beim Laden der Übungsziele:', e) } finally { setIsLoading(false) } }, [projectId]) useEffect(() => { loadGoals() }, [loadGoals]) // Auswertung übernehmen, wenn sich das Projekt ändert useEffect(() => { setEvaluation(initialEvaluation || '') }, [projectId, initialEvaluation]) const addGoal = async () => { if (!projectId || !newGoalText.trim()) return setIsAdding(true) try { const res = await fetch(`/api/projects/${projectId}/exercise-goals`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: newGoalText.trim() }), }) if (!res.ok) throw new Error('Ziel konnte nicht angelegt werden') const data = await res.json() setGoals((prev) => [...prev, data.goal]) setNewGoalText('') } catch (e) { toast({ title: 'Fehler', description: e instanceof Error ? e.message : 'Unbekannter Fehler', variant: 'destructive' }) } finally { setIsAdding(false) } } const patchGoal = async (goalId: string, patch: Partial>) => { if (!projectId) return // Optimistisch aktualisieren setGoals((prev) => prev.map((g) => (g.id === goalId ? { ...g, ...patch } : g))) try { await fetch(`/api/projects/${projectId}/exercise-goals/${goalId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }) } catch (e) { console.warn('Fehler beim Speichern des Ziels:', e) loadGoals() } } const deleteGoal = async (goalId: string) => { if (!projectId) return setGoals((prev) => prev.filter((g) => g.id !== goalId)) try { await fetch(`/api/projects/${projectId}/exercise-goals/${goalId}`, { method: 'DELETE' }) } catch (e) { console.warn('Fehler beim Löschen des Ziels:', e) loadGoals() } } // Auswertung mit Debounce speichern const handleEvaluationChange = (value: string) => { setEvaluation(value) if (!projectId) return if (evalDebounce.current) clearTimeout(evalDebounce.current) evalDebounce.current = setTimeout(async () => { setEvalSaving(true) try { await fetch(`/api/projects/${projectId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exerciseEvaluation: value }), }) onEvaluationSaved?.(value) } catch (e) { console.warn('Fehler beim Speichern der Auswertung:', e) } finally { setEvalSaving(false) } }, 800) } const reachedCount = goals.filter((g) => g.status === 'REACHED').length const total = goals.length return (
{/* Header */}

Übungs-Cockpit

{total > 0 && ( {reachedCount}/{total} erreicht )}

{projectTitle || 'Übung'}

{/* Übungsziele */}

Übungsziele

{canEdit && (
setNewGoalText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') addGoal() }} placeholder="Neues Übungsziel — z.B. Wasserbezug ab Hydrant befehlen" disabled={isAdding || !projectId} />
)} {isLoading ? (
Lädt…
) : goals.length === 0 ? (

Noch keine Ziele. Lege in der Vorbereitung fest, was die Übung auslösen soll.

) : (
    {goals.map((goal) => (
  • {goal.text}

    {canEdit && ( )}
    {/* Zielerreichung */}
    {STATUS_ORDER.map((s) => { const cfg = STATUS_CONFIG[s] const Icon = cfg.icon const active = goal.status === s return ( ) })}
    {/* Notiz zum Ziel */} {canEdit && ( setGoals((prev) => prev.map((g) => (g.id === goal.id ? { ...g, note: e.target.value } : g)))} onBlur={(e) => patchGoal(goal.id, { note: e.target.value })} placeholder="Notiz / Beobachtung (optional)" className="mt-2 h-8 text-xs" /> )} {!canEdit && goal.note && (

    {goal.note}

    )}
  • ))}
)}
{/* Auswertung */}

Auswertung {evalSaving && }