From 10f8f1f2d5fed2cd81c4e7e1408ae5d3a6ab767b Mon Sep 17 00:00:00 2001 From: Maximilian Stubhan Date: Sun, 16 Aug 2026 19:44:48 +0200 Subject: [PATCH] Keep a note on the overview until somebody ticks it off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A note with a follow-up date is a task, and the overview is where tasks are looked for. Until now it lived only in the employee file, which is the one place you go when you already know who you are looking for. Follow-ups behave differently from everything else on that card, and the difference is the point: an entry on Monday is over on Tuesday, an unfinished task is not. So there is no lower bound on the date — what was due and never ticked off stays, marked overdue in red, sorted to the top because it is sorted by date. A task that drops out of the list by itself is a forgotten task. Only "Erledigt" removes it. A note without a follow-up date never appears: it is a record, not a task. Checked against the live database — an overdue one and an upcoming one appear, one without a date and one beyond the chosen period do not, and ticking the overdue one off removes exactly it. The probe notes were deleted again. Co-Authored-By: Claude Opus 5 --- app/(app)/page.tsx | 73 ++++++++++++++++++++++------- lib/dashboard-data.ts | 19 ++++++++ lib/dashboard-filter.ts | 1 + tests/unit/dashboard-filter.test.ts | 16 +++++++ 4 files changed, 93 insertions(+), 16 deletions(-) diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 74aee6c..e4014b5 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -38,7 +38,12 @@ const DOT_STYLES: Record = { Gehaltsanpassung: "bg-warning-text", }; -const KIND_LABEL = { hire: "Eintritt", exit: "Austritt", return: "Rückkehr aus Abwesenheit" } as const; +const KIND_LABEL = { + hire: "Eintritt", + exit: "Austritt", + return: "Rückkehr aus Abwesenheit", + note: "Wiedervorlage", +} as const; export default async function DashboardPage({ searchParams, @@ -83,6 +88,7 @@ export default async function DashboardPage({ upcomingHires, upcomingExits, upcomingReturns, + upcomingNotes, history, } = await withUser(userId, (tx) => loadDashboardData(tx, { userId, today, yearStart, yearEnd, bisIso, arten })); // "Aktiv" means status Aktiv — somebody on Karenz is employed but not @@ -115,26 +121,46 @@ export default async function DashboardPage({ .sort((a, b) => b.count - a.count); const maxDivisionCount = Math.max(1, ...divisionBars.map((d) => d.count)); - type UpcomingItem = { id: string; label: string; date: string; kind: keyof typeof KIND_LABEL }; + type UpcomingItem = { + id: string; + /** Ziel des Klicks — bei einer Wiedervorlage die Akte, nicht die Notiz. */ + employeeId: string; + label: string; + /** Zweite Zeile: bei einer Wiedervorlage der Notiztext statt der Art. */ + hinweis?: string; + date: string; + kind: keyof typeof KIND_LABEL; + }; const upcomingAlle: UpcomingItem[] = [ ...(upcomingHires).map((e) => ({ id: e.id, + employeeId: e.id, label: fmtName(e.first_name, e.last_name), date: e.entry_date, kind: "hire" as const, })), ...(upcomingExits).map((e) => ({ id: e.id, + employeeId: e.id, label: fmtName(e.first_name, e.last_name), date: e.exit_date!, kind: "exit" as const, })), ...(upcomingReturns).map((e) => ({ id: e.id, + employeeId: e.id, label: fmtName(e.first_name, e.last_name), date: e.karenz_return_date!, kind: "return" as const, })), + ...(upcomingNotes).map((n) => ({ + id: n.id, + employeeId: n.employee_id!, + label: fmtName(n.first_name, n.last_name), + hinweis: n.note_text, + date: n.due_date!, + kind: "note" as const, + })), ].sort((a, b) => a.date.localeCompare(b.date)); // Die Karte bleibt eine Übersicht, keine Liste: acht Zeilen, und darunter @@ -233,20 +259,35 @@ export default async function DashboardPage({
    - {upcoming.map((item) => ( -
  • - - - {item.label} - {KIND_LABEL[item.kind]} - - {fmtDate(item.date)} - -
  • - ))} + {upcoming.map((item) => { + // Überfällig gibt es nur bei Wiedervorlagen: die anderen Arten + // haben eine untere Grenze, eine offene Aufgabe nicht. + const ueberfaellig = item.date < today; + return ( +
  • + + + {item.label} + + {KIND_LABEL[item.kind]} + {item.hinweis ? ` — ${item.hinweis}` : ""} + + + + {ueberfaellig ? "überfällig " : ""} + {fmtDate(item.date)} + + +
  • + ); + })} {upcoming.length === 0 && (

    {istEingeschraenkt(zeitraum, arten) diff --git a/lib/dashboard-data.ts b/lib/dashboard-data.ts index dfa2d8a..fef3951 100644 --- a/lib/dashboard-data.ts +++ b/lib/dashboard-data.ts @@ -102,6 +102,24 @@ export async function loadDashboardData(tx: Tx, p: DashboardParams) { .where((e) => e.lit(zeigt("return"))) ).as("upcomingReturns"), + // Wiedervorlagen. Sie verhalten sich anders als der Rest dieser Karte: + // ein Eintritt am Montag ist am Dienstag vorbei, eine Wiedervorlage + // nicht. Deshalb gibt es hier **keine untere Grenze** — was fällig war + // und nicht abgehakt wurde, bleibt stehen, bis jemand „Erledigt" klickt. + // Eine Aufgabe, die von selbst aus der Liste rutscht, ist eine + // vergessene Aufgabe. + jsonArrayFrom( + eb + .selectFrom("employee_notes as n") + .innerJoin("employees as e", "e.id", "n.employee_id") + .select(["n.id", "n.employee_id", "n.due_date", "n.category", "n.note_text", "e.first_name", "e.last_name"]) + .where("n.done", "=", false) + .where("n.due_date", "is not", null) + .where("n.due_date", "<=", bisIso) + .where((x) => x.lit(zeigt("note"))) + .orderBy("n.due_date") + ).as("upcomingNotes"), + jsonArrayFrom( eb .selectFrom("employee_history as h") @@ -135,6 +153,7 @@ export async function loadDashboardData(tx: Tx, p: DashboardParams) { upcomingHires: g.upcomingHires, upcomingExits: g.upcomingExits, upcomingReturns: g.upcomingReturns, + upcomingNotes: g.upcomingNotes, history: g.history, }; } diff --git a/lib/dashboard-filter.ts b/lib/dashboard-filter.ts index 16c91f2..a3a8502 100644 --- a/lib/dashboard-filter.ts +++ b/lib/dashboard-filter.ts @@ -14,6 +14,7 @@ export const ANSTEHEND_ARTEN = [ { value: "hire", label: "Eintritt" }, { value: "exit", label: "Austritt" }, { value: "return", label: "Rückkehr" }, + { value: "note", label: "Wiedervorlage" }, ] as const; export type AnstehendArt = (typeof ANSTEHEND_ARTEN)[number]["value"]; diff --git a/tests/unit/dashboard-filter.test.ts b/tests/unit/dashboard-filter.test.ts index 6667fd0..d2dc826 100644 --- a/tests/unit/dashboard-filter.test.ts +++ b/tests/unit/dashboard-filter.test.ts @@ -56,6 +56,22 @@ describe("parseArten", () => { }); }); +describe("Wiedervorlagen als eigene Art", () => { + it("steht in der Auswahl", () => { + expect(ANSTEHEND_ARTEN.map((a) => a.value)).toContain("note"); + }); + + it("ist ohne Angabe mit dabei", () => { + // Eine offene Aufgabe mit Termin ist der Grund, warum jemand die Übersicht + // aufschlägt — sie darf nicht erst nach einem Klick sichtbar werden. + expect(parseArten(undefined)).toContain("note"); + }); + + it("lässt sich allein auswählen", () => { + expect(parseArten("note")).toEqual(["note"]); + }); +}); + describe("istEingeschraenkt", () => { const alle = ANSTEHEND_ARTEN.map((a) => a.value);