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>
73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
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 });
|
|
}
|
|
}
|