Neues Modell Task: Titel, optionale Beschreibung, Spalte, Position, Faelligkeit und Labels. Vier Spalten - offen, in Arbeit, blockiert, erledigt. Anders als beim Projektbrett ist die Reihenfolge innerhalb einer Spalte hier bedeutsam und wird deshalb gespeichert. Der Reorder-Endpunkt nimmt je Spalte die vollstaendige Liste in ihrer neuen Reihenfolge entgegen und leitet Status und Position daraus ab. Das ist wiederholbar, kommt ohne Positionsrechnerei im Browser aus, und ein verlorener Aufruf laesst sich einfach erneut senden. Sicherheitsrelevant: der Endpunkt bekommt IDs aus dem Browser. Er aktualisiert ausschliesslich Aufgaben, die wirklich zu diesem Projekt gehoeren, und meldet uebergangene IDs zurueck - sonst liessen sich fremde Aufgaben ueber untergeschobene IDs verschieben. doneAt wird nur beim tatsaechlichen Wechsel nach "erledigt" gesetzt und beim Zurueckziehen geleert. Ein Umsortieren innerhalb der Spalte laesst das Datum in Ruhe. Der Fortschritt eines Projekts laesst sich wahlweise aus dem Anteil erledigter Aufgaben ableiten. Er wird nach jeder Aenderung neu berechnet und auch sofort beim Einschalten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { requireUserApi } from "@/lib/auth/guard";
|
|
import { prisma } from "@/lib/db";
|
|
import { jsonError, readJsonBody } from "@/lib/security/request";
|
|
import { nextPosition, refreshDerivedProgress } from "@/lib/tasks";
|
|
import { formatZodError, taskUpdateSchema } from "@/lib/validation";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type Params = { params: Promise<{ id: string }> };
|
|
|
|
/** Eine Aufgabe gehoert dem, dem das zugehoerige Projekt gehoert. */
|
|
async function ownedTask(taskId: string, ownerId: string) {
|
|
return prisma.task.findFirst({
|
|
where: { id: taskId, project: { ownerId } },
|
|
select: { id: true, projectId: true, status: true },
|
|
});
|
|
}
|
|
|
|
export async function PATCH(request: Request, { params }: Params) {
|
|
const auth = await requireUserApi();
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const { id } = await params;
|
|
const existing = await ownedTask(id, auth.user.id);
|
|
if (!existing) return jsonError("Aufgabe nicht gefunden", 404);
|
|
|
|
const body = await readJsonBody(request);
|
|
if (!body.ok) return body.response;
|
|
|
|
const parsed = taskUpdateSchema.safeParse(body.data);
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: "Validierung fehlgeschlagen", fields: formatZodError(parsed.error) },
|
|
{ status: 422 },
|
|
);
|
|
}
|
|
|
|
const data = parsed.data;
|
|
const update: Record<string, unknown> = {};
|
|
|
|
if (data.title !== undefined) update.title = data.title;
|
|
if (data.description !== undefined) update.description = data.description;
|
|
if (data.labels !== undefined) update.labels = data.labels;
|
|
if (data.dueDate !== undefined) {
|
|
update.dueDate = data.dueDate ? new Date(data.dueDate) : null;
|
|
}
|
|
|
|
if (data.status !== undefined && data.status !== existing.status) {
|
|
update.status = data.status;
|
|
// Beim Spaltenwechsel ans Ende der neuen Spalte.
|
|
update.position = await nextPosition(existing.projectId, data.status);
|
|
// doneAt nur setzen, wenn die Aufgabe wirklich nach DONE wandert.
|
|
update.doneAt = data.status === "DONE" ? new Date() : null;
|
|
}
|
|
|
|
const task = await prisma.task.update({ where: { id }, data: update });
|
|
await refreshDerivedProgress(existing.projectId);
|
|
|
|
return NextResponse.json({ task });
|
|
}
|
|
|
|
export async function DELETE(_request: Request, { params }: Params) {
|
|
const auth = await requireUserApi();
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const { id } = await params;
|
|
const existing = await ownedTask(id, auth.user.id);
|
|
if (!existing) return jsonError("Aufgabe nicht gefunden", 404);
|
|
|
|
await prisma.task.delete({ where: { id } });
|
|
await refreshDerivedProgress(existing.projectId);
|
|
|
|
return NextResponse.json({ ok: true });
|
|
}
|