feat: Add project filter to listInitiatives

Add findByProjectId to InitiativeRepository using a subquery on the
initiative_projects junction table. Extend the listInitiatives tRPC
procedure to accept an optional projectId filter that composes with
the existing status filter. Add a project dropdown to the initiatives
page alongside the status filter.
This commit is contained in:
Lukas May
2026-03-04 11:40:32 +01:00
parent b6ac797644
commit 069eb66acb
6 changed files with 62 additions and 9 deletions

View File

@@ -4,10 +4,10 @@
* Implements InitiativeRepository interface using Drizzle ORM.
*/
import { eq } from 'drizzle-orm';
import { eq, inArray } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import type { DrizzleDatabase } from '../../index.js';
import { agents, initiatives, type Initiative } from '../../schema.js';
import { agents, initiatives, initiativeProjects, type Initiative } from '../../schema.js';
import type {
InitiativeRepository,
CreateInitiativeData,
@@ -59,6 +59,18 @@ export class DrizzleInitiativeRepository implements InitiativeRepository {
.where(eq(initiatives.status, status));
}
async findByProjectId(projectId: string): Promise<Initiative[]> {
const linkedIds = this.db
.select({ id: initiativeProjects.initiativeId })
.from(initiativeProjects)
.where(eq(initiativeProjects.projectId, projectId));
return this.db
.select()
.from(initiatives)
.where(inArray(initiatives.id, linkedIds));
}
async update(id: string, data: UpdateInitiativeData): Promise<Initiative> {
const [updated] = await this.db
.update(initiatives)

View File

@@ -57,6 +57,12 @@ export interface InitiativeRepository {
*/
update(id: string, data: UpdateInitiativeData): Promise<Initiative>;
/**
* Find all initiatives linked to a specific project.
* Returns empty array if none exist.
*/
findByProjectId(projectId: string): Promise<Initiative[]>;
/**
* Delete an initiative.
* Throws if initiative not found.

View File

@@ -111,12 +111,22 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
listInitiatives: publicProcedure
.input(z.object({
status: z.enum(['active', 'completed', 'archived']).optional(),
projectId: z.string().min(1).optional(),
}).optional())
.query(async ({ ctx, input }) => {
const repo = requireInitiativeRepository(ctx);
const initiatives = input?.status
? await repo.findByStatus(input.status)
: await repo.findAll();
let initiatives;
if (input?.projectId) {
const all = await repo.findByProjectId(input.projectId);
initiatives = input.status
? all.filter(i => i.status === input.status)
: all;
} else {
initiatives = input?.status
? await repo.findByStatus(input.status)
: await repo.findAll();
}
// Fetch active architect agents once for all initiatives
const ARCHITECT_MODES = ['discuss', 'plan', 'detail', 'refine'];