feat: Replace initiative card N+1 queries with server-computed activity indicator
listInitiatives now returns an activity object (state, activePhase, phase counts) derived server-side from phases, eliminating per-card listPhases queries. Initiative cards show a StatusDot with pulse animation + label instead of a static StatusBadge. Removed redundant View and Spawn Architect buttons from cards. Added variant override prop to StatusDot.
This commit is contained in:
47
apps/server/trpc/routers/initiative-activity.ts
Normal file
47
apps/server/trpc/routers/initiative-activity.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Initiative Activity — derives current activity state from initiative + phases.
|
||||
*/
|
||||
|
||||
import type { Initiative, Phase } from '../../db/schema.js';
|
||||
import type { InitiativeActivity, InitiativeActivityState } from '@codewalk-district/shared';
|
||||
|
||||
export function deriveInitiativeActivity(initiative: Initiative, phases: Phase[]): InitiativeActivity {
|
||||
const phasesTotal = phases.length;
|
||||
const phasesCompleted = phases.filter(p => p.status === 'completed').length;
|
||||
const base = { phasesTotal, phasesCompleted };
|
||||
|
||||
if (initiative.status === 'archived') {
|
||||
return { ...base, state: 'archived' };
|
||||
}
|
||||
if (initiative.status === 'completed') {
|
||||
return { ...base, state: 'complete' };
|
||||
}
|
||||
if (phasesTotal === 0) {
|
||||
return { ...base, state: 'idle' };
|
||||
}
|
||||
|
||||
// Priority-ordered state detection (first match wins)
|
||||
const priorities: Array<{ status: Phase['status']; state: InitiativeActivityState }> = [
|
||||
{ status: 'pending_review', state: 'pending_review' },
|
||||
{ status: 'in_progress', state: 'executing' },
|
||||
{ status: 'blocked', state: 'blocked' },
|
||||
];
|
||||
|
||||
for (const { status, state } of priorities) {
|
||||
const match = phases.find(p => p.status === status);
|
||||
if (match) {
|
||||
return { ...base, state, activePhase: { id: match.id, name: match.name } };
|
||||
}
|
||||
}
|
||||
|
||||
if (phasesCompleted === phasesTotal) {
|
||||
return { ...base, state: 'complete' };
|
||||
}
|
||||
|
||||
const approved = phases.find(p => p.status === 'approved');
|
||||
if (approved) {
|
||||
return { ...base, state: 'ready', activePhase: { id: approved.id, name: approved.name } };
|
||||
}
|
||||
|
||||
return { ...base, state: 'planning' };
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
import type { ProcedureBuilder } from '../trpc.js';
|
||||
import { requireInitiativeRepository, requireProjectRepository, requireTaskRepository } from './_helpers.js';
|
||||
import { deriveInitiativeActivity } from './initiative-activity.js';
|
||||
|
||||
export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
||||
return {
|
||||
@@ -63,10 +64,22 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
||||
}).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const repo = requireInitiativeRepository(ctx);
|
||||
if (input?.status) {
|
||||
return repo.findByStatus(input.status);
|
||||
const initiatives = input?.status
|
||||
? await repo.findByStatus(input.status)
|
||||
: await repo.findAll();
|
||||
|
||||
if (ctx.phaseRepository) {
|
||||
const phaseRepo = ctx.phaseRepository;
|
||||
return Promise.all(initiatives.map(async (init) => {
|
||||
const phases = await phaseRepo.findByInitiativeId(init.id);
|
||||
return { ...init, activity: deriveInitiativeActivity(init, phases) };
|
||||
}));
|
||||
}
|
||||
return repo.findAll();
|
||||
|
||||
return initiatives.map(init => ({
|
||||
...init,
|
||||
activity: deriveInitiativeActivity(init, []),
|
||||
}));
|
||||
}),
|
||||
|
||||
getInitiative: publicProcedure
|
||||
|
||||
Reference in New Issue
Block a user