Files
planer/src/app/(app)/page.tsx
T
Florian WildeandClaude Opus 5 d8d3227c82 feat(kalender): Belegung des Tages auf der Startseite
Jeder Bereich zeigt, wie voll der heutige Tag schon ist: Termine aus allen
verbundenen Kalendern gegen die Tageskapazität dieses Bereichs.

Alle Kalender zählen gegen jeden Bereich, weil ein Termin die Zeit ohnehin
blockiert, gleich zu welchem Lebensbereich er gehört. Wer um 14 Uhr beim
Zahnarzt sitzt, kann zur selben Zeit nichts für die Firma tun.

Ohne verbundenen Kalender bleibt der Balken weg. Er wäre sonst immer bei
null und sähe nach "alles frei" aus — eine Zahl, die lügt, ist schlechter
als keine.

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

181 lines
6.6 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 Link from 'next/link'
import { AufgabenListe } from '@/components/aufgaben/aufgaben-liste'
import { Schnellerfassung } from '@/components/aufgaben/schnellerfassung'
import { Fortschrittsbalken } from '@/components/fortschrittsbalken'
import {
Card,
CardBeschreibung,
CardInhalt,
CardKopf,
CardTitel,
} from '@/components/ui/card'
import { wochenKapazitaet } from '@/lib/bereich'
import { alsDatum, alsDauer } from '@/lib/zeit'
import {
aufgabenLaden,
bereicheLaden,
projekteZurAuswahlLaden,
zaehlstaende,
} from '@/server/abfragen/aufgaben'
import { belegungHeute, hatKalenderkonto } from '@/server/abfragen/kalender'
import { verlangeAnmeldung } from '@/server/berechtigung'
import { db } from '@/server/db'
import { jetzt } from '@/server/zeitgeber'
export default async function StartSeite() {
const benutzer = await verlangeAnmeldung('/')
const heute = jetzt()
const [alleOffenen, bereiche, projekte, zahlen, bereichsListe, belegung, mitKalender] =
await Promise.all([
aufgabenLaden({ nurOffen: true, ohneIdeen: true }, 200),
bereicheLaden(),
projekteZurAuswahlLaden(),
zaehlstaende(),
db.bereich.findMany({
where: { aktiv: true },
orderBy: [{ reihenfolge: 'asc' }, { name: 'asc' }],
}),
belegungHeute(benutzer.id, heute),
hatKalenderkonto(benutzer.id),
])
// Was jetzt zählt: überfällig, heute oder morgen fällig.
const dran = alleOffenen.filter(
(a) => a.ueberfaellig || a.faelligkeit === 'heute' || a.faelligkeit === 'morgen',
)
const anrede = benutzer.name?.split(' ')[0] ?? benutzer.email
return (
<div className="flex flex-col gap-8">
<header>
<h1 className="text-2xl font-semibold tracking-tight">Guten Tag, {anrede}</h1>
<p className="text-gedaempft-vordergrund mt-1 text-sm">{alsDatum(heute)}</p>
</header>
<Card>
<CardKopf>
<CardTitel className="text-base">Schnell erfassen</CardTitel>
<CardBeschreibung>
Landet in der{' '}
<Link href="/inbox" className="text-betont underline underline-offset-4">
Inbox
</Link>{' '}
und wird später einsortiert.
</CardBeschreibung>
</CardKopf>
<CardInhalt>
<Schnellerfassung />
</CardInhalt>
</Card>
<section className="flex flex-col gap-3">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<h2 className="text-lg font-medium">Jetzt dran</h2>
<Link
href="/aufgaben"
className="text-gedaempft-vordergrund hover:text-vordergrund text-sm"
>
Alle {zahlen.offen} offenen Aufgaben
</Link>
</div>
<AufgabenListe
aufgaben={dran}
bereiche={bereiche}
projekte={projekte}
leerText="Nichts überfällig, nichts heute fällig. Ein guter Tag."
/>
</section>
<section className="flex flex-col gap-3">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<h2 className="text-lg font-medium">Deine Bereiche</h2>
<Link
href="/bereiche"
className="text-gedaempft-vordergrund hover:text-vordergrund text-sm"
>
Bereiche verwalten
</Link>
</div>
{bereichsListe.length === 0 ? (
<Card>
<CardInhalt className="text-gedaempft-vordergrund py-8 text-center text-sm">
Noch keine Bereiche angelegt.{' '}
<Link href="/bereiche" className="text-betont underline underline-offset-4">
Jetzt einrichten
</Link>
</CardInhalt>
</Card>
) : (
<ul className="grid gap-4 sm:grid-cols-2">
{bereichsListe.map((bereich) => {
const offeneHier = alleOffenen.filter((a) => a.bereich?.id === bereich.id)
const heuteHier = belegung.find((b) => b.id === bereich.id)?.belegung
return (
<li key={bereich.id}>
<Card className="h-full">
<CardKopf className="flex-row items-center gap-3">
<span
aria-hidden
className="size-3 shrink-0 rounded-full ring-1 ring-black/10"
style={{ backgroundColor: bereich.farbe }}
/>
<CardTitel className="text-base">{bereich.name}</CardTitel>
</CardKopf>
<CardInhalt className="flex flex-col gap-2">
<p className="text-gedaempft-vordergrund text-sm">
{offeneHier.length === 0
? 'Nichts offen'
: `${offeneHier.length} offen`}{' '}
· {alsDauer(wochenKapazitaet(bereich))} Kapazität je Woche
</p>
{/*
Ohne verbundenen Kalender wäre die Belegung immer null –
das sähe nach "alles frei" aus und wäre irreführend.
*/}
{mitKalender && heuteHier && (
<>
<Fortschrittsbalken
prozent={heuteHier.prozent}
beschriftung={`Belegung heute in ${bereich.name}`}
/>
<p className="text-gedaempft-vordergrund text-xs">
{heuteHier.kapazitaet === 0
? 'Heute nicht eingeplant'
: heuteHier.ueberbucht
? `Heute überbucht: ${alsDauer(heuteHier.belegt)} Termine bei ${alsDauer(heuteHier.kapazitaet)} Kapazität`
: `Heute noch ${alsDauer(heuteHier.frei)} frei von ${alsDauer(heuteHier.kapazitaet)}`}
</p>
</>
)}
</CardInhalt>
</Card>
</li>
)
})}
</ul>
)}
</section>
{zahlen.ideen > 0 && (
<Card>
<CardKopf>
<CardTitel className="text-base">
<Link href="/ideen" className="hover:underline hover:underline-offset-4">
{zahlen.ideen} Idee(n) warten
</Link>
</CardTitel>
<CardBeschreibung>
Ideen drängen sich nicht in die Aufgabenliste — bis du eine übernimmst.
</CardBeschreibung>
</CardKopf>
</Card>
)}
</div>
)
}