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:
@@ -24,6 +24,7 @@ import {
|
||||
ProjectRole,
|
||||
ProjectStatsReadModel,
|
||||
ReleaseProgressReadModel,
|
||||
SprintProgressReadModel,
|
||||
ReleaseReadModel,
|
||||
ReleaseStatus,
|
||||
StatusCategory,
|
||||
@@ -281,6 +282,37 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async sprintProgress(sprintId: string): Promise<SprintProgressReadModel> {
|
||||
const finished = { $in: ['$statusCategory', FINISHED] };
|
||||
const points = { $ifNull: ['$storyPoints', 0] };
|
||||
const [row] = await this.issues.aggregate<{
|
||||
total: number;
|
||||
finished: number;
|
||||
storyPoints: number;
|
||||
finishedStoryPoints: number;
|
||||
}>([
|
||||
{ $match: { 'sprint.id': sprintId } },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
total: { $sum: 1 },
|
||||
finished: { $sum: { $cond: [finished, 1, 0] } },
|
||||
storyPoints: { $sum: points },
|
||||
finishedStoryPoints: { $sum: { $cond: [finished, points, 0] } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
const total = row?.total ?? 0;
|
||||
return {
|
||||
total,
|
||||
finished: row?.finished ?? 0,
|
||||
percent:
|
||||
total === 0 ? 0 : Math.round(((row?.finished ?? 0) / total) * 100),
|
||||
storyPoints: row?.storyPoints ?? 0,
|
||||
finishedStoryPoints: row?.finishedStoryPoints ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
async releaseProgress(releaseId: string): Promise<ReleaseProgressReadModel> {
|
||||
const rows = await this.issues.aggregate<{
|
||||
_id: { status: string; category: StatusCategory };
|
||||
@@ -322,6 +354,8 @@ function toMongoFilter(filter: IssueFilter): FilterQuery<IssueDoc> {
|
||||
query.labels = { $all: filter.labels.map((l) => l.toLowerCase()) };
|
||||
if (filter.releaseId) query['release.id'] = filter.releaseId;
|
||||
if (filter.parentId) query['parent.id'] = filter.parentId;
|
||||
if (filter.noSprint) query['sprint'] = null;
|
||||
else if (filter.sprintId) query['sprint.id'] = filter.sprintId;
|
||||
if (filter.unassigned) query['assignee'] = null;
|
||||
else if (filter.assigneeId) query['assignee.id'] = filter.assigneeId;
|
||||
if (filter.search?.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user