From 8933169ba913e26b3620566381dce261a7c1ba3f Mon Sep 17 00:00:00 2001 From: DeeCee03 Date: Fri, 5 Jun 2026 17:31:03 +0200 Subject: [PATCH] feat(reflexion): ToDo-Titel inline editierbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/api/src/routers/day-entry.ts | 31 +++++++++ apps/web/src/routes/reflexion.tsx | 89 +++++++++++++++++++++++- packages/shared/src/schemas/day-entry.ts | 6 ++ 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/apps/api/src/routers/day-entry.ts b/apps/api/src/routers/day-entry.ts index 60fd73b..ad30b95 100644 --- a/apps/api/src/routers/day-entry.ts +++ b/apps/api/src/routers/day-entry.ts @@ -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. */ diff --git a/apps/web/src/routes/reflexion.tsx b/apps/web/src/routes/reflexion.tsx index dcdd923..6898c60 100644 --- a/apps/web/src/routes/reflexion.tsx +++ b/apps/web/src/routes/reflexion.tsx @@ -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 (
  • - {t.title} +
    { + 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, diff --git a/packages/shared/src/schemas/day-entry.ts b/packages/shared/src/schemas/day-entry.ts index fe6d7e7..b5c94cf 100644 --- a/packages/shared/src/schemas/day-entry.ts +++ b/packages/shared/src/schemas/day-entry.ts @@ -51,6 +51,12 @@ export const todoMutationInput = z.object({ }); export type TodoMutationInput = z.infer; +export const editTodoTitleInput = z.object({ + todoId: z.string().uuid(), + title: z.string().min(1).max(200), +}); +export type EditTodoTitleInput = z.infer; + 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}$/),