Vier Spalten, Karten per Ziehen verschiebbar - zwischen den Spalten und innerhalb einer Spalte. Waehrend des Ziehens fuehrt der oertliche Zustand, danach wird die neue Ordnung als Ganzes gespeichert; scheitert das, springt das Brett auf den Serverstand zurueck. Details: - Gezogen wird am Griff links, damit ein Klick auf die Karte weiter den Dialog oeffnet. - Der Spaltenwechsel passiert schon beim Darueberziehen, nicht erst beim Ablegen - sonst wirkt das Brett traege. - Der Serverstand wird nur uebernommen, wenn gerade nicht gezogen wird, sonst spraenge die Karte mitten in der Bewegung zurueck. - Faelligkeiten faerben sich: ueberfaellig rot, heute gelb, demnaechst gedaempft. Ist "Fortschritt aus Aufgaben" eingeschaltet, wird der Regler gesperrt statt einen Wert anzubieten, der beim naechsten Aufgabenwechsel ueberschrieben wuerde - im Dialog wie im Kopfbereich. Die Projektkarte zeigt das Verhaeltnis als "3/8". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.0 KiB
TypeScript
69 lines
2.0 KiB
TypeScript
import { Prisma } from "@prisma/client";
|
||
import { prisma } from "@/lib/db";
|
||
import { slugify } from "@/lib/utils";
|
||
|
||
/**
|
||
* Erzeugt einen je Besitzer eindeutigen Slug, notfalls mit angehaengtem
|
||
* Zaehler. Zwei Benutzer duerfen also beide ein Projekt "dashboard" haben.
|
||
*/
|
||
export async function uniqueSlug(
|
||
name: string,
|
||
ownerId: string,
|
||
ignoreId?: string,
|
||
): Promise<string> {
|
||
const base = slugify(name) || "projekt";
|
||
let candidate = base;
|
||
let counter = 2;
|
||
|
||
// In der Praxis endet die Schleife nach wenigen Runden.
|
||
for (let attempt = 0; attempt < 50; attempt += 1) {
|
||
const existing = await prisma.project.findFirst({
|
||
where: { slug: candidate, ownerId },
|
||
select: { id: true },
|
||
});
|
||
if (!existing || existing.id === ignoreId) return candidate;
|
||
candidate = `${base}-${counter}`;
|
||
counter += 1;
|
||
}
|
||
return `${base}-${Date.now()}`;
|
||
}
|
||
|
||
/**
|
||
* Verbindungsfelder, die an den Browser gehen duerfen.
|
||
* tokenCipher ist bewusst nicht dabei.
|
||
*/
|
||
export const connectionSafeSelect = Prisma.validator<Prisma.GitConnectionSelect>()({
|
||
id: true,
|
||
name: true,
|
||
provider: true,
|
||
baseUrl: true,
|
||
username: true,
|
||
tokenHint: true,
|
||
lastCheckedAt: true,
|
||
lastCheckOk: true,
|
||
lastCheckMessage: true,
|
||
createdAt: true,
|
||
updatedAt: true,
|
||
});
|
||
|
||
export const projectListInclude = Prisma.validator<Prisma.ProjectInclude>()({
|
||
connection: { select: connectionSafeSelect },
|
||
snapshot: true,
|
||
_count: { select: { notes: true, tasks: true } },
|
||
// Nur die offenen Aufgaben zaehlen – fuer die Anzeige "3 von 8" auf der
|
||
// Karte, ohne die Aufgaben selbst mitzuladen.
|
||
tasks: { where: { status: "DONE" }, select: { id: true } },
|
||
});
|
||
|
||
export const projectDetailInclude = Prisma.validator<Prisma.ProjectInclude>()({
|
||
connection: { select: connectionSafeSelect },
|
||
snapshot: true,
|
||
notes: {
|
||
orderBy: [{ pinned: "desc" }, { updatedAt: "desc" }],
|
||
},
|
||
tasks: {
|
||
orderBy: [{ status: "asc" }, { position: "asc" }],
|
||
},
|
||
_count: { select: { notes: true, tasks: true } },
|
||
});
|