feat(woche): Wochenansicht mit Ziehen und Ablegen
Links die einzuplanenden Aufgaben, rechts sieben Tage im Viertelstundenraster. Eine Aufgabe ins Raster ziehen legt einen Zeitblock an und schreibt ihn in den Kalender; ein Block lässt sich am Griff verschieben, das Kreuz plant ihn wieder aus. Die Dauer eines neuen Blocks kommt aus dem geschätzten Aufwand der Aufgabe — wer schätzt, bekommt die passende Größe geschenkt; ohne Schätzung sind es 60 Minuten. Fremde Termine liegen grau im Hintergrund und lassen sich hier nicht anfassen. Sie gehören dem Kalender, nicht dem Planer. Zwei Details: - Ziehen beginnt erst nach acht Pixeln Bewegung. Sonst löst jeder Klick einen Zug aus und die Knöpfe im Block wären nicht mehr bedienbar. - Ein Zeitblock steht auch im Terminspiegel; er würde sonst doppelt erscheinen, einmal als eigene Planung und einmal als fremder Termin. Die Abfrage filtert ihn heraus. Der Wochenversatz steht in der Adresse, damit eine bestimmte Woche als Lesezeichen taugt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bcb4b8eb02
commit
fada4450bd
@@ -0,0 +1,155 @@
|
||||
import 'server-only'
|
||||
import type { ZeigtAls } from '@/generated/prisma/enums'
|
||||
import { tagesBeginn, wochenBeginn } from '@/lib/zeit'
|
||||
import { db } from '@/server/db'
|
||||
|
||||
/**
|
||||
* Alles, was die Wochenansicht braucht — in einem Zug geladen.
|
||||
*
|
||||
* Die Zeitblöcke des Benutzers und die Termine seiner Kalender kommen
|
||||
* getrennt zurück: ein Zeitblock ist eigene Planung und lässt sich
|
||||
* verschieben, ein Termin ist fremd und wird nur angezeigt.
|
||||
*/
|
||||
|
||||
export type WochenZeitblock = {
|
||||
id: string
|
||||
beginn: Date
|
||||
ende: Date
|
||||
aufgabeId: string
|
||||
titel: string
|
||||
bereichName: string | null
|
||||
bereichFarbe: string | null
|
||||
kalenderName: string
|
||||
letzterFehler: string | null
|
||||
/** Noch nicht in den Kalender geschrieben. */
|
||||
offen: boolean
|
||||
}
|
||||
|
||||
export type WochenTermin = {
|
||||
id: string
|
||||
beginn: Date
|
||||
ende: Date
|
||||
betreff: string | null
|
||||
ganztaegig: boolean
|
||||
zeigtAls: ZeigtAls
|
||||
kalenderName: string
|
||||
}
|
||||
|
||||
export type WocheDaten = {
|
||||
zeitbloecke: WochenZeitblock[]
|
||||
termine: WochenTermin[]
|
||||
}
|
||||
|
||||
/** Beginn und Ende der Woche, in der der Zeitpunkt liegt. */
|
||||
export function wochenFenster(zeitpunkt: Date): { von: Date; bis: Date } {
|
||||
const montag = wochenBeginn(zeitpunkt)
|
||||
return {
|
||||
von: montag,
|
||||
bis: new Date(tagesBeginn(new Date(montag.getTime() + 7 * 86_400_000)).getTime()),
|
||||
}
|
||||
}
|
||||
|
||||
export async function wocheLaden(
|
||||
benutzerId: string,
|
||||
zeitpunkt: Date,
|
||||
): Promise<WocheDaten> {
|
||||
const fenster = wochenFenster(zeitpunkt)
|
||||
|
||||
const [zeitbloecke, termine] = await Promise.all([
|
||||
db.zeitblock.findMany({
|
||||
where: {
|
||||
kalenderkonto: { benutzerId },
|
||||
beginn: { lt: fenster.bis },
|
||||
ende: { gt: fenster.von },
|
||||
},
|
||||
orderBy: { beginn: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
beginn: true,
|
||||
ende: true,
|
||||
extId: true,
|
||||
letzterFehler: true,
|
||||
aufgabe: {
|
||||
select: {
|
||||
id: true,
|
||||
titel: true,
|
||||
bereich: { select: { name: true, farbe: true } },
|
||||
},
|
||||
},
|
||||
kalenderkonto: { select: { anzeigename: true } },
|
||||
},
|
||||
}),
|
||||
db.termin.findMany({
|
||||
where: {
|
||||
kalenderkonto: { benutzerId, aktiv: true },
|
||||
beginn: { lt: fenster.bis },
|
||||
ende: { gt: fenster.von },
|
||||
},
|
||||
orderBy: { beginn: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
extId: true,
|
||||
beginn: true,
|
||||
ende: true,
|
||||
betreff: true,
|
||||
ganztaegig: true,
|
||||
zeigtAls: true,
|
||||
kalenderkonto: { select: { anzeigename: true } },
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
// Ein Zeitblock steht auch im Kalender und damit im Spiegel. Er würde sonst
|
||||
// doppelt erscheinen — einmal als eigene Planung, einmal als fremder Termin.
|
||||
const eigeneExtIds = new Set(
|
||||
zeitbloecke.map((z) => z.extId).filter((id): id is string => id !== null),
|
||||
)
|
||||
|
||||
return {
|
||||
zeitbloecke: zeitbloecke.map((z) => ({
|
||||
id: z.id,
|
||||
beginn: z.beginn,
|
||||
ende: z.ende,
|
||||
aufgabeId: z.aufgabe.id,
|
||||
titel: z.aufgabe.titel,
|
||||
bereichName: z.aufgabe.bereich?.name ?? null,
|
||||
bereichFarbe: z.aufgabe.bereich?.farbe ?? null,
|
||||
kalenderName: z.kalenderkonto.anzeigename,
|
||||
letzterFehler: z.letzterFehler,
|
||||
offen: z.extId === null,
|
||||
})),
|
||||
termine: termine
|
||||
.filter((t) => !eigeneExtIds.has(t.extId))
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
beginn: t.beginn,
|
||||
ende: t.ende,
|
||||
betreff: t.betreff,
|
||||
ganztaegig: t.ganztaegig,
|
||||
zeigtAls: t.zeigtAls,
|
||||
kalenderName: t.kalenderkonto.anzeigename,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/** Aufgaben, die sich einplanen lassen: offen, keine Idee, mit Aufwand. */
|
||||
export async function planbareAufgabenLaden(grenze = 50) {
|
||||
return db.aufgabe.findMany({
|
||||
where: {
|
||||
status: { in: ['OFFEN', 'GEPLANT', 'WARTET'] },
|
||||
typ: { not: 'IDEE' },
|
||||
},
|
||||
orderBy: [{ faelligAm: 'asc' }, { prioritaet: 'asc' }],
|
||||
take: grenze,
|
||||
select: {
|
||||
id: true,
|
||||
titel: true,
|
||||
aufwandMinuten: true,
|
||||
faelligAm: true,
|
||||
prioritaet: true,
|
||||
status: true,
|
||||
bereich: { select: { id: true, name: true, farbe: true } },
|
||||
_count: { select: { zeitbloecke: true } },
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user