Files
planer/src/app/(app)/woche/page.tsx
T
Florian WildeandClaude Opus 5 0b967d4a33 fix(woche): Wochenansicht aus dem schmalen Inhaltsrahmen befreien
Der Fehler fiel erst im aufgenommenen Bild auf: Bei sieben Spalten plus
Aufgabenliste war der Sonntag abgeschnitten — der Inhaltsrahmen von 64rem
ist für eine Wochenansicht zu eng.

Genau diese eine Seite bricht jetzt aus und nimmt bis zu 100rem, mittig
und mit demselben Seitenabstand wie überall. Die anderen Seiten bleiben
bei ihrer Lesebreite; eine Aufgabenliste über 100rem zu ziehen wäre keine
Verbesserung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 23:32:18 +02:00

214 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Metadata } from 'next'
import Link from 'next/link'
import { CalendarRange } from 'lucide-react'
import { Wochenraster } from '@/app/(app)/woche/wochenraster'
import { Card, CardInhalt } from '@/components/ui/card'
import { faelligkeitEinordnen, faelligkeitText, istUeberfaellig } from '@/lib/aufgabe'
import {
belegteMinuten,
arbeitsfensterAmTag,
kapazitaetAmTag,
} from '@/lib/kalender/belegung'
import { fensterHuelle, rasterBauen } from '@/lib/woche'
import { alsDatum, gleicherTag, tagesBeginn } from '@/lib/zeit'
import { verlangeAnmeldung } from '@/server/berechtigung'
import { db } from '@/server/db'
import { planbareAufgabenLaden, wocheLaden } from '@/server/abfragen/woche'
import { jetzt } from '@/server/zeitgeber'
export const metadata: Metadata = { title: 'Woche' }
const WOCHENTAGE = [
'Montag',
'Dienstag',
'Mittwoch',
'Donnerstag',
'Freitag',
'Samstag',
'Sonntag',
]
export default async function WocheSeite({
searchParams,
}: {
searchParams: Promise<{ versatz?: string }>
}) {
const benutzer = await verlangeAnmeldung('/woche')
const parameter = await searchParams
// Über den Versatz lässt sich blättern; er steht in der Adresse, damit eine
// bestimmte Woche als Lesezeichen taugt.
const versatz = Number.parseInt(parameter.versatz ?? '0', 10)
const sichereWochen = Number.isFinite(versatz)
? Math.max(-52, Math.min(52, versatz))
: 0
const heute = jetzt()
const zeitpunkt = new Date(heute.getTime() + sichereWochen * 7 * 86_400_000)
const [daten, aufgaben, bereiche, konto] = await Promise.all([
wocheLaden(benutzer.id, zeitpunkt),
planbareAufgabenLaden(),
db.bereich.findMany({
where: { aktiv: true },
orderBy: [{ reihenfolge: 'asc' }, { name: 'asc' }],
}),
// Für den ersten Wurf: der Zeitblock landet im ersten verbundenen
// Kalender. Die Auswahl je Bereich kommt, sobald es mehrere gibt.
db.kalenderkonto.findFirst({
where: { benutzerId: benutzer.id, aktiv: true },
orderBy: { erstelltAm: 'asc' },
select: { id: true, anzeigename: true },
}),
])
const huelle = fensterHuelle(bereiche)
const raster = rasterBauen(zeitpunkt, huelle.von, huelle.bis)
// Kapazität und Belegung je Tag: alle Bereiche zusammengenommen, denn ein
// Termin blockiert die Zeit für alles gleichermaßen.
const spalten = raster.spalten.map((spalte) => {
const tag = spalte.tagesBeginn
const kapazitaet = bereiche.reduce((summe, b) => summe + kapazitaetAmTag(b, tag), 0)
const belegend = [
...daten.termine.map((t) => ({
beginn: t.beginn,
ende: t.ende,
ganztaegig: t.ganztaegig,
zeigtAls: t.zeigtAls,
})),
...daten.zeitbloecke.map((z) => ({
beginn: z.beginn,
ende: z.ende,
ganztaegig: false,
zeigtAls: 'GEBUCHT' as const,
})),
]
// Gegen die Hülle rechnen, nicht gegen ein einzelnes Arbeitsfenster:
// die Tageskapazität ist hier die Summe über alle Bereiche.
const fenster = arbeitsfensterAmTag(tag, huelle.von, huelle.bis)
const belegt = belegteMinuten(belegend, fenster)
return {
schluessel: spalte.schluessel,
tagesBeginnMs: spalte.tagesBeginn.getTime(),
name: WOCHENTAGE[spalte.wochentag] ?? '',
datum: alsDatum(tag).split(', ')[1] ?? '',
heute: gleicherTag(tag, heute),
kapazitaet,
belegt,
ueberbucht: kapazitaet > 0 && belegt > kapazitaet,
}
})
const montag = tagesBeginn(raster.spalten[0]?.tagesBeginn ?? zeitpunkt)
const sonntag = raster.spalten[6]?.tagesBeginn ?? zeitpunkt
return (
/*
Die Wochenansicht braucht mehr Breite als der Inhaltsrahmen von 64rem
hergibt – bei sieben Spalten plus Aufgabenliste wäre sonst der Sonntag
abgeschnitten. Deshalb bricht genau diese Seite aus und nimmt bis zu
100rem, mittig und mit demselben Seitenabstand wie überall.
*/
<div className="relative left-1/2 w-screen max-w-[100rem] -translate-x-1/2 px-4 sm:px-6">
<div className="flex flex-col gap-6">
<header className="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 className="flex items-center gap-2 text-2xl font-semibold tracking-tight">
<CalendarRange className="size-6" aria-hidden />
Woche
</h1>
<p className="text-gedaempft-vordergrund mt-1 text-sm">
{alsDatum(montag)} bis {alsDatum(sonntag)}
{konto ? ` · schreibt nach ${konto.anzeigename}` : ''}
</p>
</div>
<nav className="flex items-center gap-2" aria-label="Woche wechseln">
<Link
href={`/woche?versatz=${sichereWochen - 1}`}
className="border-rand hover:bg-gedaempft rounded-md border px-3 py-1.5 text-sm"
>
Zurück
</Link>
<Link
href="/woche"
className="border-rand hover:bg-gedaempft rounded-md border px-3 py-1.5 text-sm"
>
Diese Woche
</Link>
<Link
href={`/woche?versatz=${sichereWochen + 1}`}
className="border-rand hover:bg-gedaempft rounded-md border px-3 py-1.5 text-sm"
>
Weiter
</Link>
</nav>
</header>
{!konto && (
<Card>
<CardInhalt className="py-4 text-sm">
Noch kein Kalender verbunden. Zeitblöcke landen erst dann im Kalender —{' '}
<Link
href="/kalenderkonten"
className="text-betont underline underline-offset-4"
>
jetzt verbinden
</Link>
.
</CardInhalt>
</Card>
)}
<Wochenraster
spalten={spalten}
zeilen={raster.zeilen}
vonMinute={raster.vonMinute}
bisMinute={raster.bisMinute}
kalenderkontoId={konto?.id ?? null}
zeitbloecke={daten.zeitbloecke.map((z) => ({
id: z.id,
beginnMs: z.beginn.getTime(),
endeMs: z.ende.getTime(),
titel: z.titel,
bereichFarbe: z.bereichFarbe,
kalenderName: z.kalenderName,
letzterFehler: z.letzterFehler,
offen: z.offen,
}))}
termine={daten.termine.map((t) => ({
id: t.id,
beginnMs: t.beginn.getTime(),
endeMs: t.ende.getTime(),
betreff: t.betreff,
ganztaegig: t.ganztaegig,
frei: t.zeigtAls === 'FREI',
kalenderName: t.kalenderName,
}))}
aufgaben={aufgaben.map((a) => ({
id: a.id,
titel: a.titel,
aufwandMinuten: a.aufwandMinuten,
bereichName: a.bereich?.name ?? null,
bereichFarbe: a.bereich?.farbe ?? null,
faelligText: a.faelligAm
? faelligkeitText(faelligkeitEinordnen(a.faelligAm, heute))
: null,
ueberfaellig: istUeberfaellig(a.faelligAm, a.status, heute),
bereitsGeplant: a._count.zeitbloecke,
}))}
/>
<p className="text-gedaempft-vordergrund text-xs">
Aufgabe aus der linken Spalte in ein Zeitfenster ziehen, um sie einzuplanen. Ein
Zeitblock lässt sich am Griff verschieben; das Kreuz plant ihn wieder aus. Graue
Balken sind Termine aus dem Kalender — sie lassen sich hier nicht verschieben.
</p>
</div>
</div>
)
}