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
This commit is contained in:
Lukas May
2026-01-31 19:12:02 +01:00
parent 4d42da9496
commit d865330f85

View File

@@ -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;
}),
});
/**