From 6acf9977ec777d66674880a269a4d730ac513f22 Mon Sep 17 00:00:00 2001 From: Raffael Sheikh Date: Sun, 7 Jun 2026 13:04:52 +0200 Subject: [PATCH] =?UTF-8?q?Historie:=20Task-Notizen=20+=20Link=20sichtbar,?= =?UTF-8?q?=20Navigation=20zu=20Tagen=20mit=20Eintr=C3=A4gen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Notizfeld pro Task in der Reflexion (optional, jeder Status) — nutzt das bestehende dropReason-Feld (Limit 200->500). Wird in der Historie bei JEDEM Status angezeigt (bisher nur bei "verworfen"). - Optionaler Tages-Link wird in der Historie jetzt angezeigt. - Historie-Pfeile springen zum naechsten/vorherigen Tag MIT Eintraegen statt starr ueber Werktage — Wochenend-Arbeit (z. B. Sa 6.6.) ist nun erreichbar. Default-Tag = juengster Tag vor heute mit Eintrag. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/routers/day-entry.ts | 39 ++++++++++-- apps/web/src/routes/historie.tsx | 51 ++++++++++------ apps/web/src/routes/reflexion.tsx | 78 ++++++++++++++---------- packages/shared/src/schemas/day-entry.ts | 4 +- 4 files changed, 116 insertions(+), 56 deletions(-) diff --git a/apps/api/src/routers/day-entry.ts b/apps/api/src/routers/day-entry.ts index ed81cdf..f627c71 100644 --- a/apps/api/src/routers/day-entry.ts +++ b/apps/api/src/routers/day-entry.ts @@ -1,12 +1,11 @@ import { TRPCError } from '@trpc/server'; -import { and, eq, gte, isNull, lt, lte, asc, desc, inArray } from 'drizzle-orm'; +import { and, eq, gt, gte, isNull, lt, lte, asc, desc, inArray } from 'drizzle-orm'; import { dayEntry, todo, blocker, user, type Db, type DayEntry, type Todo } from '@cockpit/db'; import { DayEntrySchemas } from '@cockpit/shared'; import { router, protectedProcedure } from '../trpc.js'; import { todayIso, previousWerktag, - nextWerktag, addCalendarDays, workdayCutoffMs, isWorkdaySealed, @@ -618,7 +617,18 @@ export const dayEntryRouter = router({ .input(DayEntrySchemas.historyForDateInput) .query(async ({ ctx, input }) => { const today = todayIso(); - const date = input.date ?? previousWerktag(today); + // Default-Tag = der jüngste Tag VOR heute, an dem es überhaupt Einträge gibt + // (nicht starr der vorige Werktag — sonst sieht man Wochenend-Arbeit nicht). + const fallbackDate = + ( + await ctx.db + .select({ d: dayEntry.date }) + .from(dayEntry) + .where(lt(dayEntry.date, today)) + .orderBy(desc(dayEntry.date)) + .limit(1) + )[0]?.d ?? previousWerktag(today); + const date = input.date ?? fallbackDate; const isToday = date === today; const entries = await ctx.db.select().from(dayEntry).where(eq(dayEntry.date, date)); @@ -669,11 +679,28 @@ export const dayEntryRouter = router({ .map((u) => u.name) .sort((a, b) => a.localeCompare(b)); - const nw = nextWerktag(date); + // Pfeil-Navigation springt zum nächsten/vorherigen Tag MIT Einträgen + // (überspringt leere Tage, erreicht aber Wochenend-Arbeit). + const prevRow = ( + await ctx.db + .select({ d: dayEntry.date }) + .from(dayEntry) + .where(lt(dayEntry.date, date)) + .orderBy(desc(dayEntry.date)) + .limit(1) + )[0]; + const nextRow = ( + await ctx.db + .select({ d: dayEntry.date }) + .from(dayEntry) + .where(and(gt(dayEntry.date, date), lte(dayEntry.date, today))) + .orderBy(asc(dayEntry.date)) + .limit(1) + )[0]; return { date, - prevDate: previousWerktag(date), - nextDate: nw < today ? nw : null, + prevDate: prevRow?.d ?? null, + nextDate: nextRow?.d ?? null, entries: list, ohneEintrag, }; diff --git a/apps/web/src/routes/historie.tsx b/apps/web/src/routes/historie.tsx index b048ae1..ed0ca87 100644 --- a/apps/web/src/routes/historie.tsx +++ b/apps/web/src/routes/historie.tsx @@ -68,9 +68,10 @@ export function HistorieRoute() {
@@ -79,7 +80,7 @@ export function HistorieRoute() { type="button" onClick={() => nextDate && setDate(nextDate)} disabled={!nextDate} - aria-label="Nächster Werktag" + aria-label="Nächster Tag mit Eintrag" className="inline-flex h-9 w-9 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-ink)] hover:text-[var(--color-ink)] disabled:opacity-40 disabled:hover:border-[var(--color-border)]" > @@ -154,21 +155,25 @@ function PersonCard({ person: p }: { person: HistoryPerson }) { : 'bg-[var(--color-border-strong)]', )} /> - - {t.title} - {t.unplanned && ( - - ungeplant +
+ + {t.title} + {t.unplanned && ( + + ungeplant + + )} + + {t.dropReason && ( + + {t.dropReason} )} - {t.status === 'dropped' && t.dropReason && ( - — {t.dropReason} - )} - +
))} @@ -177,7 +182,17 @@ function PersonCard({ person: p }: { person: HistoryPerson }) { {p.reflectionText && (

{p.reflectionText}

)} - {p.todos.length === 0 && !p.reflectionText && ( + {p.url && ( + + {p.url} + + )} + {p.todos.length === 0 && !p.reflectionText && !p.url && (

Keine Details.

)} diff --git a/apps/web/src/routes/reflexion.tsx b/apps/web/src/routes/reflexion.tsx index fd3a332..7bc7d14 100644 --- a/apps/web/src/routes/reflexion.tsx +++ b/apps/web/src/routes/reflexion.tsx @@ -51,12 +51,13 @@ function ReflectForm({ status: TodoStatus; bezugKind: 'project' | 'deal' | 'campaign' | 'allgemein' | null; unplanned?: boolean; + dropReason?: string | null; }>; onDone: () => void; }) { const [todoStates, setTodoStates] = useState< Record - >(Object.fromEntries(todos.map((t) => [t.id, { status: t.status }]))); + >(Object.fromEntries(todos.map((t) => [t.id, { status: t.status, reason: t.dropReason ?? '' }]))); const [reflectionText, setReflectionText] = useState(''); const [ampel, setAmpel] = useState(null); const [trend, setTrend] = useState(null); @@ -86,6 +87,10 @@ function ReflectForm({ setTodoStates((prev) => ({ ...prev, [id]: { ...prev[id], status } })); } + function setTodoReason(id: string, reason: string) { + setTodoStates((prev) => ({ ...prev, [id]: { status: prev[id]?.status ?? 'open', reason } })); + } + function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!ampel) { @@ -107,7 +112,7 @@ function ReflectForm({ todos: todos.map((t) => ({ id: t.id, status: todoStates[t.id]?.status ?? t.status, - dropReason: todoStates[t.id]?.reason, + dropReason: todoStates[t.id]?.reason?.trim() || undefined, })), reflectionText, ampel, @@ -138,36 +143,47 @@ function ReflectForm({ {todos.map((t) => { const status = todoStates[t.id]?.status ?? t.status; return ( -
  • - - {t.unplanned && ( - - ungeplant - - )} -
    - setTodoStatus(t.id, 'done')} - label="Erledigt" - > - - - setTodoStatus(t.id, 'open')} - label="Offen" - > - - - setTodoStatus(t.id, 'dropped')} - label="Verworfen" - > - - +
  • +
    + + {t.unplanned && ( + + ungeplant + + )} +
    + setTodoStatus(t.id, 'done')} + label="Erledigt" + > + + + setTodoStatus(t.id, 'open')} + label="Offen" + > + + + setTodoStatus(t.id, 'dropped')} + label="Verworfen" + > + + +
    + setTodoReason(t.id, e.target.value)} + placeholder="Notiz (optional) — Ergebnis, Kontext, Link …" + maxLength={500} + aria-label={`Notiz zu ${t.title}`} + className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1 text-xs focus:border-[var(--color-ink)] focus:outline-none" + />
  • ); })} diff --git a/packages/shared/src/schemas/day-entry.ts b/packages/shared/src/schemas/day-entry.ts index 21f31d8..7267c35 100644 --- a/packages/shared/src/schemas/day-entry.ts +++ b/packages/shared/src/schemas/day-entry.ts @@ -28,7 +28,9 @@ export const submitReflectionInput = z.object({ z.object({ id: z.string().uuid(), status: todoStatusSchema, - dropReason: z.string().max(200).optional(), + // Optionale Notiz pro Task (frühere „Verworfen-Grund"-Spalte, jetzt für + // jeden Status als freie Beschreibung nutzbar). + dropReason: z.string().max(500).optional(), }), ) .default([]),