Calcule du temps estimé des Milestone et Epic

This commit is contained in:
2026-05-30 06:59:54 +02:00
parent e81d465903
commit b1a114aaa8
6 changed files with 119 additions and 1 deletions
@@ -94,6 +94,10 @@
<span class="text-secondary small" style="min-width: 2.5rem; text-align: right;">{{ progress }}%</span>
</div>
</div>
<div class="col-md-4">
<label class="field-label">Temps estimé total (h)</label>
<div class="form-control form-control-sm bg-body-secondary text-secondary">{{ totalEstimatedTime !== null ? totalEstimatedTime : '—' }}</div>
</div>
}
</div>
</div>
@@ -580,6 +580,69 @@ describe('MilestoneDetail', () => {
expect((component as any).milestone.description).toContain('![image]');
});
});
describe('totalEstimatedTime', () => {
it('returns null when no linked issues', () => {
issuesStore.seed([]);
(component as any).milestone.issueIds = [];
expect((component as any).totalEstimatedTime).toBeNull();
});
it('returns null when all linked issues have null estimatedTime', () => {
issuesStore.seed([
makeIssue({ id: 1, estimatedTime: null }),
makeIssue({ id: 2, estimatedTime: null }),
]);
(component as any).milestone.issueIds = [1, 2];
expect((component as any).totalEstimatedTime).toBeNull();
});
it('returns the sum of estimatedTime for non-Epic issues', () => {
issuesStore.seed([
makeIssue({ id: 1, estimatedTime: 8 }),
makeIssue({ id: 2, estimatedTime: 4 }),
]);
(component as any).milestone.issueIds = [1, 2];
expect((component as any).totalEstimatedTime).toBe(12);
});
it('ignores null estimatedTime in the sum', () => {
issuesStore.seed([
makeIssue({ id: 1, estimatedTime: 8 }),
makeIssue({ id: 2, estimatedTime: null }),
]);
(component as any).milestone.issueIds = [1, 2];
expect((component as any).totalEstimatedTime).toBe(8);
});
it('uses the Epic own estimatedTime, not its children', () => {
issuesStore.seed([
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', estimatedTime: 10 }),
makeIssue({ id: 2, epic: 'My Epic', estimatedTime: 5 }),
makeIssue({ id: 3, epic: 'My Epic', estimatedTime: 3 }),
]);
(component as any).milestone.issueIds = [1];
expect((component as any).totalEstimatedTime).toBe(10);
});
it('returns null for an Epic with null estimatedTime', () => {
issuesStore.seed([
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', estimatedTime: null }),
makeIssue({ id: 2, epic: 'My Epic', estimatedTime: 5 }),
]);
(component as any).milestone.issueIds = [1];
expect((component as any).totalEstimatedTime).toBeNull();
});
it('mixes Epics and plain issues correctly', () => {
issuesStore.seed([
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', estimatedTime: 8 }),
makeIssue({ id: 3, type: 'Story', estimatedTime: 6 }),
]);
(component as any).milestone.issueIds = [1, 3];
expect((component as any).totalEstimatedTime).toBe(14);
});
});
});
describe('MilestoneDetail — new route', () => {
@@ -110,6 +110,13 @@ export class MilestoneDetail {
return tasks;
}
protected get totalEstimatedTime(): number | null {
const times = this.linkedIssues
.filter((i): i is IssueEntity & { estimatedTime: number } => i.estimatedTime !== null)
.map((i) => i.estimatedTime);
return times.length === 0 ? null : times.reduce((a, b) => a + b, 0);
}
protected get progress(): number {
if (this.linkedIssues.length === 0) return 0;
return Math.round(