Keep a note on the overview until somebody ticks it off
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 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,12 @@ const DOT_STYLES: Record<string, string> = {
|
|||||||
Gehaltsanpassung: "bg-warning-text",
|
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({
|
export default async function DashboardPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -83,6 +88,7 @@ export default async function DashboardPage({
|
|||||||
upcomingHires,
|
upcomingHires,
|
||||||
upcomingExits,
|
upcomingExits,
|
||||||
upcomingReturns,
|
upcomingReturns,
|
||||||
|
upcomingNotes,
|
||||||
history,
|
history,
|
||||||
} = await withUser(userId, (tx) => loadDashboardData(tx, { userId, today, yearStart, yearEnd, bisIso, arten }));
|
} = await withUser(userId, (tx) => loadDashboardData(tx, { userId, today, yearStart, yearEnd, bisIso, arten }));
|
||||||
// "Aktiv" means status Aktiv — somebody on Karenz is employed but not
|
// "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);
|
.sort((a, b) => b.count - a.count);
|
||||||
const maxDivisionCount = Math.max(1, ...divisionBars.map((d) => d.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[] = [
|
const upcomingAlle: UpcomingItem[] = [
|
||||||
...(upcomingHires).map((e) => ({
|
...(upcomingHires).map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
|
employeeId: e.id,
|
||||||
label: fmtName(e.first_name, e.last_name),
|
label: fmtName(e.first_name, e.last_name),
|
||||||
date: e.entry_date,
|
date: e.entry_date,
|
||||||
kind: "hire" as const,
|
kind: "hire" as const,
|
||||||
})),
|
})),
|
||||||
...(upcomingExits).map((e) => ({
|
...(upcomingExits).map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
|
employeeId: e.id,
|
||||||
label: fmtName(e.first_name, e.last_name),
|
label: fmtName(e.first_name, e.last_name),
|
||||||
date: e.exit_date!,
|
date: e.exit_date!,
|
||||||
kind: "exit" as const,
|
kind: "exit" as const,
|
||||||
})),
|
})),
|
||||||
...(upcomingReturns).map((e) => ({
|
...(upcomingReturns).map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
|
employeeId: e.id,
|
||||||
label: fmtName(e.first_name, e.last_name),
|
label: fmtName(e.first_name, e.last_name),
|
||||||
date: e.karenz_return_date!,
|
date: e.karenz_return_date!,
|
||||||
kind: "return" as const,
|
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));
|
].sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
// Die Karte bleibt eine Übersicht, keine Liste: acht Zeilen, und darunter
|
// Die Karte bleibt eine Übersicht, keine Liste: acht Zeilen, und darunter
|
||||||
@@ -233,20 +259,35 @@ export default async function DashboardPage({
|
|||||||
<AnstehendFilter zeitraum={zeitraum} arten={arten} />
|
<AnstehendFilter zeitraum={zeitraum} arten={arten} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<ul className="flex flex-col divide-y divide-border-subtle">
|
<ul className="flex flex-col divide-y divide-border-subtle">
|
||||||
{upcoming.map((item) => (
|
{upcoming.map((item) => {
|
||||||
<li key={`${item.kind}-${item.id}`}>
|
// Überfällig gibt es nur bei Wiedervorlagen: die anderen Arten
|
||||||
<Link
|
// haben eine untere Grenze, eine offene Aufgabe nicht.
|
||||||
href={`/employees/${item.id}`}
|
const ueberfaellig = item.date < today;
|
||||||
className="-mx-2 flex items-center justify-between gap-2 rounded px-2 py-2.5 text-sm hover:bg-surface"
|
return (
|
||||||
>
|
<li key={`${item.kind}-${item.id}`}>
|
||||||
<span className="min-w-0">
|
<Link
|
||||||
<span className="block truncate font-semibold text-ink">{item.label}</span>
|
href={`/employees/${item.employeeId}`}
|
||||||
<span className="text-xs text-ink-muted">{KIND_LABEL[item.kind]}</span>
|
className="-mx-2 flex items-center justify-between gap-2 rounded px-2 py-2.5 text-sm hover:bg-surface"
|
||||||
</span>
|
>
|
||||||
<span className="shrink-0 text-xs font-semibold tabular-nums text-ink-muted">{fmtDate(item.date)}</span>
|
<span className="min-w-0">
|
||||||
</Link>
|
<span className="block truncate font-semibold text-ink">{item.label}</span>
|
||||||
</li>
|
<span className="block truncate text-xs text-ink-muted">
|
||||||
))}
|
{KIND_LABEL[item.kind]}
|
||||||
|
{item.hinweis ? ` — ${item.hinweis}` : ""}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`shrink-0 text-xs font-semibold tabular-nums ${
|
||||||
|
ueberfaellig ? "text-danger-text" : "text-ink-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{ueberfaellig ? "überfällig " : ""}
|
||||||
|
{fmtDate(item.date)}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{upcoming.length === 0 && (
|
{upcoming.length === 0 && (
|
||||||
<p className="py-2 text-sm text-ink-muted">
|
<p className="py-2 text-sm text-ink-muted">
|
||||||
{istEingeschraenkt(zeitraum, arten)
|
{istEingeschraenkt(zeitraum, arten)
|
||||||
|
|||||||
@@ -102,6 +102,24 @@ export async function loadDashboardData(tx: Tx, p: DashboardParams) {
|
|||||||
.where((e) => e.lit(zeigt("return")))
|
.where((e) => e.lit(zeigt("return")))
|
||||||
).as("upcomingReturns"),
|
).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(
|
jsonArrayFrom(
|
||||||
eb
|
eb
|
||||||
.selectFrom("employee_history as h")
|
.selectFrom("employee_history as h")
|
||||||
@@ -135,6 +153,7 @@ export async function loadDashboardData(tx: Tx, p: DashboardParams) {
|
|||||||
upcomingHires: g.upcomingHires,
|
upcomingHires: g.upcomingHires,
|
||||||
upcomingExits: g.upcomingExits,
|
upcomingExits: g.upcomingExits,
|
||||||
upcomingReturns: g.upcomingReturns,
|
upcomingReturns: g.upcomingReturns,
|
||||||
|
upcomingNotes: g.upcomingNotes,
|
||||||
history: g.history,
|
history: g.history,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const ANSTEHEND_ARTEN = [
|
|||||||
{ value: "hire", label: "Eintritt" },
|
{ value: "hire", label: "Eintritt" },
|
||||||
{ value: "exit", label: "Austritt" },
|
{ value: "exit", label: "Austritt" },
|
||||||
{ value: "return", label: "Rückkehr" },
|
{ value: "return", label: "Rückkehr" },
|
||||||
|
{ value: "note", label: "Wiedervorlage" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type AnstehendArt = (typeof ANSTEHEND_ARTEN)[number]["value"];
|
export type AnstehendArt = (typeof ANSTEHEND_ARTEN)[number]["value"];
|
||||||
|
|||||||
@@ -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", () => {
|
describe("istEingeschraenkt", () => {
|
||||||
const alle = ANSTEHEND_ARTEN.map((a) => a.value);
|
const alle = ANSTEHEND_ARTEN.map((a) => a.value);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user