import { StatusCategory } from '../project/project-settings'; import { UserRefReadModel } from './user-ref.read-model'; /** * One step in a ticket's life (projected from its events): the raw material * of the statistics. `status` entries record where the ticket went (with the * category it had then), `release` entries which release it joined or left. */ export class IssueTimelineEntry { /** Id of the event that produced it (projection is idempotent). */ id!: string; issueId!: string; projectId!: string; at!: string; kind!: 'status' | 'release' | 'sprint'; status?: string; category?: StatusCategory; /** Assignee when the status changed (throughput per person). */ assigneeId?: string | null; releaseId?: string | null; sprintId?: string | null; } export class DurationStats { count!: number; averageDays!: number | null; /** 85th percentile: "85% of tickets take at most this long". */ p85Days!: number | null; } /** Statistics of a project over a period. */ export class ProjectAnalyticsReadModel { from!: string; to!: string; /** Per week (Monday): tickets created and tickets finished. */ createdVsResolved!: { week: string; created: number; resolved: number }[]; /** Tickets in each status at the end of every day (cumulative flow). */ cumulativeFlow!: { dates: string[]; series: { status: string; counts: number[] }[]; }; /** From starting work to done. */ cycleTime!: DurationStats; /** From creation to done. */ leadTime!: DurationStats; /** Tickets finished per person and week. */ throughput!: { week: string; user: UserRefReadModel | null; count: number }[]; /** Tickets leaving QA: to done (passed) or back to work (failed). */ qa!: { passed: number; failed: number; failureRate: number | null }; } /** Scope vs finished tickets of a release, day by day. */ export class ReleaseBurnupReadModel { releaseId!: string; targetDate!: string | null; dates!: string[]; scope!: number[]; done!: number[]; }