feat: portfolio of all my projects

/cartera (and GraphQL portfolio, REST /api/me/portfolio, MCP
get_portfolio) shows every active project I belong to (not archived, not
as a guest) at a glance: open, overdue, blocked and stuck tickets, the
sprint under way and whether it gets there, planned releases with their
forecast, and the open epics with their progress. It reuses the
dashboard's computations. A release with no tickets now reads «Sin
tickets» instead of «Todo hecho», here and on the dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 18:27:46 +02:00
parent 8bf7d47b2e
commit affce615a7
35 changed files with 1603 additions and 80 deletions

View File

@@ -32,6 +32,7 @@ import {
CommandResultGql,
ProjectAnalyticsGql,
ProjectInsightsGql,
PortfolioProjectGql,
ProjectGql,
ProjectSettingsGql,
ProjectStatsGql,
@@ -47,6 +48,14 @@ export class ProjectResolver {
return this.projects.findMine(me.id);
}
@Query(() => [PortfolioProjectGql], {
description:
'My active projects (not archived, not as a guest) at a glance: open work, risks, sprint, release forecasts and epics',
})
portfolio(@CurrentUser() me: User) {
return this.projects.portfolio(me.id);
}
@OptionalAuth()
@Query(() => ProjectGql, {
description: 'A project of mine, or any public one (no sign-in needed)',

View File

@@ -1073,3 +1073,27 @@ export class PresenceGql {
editing!: boolean;
@Field({ description: 'Last heartbeat' }) at!: string;
}
@ObjectType('PortfolioProject', {
description:
'One of my active projects at a glance: open work, risks, sprint, releases and epics',
})
export class PortfolioProjectGql {
@Field(() => ID) projectId!: string;
@Field() key!: string;
@Field() name!: string;
@Field() isPublic!: boolean;
@Field(() => Int) total!: number;
@Field(() => Int) open!: number;
@Field(() => Int) overdue!: number;
@Field(() => Int) blocked!: number;
@Field(() => Int) stuck!: number;
@Field(() => SprintPulseGql, { nullable: true })
sprint!: SprintPulseGql | null;
@Field(() => [ReleaseForecastGql]) releases!: ReleaseForecastGql[];
@Field(() => [EpicGql], {
description: 'Open epics, nearest due date first (six at most)',
})
epics!: EpicGql[];
@Field() updatedAt!: string;
}