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

@@ -31,3 +31,9 @@ export * from './issue-checklist-item-added.event';
export * from './issue-checklist-item-updated.event';
export * from './issue-checklist-item-removed.event';
export * from './issue-ranked.event';
export * from './sprint-created.event';
export * from './sprint-updated.event';
export * from './sprint-started.event';
export * from './sprint-closed.event';
export * from './sprint-deleted.event';
export * from './issue-sprint-changed.event';

View File

@@ -0,0 +1,16 @@
import { AutowiredEvent } from '@task-stream/api/shared/domain';
import { TrackerEvent } from './tracker.event';
/** A ticket was moved in or out of a sprint. */
@AutowiredEvent
export class IssueSprintChangedEvent extends TrackerEvent {
constructor(
actorId: string,
issueId: string,
projectId: string,
readonly sprintId: string | null,
readonly previousSprintId: string | null,
) {
super(actorId, issueId, projectId);
}
}

View File

@@ -0,0 +1,19 @@
import { AutowiredEvent } from '@task-stream/api/shared/domain';
import { TrackerEvent } from './tracker.event';
/**
* A sprint ended. Its unfinished tickets move to `carryOverTo` (null: back
* to the backlog); the SprintCarryOverSaga moves them.
*/
@AutowiredEvent
export class SprintClosedEvent extends TrackerEvent {
constructor(
actorId: string,
sprintId: string,
projectId: string,
readonly carryOverTo: string | null,
readonly unfinishedIssues: number,
) {
super(actorId, sprintId, projectId);
}
}

View File

@@ -0,0 +1,14 @@
import { AutowiredEvent } from '@task-stream/api/shared/domain';
import type { SprintProps } from '../sprint/sprint';
import { TrackerEvent } from './tracker.event';
/** A sprint was planned. */
@AutowiredEvent
export class SprintCreatedEvent extends TrackerEvent {
constructor(
actorId: string,
readonly sprint: SprintProps,
) {
super(actorId, sprint.id, sprint.projectId);
}
}

View File

@@ -0,0 +1,10 @@
import { AutowiredEvent } from '@task-stream/api/shared/domain';
import { TrackerEvent } from './tracker.event';
/** A sprint was deleted: its tickets go back to the backlog. */
@AutowiredEvent
export class SprintDeletedEvent extends TrackerEvent {
constructor(actorId: string, sprintId: string, projectId: string) {
super(actorId, sprintId, projectId);
}
}

View File

@@ -0,0 +1,10 @@
import { AutowiredEvent } from '@task-stream/api/shared/domain';
import { TrackerEvent } from './tracker.event';
/** A sprint got under way. */
@AutowiredEvent
export class SprintStartedEvent extends TrackerEvent {
constructor(actorId: string, sprintId: string, projectId: string) {
super(actorId, sprintId, projectId);
}
}

View File

@@ -0,0 +1,16 @@
import { AutowiredEvent } from '@task-stream/api/shared/domain';
import type { SprintDetails } from '../sprint/sprint';
import { TrackerEvent } from './tracker.event';
/** A sprint was renamed, re-dated or got another goal (only what changed). */
@AutowiredEvent
export class SprintUpdatedEvent extends TrackerEvent {
constructor(
actorId: string,
sprintId: string,
projectId: string,
readonly changes: SprintDetails,
) {
super(actorId, sprintId, projectId);
}
}