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

@@ -39,6 +39,13 @@ export enum ReleaseStatus {
ARCHIVED = 'ARCHIVED',
}
export enum SprintStatus {
PLANNED = 'PLANNED',
/** Under way: one per project at a time. */
ACTIVE = 'ACTIVE',
CLOSED = 'CLOSED',
}
/** Why someone is told. */
export enum NotificationKind {
ASSIGNED = 'ASSIGNED',

View File

@@ -86,4 +86,22 @@ export interface BoardFilter {
labels?: string[];
/** Matches key, title or description. */
search?: string;
sprintId?: string;
}
export interface CreateSprintInput {
projectId: string;
name: string;
goal?: string | null;
/** `YYYY-MM-DD`. */
startDate: string;
endDate: string;
}
export interface UpdateSprintInput {
sprintId: string;
name?: string;
goal?: string | null;
startDate?: string;
endDate?: string;
}

View File

@@ -4,6 +4,7 @@ import {
IssuePriority,
ProjectRole,
ReleaseStatus,
SprintStatus,
StatusCategory,
} from './enums';
import { ProjectSettings } from './workflow';
@@ -107,6 +108,7 @@ export interface IssueSummary {
priority: IssuePriority;
assignee: UserRef | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
labels: string[];
/** The epic or ticket this one breaks down. */
parent: IssueParent | null;
@@ -159,6 +161,40 @@ export interface Comment {
editedAt: string | null;
}
export interface SprintProgress {
total: number;
/** Done or cancelled. */
finished: number;
percent: number;
storyPoints: number;
finishedStoryPoints: number;
}
/** A time-boxed iteration of a project. */
export interface Sprint {
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;
progress: SprintProgress;
}
/** Per day of a sprint; null for the days to come. */
export interface SprintBurndown {
sprintId: string;
dates: string[];
scope: (number | null)[];
remaining: (number | null)[];
remainingPoints: (number | null)[];
ideal: number[];
}
export interface Release {
id: string;
projectId: string;

View File

@@ -6,6 +6,8 @@ import {
Comment,
Epic,
SavedFilter,
Sprint,
SprintBurndown,
SavedFilterCriteria,
CommandResult,
Issue,
@@ -32,6 +34,8 @@ import {
MyIssuesFilter,
UpdateIssueInput,
UpdateProjectInput,
CreateSprintInput,
UpdateSprintInput,
} from '../models/inputs';
/*
@@ -173,6 +177,25 @@ export abstract class ReleasesGateway {
abstract burnup(releaseId: string): Observable<ReleaseBurnup>;
}
export abstract class SprintsGateway {
/** With their progress: under way, planned, then closed. */
abstract list(projectId: string): Observable<Sprint[]>;
abstract burndown(sprintId: string): Observable<SprintBurndown>;
abstract create(input: CreateSprintInput): Observable<CommandResult>;
abstract update(input: UpdateSprintInput): Observable<CommandResult>;
abstract start(sprintId: string): Observable<CommandResult>;
/** Unfinished tickets go to `carryOverTo` (null: the backlog). */
abstract close(
sprintId: string,
carryOverTo: string | null,
): Observable<CommandResult>;
abstract remove(sprintId: string): Observable<CommandResult>;
abstract setIssueSprint(
issueId: string,
sprintId: string | null,
): Observable<CommandResult>;
}
export abstract class ActivityGateway {
abstract list(projectId: string, issueId?: string): Observable<Activity[]>;
}
@@ -219,6 +242,8 @@ export abstract class TrackerRealtimeGateway {
abstract commentChanges(issueId: string): Observable<CommentChanged>;
abstract activity(projectId: string): Observable<Activity>;
abstract releaseChanges(projectId: string): Observable<Release>;
/** Something about a sprint of the project changed (or it was deleted). */
abstract sprintChanges(projectId: string): Observable<{ sprintId: string }>;
abstract projectChanges(projectId: string): Observable<ProjectChanged>;
abstract myProjectsChanges(): Observable<ProjectChanged>;
abstract notifications(): Observable<Notification>;