feat(board): freie Reihenfolge im Projektbrett

Karten lassen sich innerhalb einer Spalte anordnen, die Position bleibt am
Projekt. Das Aufgabenbrett kann das laengst - dass ausgerechnet das
Projektbrett der gewaehlten Sortierung folgte, war ein Bruch.

Die Sortierung im Kopf ist in dieser Ansicht deshalb abgeschaltet. Sie
stehen zu lassen, ohne dass sie wirkt, waere irrefuehrend.

Umsortieren fasst das Aenderungsdatum nicht an. Sonst schoebe ein Aufraeumen
im Brett saemtliche Projekte in "Zuletzt geaendert" nach oben - das Brett
wuerde die Liste umsortieren, ohne dass sich inhaltlich etwas geaendert hat.
Ein Statuswechsel dagegen ist eine echte Aenderung und darf das Datum
setzen. Nur er kommt auch in den Verlauf; ein blosser Rangwechsel nicht,
sonst bestuende der Verlauf bald aus nichts anderem.

Dabei fiel auf, dass neu angelegte, importierte und per Status verschobene
Projekte gar keine Position bekamen und sich alle auf 0 stapelten. Der Rang
wird jetzt an allen vier Stellen gesetzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 14:30:17 +02:00
co-authored by Claude Opus 5
parent 5253b0a317
commit 1400bca047
9 changed files with 370 additions and 70 deletions
+20 -1
View File
@@ -1,4 +1,4 @@
import { Prisma } from "@prisma/client";
import { Prisma, type ProjectStatus } from "@prisma/client";
import { prisma } from "@/lib/db";
import { slugify } from "@/lib/utils";
@@ -28,6 +28,25 @@ export async function uniqueSlug(
return `${base}-${Date.now()}`;
}
/**
* Naechste freie Position am Ende einer Statusspalte im Brett.
*
* Muss bei jedem Anlegen und bei jedem Statuswechsel gesetzt werden – sonst
* stehen alle auf 0 und das Brett zeigt eine Reihenfolge, die sich bei jedem
* Laden anders ergeben kann.
*/
export async function nextProjectPosition(
ownerId: string,
status: ProjectStatus,
): Promise<number> {
const last = await prisma.project.findFirst({
where: { ownerId, status },
orderBy: { position: "desc" },
select: { position: true },
});
return (last?.position ?? -1) + 1;
}
/**
* Verbindungsfelder, die an den Browser gehen duerfen.
* tokenCipher ist bewusst nicht dabei.
+35
View File
@@ -35,6 +35,41 @@ export const projectCreateSchema = z.object({
export const projectUpdateSchema = projectCreateSchema.partial();
/**
* Neue Reihenfolge im Brett. Uebergeben wird je betroffener Spalte die
* vollstaendige Liste in ihrer neuen Reihenfolge – daraus ergeben sich Status
* und Position eindeutig, und der Aufruf laesst sich gefahrlos wiederholen.
*/
export const projectReorderSchema = z.object({
columns: z
.record(z.enum(STATUS), z.array(z.string().min(1)).max(500))
.refine((value) => Object.keys(value).length > 0, "Keine Spalte angegeben"),
});
/** Dieselbe Aenderung fuer mehrere Projekte auf einmal. */
export const projectBulkSchema = z
.object({
ids: z.array(z.string().min(1)).min(1).max(200),
action: z.enum(["status", "tags", "favorite", "delete"]),
status: z.enum(STATUS).optional(),
/** Bei action "tags": ergaenzen oder entfernen. */
tags: z.array(z.string().trim().min(1).max(30)).max(12).optional(),
tagMode: z.enum(["add", "remove"]).default("add"),
favorite: z.boolean().optional(),
})
.refine((value) => value.action !== "status" || value.status !== undefined, {
message: "Ohne Zielstatus laesst sich nichts verschieben",
path: ["status"],
})
.refine((value) => value.action !== "tags" || (value.tags?.length ?? 0) > 0, {
message: "Ohne Tags gibt es nichts zu aendern",
path: ["tags"],
})
.refine((value) => value.action !== "favorite" || value.favorite !== undefined, {
message: "Kein Zielzustand angegeben",
path: ["favorite"],
});
// ── Repositories ─────────────────────────────────────────────
export const repositoryCreateSchema = z.object({