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>
174 lines
4.8 KiB
TypeScript
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.`,
|
|
}
|
|
}
|