Initial commit: everythingbutwork MVP
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:
commit
4f33509a0f
131 files changed
+34339
No files matched your search
@@ -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));
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user