feat(projects): Projektvorlagen

Ein Projekt, das sich bewaehrt hat, laesst sich im Projektkopf als Vorlage
sichern: Status, Prioritaet, Farbe, Tags, Notizen und offene Aufgaben. Beim
Anlegen steht sie oben im Dialog zur Auswahl.

Benutzereigen statt fest eingebaut. Welche Tags und welche Startaufgaben
sinnvoll sind, weiss nur, wer damit arbeitet - eine mitgelieferte Liste
waere geraten.

Zwei Dinge gehen bewusst nicht mit. Erledigte Aufgaben nicht: eine Vorlage
beschreibt, was zu tun ist, nicht was einmal getan wurde. Und Repositories
nicht: eine Vorlage, die jedes neue Projekt an dasselbe Repository haengt,
waere eine Falle.

Die Stammdaten uebernimmt der Dialog sichtbar in die Felder, damit man sie
vor dem Speichern noch aendern kann; bereits Getipptes bleibt stehen.
Notizen und Aufgaben legt erst der Server an - im Dialog liessen sie sich
nicht darstellen, und wer die Vorlage wieder abwaehlt, soll sie nicht
mitgeschleppt bekommen.

templateId gilt nur beim Anlegen. Ein bestehendes Projekt nachtraeglich aus
einer Vorlage zu befuellen wuerde Notizen und Aufgaben verdoppeln.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 15:02:38 +02:00
co-authored by Claude Opus 5
parent ca6181b58f
commit 57c50cfb7f
9 changed files with 701 additions and 9 deletions
+72
View File
@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
import { requireUserApi } from "@/lib/auth/guard";
import { prisma } from "@/lib/db";
import { jsonError, readJsonBody } from "@/lib/security/request";
import { formatZodError, templateCreateSchema } from "@/lib/validation";
export const dynamic = "force-dynamic";
/** Hoechstzahl je Konto. Wer mehr braucht, hat keine Vorlagen mehr, sondern Projekte. */
const MAX_TEMPLATES = 30;
export async function GET() {
const auth = await requireUserApi();
if (!auth.ok) return auth.response;
const templates = await prisma.projectTemplate.findMany({
where: { ownerId: auth.user.id },
orderBy: { name: "asc" },
});
return NextResponse.json({ templates });
}
export async function POST(request: Request) {
const auth = await requireUserApi();
if (!auth.ok) return auth.response;
const body = await readJsonBody(request);
if (!body.ok) return body.response;
const parsed = templateCreateSchema.safeParse(body.data);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validierung fehlgeschlagen", fields: formatZodError(parsed.error) },
{ status: 422 },
);
}
const vorhanden = await prisma.projectTemplate.count({ where: { ownerId: auth.user.id } });
if (vorhanden >= MAX_TEMPLATES) {
return jsonError(`Mehr als ${MAX_TEMPLATES} Vorlagen sind nicht vorgesehen.`, 409);
}
const data = parsed.data;
// Der Name ist je Konto eindeutig; ein Duplikat soll eine verstaendliche
// Meldung geben statt eines Datenbankfehlers.
const doppelt = await prisma.projectTemplate.findFirst({
where: { ownerId: auth.user.id, name: data.name },
select: { id: true },
});
if (doppelt) return jsonError(`Eine Vorlage „${data.name}“ gibt es bereits.`, 409);
const template = await prisma.projectTemplate.create({
data: {
ownerId: auth.user.id,
name: data.name,
description: data.description ?? null,
summary: data.summary ?? null,
projectDescription: data.projectDescription ?? null,
status: data.status,
priority: data.priority,
accent: data.accent,
tags: data.tags,
progressFromTasks: data.progressFromTasks,
notes: data.notes,
tasks: data.tasks,
},
});
return NextResponse.json({ template }, { status: 201 });
}