import { useState } from 'react';
import { useNavigate } from 'react-router';
import { trpc } from '../lib/trpc';
import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { toast } from 'sonner';
import { clsx } from 'clsx';
import { Check, X, Minus, TrendingUp, TrendingDown, ArrowRight, Pencil } from 'lucide-react';
type Ampel = 'green' | 'amber' | 'red';
type Trend = 'besser' | 'gleich' | 'schlechter';
type TodoStatus = 'open' | 'done' | 'dropped';
export function ReflexionRoute() {
const today = trpc.dayEntry.today.useQuery();
const navigate = useNavigate();
if (today.isPending) return ;
if (!today.data?.entry) {
return (
Du bist heute noch nicht locked in. {voice.hint.notCheckedInToday}
);
}
return (
navigate('/dashboard')}
/>
);
}
function ReflectForm({
entry,
todos,
onDone,
}: {
entry: { id: string };
todos: Array<{
id: string;
title: string;
status: TodoStatus;
bezugKind: 'project' | 'deal' | 'campaign' | 'allgemein' | null;
unplanned?: boolean;
}>;
onDone: () => void;
}) {
const [todoStates, setTodoStates] = useState<
Record
>(Object.fromEntries(todos.map((t) => [t.id, { status: t.status }])));
const [reflectionText, setReflectionText] = useState('');
const [ampel, setAmpel] = useState(null);
const [trend, setTrend] = useState(null);
const [blockerEnabled, setBlockerEnabled] = useState(false);
const [blockerTitle, setBlockerTitle] = useState('');
const [blockerDescription, setBlockerDescription] = useState('');
const [unplannedTitle, setUnplannedTitle] = useState('');
const hasProjectBezug = todos.some((t) => t.bezugKind === 'project');
const utils = trpc.useUtils();
const addUnplanned = trpc.dayEntry.addUnplanned.useMutation({
onSuccess: async () => {
setUnplannedTitle('');
toast('Ungeplant erledigt eingetragen.');
await utils.dayEntry.today.invalidate();
},
});
const mutation = trpc.dayEntry.submitReflection.useMutation({
onSuccess: async () => {
toast(voice.toast.reflected);
await utils.dayEntry.today.invalidate();
onDone();
},
});
function setTodoStatus(id: string, status: TodoStatus) {
setTodoStates((prev) => ({ ...prev, [id]: { ...prev[id], status } }));
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!ampel) {
toast('Bitte Ampel setzen.');
return;
}
if (hasProjectBezug && !trend) {
toast('Bei Projektbezug ist der Trend Pflicht.');
return;
}
if (blockerEnabled && !blockerTitle.trim()) {
toast('Blocker braucht einen Titel.');
return;
}
mutation.mutate({
dayEntryId: entry.id,
// Über die aktuelle ToDo-Liste mappen (nicht nur den initialen Snapshot),
// damit mitten am Tag ergänzte ToDos ihren Status behalten.
todos: todos.map((t) => ({
id: t.id,
status: todoStates[t.id]?.status ?? t.status,
dropReason: todoStates[t.id]?.reason,
})),
reflectionText,
ampel,
trend: trend ?? null,
...(blockerEnabled
? { blocker: { title: blockerTitle, description: blockerDescription || undefined } }
: {}),
});
}
return (
);
}
function EditableTodoTitle({ id, title }: { id: string; title: string }) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(title);
const utils = trpc.useUtils();
const edit = trpc.dayEntry.editTodoTitle.useMutation({
onSuccess: async () => {
await utils.dayEntry.today.invalidate();
setEditing(false);
},
});
function save() {
const v = draft.trim();
if (!v || v === title) {
setEditing(false);
return;
}
edit.mutate({ todoId: id, title: v });
}
if (editing) {
return (
el?.focus()}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
save();
} else if (e.key === 'Escape') {
setDraft(title);
setEditing(false);
}
}}
maxLength={200}
aria-label="ToDo-Titel bearbeiten"
className="flex-1 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1 text-sm focus:border-[var(--color-ink)] focus:outline-none"
/>
);
}
return (
);
}
function StatusBtn({
active,
onClick,
children,
label,
}: {
active: boolean;
onClick: () => void;
children: React.ReactNode;
label: string;
}) {
return (
);
}
function AmpelBtn({
value,
current,
onChange,
label,
}: {
value: Ampel;
current: Ampel | null;
onChange: (a: Ampel) => void;
label: string;
}) {
const bgMap: Record = {
green: 'bg-[var(--color-ampel-green-bg)] text-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-bg)] text-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-bg)] text-[var(--color-ampel-red-fg)]',
};
const dotMap: Record = {
green: 'bg-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-fg)]',
};
const active = current === value;
return (
);
}
function TrendBtn({
value,
current,
onChange,
children,
}: {
value: Trend;
current: Trend | null;
onChange: (t: Trend) => void;
children: React.ReactNode;
}) {
const active = current === value;
return (
);
}