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

@@ -594,6 +594,14 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
>
<pi-label>Release</pi-label>
</pi-select-field>
<pi-select-field
[options]="sprintOptions()"
[disabled]="!project.canContribute() || sprintLocked()"
[value]="issue.sprint?.id ?? ''"
(valueChange)="facade.setSprint($event || null)"
>
<pi-label>Sprint</pi-label>
</pi-select-field>
<pi-select-field
[options]="typeOptions()"
[disabled]="!project.canContribute()"
@@ -768,6 +776,30 @@ export class IssuePage {
.map((r) => ({ value: r.id, label: r.name })),
],
);
/** Open sprints, plus the ticket's own even if it closed since. */
protected readonly sprintOptions = computed<PiSelectFieldOption<string>[]>(
() => {
const current = this.facade.issue()?.sprint;
const open = this.project.openSprints();
return [
{ value: NONE, label: 'Backlog (sin sprint)' },
...open.map((s) => ({
value: s.id,
label: s.status === 'ACTIVE' ? `${s.name} · en curso` : s.name,
})),
...(current && !open.some((s) => s.id === current.id)
? [{ value: current.id, label: `${current.name} · cerrado` }]
: []),
];
},
);
/** A closed sprint keeps its tickets. */
protected readonly sprintLocked = computed(() => {
const current = this.facade.issue()?.sprint;
return (
!!current && !this.project.openSprints().some((s) => s.id === current.id)
);
});
protected readonly priorityOptions = options(PRIORITY);
/** Active values, plus the ticket's own even if archived since. */
protected readonly typeOptions = computed<PiSelectFieldOption<string>[]>(