Wochenrueckblick ueber alle Projekte
CI / pruefen (pull_request) Canceled after 0s

Eine Seite, die aus dem vorhandenen Aktivitaetsverlauf zusammenfasst, was in
einer Woche ueber alle Projekte hinweg passiert ist: Kennzahlen oben, darunter
die Vorgaenge nach Kalendertagen gebuendelt, neuester Tag zuerst. Blaettern in
frueher Wochen ueber ?woche=-n.

Der Verlauf kennt keinen Besitzer - die Trennung laeuft ueber das Projekt,
"project: { ownerId }" steht als erste Bedingung.

Zwei Dinge, die sonst still falsch werden:

- **Wochengrenzen** stehen als pruefbare Funktion in lib/review, nicht in der
  Seite. Gerechnet wird ueber Berliner Kalendertage, nicht ueber "sieben mal
  24 Stunden": liegt eine Zeitumstellung in der Woche, hat sie 167 oder 169
  Stunden. Und im Container laeuft die Anwendung nach UTC, ein Eintrag von
  Sonntagabend fiele sonst in die falsche Woche.
- **"Aufgaben erledigt"** wird am meta-Feld abgelesen (to: "DONE") und nicht
  am Anzeigetext. Der Text ist zum Lesen da und aendert sich mit der
  Formulierung; eine Zaehlung daran wuerde beim naechsten umformulierten
  Einzeiler unbemerkt kippen. Eintraege ohne meta zaehlen nicht mit - lieber
  untertreiben als raten.

Damit completeTaskFromIssue dieselbe Grundlage liefert, schreibt es jetzt
ebenfalls from/to ins meta, wie es das Verschieben von Hand schon tut.

Die Zuordnung Symbol/Farbe je Eintragsart lag bisher in ActivityFeed und wird
jetzt von zwei Stellen gebraucht - herausgezogen nach components/activityKinds,
sonst laufen die Kopien beim naechsten neuen Eintragstyp auseinander.

Erreichbar ueber die Schnellsuche und einen Verweis vom Dashboard. Bewusst
kein vierter Punkt in der Kopfleiste - die ist auf dem Telefon schon eng.

Geprueft: 20 neue Unit-Tests (Wochengrenzen inklusive beider Zeitumstellungen,
Jahreswechsel, Zaehlung), ein E2E-Test ueber Aufruf und Blaettern, axe-core auf
375px und 1280px ueber drei Zustaende - auch die leere Woche - ohne
Beanstandung, kein seitlicher Ueberlauf.

Refs #2
This commit is contained in:
2026-08-18 00:55:02 +02:00
parent 7b60667633
commit 52f2d0ce8e
11 changed files with 827 additions and 35 deletions
+11 -1
View File
@@ -1,4 +1,5 @@
import { CircleDot, GitBranch, Lightbulb, Rocket, Trophy } from "lucide-react";
import Link from "next/link";
import { CalendarRange, CircleDot, GitBranch, Lightbulb, Rocket, Trophy } from "lucide-react";
import { ProjectBoard } from "@/components/ProjectBoard";
import { StatCard } from "@/components/StatCard";
import { requireUserPage } from "@/lib/auth/guard";
@@ -50,6 +51,15 @@ export default async function DashboardPage() {
Ideen festhalten, Status pflegen, Notizen sammeln – und live sehen, was in den
verknuepften Repositories bei GitHub, GitLab und Gitea passiert.
</p>
{/* Kein weiterer Punkt in der Kopfleiste – die ist auf dem Telefon
schon eng. Der Rueckblick haengt dort, wo man ohnehin hinsieht. */}
<Link
href="/rueckblick"
className="mt-3 inline-flex items-center gap-1.5 text-sm text-violet-300 underline-offset-2 hover:underline"
>
<CalendarRange className="h-4 w-4" />
Wochenrückblick ansehen
</Link>
</header>
{/* Zwei Spalten schon auf dem Telefon: einspaltig fuellten die fuenf
+135
View File
@@ -0,0 +1,135 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowLeft, ArrowRight, CalendarRange } from "lucide-react";
import { WeeklyReview } from "@/components/WeeklyReview";
import { requireUserPage } from "@/lib/auth/guard";
import { prisma } from "@/lib/db";
import { tagesschluessel, wocheUm, wochenName, wochenTitel, zaehleWoche } from "@/lib/review";
import type { ReviewDay, ReviewEntry } from "@/types";
export const dynamic = "force-dynamic";
export const metadata: Metadata = { title: "Wochenrückblick" };
/** Wie weit sich zurueckblaettern laesst – der Verlauf haelt ohnehin nur 200 je Projekt. */
const MAX_RUECKBLICK = 25;
export default async function ReviewPage({
searchParams,
}: {
searchParams: Promise<{ woche?: string }>;
}) {
const user = await requireUserPage();
const params = await searchParams;
// Nur nach hinten blaettern: die Zukunft ist im Rueckblick leer.
const gewuenscht = Number(params.woche ?? 0);
const versatz = Number.isFinite(gewuenscht)
? Math.min(0, Math.max(-MAX_RUECKBLICK, Math.trunc(gewuenscht)))
: 0;
const woche = wocheUm(new Date(), versatz);
/**
* Der Verlauf kennt keinen Besitzer – die Trennung laeuft ueber das
* Projekt. Die Bedingung steht deshalb als erste in der where-Klausel.
*/
const activities = await prisma.activity.findMany({
where: {
project: { ownerId: user.id },
createdAt: { gte: woche.von, lt: woche.bis },
},
include: {
project: { select: { id: true, name: true, accent: true } },
user: { select: { username: true, displayName: true } },
},
// meta kommt mit, weil die Kennzahlen daran ablesen, was erledigt
// wurde – verlaesslicher als der Anzeigetext.
orderBy: { createdAt: "desc" },
// Deckel gegen eine Woche, in der ein Abgleich Amok gelaufen ist.
take: 500,
});
/**
* Nach Kalendertagen buendeln – auf dem Server, damit Server-Render und
* Hydration nicht auseinanderlaufen. "Welcher Tag" haengt an der Zeitzone.
*/
const nachTag = new Map<string, ReviewEntry[]>();
for (const eintrag of activities) {
const schluessel = tagesschluessel(eintrag.createdAt);
const liste = nachTag.get(schluessel) ?? [];
liste.push({
id: eintrag.id,
kind: eintrag.kind,
summary: eintrag.summary,
createdAt: eintrag.createdAt.toISOString(),
project: eintrag.project,
user: eintrag.user,
});
nachTag.set(schluessel, liste);
}
// Neuester Tag zuerst, und nur Tage, an denen etwas war.
const tage: ReviewDay[] = [...woche.tage]
.reverse()
.map((tag) => ({ tag, eintraege: nachTag.get(tag) ?? [] }))
.filter((eintrag) => eintrag.eintraege.length > 0);
const kennzahlen = zaehleWoche(activities);
return (
<div className="space-y-5">
<header className="fade-up pt-4">
<p className="text-[11px] font-semibold uppercase tracking-[0.3em] text-violet-300/70">
Über alle Projekte
</p>
<div className="mt-2 flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="text-2xl font-semibold tracking-tight text-white sm:text-3xl">
{wochenName(woche)}
</h1>
<p className="mt-1 flex items-center gap-1.5 text-sm text-slate-400">
<CalendarRange className="h-3.5 w-3.5" />
{wochenTitel(woche)}
</p>
</div>
<nav className="flex items-center gap-2" aria-label="Woche wählen">
<Link
href={`/rueckblick?woche=${versatz - 1}`}
className="btn btn-ghost px-2.5 py-1.5 text-xs"
aria-label="Eine Woche zurück"
// Am Ende des Zeitraums bleibt der Verweis stehen, wird aber
// wirkungslos – die Seite klemmt den Wert ohnehin.
aria-disabled={versatz <= -MAX_RUECKBLICK}
>
<ArrowLeft className="h-3.5 w-3.5" />
Früher
</Link>
{versatz < 0 ? (
<Link
href={`/rueckblick?woche=${versatz + 1}`}
className="btn btn-ghost px-2.5 py-1.5 text-xs"
aria-label="Eine Woche vor"
>
Später
<ArrowRight className="h-3.5 w-3.5" />
</Link>
) : null}
{versatz !== 0 ? (
<Link href="/rueckblick" className="btn btn-ghost px-2.5 py-1.5 text-xs">
Diese Woche
</Link>
) : null}
</nav>
</div>
</header>
<WeeklyReview
tage={tage}
kennzahlen={kennzahlen}
istDieseWoche={versatz === 0}
/>
</div>
);
}