Files
task-stream/libs/api/tracker/domain/src/lib/read-models/insights.read-model.ts
Josep Piguillem affce615a7 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>
2026-10-15 18:27:46 +02:00

213 lines
6.5 KiB
TypeScript

import { EpicReadModel } from './issue.read-model';
import { UserRefReadModel } from './user-ref.read-model';
/** A ticket named by the dashboard, with the number that put it there. */
export class InsightIssueReadModel {
id!: string;
key!: string;
title!: string;
/** Status id of the project's settings. */
status!: string;
assignee!: UserRefReadModel | null;
/** Days in its current status (aging, stuck) or past/to its due date. */
days!: number;
dueDate!: string | null;
}
/** The sprint under way: time gone against work done. */
export class SprintPulseReadModel {
sprintId!: string;
name!: string;
startDate!: string;
endDate!: string;
/** Days of the sprint, both ends included. */
totalDays!: number;
/** Days left after today (0 on the last day; negative once it is over). */
daysLeft!: number;
tickets!: number;
doneTickets!: number;
points!: number;
donePoints!: number;
/** Points closed per day so far (null on the first day). */
pacePerDay!: number | null;
/** Points per day still needed to finish (null when nothing is left). */
neededPerDay!: number | null;
/** At this pace, what is open would not be closed in time. */
behind!: boolean;
}
/** Tickets to look at now. Lists are the worst first, a few at most. */
export class RiskReadModel {
overdue!: InsightIssueReadModel[];
overdueCount!: number;
/** Due within the next 7 days. */
dueSoon!: InsightIssueReadModel[];
dueSoonCount!: number;
/** Waiting for an open ticket that blocks them (or they depend on). */
blocked!: InsightIssueReadModel[];
blockedCount!: number;
/** In progress or in QA for `stuckDays` or more without moving. */
stuck!: InsightIssueReadModel[];
stuckCount!: number;
stuckDays!: number;
}
/** How long the work under way has been where it is. */
export class AgingReadModel {
issues!: InsightIssueReadModel[];
/** 85% of the tickets finished lately took at most this (cycle time). */
thresholdDays!: number | null;
}
/** Time tickets spend in a status of the flow, and how many are there. */
export class StatusTimeReadModel {
status!: string;
/** Average stay of the tickets that left it in the last 90 days. */
averageDays!: number | null;
/** Stays measured. */
samples!: number;
open!: number;
wipLimit!: number | null;
overLimit!: boolean;
}
/** When a planned release would be finished, at the team's real pace. */
export class ReleaseForecastReadModel {
releaseId!: string;
name!: string;
targetDate!: string | null;
/** Tickets in it (cancelled ones aside). */
total!: number;
finished!: number;
/** Half of the simulated futures finish by then (null: no pace yet). */
likelyDate!: string | null;
/** 85% of them do. */
safeDate!: string | null;
/**
* DONE: nothing left. NO_DATE: no target to compare. UNKNOWN: nothing
* finished lately to measure a pace. ON_TRACK: 85% in time. AT_RISK: half
* in time. LATE: less than half.
*/
verdict!: 'DONE' | 'NO_DATE' | 'UNKNOWN' | 'ON_TRACK' | 'AT_RISK' | 'LATE';
}
/** What has happened since a moment (the viewer's last visit). */
export class ChangesSinceReadModel {
since!: string;
created!: InsightIssueReadModel[];
createdCount!: number;
finished!: InsightIssueReadModel[];
finishedCount!: number;
/** Sent back from QA. */
returned!: InsightIssueReadModel[];
returnedCount!: number;
/** Times the viewer was mentioned in this project. */
mentions!: number;
}
/** One person's open work against what they usually close. */
export class TeamLoadReadModel {
user!: UserRefReadModel;
openTickets!: number;
openPoints!: number;
/** Their open tickets waiting for another open one. */
blocked!: number;
/** Points they closed per sprint, over the last three closed sprints. */
pointsPerSprint!: number | null;
/** More open points than they usually close in a sprint. */
overloaded!: boolean;
}
/** Open tickets missing what planning needs. */
export class BacklogHealthReadModel {
open!: number;
unestimated!: number;
noArea!: number;
unassigned!: number;
/** Not updated in `staleDays` days. */
stale!: number;
staleDays!: number;
}
/** Bugs created and closed in one week (Monday, UTC). */
export class BugWeekReadModel {
week!: string;
created!: number;
closed!: number;
}
/** What QA sent back, and let through, per area (last 90 days). */
export class QaByAreaReadModel {
/** Area id; null: tickets with no area. */
area!: string | null;
returned!: number;
passed!: number;
}
/** Bugs reported in the 14 days after a release shipped. */
export class EscapedBugsReadModel {
releaseId!: string;
name!: string;
releasedAt!: string;
bugs!: number;
}
/** Bugs (types drawn with the bug icon) and QA. */
export class QualityReadModel {
/** Open bugs per priority (all five, most urgent first). */
openBugsByPriority!: { priority: string; count: number }[];
/** The last 8 weeks, oldest first. */
bugWeeks!: BugWeekReadModel[];
qaByArea!: QaByAreaReadModel[];
/** The last three shipped releases, newest first. */
escaped!: EscapedBugsReadModel[];
}
/** Open tickets per option of a list custom field. */
export class FieldBreakdownReadModel {
fieldId!: string;
counts!: { value: string; count: number }[];
/** Open tickets with no value in it. */
none!: number;
}
/** What the project dashboard reads beyond the counts. */
export class ProjectInsightsReadModel {
generatedAt!: string;
sprint!: SprintPulseReadModel | null;
risks!: RiskReadModel;
aging!: AgingReadModel;
/** In-progress and QA statuses, in board order. */
bottlenecks!: StatusTimeReadModel[];
/** Planned releases, by target date. */
releases!: ReleaseForecastReadModel[];
/** Null unless asked with a `since`. */
changes!: ChangesSinceReadModel | null;
/** Members (and others with open tickets), busiest first. */
team!: TeamLoadReadModel[];
backlog!: BacklogHealthReadModel;
quality!: QualityReadModel;
/** One per active list (SELECT) custom field. */
fields!: FieldBreakdownReadModel[];
}
/** One of my projects at a glance, for the portfolio. */
export class PortfolioProjectReadModel {
projectId!: string;
key!: string;
name!: string;
isPublic!: boolean;
total!: number;
open!: number;
overdue!: number;
blocked!: number;
stuck!: number;
/** The sprint under way, if any. */
sprint!: SprintPulseReadModel | null;
/** Planned releases with their forecast. */
releases!: ReleaseForecastReadModel[];
/** Open epics (nearest due date first), at most six. */
epics!: EpicReadModel[];
updatedAt!: string;
}