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>
214 lines
7.4 KiB
TypeScript
214 lines
7.4 KiB
TypeScript
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>
|
||
)
|
||
}
|