Operationalisiert den ToolCTRL-Projektmanagement-Leitfaden auf der Projekt-Detailseite (hybrid: Cockpit-Primitive nativ, Schwergewichtiges per Outline-Link): - Projekt-Modus (Relax/Standard/Eskalation/Taskforce) + Dashboard-Surfacing - Risiko- & Chancen-Register (Wahrscheinlichkeit×Auswirkung, Showstopper, Owner, Strategie) — offene Showstopper landen auf dem Dashboard - Stakeholder-Analyse mit Macht/Interesse-Matrix - Ziel & Definition: SMART-Ziel, Nicht-Ziele, Liefergegenstaende, Maxime, Zieldreieck (welche Ecke gibt nach) - Meilensteine editierbar inkl. Ergebnis/Kundennutzen, Freitags-/ Feiertags-Warnung, Mindest-2-Hinweis - Phasen-Lebenszyklus (Definition→Planung→Umsetzung→Abschluss) mit Gate-Reviews - Projekt-Statusbericht (Ampel/Trend/Text, extern-Flag, Verlauf) — speist statusCache & Pflicht-Minimum - Änderungs-Log (Scope-Creep-Schutz), strukturierter Abschluss mit Checkliste + Abschlussvermerk Migrationen 0009_pm_insights + 0010_leitfaden_phases. Enthält auch die gemeinsame schema.ts (inkl. Lead-Tabellen aus dem Vor-Commit) und Seed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
import { TRPCError } from '@trpc/server';
|
|
import { desc, eq } from 'drizzle-orm';
|
|
import { risk } from '@cockpit/db';
|
|
import { RiskSchemas } from '@cockpit/shared';
|
|
import { router, protectedProcedure } from '../trpc.js';
|
|
|
|
export const riskRouter = router({
|
|
forProject: protectedProcedure
|
|
.input(RiskSchemas.riskForProjectInput)
|
|
.query(async ({ ctx, input }) => {
|
|
return ctx.db
|
|
.select()
|
|
.from(risk)
|
|
.where(eq(risk.projectId, input.projectId))
|
|
.orderBy(desc(risk.showstopper), desc(risk.createdAt));
|
|
}),
|
|
|
|
create: protectedProcedure
|
|
.input(RiskSchemas.createRiskInput)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const [created] = await ctx.db
|
|
.insert(risk)
|
|
.values({
|
|
projectId: input.projectId,
|
|
kind: input.kind,
|
|
title: input.title,
|
|
wahrscheinlichkeit: input.wahrscheinlichkeit,
|
|
auswirkung: input.auswirkung,
|
|
herkunft: input.herkunft ?? null,
|
|
showstopper: input.showstopper,
|
|
massnahme: input.massnahme ?? null,
|
|
strategie: input.strategie ?? null,
|
|
ownerUserId: input.ownerUserId ?? null,
|
|
status: input.status,
|
|
createdBy: ctx.user.id,
|
|
})
|
|
.returning();
|
|
return created;
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.input(RiskSchemas.updateRiskInput)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const { id, ...patch } = input;
|
|
if (Object.keys(patch).length === 0) {
|
|
const existing = (await ctx.db.select().from(risk).where(eq(risk.id, id)).limit(1))[0];
|
|
if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
|
|
return existing;
|
|
}
|
|
const [updated] = await ctx.db
|
|
.update(risk)
|
|
.set({ ...patch, updatedAt: new Date() })
|
|
.where(eq(risk.id, id))
|
|
.returning();
|
|
if (!updated) throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
|
|
return updated;
|
|
}),
|
|
|
|
remove: protectedProcedure
|
|
.input(RiskSchemas.removeRiskInput)
|
|
.mutation(async ({ ctx, input }) => {
|
|
await ctx.db.delete(risk).where(eq(risk.id, input.id));
|
|
return { ok: true as const };
|
|
}),
|
|
});
|