79fb2b9ca6
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
5.0 KiB
TypeScript
132 lines
5.0 KiB
TypeScript
"use client";
|
||
|
||
// Der FPT-Assistent -- das Herzstück der Anwendung.
|
||
//
|
||
// Er ersetzt die frühere Karte «Nächste Schritte». Der Unterschied ist nicht kosmetisch: Die
|
||
// alte Karte leitete AB, was zu tun wäre, und liess einen damit allein. Hier führt jeder
|
||
// Schritt sein eigenes Werkzeug mit sich -- und davor eine Seite, die erklärt, worum es
|
||
// überhaupt geht.
|
||
//
|
||
// Zwei Gestaltungsentscheide, die zusammengehören:
|
||
//
|
||
// 1. Der Haken ist MANUELL. Das Tool masst sich nicht an zu wissen, wann jemand mit einem
|
||
// Schritt fertig ist -- «genug geplant» ist eine Einschätzung, keine Messgrösse.
|
||
// 2. Daneben steht der ABGELEITETE Stand («0 Lebensphasen»). Ein abgehakter Schritt ohne
|
||
// Substanz fällt so auf, ohne dass das Tool den Haken verweigert.
|
||
//
|
||
// Erledigte Schritte rutschen nach unten und werden blass -- oben steht immer das, was als
|
||
// Nächstes ansteht.
|
||
|
||
import { useState } from "react";
|
||
import { Check, ChevronRight, Lock, Sparkles } from "lucide-react";
|
||
import { api } from "@/lib/api-client";
|
||
import { ASSISTANT_STEPS, stepBlockedReason, stepStatus, type AssistantProgress } from "@/lib/assistant";
|
||
import type { PlanInput } from "@/lib/types";
|
||
|
||
export function Assistant({
|
||
plan,
|
||
progress,
|
||
onOpenStep,
|
||
onChanged,
|
||
}: {
|
||
plan: PlanInput;
|
||
progress: AssistantProgress;
|
||
onOpenStep: (index: number) => void;
|
||
onChanged: () => void;
|
||
}) {
|
||
const [busy, setBusy] = useState<number | null>(null);
|
||
const done = progress.filter(Boolean).length;
|
||
const allDone = done === ASSISTANT_STEPS.length;
|
||
|
||
async function toggle(index: number, next: boolean) {
|
||
setBusy(index);
|
||
try {
|
||
await api.post(`/api/scenarios/${plan.id}/assistant`, { step: index, done: next });
|
||
onChanged();
|
||
} finally {
|
||
setBusy(null);
|
||
}
|
||
}
|
||
|
||
// Offene zuerst, erledigte darunter -- die Reihenfolge innerhalb der Gruppen bleibt.
|
||
const ordered = [...ASSISTANT_STEPS].sort((a, b) => {
|
||
const da = progress[a.index] ? 1 : 0;
|
||
const db = progress[b.index] ? 1 : 0;
|
||
return da - db || a.index - b.index;
|
||
});
|
||
|
||
return (
|
||
<div
|
||
data-tour="assistant"
|
||
className={`flex h-full flex-col rounded-xl border px-3 py-2.5 shadow-sm transition-colors ${
|
||
allDone ? "border-success bg-success-soft" : "border-attention bg-attention-soft"
|
||
}`}
|
||
>
|
||
<div className="mb-2 flex items-center gap-2">
|
||
<Sparkles className={`h-4 w-4 ${allDone ? "text-success" : "text-attention-fg"}`} />
|
||
<span className="text-xs font-semibold uppercase tracking-wide text-fg">FPT-Assistent</span>
|
||
<span className={`ml-auto text-[11px] font-semibold ${allDone ? "text-success" : "text-muted"}`}>
|
||
{done} / {ASSISTANT_STEPS.length}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex min-h-0 flex-1 flex-col gap-1 overflow-auto">
|
||
{ordered.map((step) => {
|
||
const isDone = progress[step.index] === true;
|
||
const blocked = stepBlockedReason(plan, step.index);
|
||
const status = stepStatus(plan, step.index);
|
||
return (
|
||
<div
|
||
key={step.index}
|
||
className={`flex items-start gap-2 rounded-lg px-1.5 py-1 transition-colors ${
|
||
isDone ? "opacity-50" : "hover:bg-surface/60"
|
||
}`}
|
||
>
|
||
<button
|
||
type="button"
|
||
aria-label={isDone ? "Als offen markieren" : "Als erledigt markieren"}
|
||
disabled={busy === step.index}
|
||
onClick={() => toggle(step.index, !isDone)}
|
||
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors ${
|
||
isDone ? "border-success bg-success text-white" : "border-border bg-surface hover:border-accent"
|
||
}`}
|
||
>
|
||
{isDone && <Check className="h-3 w-3" />}
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => onOpenStep(step.index)}
|
||
className="min-w-0 flex-1 text-left"
|
||
>
|
||
<span
|
||
className={`flex items-center gap-1 text-xs font-semibold ${
|
||
isDone ? "text-muted line-through" : "text-fg"
|
||
}`}
|
||
>
|
||
<span className="truncate">
|
||
{step.index + 1}. {step.title}
|
||
</span>
|
||
{blocked && !isDone && <Lock className="h-3 w-3 shrink-0 text-faint" />}
|
||
<ChevronRight className="h-3 w-3 shrink-0 text-faint" />
|
||
</span>
|
||
{!isDone && (
|
||
<span className="mt-0.5 block truncate text-[11px] text-muted">
|
||
{status || step.short}
|
||
</span>
|
||
)}
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{allDone && (
|
||
<p className="mt-2 border-t border-success/30 pt-2 text-[11px] text-success">
|
||
Alle Schritte erledigt. Dein Plan steht – verfeinere ihn jederzeit über die Matrix.
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|