From d865330f85ee4d332df97852ad3ea695fe58c64c Mon Sep 17 00:00:00 2001 From: Lukas May Date: Sat, 31 Jan 2026 19:12:02 +0100 Subject: [PATCH] feat(11-04): add phase tRPC procedures - Add requirePhaseRepository helper function - Add createPhase mutation with auto-numbering - Add listPhases query by initiative ID - Add getPhase query with NOT_FOUND error handling - Add updatePhase mutation procedure - Add createPhasesFromBreakdown bulk create mutation --- src/trpc/router.ts | 101 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/trpc/router.ts b/src/trpc/router.ts index a9603de..fe22009 100644 --- a/src/trpc/router.ts +++ b/src/trpc/router.ts @@ -735,6 +735,107 @@ export const appRouter = router({ const { id, ...data } = input; return repo.update(id, data); }), + + // =========================================================================== + // Phase Procedures + // =========================================================================== + + /** + * Create a new phase for an initiative. + * Auto-assigns the next phase number. + */ + createPhase: publicProcedure + .input(z.object({ + initiativeId: z.string().min(1), + name: z.string().min(1), + description: z.string().optional(), + })) + .mutation(async ({ ctx, input }) => { + const repo = requirePhaseRepository(ctx); + const nextNumber = await repo.getNextNumber(input.initiativeId); + return repo.create({ + initiativeId: input.initiativeId, + number: nextNumber, + name: input.name, + description: input.description ?? null, + status: 'pending', + }); + }), + + /** + * List phases for an initiative. + * Returns phases ordered by number. + */ + listPhases: publicProcedure + .input(z.object({ initiativeId: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const repo = requirePhaseRepository(ctx); + return repo.findByInitiativeId(input.initiativeId); + }), + + /** + * Get a phase by ID. + * Throws NOT_FOUND if phase doesn't exist. + */ + getPhase: publicProcedure + .input(z.object({ id: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const repo = requirePhaseRepository(ctx); + const phase = await repo.findById(input.id); + if (!phase) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Phase '${input.id}' not found`, + }); + } + return phase; + }), + + /** + * Update a phase. + * Returns the updated phase. + */ + updatePhase: publicProcedure + .input(z.object({ + id: z.string().min(1), + name: z.string().min(1).optional(), + description: z.string().optional(), + status: z.enum(['pending', 'in_progress', 'completed']).optional(), + })) + .mutation(async ({ ctx, input }) => { + const repo = requirePhaseRepository(ctx); + const { id, ...data } = input; + return repo.update(id, data); + }), + + /** + * Create multiple phases from Architect breakdown output. + * Accepts pre-numbered phases and creates them in bulk. + */ + createPhasesFromBreakdown: publicProcedure + .input(z.object({ + initiativeId: z.string().min(1), + phases: z.array(z.object({ + number: z.number(), + name: z.string().min(1), + description: z.string(), + })), + })) + .mutation(async ({ ctx, input }) => { + const repo = requirePhaseRepository(ctx); + const created: Phase[] = []; + for (const p of input.phases) { + const phase = await repo.create({ + initiativeId: input.initiativeId, + number: p.number, + name: p.name, + description: p.description, + status: 'pending', + }); + created.push(phase); + } + return created; + }), }); /**