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

@@ -8,6 +8,7 @@ import {
ProjectReadModel,
ReleaseReadModel,
SavedFilterReadModel,
SprintReadModel,
implicitRank,
} from '@task-stream/api/tracker/domain';
import { Schema } from 'mongoose';
@@ -24,6 +25,7 @@ export type IssueDoc = Doc<Omit<IssueReadModel, 'links'>> & {
};
export type CommentDoc = Doc<CommentReadModel>;
export type ReleaseDoc = Doc<ReleaseReadModel>;
export type SprintDoc = Doc<SprintReadModel>;
export type ActivityDoc = Doc<ActivityReadModel>;
export type TimelineDoc = Doc<IssueTimelineEntry>;
export type NotificationDoc = Doc<NotificationReadModel>;
@@ -43,6 +45,7 @@ export const ReadModels = {
Notification: 'NotificationReadModel',
NotificationSettings: 'NotificationSettings',
SavedFilter: 'SavedFilter',
Sprint: 'SprintReadModel',
} as const;
const userRef = { _id: false, id: String, name: String };
@@ -79,6 +82,7 @@ export const IssueSchema = new Schema<IssueDoc>(
reporter: userRef,
assignee: userRef,
release: { _id: false, id: String, name: String },
sprint: { _id: false, id: String, name: String },
outwardLinks: [link],
inwardLinks: [link],
watchers: [userRef],
@@ -108,6 +112,7 @@ IssueSchema.index({ 'watchers.id': 1 });
IssueSchema.index({ 'parent.id': 1 });
IssueSchema.index({ projectId: 1, rank: 1 });
IssueSchema.index({ 'release.id': 1 });
IssueSchema.index({ 'sprint.id': 1 });
IssueSchema.index({ title: 'text', description: 'text', key: 'text' });
export const CommentSchema = new Schema<CommentDoc>(
@@ -126,6 +131,16 @@ export const ReleaseSchema = new Schema<ReleaseDoc>(
{ ...loose, collection: 'releases_view' },
);
export const SprintSchema = new Schema<SprintDoc>(
{
_id: String,
projectId: { type: String, index: true },
status: String,
version: Number,
},
{ ...loose, collection: 'sprints_view' },
);
export const ActivitySchema = new Schema<ActivityDoc>(
{
_id: String,
@@ -182,6 +197,7 @@ export const READ_MODEL_SCHEMAS = [
{ name: ReadModels.Issue, schema: IssueSchema },
{ name: ReadModels.Comment, schema: CommentSchema },
{ name: ReadModels.Release, schema: ReleaseSchema },
{ name: ReadModels.Sprint, schema: SprintSchema },
{ name: ReadModels.Activity, schema: ActivitySchema },
{ name: ReadModels.Timeline, schema: TimelineSchema },
{ name: ReadModels.Notification, schema: NotificationSchema },
@@ -207,6 +223,7 @@ export function issueFromDoc(doc: IssueDoc): IssueReadModel {
watchers: rest.watchers ?? [],
parent: rest.parent?.id ? rest.parent : null,
rank: rest.rank ?? implicitRank(rest.number),
sprint: rest.sprint?.id ? rest.sprint : null,
checklist: rest.checklist ?? [],
};
}