feat: sprints with burndown and carry-over

Plan sprints with dates and a goal, run one at a time per project, and
put tickets in them from the ticket page. The Sprints tab follows the one
under way with its progress and a burndown in tickets or points against
the ideal line. Closing a sprint sends what was left to the next one or
back to the backlog; the board can be filtered by sprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:59:01 +02:00
parent d2e31789a7
commit 50d3ffd1dd
104 changed files with 7209 additions and 33 deletions

View File

@@ -12,12 +12,13 @@ export class IssueTimelineEntry {
issueId!: string;
projectId!: string;
at!: string;
kind!: 'status' | 'release';
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 {

View File

@@ -8,3 +8,4 @@ export * from './project-stats.read-model';
export * from './analytics.read-model';
export * from './notification.read-model';
export * from './saved-filter.read-model';
export * from './sprint.read-model';

View File

@@ -70,6 +70,7 @@ export class IssueReadModel {
reporter!: UserRefReadModel;
assignee!: UserRefReadModel | null;
release!: { id: string; name: string } | null;
sprint!: { id: string; name: string } | null;
labels!: string[];
figmaUrl!: string | null;
storyPoints!: number | null;

View File

@@ -0,0 +1,40 @@
import { SprintStatus } from '../sprint/sprint-status';
export class SprintReadModel {
id!: string;
projectId!: string;
name!: string;
goal!: string | null;
/** `YYYY-MM-DD`, both included. */
startDate!: string;
endDate!: string;
status!: SprintStatus;
startedAt!: string | null;
closedAt!: string | null;
createdAt!: string;
version!: number;
}
/** How far a sprint's tickets have got. */
export class SprintProgressReadModel {
total!: number;
/** Done or cancelled. */
finished!: number;
percent!: number;
storyPoints!: number;
finishedStoryPoints!: number;
}
/**
* Per day of the sprint: tickets in it (cancelled ones aside), how many are
* still open and their points. Days still to come are null; `ideal` falls in
* a straight line from what was open on the first day to zero on the last.
*/
export class SprintBurndownReadModel {
sprintId!: string;
dates!: string[];
scope!: (number | null)[];
remaining!: (number | null)[];
remainingPoints!: (number | null)[];
ideal!: number[];
}