refactor: Einplanen aus der Serveraktion herausloesen
Die Oberflaeche und der MCP-Server sollen dieselbe Stelle benutzen. Zwei Umsetzungen desselben Vorgangs waeren zwei Gelegenheiten, die Regeln unterschiedlich hinzuschreiben — und der Kalenderschreibweg samt Schleifenschutz ist nichts, was man zweimal richtig hinbekommt. Der Kern liegt bewusst nicht in einer Datei mit 'use server': Dort wird jede exportierte Funktion zu einem Endpunkt, den der Browser aufrufen kann. Eine Funktion, die den Akteur als Parameter nimmt, waere dann ein offenes Tor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e2642e51a7
commit
5f45dff1a7
Generated
+784
-29
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@
|
|||||||
"@date-fns/tz": "1.5.0",
|
"@date-fns/tz": "1.5.0",
|
||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||||
"@prisma/adapter-pg": "7.10.0",
|
"@prisma/adapter-pg": "7.10.0",
|
||||||
"@prisma/client": "7.10.0",
|
"@prisma/client": "7.10.0",
|
||||||
"@radix-ui/react-avatar": "1.2.6",
|
"@radix-ui/react-avatar": "1.2.6",
|
||||||
|
|||||||
@@ -2,12 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import {
|
import { zeitraumHash, LAGE_FEHLER_TEXT, lagePruefen } from '@/lib/zeitblock'
|
||||||
zeitblockSchema,
|
|
||||||
zeitraumHash,
|
|
||||||
LAGE_FEHLER_TEXT,
|
|
||||||
lagePruefen,
|
|
||||||
} from '@/lib/zeitblock'
|
|
||||||
import { auditSchreiben } from '@/server/audit'
|
import { auditSchreiben } from '@/server/audit'
|
||||||
import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung'
|
import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung'
|
||||||
import { db } from '@/server/db'
|
import { db } from '@/server/db'
|
||||||
@@ -18,6 +13,7 @@ import {
|
|||||||
terminAnlegen,
|
terminAnlegen,
|
||||||
terminEntfernen,
|
terminEntfernen,
|
||||||
} from '@/server/kalender/schreiben'
|
} from '@/server/kalender/schreiben'
|
||||||
|
import { betreffFuer, zeitblockAnlegenFuer } from '@/server/zeitblock/dienst'
|
||||||
import { jetzt } from '@/server/zeitgeber'
|
import { jetzt } from '@/server/zeitgeber'
|
||||||
|
|
||||||
export type Rueckmeldung =
|
export type Rueckmeldung =
|
||||||
@@ -56,11 +52,6 @@ async function eigenerZeitblock(id: string, benutzerId: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Was im Kalender als Betreff steht. */
|
|
||||||
function betreffFuer(titel: string): string {
|
|
||||||
return titel
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Anlegen ---------------------------------------------------------------
|
// --- Anlegen ---------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,99 +67,28 @@ export async function zeitblockAnlegen(daten: FormData): Promise<Rueckmeldung> {
|
|||||||
if ('antwort' in geprueft) return geprueft.antwort
|
if ('antwort' in geprueft) return geprueft.antwort
|
||||||
const { akteur } = geprueft
|
const { akteur } = geprueft
|
||||||
|
|
||||||
const gelesen = zeitblockSchema.safeParse({
|
const ergebnis = await zeitblockAnlegenFuer(
|
||||||
aufgabeId: daten.get('aufgabeId'),
|
{ id: akteur.id, email: akteur.email },
|
||||||
kalenderkontoId: daten.get('kalenderkontoId'),
|
{
|
||||||
beginn: daten.get('beginn'),
|
aufgabeId: String(daten.get('aufgabeId') ?? ''),
|
||||||
ende: daten.get('ende'),
|
kalenderkontoId: String(daten.get('kalenderkontoId') ?? ''),
|
||||||
})
|
beginn: new Date(String(daten.get('beginn') ?? '')),
|
||||||
if (!gelesen.success) {
|
ende: new Date(String(daten.get('ende') ?? '')),
|
||||||
const lage = gelesen.error.issues.find((i) => i.path.includes('ende'))
|
|
||||||
return { ok: false, fehler: lage?.message ?? 'Die Angaben stimmen nicht.' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const [aufgabe, konto] = await Promise.all([
|
|
||||||
db.aufgabe.findUnique({
|
|
||||||
where: { id: gelesen.data.aufgabeId },
|
|
||||||
select: { id: true, titel: true, notiz: true, status: true },
|
|
||||||
}),
|
|
||||||
db.kalenderkonto.findFirst({
|
|
||||||
where: { id: gelesen.data.kalenderkontoId, benutzerId: akteur.id, aktiv: true },
|
|
||||||
select: { id: true, anbieter: true, kalenderId: true, anzeigename: true },
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!aufgabe) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht.' }
|
|
||||||
if (!konto) return { ok: false, fehler: 'Dieses Kalenderkonto gibt es nicht.' }
|
|
||||||
|
|
||||||
const zeitblock = await db.zeitblock.create({
|
|
||||||
data: {
|
|
||||||
aufgabeId: aufgabe.id,
|
|
||||||
kalenderkontoId: konto.id,
|
|
||||||
beginn: gelesen.data.beginn,
|
|
||||||
ende: gelesen.data.ende,
|
|
||||||
},
|
},
|
||||||
select: { id: true },
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// Eine eingeplante Aufgabe ist geplant, nicht mehr bloß offen.
|
if (!ergebnis.ok) return ergebnis
|
||||||
if (aufgabe.status === 'OFFEN') {
|
|
||||||
await db.aufgabe.update({ where: { id: aufgabe.id }, data: { status: 'GEPLANT' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
let schreibfehler: string | null = null
|
|
||||||
try {
|
|
||||||
const token = await gueltigesTokenFuer(konto.id, jetzt())
|
|
||||||
const ergebnis = await terminAnlegen({
|
|
||||||
anbieter: konto.anbieter,
|
|
||||||
kalenderId: konto.kalenderId,
|
|
||||||
token,
|
|
||||||
betreff: betreffFuer(aufgabe.titel),
|
|
||||||
beschreibung: aufgabe.notiz,
|
|
||||||
beginn: gelesen.data.beginn,
|
|
||||||
ende: gelesen.data.ende,
|
|
||||||
})
|
|
||||||
|
|
||||||
await db.zeitblock.update({
|
|
||||||
where: { id: zeitblock.id },
|
|
||||||
data: {
|
|
||||||
extId: ergebnis.extId,
|
|
||||||
etag: ergebnis.etag,
|
|
||||||
hash: zeitraumHash(gelesen.data.beginn, gelesen.data.ende),
|
|
||||||
letzterFehler: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (fehler) {
|
|
||||||
schreibfehler =
|
|
||||||
fehler instanceof Error ? fehler.message : 'Der Termin ließ sich nicht anlegen.'
|
|
||||||
await db.zeitblock.update({
|
|
||||||
where: { id: zeitblock.id },
|
|
||||||
data: { letzterFehler: schreibfehler },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await auditSchreiben({
|
|
||||||
akteur: { id: akteur.id, email: akteur.email },
|
|
||||||
aktion: 'zeitblock.angelegt',
|
|
||||||
objektTyp: 'Zeitblock',
|
|
||||||
objektId: zeitblock.id,
|
|
||||||
details: {
|
|
||||||
aufgabe: aufgabe.titel,
|
|
||||||
kalender: konto.anzeigename,
|
|
||||||
geschrieben: schreibfehler === null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
revalidatePath('/woche')
|
revalidatePath('/woche')
|
||||||
revalidatePath('/aufgaben')
|
revalidatePath('/aufgaben')
|
||||||
revalidatePath('/')
|
revalidatePath('/')
|
||||||
|
|
||||||
return schreibfehler
|
return ergebnis.geschrieben
|
||||||
? {
|
? { ok: true, id: ergebnis.id, meldung: `„${ergebnis.titel}" eingeplant.` }
|
||||||
|
: {
|
||||||
ok: false,
|
ok: false,
|
||||||
fehler: `Eingeplant, aber nicht in den Kalender geschrieben: ${schreibfehler}`,
|
fehler: `Eingeplant, aber nicht in den Kalender geschrieben: ${ergebnis.fehler ?? 'unbekannter Fehler'}`,
|
||||||
}
|
}
|
||||||
: { ok: true, id: zeitblock.id, meldung: `„${aufgabe.titel}" eingeplant.` }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Verschieben -----------------------------------------------------------
|
// --- Verschieben -----------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import 'server-only'
|
||||||
|
import { zeitblockSchema, zeitraumHash } from '@/lib/zeitblock'
|
||||||
|
import { auditSchreiben } from '@/server/audit'
|
||||||
|
import { db } from '@/server/db'
|
||||||
|
import { gueltigesTokenFuer } from '@/server/kalender/abgleich'
|
||||||
|
import { terminAnlegen } from '@/server/kalender/schreiben'
|
||||||
|
import { jetzt } from '@/server/zeitgeber'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Einplanen selbst — ohne Sitzung, ohne Formular.
|
||||||
|
*
|
||||||
|
* Die Serveraktion aus der Oberfläche und der MCP-Server benutzen dieselbe
|
||||||
|
* Stelle. Zwei Umsetzungen desselben Vorgangs wären zwei Gelegenheiten, die
|
||||||
|
* Regeln unterschiedlich hinzuschreiben — und der Kalenderschreibweg samt
|
||||||
|
* Schleifenschutz ist nichts, was man zweimal richtig hinbekommt.
|
||||||
|
*
|
||||||
|
* Bewusst **nicht** in einer Datei mit `'use server'`: Dort wird jede
|
||||||
|
* exportierte Funktion zu einem Endpunkt, den der Browser aufrufen kann. Eine
|
||||||
|
* Funktion, die den Akteur als Parameter nimmt, wäre dann ein offenes Tor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Akteur = { id: string; email: string }
|
||||||
|
|
||||||
|
export type Einplanung = {
|
||||||
|
aufgabeId: string
|
||||||
|
kalenderkontoId: string
|
||||||
|
beginn: Date
|
||||||
|
ende: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Einplanungsergebnis =
|
||||||
|
| { ok: true; id: string; titel: string; geschrieben: boolean; fehler: string | null }
|
||||||
|
| { ok: false; fehler: string }
|
||||||
|
|
||||||
|
/** Legt einen Zeitblock an und schreibt ihn in den Kalender. */
|
||||||
|
export async function zeitblockAnlegenFuer(
|
||||||
|
akteur: Akteur,
|
||||||
|
eingabe: Einplanung,
|
||||||
|
): Promise<Einplanungsergebnis> {
|
||||||
|
const gelesen = zeitblockSchema.safeParse(eingabe)
|
||||||
|
if (!gelesen.success) {
|
||||||
|
const lage = gelesen.error.issues.find((i) => i.path.includes('ende'))
|
||||||
|
return { ok: false, fehler: lage?.message ?? 'Die Angaben stimmen nicht.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [aufgabe, konto] = await Promise.all([
|
||||||
|
db.aufgabe.findUnique({
|
||||||
|
where: { id: gelesen.data.aufgabeId },
|
||||||
|
select: { id: true, titel: true, notiz: true, status: true },
|
||||||
|
}),
|
||||||
|
db.kalenderkonto.findFirst({
|
||||||
|
where: { id: gelesen.data.kalenderkontoId, benutzerId: akteur.id, aktiv: true },
|
||||||
|
select: { id: true, anbieter: true, kalenderId: true, anzeigename: true },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!aufgabe) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht.' }
|
||||||
|
if (!konto) return { ok: false, fehler: 'Dieses Kalenderkonto gibt es nicht.' }
|
||||||
|
|
||||||
|
const zeitblock = await db.zeitblock.create({
|
||||||
|
data: {
|
||||||
|
aufgabeId: aufgabe.id,
|
||||||
|
kalenderkontoId: konto.id,
|
||||||
|
beginn: gelesen.data.beginn,
|
||||||
|
ende: gelesen.data.ende,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Eine eingeplante Aufgabe ist geplant, nicht mehr bloß offen.
|
||||||
|
if (aufgabe.status === 'OFFEN') {
|
||||||
|
await db.aufgabe.update({ where: { id: aufgabe.id }, data: { status: 'GEPLANT' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
let schreibfehler: string | null = null
|
||||||
|
try {
|
||||||
|
const token = await gueltigesTokenFuer(konto.id, jetzt())
|
||||||
|
const ergebnis = await terminAnlegen({
|
||||||
|
anbieter: konto.anbieter,
|
||||||
|
kalenderId: konto.kalenderId,
|
||||||
|
token,
|
||||||
|
betreff: betreffFuer(aufgabe.titel),
|
||||||
|
beschreibung: aufgabe.notiz,
|
||||||
|
beginn: gelesen.data.beginn,
|
||||||
|
ende: gelesen.data.ende,
|
||||||
|
})
|
||||||
|
|
||||||
|
await db.zeitblock.update({
|
||||||
|
where: { id: zeitblock.id },
|
||||||
|
data: {
|
||||||
|
extId: ergebnis.extId,
|
||||||
|
etag: ergebnis.etag,
|
||||||
|
hash: zeitraumHash(gelesen.data.beginn, gelesen.data.ende),
|
||||||
|
letzterFehler: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (fehler) {
|
||||||
|
// Die Planung überlebt einen Netzhänger; der Zeitblock bleibt mit
|
||||||
|
// vermerktem Fehler stehen.
|
||||||
|
schreibfehler =
|
||||||
|
fehler instanceof Error ? fehler.message : 'Der Termin ließ sich nicht anlegen.'
|
||||||
|
await db.zeitblock.update({
|
||||||
|
where: { id: zeitblock.id },
|
||||||
|
data: { letzterFehler: schreibfehler },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await auditSchreiben({
|
||||||
|
akteur,
|
||||||
|
aktion: 'zeitblock.angelegt',
|
||||||
|
objektTyp: 'Zeitblock',
|
||||||
|
objektId: zeitblock.id,
|
||||||
|
details: {
|
||||||
|
aufgabe: aufgabe.titel,
|
||||||
|
kalender: konto.anzeigename,
|
||||||
|
geschrieben: schreibfehler === null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
id: zeitblock.id,
|
||||||
|
titel: aufgabe.titel,
|
||||||
|
geschrieben: schreibfehler === null,
|
||||||
|
fehler: schreibfehler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Der Betreff, unter dem ein Zeitblock im Kalender steht. */
|
||||||
|
export function betreffFuer(titel: string): string {
|
||||||
|
return titel
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user