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:
+11
-1
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user