Initial commit: everythingbutwork MVP
ci / quality (push) Has been cancelled
ci / build (push) Has been cancelled

Internes Project-Management-Tool für ToolCTRL — ergänzend zu Plane (Tickets)
und Outline (Doku). Umfasst Puls-Übersicht, Tagesrhythmus (Lock-in + Reflexion),
Bereichs-Detail-Views (Entwicklung/Vertrieb/Marketing), Pflicht-Minimum-Check
und Blocker-Lifecycle gemäß Projektmanagement-Leitfaden v3.0.

Stack: pnpm-Monorepo · Vite + React 18 + Tailwind · Fastify + tRPC v11 +
Drizzle ORM · Postgres 16 · Authentik OIDC (Phase 2, Dev-Auto-Login aktiv).

Planungs-Artefakte unter _bmad-output/planning-artifacts/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.7 committed 2026-05-25 22:47:26 +02:00
commit 4f33509a0f
131 files changed
+34339

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { and, eq, desc } from 'drizzle-orm';
import { project, milestone, area, user, type Project } from '@cockpit/db';
import { ProjectSchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
const byAreaInput = z.object({
slug: z.string().min(1),
includeArchived: z.boolean().optional(),
});
const byIdInput = z.object({ id: z.string().uuid() });
export const projectRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
const rows = await ctx.db
.select({
project: project,
area: area,
verantwortliche: user,
})
.from(project)
.innerJoin(area, eq(project.areaId, area.id))
.leftJoin(user, eq(user.id, project.verantwortlicheUserId))
.where(eq(project.archiviert, false));
return rows;
}),
byAreaSlug: protectedProcedure.input(byAreaInput).query(async ({ ctx, input }) => {
const a = (await ctx.db.select().from(area).where(eq(area.slug, input.slug)).limit(1))[0];
if (!a) return [];
const projects = await ctx.db
.select()
.from(project)
.where(
input.includeArchived
? eq(project.areaId, a.id)
: and(eq(project.areaId, a.id), eq(project.archiviert, false)),
)
.orderBy(desc(project.createdAt));
return projects;
}),
byId: protectedProcedure.input(byIdInput).query(async ({ ctx, input }) => {
const rows = await ctx.db
.select({ project: project, area: area })
.from(project)
.innerJoin(area, eq(area.id, project.areaId))
.where(eq(project.id, input.id))
.limit(1);
const row = rows[0];
if (!row) throw new TRPCError({ code: 'NOT_FOUND', message: 'Projekt nicht gefunden.' });
const ms = await ctx.db.select().from(milestone).where(eq(milestone.projectId, row.project.id));
return { project: row.project, area: row.area, milestones: ms };
}),
create: protectedProcedure
.input(ProjectSchemas.createProjectInput)
.mutation(async ({ ctx, input }) => {
const result = await ctx.db.transaction(async (tx) => {
const [created] = await tx
.insert(project)
.values({
areaId: input.areaId,
name: input.name,
kind: input.kind,
verantwortlicheUserId: input.verantwortlicheUserId ?? null,
arbeitsmodus: input.arbeitsmodus ?? null,
outlineSteckbriefUrl: input.outlineSteckbriefUrl,
planeProjectUrl: input.planeProjectUrl,
risikolisteUrl: input.risikolisteUrl ?? null,
dodText: input.dodText ?? null,
start: input.start ?? null,
deadline: input.deadline ?? null,
createdBy: ctx.user.id,
pflichtMinimumState: {
steckbrief_freigegeben: false,
verantwortlicher: !!input.verantwortlicheUserId,
plane_board: true,
meilensteine: input.milestones.length > 0,
status_aktuell: false,
retrospektive: false,
},
})
.returning();
if (!created) throw new Error('Projekt konnte nicht angelegt werden.');
if (input.milestones.length > 0) {
await tx.insert(milestone).values(
input.milestones.map((m, idx) => ({
projectId: created.id,
title: m.title,
due: m.due ?? null,
order: idx,
})),
);
}
return created;
});
return result;
}),
archive: protectedProcedure
.input(ProjectSchemas.archiveProjectInput)
.mutation(async ({ ctx, input }) => {
const p = (
await ctx.db.select().from(project).where(eq(project.id, input.projectId)).limit(1)
)[0];
if (!p) throw new TRPCError({ code: 'NOT_FOUND', message: 'Projekt nicht gefunden.' });
if (p.verantwortlicheUserId !== ctx.user.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Nur die/der Projektverantwortliche darf das Projekt abschließen.',
});
}
const [updated] = await ctx.db
.update(project)
.set({
archiviert: true,
archiviertAm: new Date(),
lessonsLearnedUrl: input.lessonsLearnedUrl,
pflichtMinimumState: { ...p.pflichtMinimumState, retrospektive: true },
})
.where(eq(project.id, input.projectId))
.returning();
return updated as Project;
}),
setMilestoneStatus: protectedProcedure
.input(ProjectSchemas.setMilestoneStatusInput)
.mutation(async ({ ctx, input }) => {
const [updated] = await ctx.db
.update(milestone)
.set({ status: input.status })
.where(eq(milestone.id, input.milestoneId))
.returning();
return updated;
}),
listArchived: protectedProcedure.query(async ({ ctx }) => {
return await ctx.db
.select()
.from(project)
.where(eq(project.archiviert, true))
.orderBy(desc(project.archiviertAm));
}),
});