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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
42c98c8b29
commit
d8d3227c82
+38
-12
@@ -1,6 +1,7 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { AufgabenListe } from '@/components/aufgaben/aufgaben-liste'
|
import { AufgabenListe } from '@/components/aufgaben/aufgaben-liste'
|
||||||
import { Schnellerfassung } from '@/components/aufgaben/schnellerfassung'
|
import { Schnellerfassung } from '@/components/aufgaben/schnellerfassung'
|
||||||
|
import { Fortschrittsbalken } from '@/components/fortschrittsbalken'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardBeschreibung,
|
CardBeschreibung,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
projekteZurAuswahlLaden,
|
projekteZurAuswahlLaden,
|
||||||
zaehlstaende,
|
zaehlstaende,
|
||||||
} from '@/server/abfragen/aufgaben'
|
} from '@/server/abfragen/aufgaben'
|
||||||
|
import { belegungHeute, hatKalenderkonto } from '@/server/abfragen/kalender'
|
||||||
import { verlangeAnmeldung } from '@/server/berechtigung'
|
import { verlangeAnmeldung } from '@/server/berechtigung'
|
||||||
import { db } from '@/server/db'
|
import { db } from '@/server/db'
|
||||||
import { jetzt } from '@/server/zeitgeber'
|
import { jetzt } from '@/server/zeitgeber'
|
||||||
@@ -24,16 +26,19 @@ export default async function StartSeite() {
|
|||||||
const benutzer = await verlangeAnmeldung('/')
|
const benutzer = await verlangeAnmeldung('/')
|
||||||
const heute = jetzt()
|
const heute = jetzt()
|
||||||
|
|
||||||
const [alleOffenen, bereiche, projekte, zahlen, bereichsListe] = await Promise.all([
|
const [alleOffenen, bereiche, projekte, zahlen, bereichsListe, belegung, mitKalender] =
|
||||||
aufgabenLaden({ nurOffen: true, ohneIdeen: true }, 200),
|
await Promise.all([
|
||||||
bereicheLaden(),
|
aufgabenLaden({ nurOffen: true, ohneIdeen: true }, 200),
|
||||||
projekteZurAuswahlLaden(),
|
bereicheLaden(),
|
||||||
zaehlstaende(),
|
projekteZurAuswahlLaden(),
|
||||||
db.bereich.findMany({
|
zaehlstaende(),
|
||||||
where: { aktiv: true },
|
db.bereich.findMany({
|
||||||
orderBy: [{ reihenfolge: 'asc' }, { name: 'asc' }],
|
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.
|
// Was jetzt zählt: überfällig, heute oder morgen fällig.
|
||||||
const dran = alleOffenen.filter(
|
const dran = alleOffenen.filter(
|
||||||
@@ -108,9 +113,10 @@ export default async function StartSeite() {
|
|||||||
<ul className="grid gap-4 sm:grid-cols-2">
|
<ul className="grid gap-4 sm:grid-cols-2">
|
||||||
{bereichsListe.map((bereich) => {
|
{bereichsListe.map((bereich) => {
|
||||||
const offeneHier = alleOffenen.filter((a) => a.bereich?.id === bereich.id)
|
const offeneHier = alleOffenen.filter((a) => a.bereich?.id === bereich.id)
|
||||||
|
const heuteHier = belegung.find((b) => b.id === bereich.id)?.belegung
|
||||||
return (
|
return (
|
||||||
<li key={bereich.id}>
|
<li key={bereich.id}>
|
||||||
<Card>
|
<Card className="h-full">
|
||||||
<CardKopf className="flex-row items-center gap-3">
|
<CardKopf className="flex-row items-center gap-3">
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
@@ -119,13 +125,33 @@ export default async function StartSeite() {
|
|||||||
/>
|
/>
|
||||||
<CardTitel className="text-base">{bereich.name}</CardTitel>
|
<CardTitel className="text-base">{bereich.name}</CardTitel>
|
||||||
</CardKopf>
|
</CardKopf>
|
||||||
<CardInhalt>
|
<CardInhalt className="flex flex-col gap-2">
|
||||||
<p className="text-gedaempft-vordergrund text-sm">
|
<p className="text-gedaempft-vordergrund text-sm">
|
||||||
{offeneHier.length === 0
|
{offeneHier.length === 0
|
||||||
? 'Nichts offen'
|
? 'Nichts offen'
|
||||||
: `${offeneHier.length} offen`}{' '}
|
: `${offeneHier.length} offen`}{' '}
|
||||||
· {alsDauer(wochenKapazitaet(bereich))} Kapazität je Woche
|
· {alsDauer(wochenKapazitaet(bereich))} Kapazität je Woche
|
||||||
</p>
|
</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>
|
</CardInhalt>
|
||||||
</Card>
|
</Card>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'server-only'
|
||||||
|
import {
|
||||||
|
tagesBelegung,
|
||||||
|
termineAmTag,
|
||||||
|
type BelegenderTermin,
|
||||||
|
type TagesBelegung,
|
||||||
|
} from '@/lib/kalender/belegung'
|
||||||
|
import { tagesBeginn, tagesEnde } from '@/lib/zeit'
|
||||||
|
import { db } from '@/server/db'
|
||||||
|
import { jetzt } from '@/server/zeitgeber'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abfragen rund um die gespiegelten Termine.
|
||||||
|
*
|
||||||
|
* Die Termine gehören dem Benutzer über sein Kalenderkonto; die Kennung geht
|
||||||
|
* deshalb immer in die where-Bedingung.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type BereichMitBelegung = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
farbe: string
|
||||||
|
belegung: TagesBelegung
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alle Termine des Benutzers, die diesen Kalendertag berühren. */
|
||||||
|
export async function termineAmTagLaden(
|
||||||
|
benutzerId: string,
|
||||||
|
tag: Date,
|
||||||
|
): Promise<BelegenderTermin[]> {
|
||||||
|
const zeilen = await db.termin.findMany({
|
||||||
|
where: {
|
||||||
|
kalenderkonto: { benutzerId, aktiv: true },
|
||||||
|
beginn: { lte: tagesEnde(tag) },
|
||||||
|
ende: { gt: tagesBeginn(tag) },
|
||||||
|
},
|
||||||
|
select: { beginn: true, ende: true, ganztaegig: true, zeigtAls: true },
|
||||||
|
orderBy: { beginn: 'asc' },
|
||||||
|
})
|
||||||
|
return zeilen
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wie voll heute in jedem Bereich schon ist.
|
||||||
|
*
|
||||||
|
* Alle Bereiche rechnen gegen dieselben Termine — der Planer weiß nicht,
|
||||||
|
* welcher Termin zu welchem Bereich gehört, und ein Termin blockiert die Zeit
|
||||||
|
* ohnehin für alles andere mit.
|
||||||
|
*/
|
||||||
|
export async function belegungHeute(
|
||||||
|
benutzerId: string,
|
||||||
|
zeitpunkt: Date = jetzt(),
|
||||||
|
): Promise<BereichMitBelegung[]> {
|
||||||
|
const [bereiche, termine] = await Promise.all([
|
||||||
|
db.bereich.findMany({
|
||||||
|
where: { aktiv: true },
|
||||||
|
orderBy: [{ reihenfolge: 'asc' }, { name: 'asc' }],
|
||||||
|
}),
|
||||||
|
termineAmTagLaden(benutzerId, zeitpunkt),
|
||||||
|
])
|
||||||
|
|
||||||
|
const heute = termineAmTag(termine, zeitpunkt)
|
||||||
|
|
||||||
|
return bereiche.map((bereich) => ({
|
||||||
|
id: bereich.id,
|
||||||
|
name: bereich.name,
|
||||||
|
farbe: bereich.farbe,
|
||||||
|
belegung: tagesBelegung(bereich, heute, zeitpunkt),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hat der Benutzer überhaupt einen Kalender verbunden? */
|
||||||
|
export async function hatKalenderkonto(benutzerId: string): Promise<boolean> {
|
||||||
|
return (await db.kalenderkonto.count({ where: { benutzerId, aktiv: true } })) > 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user