Files
nebula/src/lib/projects.ts
T
florian.wildeandClaude Opus 5 0212dad2bf feat(ui): Aufgabenbrett auf der Projektseite
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>
2026-07-26 09:29:28 +02:00

69 lines
2.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 } },
});