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,65 @@
|
||||
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
|
||||
import {
|
||||
SprintClosedEvent,
|
||||
SprintCreatedEvent,
|
||||
SprintDeletedEvent,
|
||||
SprintStartedEvent,
|
||||
SprintStatus,
|
||||
SprintUpdatedEvent,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
import { UpsertMaterializedIssueRepository } from '../ports/upsert-materialized-issue.repository';
|
||||
import { UpsertMaterializedSprintRepository } from '../ports/upsert-materialized-sprint.repository';
|
||||
import { ReadModelNotifier, versionOf } from './read-model-notifier';
|
||||
|
||||
type SprintEvent =
|
||||
| SprintCreatedEvent
|
||||
| SprintUpdatedEvent
|
||||
| SprintStartedEvent
|
||||
| SprintClosedEvent
|
||||
| SprintDeletedEvent;
|
||||
|
||||
/** The sprints view (`sprints_view`), one document per sprint. */
|
||||
@EventsHandler(
|
||||
SprintCreatedEvent,
|
||||
SprintUpdatedEvent,
|
||||
SprintStartedEvent,
|
||||
SprintClosedEvent,
|
||||
SprintDeletedEvent,
|
||||
)
|
||||
export class SprintEventHandler implements IEventHandler<SprintEvent> {
|
||||
constructor(
|
||||
private readonly sprints: UpsertMaterializedSprintRepository,
|
||||
private readonly issues: UpsertMaterializedIssueRepository,
|
||||
private readonly notifier: ReadModelNotifier,
|
||||
) {}
|
||||
|
||||
async handle(event: SprintEvent) {
|
||||
const version = versionOf(event);
|
||||
const id = event.aggregateId;
|
||||
if (event instanceof SprintCreatedEvent) {
|
||||
await this.sprints.upsert({ ...event.sprint, version });
|
||||
} else if (event instanceof SprintUpdatedEvent) {
|
||||
await this.sprints.upsert({ id, ...event.changes, version });
|
||||
// Tickets show its name.
|
||||
if (event.changes.name)
|
||||
await this.issues.renameSprint(id, event.changes.name);
|
||||
} else if (event instanceof SprintStartedEvent) {
|
||||
await this.sprints.upsert({
|
||||
id,
|
||||
status: SprintStatus.ACTIVE,
|
||||
startedAt: event.occurredAt,
|
||||
version,
|
||||
});
|
||||
} else if (event instanceof SprintClosedEvent) {
|
||||
await this.sprints.upsert({
|
||||
id,
|
||||
status: SprintStatus.CLOSED,
|
||||
closedAt: event.occurredAt,
|
||||
version,
|
||||
});
|
||||
} else {
|
||||
await this.sprints.remove(id);
|
||||
}
|
||||
await this.notifier.sprint(id, event);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user