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
@@ -2,12 +2,7 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
zeitblockSchema,
|
||||
zeitraumHash,
|
||||
LAGE_FEHLER_TEXT,
|
||||
lagePruefen,
|
||||
} from '@/lib/zeitblock'
|
||||
import { zeitraumHash, LAGE_FEHLER_TEXT, lagePruefen } from '@/lib/zeitblock'
|
||||
import { auditSchreiben } from '@/server/audit'
|
||||
import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung'
|
||||
import { db } from '@/server/db'
|
||||
@@ -18,6 +13,7 @@ import {
|
||||
terminAnlegen,
|
||||
terminEntfernen,
|
||||
} from '@/server/kalender/schreiben'
|
||||
import { betreffFuer, zeitblockAnlegenFuer } from '@/server/zeitblock/dienst'
|
||||
import { jetzt } from '@/server/zeitgeber'
|
||||
|
||||
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 ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -76,99 +67,28 @@ export async function zeitblockAnlegen(daten: FormData): Promise<Rueckmeldung> {
|
||||
if ('antwort' in geprueft) return geprueft.antwort
|
||||
const { akteur } = geprueft
|
||||
|
||||
const gelesen = zeitblockSchema.safeParse({
|
||||
aufgabeId: daten.get('aufgabeId'),
|
||||
kalenderkontoId: daten.get('kalenderkontoId'),
|
||||
beginn: daten.get('beginn'),
|
||||
ende: daten.get('ende'),
|
||||
})
|
||||
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,
|
||||
const ergebnis = await zeitblockAnlegenFuer(
|
||||
{ id: akteur.id, email: akteur.email },
|
||||
{
|
||||
aufgabeId: String(daten.get('aufgabeId') ?? ''),
|
||||
kalenderkontoId: String(daten.get('kalenderkontoId') ?? ''),
|
||||
beginn: new Date(String(daten.get('beginn') ?? '')),
|
||||
ende: new Date(String(daten.get('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) {
|
||||
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,
|
||||
},
|
||||
})
|
||||
if (!ergebnis.ok) return ergebnis
|
||||
|
||||
revalidatePath('/woche')
|
||||
revalidatePath('/aufgaben')
|
||||
revalidatePath('/')
|
||||
|
||||
return schreibfehler
|
||||
? {
|
||||
return ergebnis.geschrieben
|
||||
? { ok: true, id: ergebnis.id, meldung: `„${ergebnis.titel}" eingeplant.` }
|
||||
: {
|
||||
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 -----------------------------------------------------------
|
||||
|
||||
@@ -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