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:
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import {
|
||||
FindSprintsRepository,
|
||||
UpsertMaterializedSprintRepository,
|
||||
} from '@task-stream/api/tracker/application';
|
||||
import { SprintReadModel, SprintStatus } from '@task-stream/api/tracker/domain';
|
||||
import { Model } from 'mongoose';
|
||||
import { ReadModels, SprintDoc, fromDoc } from './read-model.schemas';
|
||||
import { upsertIfNewer } from './upsert-if-newer';
|
||||
|
||||
const ORDER: Record<SprintStatus, number> = {
|
||||
[SprintStatus.ACTIVE]: 0,
|
||||
[SprintStatus.PLANNED]: 1,
|
||||
[SprintStatus.CLOSED]: 2,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MongoFindSprintsRepository extends FindSprintsRepository {
|
||||
constructor(
|
||||
@InjectModel(ReadModels.Sprint) private readonly sprints: Model<SprintDoc>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<SprintReadModel | null> {
|
||||
const doc = await this.sprints.findById(id).lean<SprintDoc>();
|
||||
return doc ? fromDoc(doc) : null;
|
||||
}
|
||||
|
||||
async findByProject(projectId: string): Promise<SprintReadModel[]> {
|
||||
const docs = await this.sprints.find({ projectId }).lean<SprintDoc[]>();
|
||||
return docs
|
||||
.map(fromDoc)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
ORDER[a.status] - ORDER[b.status] ||
|
||||
(a.status === SprintStatus.CLOSED
|
||||
? b.startDate.localeCompare(a.startDate)
|
||||
: a.startDate.localeCompare(b.startDate)) ||
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
}
|
||||
|
||||
async findActive(projectId: string): Promise<SprintReadModel | null> {
|
||||
const doc = await this.sprints
|
||||
.findOne({ projectId, status: SprintStatus.ACTIVE })
|
||||
.lean<SprintDoc>();
|
||||
return doc ? fromDoc(doc) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MongoUpsertMaterializedSprintRepository extends UpsertMaterializedSprintRepository {
|
||||
constructor(
|
||||
@InjectModel(ReadModels.Sprint) private readonly sprints: Model<SprintDoc>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async upsert({
|
||||
id,
|
||||
version,
|
||||
...fields
|
||||
}: Pick<SprintReadModel, 'id' | 'version'> & Partial<SprintReadModel>) {
|
||||
await upsertIfNewer(this.sprints, id, version, fields);
|
||||
}
|
||||
|
||||
async remove(sprintId: string) {
|
||||
await this.sprints.deleteOne({ _id: sprintId });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user