Files
FPT/src/components/Assistant.tsx
T

132 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}