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>
This commit is contained in:
Florian Wilde
2026-09-10 16:17:43 +02:00
co-authored by Claude Opus 5
parent 63c3c15a3c
commit b3ca1c9ce7
3 changed files with 739 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
import 'server-only'
import type { Prisma } from '@/generated/prisma/client'
import type { AufgabenStatus, AufgabenTyp } from '@/generated/prisma/enums'
import { nachDringlichkeit } from '@/lib/aufgabe'
import { fuerAnzeige, type AufgabeAnzeige } from '@/lib/aufgabe-anzeige'
import { tagesBeginn } from '@/lib/zeit'
import { db } from '@/server/db'
import { jetzt } from '@/server/zeitgeber'
/**
* Abfragen rund um Aufgaben.
*
* Alle Seiten laden über diese Stelle, damit Auswahl, Sortierung und
* Aufbereitung überall gleich sind.
*/
const AUSWAHL = {
id: true,
titel: true,
notiz: true,
typ: true,
status: true,
prioritaet: true,
aufwandMinuten: true,
faelligAm: true,
quelle: true,
quelleUrl: true,
bereich: { select: { id: true, name: true, farbe: true } },
projekt: { select: { id: true, name: true } },
} satisfies Prisma.AufgabeSelect
export type AufgabenFilter = {
bereichId?: string | undefined
projektId?: string | undefined
status?: AufgabenStatus | undefined
typ?: AufgabenTyp | undefined
/** Nur Aufgaben ohne Bereich (Inbox). */
nurInbox?: boolean | undefined
/** Nur was noch Arbeit bedeutet. */
nurOffen?: boolean | undefined
/** Freitext über Titel und Notiz. */
suche?: string | undefined
}
function alsWhere(filter: AufgabenFilter): Prisma.AufgabeWhereInput {
const where: Prisma.AufgabeWhereInput = {}
if (filter.nurInbox) where.bereichId = null
else if (filter.bereichId) where.bereichId = filter.bereichId
if (filter.projektId) where.projektId = filter.projektId
if (filter.typ) where.typ = filter.typ
if (filter.status) where.status = filter.status
else if (filter.nurOffen) where.status = { in: ['OFFEN', 'GEPLANT', 'WARTET'] }
if (filter.suche) {
where.OR = [
{ titel: { contains: filter.suche, mode: 'insensitive' } },
{ notiz: { contains: filter.suche, mode: 'insensitive' } },
]
}
return where
}
/**
* Lädt Aufgaben und bringt sie in die Reihenfolge der Dringlichkeit.
*
* Sortiert wird bewusst in der Anwendung, nicht in SQL: die Regel ist
* mehrstufig (offen vor abgeschlossen, dann Termin, dann Priorität, dann
* Titel mit deutscher Sortierung) und steht als geprüfte reine Funktion in
* src/lib. Zwei Fassungen derselben Regel wären eine Fehlerquelle.
*/
export async function aufgabenLaden(
filter: AufgabenFilter = {},
grenze = 500,
): Promise<AufgabeAnzeige[]> {
const zeilen = await db.aufgabe.findMany({
where: alsWhere(filter),
select: AUSWAHL,
orderBy: [{ faelligAm: 'asc' }, { prioritaet: 'asc' }],
take: grenze,
})
const zeitpunkt = jetzt()
return zeilen
.slice()
.sort(nachDringlichkeit)
.map((zeile) => fuerAnzeige(zeile, zeitpunkt))
}
/** Bereiche zur Auswahl in Formularen und Filtern. */
export async function bereicheLaden() {
return db.bereich.findMany({
where: { aktiv: true },
orderBy: [{ reihenfolge: 'asc' }, { name: 'asc' }],
select: { id: true, name: true, farbe: true },
})
}
/** Projekte zur Auswahl; abgeschlossene bleiben draußen. */
export async function projekteZurAuswahlLaden() {
return db.projekt.findMany({
where: { status: { in: ['AKTIV', 'PAUSIERT'] } },
orderBy: { name: 'asc' },
select: { id: true, name: true },
})
}
/** Zählstände für die Navigation und die Startseite. */
export async function zaehlstaende(): Promise<{
inbox: number
offen: number
ueberfaellig: number
ideen: number
}> {
const zeitpunkt = jetzt()
const heuteBeginn = tagesBeginn(zeitpunkt)
const [inbox, offen, ueberfaellig, ideen] = await Promise.all([
db.aufgabe.count({
where: {
bereichId: null,
typ: { not: 'IDEE' },
status: { in: ['OFFEN', 'GEPLANT', 'WARTET'] },
},
}),
db.aufgabe.count({
where: { typ: { not: 'IDEE' }, status: { in: ['OFFEN', 'GEPLANT', 'WARTET'] } },
}),
db.aufgabe.count({
where: {
status: { in: ['OFFEN', 'GEPLANT', 'WARTET'] },
faelligAm: { lt: heuteBeginn },
},
}),
db.aufgabe.count({
where: { typ: 'IDEE', status: { in: ['OFFEN', 'GEPLANT', 'WARTET'] } },
}),
])
return { inbox, offen, ueberfaellig, ideen }
}
+422
View File
@@ -0,0 +1,422 @@
'use server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
import type { AufgabenStatus } from '@/generated/prisma/enums'
import {
aufgabeSchema,
istAbgeschlossen,
schnellerfassungSchema,
uebergangErlaubt,
} from '@/lib/aufgabe'
import { auditSchreiben } from '@/server/audit'
import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung'
import { db } from '@/server/db'
import { jetzt } from '@/server/zeitgeber'
/**
* Server Actions für Aufgaben.
*
* Jede prüft die Rolle selbst — die Seite tut es ebenfalls, aber eine Action
* ist ein eigener Einstiegspunkt. Jede Änderung hinterlässt einen
* Audit-Eintrag.
*/
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
}
/** Prüft die Rolle und gibt bei Ablehnung eine Rückmeldung statt zu werfen. */
async function akteurOder(
rueckgabe: (fehler: string) => Rueckmeldung,
): Promise<
{ akteur: Awaited<ReturnType<typeof pruefeRolle>> } | { antwort: Rueckmeldung }
> {
try {
return { akteur: await pruefeRolle('BENUTZER') }
} catch (fehler) {
if (fehler instanceof NichtErlaubt) return { antwort: rueckgabe(fehler.message) }
throw fehler
}
}
function alsFormularwerte(daten: FormData): Record<string, unknown> {
return Object.fromEntries(daten.entries())
}
/**
* Prüft, dass angegebener Bereich und Projekt wirklich existieren. Ein
* erfundener Fremdschlüssel soll eine Meldung geben, keinen Datenbankfehler.
*/
async function verweiseGueltig(
bereichId: string | null,
projektId: string | null,
): Promise<Record<string, string> | null> {
const felder: Record<string, string> = {}
if (bereichId) {
const bereich = await db.bereich.findUnique({
where: { id: bereichId },
select: { id: true },
})
if (!bereich) felder.bereichId = 'Diesen Bereich gibt es nicht.'
}
if (projektId) {
const projekt = await db.projekt.findUnique({
where: { id: projektId },
select: { id: true },
})
if (!projekt) felder.projektId = 'Dieses Projekt gibt es nicht.'
}
return Object.keys(felder).length > 0 ? felder : null
}
// --- Schnellerfassung ------------------------------------------------------
/**
* Erfasst mit einem einzigen Feld. Ohne Bereich landet die Aufgabe in der
* Inbox und wird später einsortiert.
*/
export async function schnellErfassen(daten: FormData): Promise<Rueckmeldung> {
const geprueft = await akteurOder((fehler) => ({ ok: false, fehler }))
if ('antwort' in geprueft) return geprueft.antwort
const { akteur } = geprueft
const gelesen = schnellerfassungSchema.safeParse({
titel: daten.get('titel'),
typ: daten.get('typ') ?? 'AUFGABE',
})
if (!gelesen.success) {
return {
ok: false,
fehler: 'Bitte einen Titel angeben.',
felder: feldFehler(gelesen.error),
}
}
const aufgabe = await db.aufgabe.create({
data: { titel: gelesen.data.titel, typ: gelesen.data.typ },
select: { id: true, titel: true, typ: true },
})
await auditSchreiben({
akteur: { id: akteur.id, email: akteur.email },
aktion: 'aufgabe.angelegt',
objektTyp: 'Aufgabe',
objektId: aufgabe.id,
details: { titel: aufgabe.titel, typ: aufgabe.typ, weg: 'schnellerfassung' },
})
revalidatePath('/inbox')
revalidatePath('/aufgaben')
revalidatePath('/ideen')
return {
ok: true,
id: aufgabe.id,
meldung:
gelesen.data.typ === 'IDEE' ? 'Idee notiert.' : 'Aufgabe in der Inbox abgelegt.',
}
}
// --- Anlegen und Ändern ----------------------------------------------------
export async function aufgabeAnlegen(daten: FormData): Promise<Rueckmeldung> {
const geprueft = await akteurOder((fehler) => ({ ok: false, fehler }))
if ('antwort' in geprueft) return geprueft.antwort
const { akteur } = geprueft
const gelesen = aufgabeSchema.safeParse(alsFormularwerte(daten))
if (!gelesen.success) {
return {
ok: false,
fehler: 'Bitte die markierten Felder prüfen.',
felder: feldFehler(gelesen.error),
}
}
const verweisFehler = await verweiseGueltig(
gelesen.data.bereichId,
gelesen.data.projektId,
)
if (verweisFehler) {
return { ok: false, fehler: 'Bitte die Zuordnung prüfen.', felder: verweisFehler }
}
const aufgabe = await db.aufgabe.create({
data: {
...gelesen.data,
erledigtAm: istAbgeschlossen(gelesen.data.status) ? jetzt() : null,
},
select: { id: true, titel: true },
})
await auditSchreiben({
akteur: { id: akteur.id, email: akteur.email },
aktion: 'aufgabe.angelegt',
objektTyp: 'Aufgabe',
objektId: aufgabe.id,
details: { titel: aufgabe.titel, typ: gelesen.data.typ, status: gelesen.data.status },
})
revalidatePath('/aufgaben')
revalidatePath('/inbox')
revalidatePath('/ideen')
revalidatePath('/projekte')
return { ok: true, id: aufgabe.id, meldung: `„${aufgabe.titel}" angelegt.` }
}
export async function aufgabeAendern(id: string, daten: FormData): Promise<Rueckmeldung> {
const geprueft = await akteurOder((fehler) => ({ ok: false, fehler }))
if ('antwort' in geprueft) return geprueft.antwort
const { akteur } = geprueft
const gelesen = aufgabeSchema.safeParse(alsFormularwerte(daten))
if (!gelesen.success) {
return {
ok: false,
fehler: 'Bitte die markierten Felder prüfen.',
felder: feldFehler(gelesen.error),
}
}
const vorher = await db.aufgabe.findUnique({
where: { id },
select: { id: true, titel: true, status: true, erledigtAm: true },
})
if (!vorher) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht mehr.' }
if (!uebergangErlaubt(vorher.status, gelesen.data.status)) {
return {
ok: false,
fehler:
'Dieser Statuswechsel ist nicht vorgesehen. Abgeschlossenes wird zuerst wieder geöffnet.',
felder: { status: 'Nicht möglich von hier aus.' },
}
}
const verweisFehler = await verweiseGueltig(
gelesen.data.bereichId,
gelesen.data.projektId,
)
if (verweisFehler) {
return { ok: false, fehler: 'Bitte die Zuordnung prüfen.', felder: verweisFehler }
}
const wirdAbgeschlossen =
istAbgeschlossen(gelesen.data.status) && !istAbgeschlossen(vorher.status)
const wirdGeoeffnet =
!istAbgeschlossen(gelesen.data.status) && istAbgeschlossen(vorher.status)
await db.aufgabe.update({
where: { id },
data: {
...gelesen.data,
...(wirdAbgeschlossen ? { erledigtAm: jetzt() } : {}),
...(wirdGeoeffnet ? { erledigtAm: null } : {}),
},
})
await auditSchreiben({
akteur: { id: akteur.id, email: akteur.email },
aktion: 'aufgabe.geaendert',
objektTyp: 'Aufgabe',
objektId: id,
details: {
titel: gelesen.data.titel,
statusVorher: vorher.status,
statusNachher: gelesen.data.status,
},
})
revalidatePath('/aufgaben')
revalidatePath('/inbox')
revalidatePath('/ideen')
revalidatePath('/projekte')
return { ok: true, id, meldung: `„${gelesen.data.titel}" gespeichert.` }
}
// --- Kleine Handgriffe -----------------------------------------------------
const statusEingabe = z.object({
id: z.string().min(1),
status: z.enum(['OFFEN', 'GEPLANT', 'WARTET', 'ERLEDIGT', 'VERWORFEN']),
})
/** Statuswechsel mit einem Klick, etwa das Abhaken in der Liste. */
export async function statusSetzen(daten: FormData): Promise<Rueckmeldung> {
const geprueft = await akteurOder((fehler) => ({ ok: false, fehler }))
if ('antwort' in geprueft) return geprueft.antwort
const { akteur } = geprueft
const gelesen = statusEingabe.safeParse({
id: daten.get('id'),
status: daten.get('status'),
})
if (!gelesen.success) return { ok: false, fehler: 'Ungültige Anfrage.' }
const vorher = await db.aufgabe.findUnique({
where: { id: gelesen.data.id },
select: { id: true, titel: true, status: true },
})
if (!vorher) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht mehr.' }
const neu: AufgabenStatus = gelesen.data.status
if (!uebergangErlaubt(vorher.status, neu)) {
return { ok: false, fehler: 'Dieser Statuswechsel ist nicht vorgesehen.' }
}
await db.aufgabe.update({
where: { id: vorher.id },
data: {
status: neu,
erledigtAm: istAbgeschlossen(neu) ? jetzt() : null,
},
})
await auditSchreiben({
akteur: { id: akteur.id, email: akteur.email },
aktion: 'aufgabe.status',
objektTyp: 'Aufgabe',
objektId: vorher.id,
details: { titel: vorher.titel, vorher: vorher.status, nachher: neu },
})
revalidatePath('/aufgaben')
revalidatePath('/inbox')
revalidatePath('/ideen')
revalidatePath('/projekte')
return { ok: true, meldung: `„${vorher.titel}" ist jetzt ${neu.toLowerCase()}.` }
}
const einsortierenEingabe = z.object({
id: z.string().min(1),
bereichId: z.string().min(1),
})
/** Aus der Inbox in einen Bereich schieben. */
export async function einsortieren(daten: FormData): Promise<Rueckmeldung> {
const geprueft = await akteurOder((fehler) => ({ ok: false, fehler }))
if ('antwort' in geprueft) return geprueft.antwort
const { akteur } = geprueft
const gelesen = einsortierenEingabe.safeParse({
id: daten.get('id'),
bereichId: daten.get('bereichId'),
})
if (!gelesen.success) return { ok: false, fehler: 'Bitte einen Bereich wählen.' }
const [aufgabe, bereich] = await Promise.all([
db.aufgabe.findUnique({
where: { id: gelesen.data.id },
select: { id: true, titel: true },
}),
db.bereich.findUnique({
where: { id: gelesen.data.bereichId },
select: { id: true, name: true },
}),
])
if (!aufgabe) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht mehr.' }
if (!bereich) return { ok: false, fehler: 'Diesen Bereich gibt es nicht.' }
await db.aufgabe.update({
where: { id: aufgabe.id },
data: { bereichId: bereich.id },
})
await auditSchreiben({
akteur: { id: akteur.id, email: akteur.email },
aktion: 'aufgabe.einsortiert',
objektTyp: 'Aufgabe',
objektId: aufgabe.id,
details: { titel: aufgabe.titel, bereich: bereich.name },
})
revalidatePath('/inbox')
revalidatePath('/aufgaben')
return { ok: true, meldung: `„${aufgabe.titel}" liegt jetzt in ${bereich.name}.` }
}
/** Aus einer Idee wird eine Aufgabe. */
export async function ideeUebernehmen(daten: FormData): Promise<Rueckmeldung> {
const geprueft = await akteurOder((fehler) => ({ ok: false, fehler }))
if ('antwort' in geprueft) return geprueft.antwort
const { akteur } = geprueft
const id = daten.get('id')
if (typeof id !== 'string' || id.length === 0) {
return { ok: false, fehler: 'Ungültige Anfrage.' }
}
const idee = await db.aufgabe.findUnique({
where: { id },
select: { id: true, titel: true, typ: true },
})
if (!idee) return { ok: false, fehler: 'Diese Idee gibt es nicht mehr.' }
if (idee.typ !== 'IDEE') {
return { ok: false, fehler: 'Das ist keine Idee.' }
}
await db.aufgabe.update({
where: { id: idee.id },
data: { typ: 'AUFGABE', status: 'OFFEN', erledigtAm: null },
})
await auditSchreiben({
akteur: { id: akteur.id, email: akteur.email },
aktion: 'aufgabe.aus-idee',
objektTyp: 'Aufgabe',
objektId: idee.id,
details: { titel: idee.titel },
})
revalidatePath('/ideen')
revalidatePath('/aufgaben')
revalidatePath('/inbox')
return { ok: true, meldung: `„${idee.titel}" ist jetzt eine Aufgabe.` }
}
// --- Löschen ---------------------------------------------------------------
export async function aufgabeLoeschen(daten: FormData): Promise<Rueckmeldung> {
const geprueft = await akteurOder((fehler) => ({ ok: false, fehler }))
if ('antwort' in geprueft) return geprueft.antwort
const { akteur } = geprueft
const id = daten.get('id')
if (typeof id !== 'string' || id.length === 0) {
return { ok: false, fehler: 'Ungültige Anfrage.' }
}
const aufgabe = await db.aufgabe.findUnique({
where: { id },
select: { id: true, titel: true, typ: true },
})
if (!aufgabe) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht mehr.' }
await db.aufgabe.delete({ where: { id: aufgabe.id } })
await auditSchreiben({
akteur: { id: akteur.id, email: akteur.email },
aktion: 'aufgabe.geloescht',
objektTyp: 'Aufgabe',
objektId: aufgabe.id,
details: { titel: aufgabe.titel, typ: aufgabe.typ },
})
revalidatePath('/aufgaben')
revalidatePath('/inbox')
revalidatePath('/ideen')
revalidatePath('/projekte')
return { ok: true, meldung: `„${aufgabe.titel}" gelöscht.` }
}
+173
View File
@@ -0,0 +1,173 @@
'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.`,
}
}