feat(reflexion): ToDo-Titel inline editierbar

ToDos in der Reflexion lassen sich per Klick auf den Titel korrigieren
(z. B. Tippfehler), Enter speichert / Escape bricht ab. Neuer Endpoint
dayEntry.editTodoTitle — nur eigene ToDos vom heutigen Berlin-Tag,
ändert den Titel in-place (Status/Reihenfolge/IDs bleiben).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-05 17:31:03 +02:00
1 parent 804b7637e7
commit 8933169ba9
3 files changed
+124 -2

No files matched your search

+31
View File
@@ -209,6 +209,37 @@ export const dayEntryRouter = router({
return result;
}),
/**
* Einzelnen ToDo-Titel korrigieren (z. B. Tippfehler) — auch noch in der
* Reflexion. Erlaubt, solange das ToDo der eigenen Person gehört und vom
* heutigen Berlin-Tag ist (gleiche Editierbarkeits-Regel wie die Reflexion).
*/
editTodoTitle: protectedProcedure
.input(DayEntrySchemas.editTodoTitleInput)
.mutation(async ({ ctx, input }) => {
const rows = await ctx.db
.select()
.from(todo)
.where(and(eq(todo.id, input.todoId), eq(todo.userId, ctx.user.id)))
.limit(1);
const existing = rows[0];
if (!existing) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'ToDo nicht gefunden.' });
}
if (existing.date !== todayIso(new Date())) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'ToDos eines Vortags können nicht mehr bearbeitet werden.',
});
}
const [updated] = await ctx.db
.update(todo)
.set({ title: input.title })
.where(eq(todo.id, input.todoId))
.returning();
return updated;
}),
/**
* Abend-Reflexion. Setzt Ampel + Trend, optional Blocker.
*/
+87 -2
View File
@@ -7,7 +7,7 @@ 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 } from 'lucide-react';
import { Check, X, Minus, TrendingUp, TrendingDown, ArrowRight, Pencil } from 'lucide-react';
type Ampel = 'green' | 'amber' | 'red';
type Trend = 'besser' | 'gleich' | 'schlechter';
@@ -128,7 +128,7 @@ function ReflectForm({
const status = todoStates[t.id]?.status ?? 'open';
return (
<li key={t.id} className="flex items-center gap-2">
<span className="flex-1 text-sm">{t.title}</span>
<EditableTodoTitle id={t.id} title={t.title} />
<div className="flex gap-1">
<StatusBtn
active={status === 'done'}
@@ -269,6 +269,91 @@ function ReflectForm({
);
}
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 (
<div className="flex flex-1 items-center gap-1">
<input
ref={(el) => 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"
/>
<button
type="button"
onClick={save}
disabled={edit.isPending}
aria-label="Titel speichern"
title="Speichern"
className="inline-flex h-7 w-7 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-ink)] bg-[var(--color-ink)] text-[var(--color-brand-fg)]"
>
<Check size={14} />
</button>
<button
type="button"
onClick={() => {
setDraft(title);
setEditing(false);
}}
aria-label="Bearbeiten abbrechen"
title="Abbrechen"
className="inline-flex h-7 w-7 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-border-strong)]"
>
<X size={14} />
</button>
</div>
);
}
return (
<button
type="button"
onClick={() => {
setDraft(title);
setEditing(true);
}}
title="Zum Bearbeiten klicken"
className="group flex flex-1 items-center gap-1.5 text-left text-sm"
>
<span>{title}</span>
<Pencil
size={12}
className="text-[var(--color-ink-faint)] opacity-0 transition-opacity group-hover:opacity-100"
/>
</button>
);
}
function StatusBtn({
active,
onClick,
+6
View File
@@ -51,6 +51,12 @@ export const todoMutationInput = z.object({
});
export type TodoMutationInput = z.infer<typeof todoMutationInput>;
export const editTodoTitleInput = z.object({
todoId: z.string().uuid(),
title: z.string().min(1).max(200),
});
export type EditTodoTitleInput = z.infer<typeof editTodoTitleInput>;
export const markAbsentInput = z.object({
from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),