Files
planer/src/server/aktionen/projekte.ts
T
Florian WildeandClaude Opus 5 b3ca1c9ce7 feat(aufgaben): Server Actions und Abfrageschicht
Anlegen, ändern, Status setzen, einsortieren, aus einer Idee eine Aufgabe
machen und löschen — jede Aktion prüft die Rolle selbst und hinterlässt
einen Audit-Eintrag.

Statuswechsel gehen nur die Wege, die uebergangErlaubt() zulässt:
Abgeschlossenes wird erst wieder geöffnet, bevor es weitergeht. Wer per
Formular etwas anderes einreicht, bekommt eine Meldung statt eines stillen
Zustandssprungs.

Sortiert wird in der Anwendung, nicht in SQL. Die Regel ist mehrstufig
und steht als geprüfte reine Funktion in src/lib — zwei Fassungen
derselben Regel wären eine Fehlerquelle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:17:43 +02:00

174 lines
4.8 KiB
TypeScript

'use server'
import { revalidatePath } from 'next/cache'
import type { z } from 'zod'
import { projektSchema } from '@/lib/aufgabe'
import { auditSchreiben } from '@/server/audit'
import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung'
import { db } from '@/server/db'
export type Rueckmeldung =
| { ok: true; meldung: string; id?: string }
| { ok: false; fehler: string; felder?: Record<string, string> }
function feldFehler(fehler: z.ZodError): Record<string, string> {
const felder: Record<string, string> = {}
for (const problem of fehler.issues) {
const feld = problem.path.join('.')
felder[feld] ??= problem.message
}
return felder
}
async function akteur() {
return pruefeRolle('BENUTZER')
}
export async function projektAnlegen(daten: FormData): Promise<Rueckmeldung> {
let ich
try {
ich = await akteur()
} catch (fehler) {
if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message }
throw fehler
}
const gelesen = projektSchema.safeParse(Object.fromEntries(daten.entries()))
if (!gelesen.success) {
return {
ok: false,
fehler: 'Bitte die markierten Felder prüfen.',
felder: feldFehler(gelesen.error),
}
}
const vorhanden = await db.projekt.findUnique({
where: { name: gelesen.data.name },
select: { id: true },
})
if (vorhanden) {
return {
ok: false,
fehler: 'Diesen Namen gibt es schon.',
felder: { name: 'Diesen Namen gibt es schon.' },
}
}
const projekt = await db.projekt.create({
data: gelesen.data,
select: { id: true, name: true },
})
await auditSchreiben({
akteur: { id: ich.id, email: ich.email },
aktion: 'projekt.angelegt',
objektTyp: 'Projekt',
objektId: projekt.id,
details: { name: projekt.name },
})
revalidatePath('/projekte')
return { ok: true, id: projekt.id, meldung: `Projekt „${projekt.name}" angelegt.` }
}
export async function projektAendern(id: string, daten: FormData): Promise<Rueckmeldung> {
let ich
try {
ich = await akteur()
} catch (fehler) {
if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message }
throw fehler
}
const gelesen = projektSchema.safeParse(Object.fromEntries(daten.entries()))
if (!gelesen.success) {
return {
ok: false,
fehler: 'Bitte die markierten Felder prüfen.',
felder: feldFehler(gelesen.error),
}
}
const vorher = await db.projekt.findUnique({
where: { id },
select: { id: true, name: true, status: true },
})
if (!vorher) return { ok: false, fehler: 'Dieses Projekt gibt es nicht mehr.' }
const dublette = await db.projekt.findFirst({
where: { name: gelesen.data.name, id: { not: id } },
select: { id: true },
})
if (dublette) {
return {
ok: false,
fehler: 'Diesen Namen gibt es schon.',
felder: { name: 'Diesen Namen gibt es schon.' },
}
}
await db.projekt.update({ where: { id }, data: gelesen.data })
await auditSchreiben({
akteur: { id: ich.id, email: ich.email },
aktion: 'projekt.geaendert',
objektTyp: 'Projekt',
objektId: id,
details: {
vorher: vorher.name,
nachher: gelesen.data.name,
statusVorher: vorher.status,
statusNachher: gelesen.data.status,
},
})
revalidatePath('/projekte')
revalidatePath(`/projekte/${id}`)
return { ok: true, id, meldung: `Projekt „${gelesen.data.name}" gespeichert.` }
}
/**
* Löscht ein Projekt. Die enthaltenen Aufgaben bleiben bestehen und verlieren
* nur ihre Zuordnung — sonst verschwände mit dem Projekt auch die Arbeit.
*/
export async function projektLoeschen(daten: FormData): Promise<Rueckmeldung> {
let ich
try {
ich = await akteur()
} catch (fehler) {
if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message }
throw fehler
}
const id = daten.get('id')
if (typeof id !== 'string' || id.length === 0) {
return { ok: false, fehler: 'Ungültige Anfrage.' }
}
const projekt = await db.projekt.findUnique({
where: { id },
select: { id: true, name: true, _count: { select: { aufgaben: true } } },
})
if (!projekt) return { ok: false, fehler: 'Dieses Projekt gibt es nicht mehr.' }
await db.projekt.delete({ where: { id: projekt.id } })
await auditSchreiben({
akteur: { id: ich.id, email: ich.email },
aktion: 'projekt.geloescht',
objektTyp: 'Projekt',
objektId: projekt.id,
details: { name: projekt.name, aufgabenGeloest: projekt._count.aufgaben },
})
revalidatePath('/projekte')
revalidatePath('/aufgaben')
return {
ok: true,
meldung:
projekt._count.aufgaben > 0
? `Projekt „${projekt.name}" gelöscht. ${projekt._count.aufgaben} Aufgabe(n) bleiben erhalten.`
: `Projekt „${projekt.name}" gelöscht.`,
}
}