Ajoute du tableau de bord

Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
2026-05-28 19:05:27 +02:00
parent 43421b5fb1
commit 6f4d431f10
9 changed files with 848 additions and 2 deletions
+166
View File
@@ -0,0 +1,166 @@
import { Component, computed, inject } from '@angular/core';
import { Router } from '@angular/router';
import { IssueEntity, IssuesStore } from '../issues/issues.store';
import { MilestoneEntity, MilestonesStore } from '../milestones/milestones.store';
@Component({
selector: 'app-dashboard',
imports: [],
templateUrl: './dashboard.html',
styleUrl: './dashboard.css',
})
export class Dashboard {
private readonly router = inject(Router);
private readonly issuesStore = inject(IssuesStore);
private readonly milestonesStore = inject(MilestonesStore);
constructor() {
this.issuesStore.load();
this.milestonesStore.load();
}
protected readonly totalIssues = computed(() => this.issuesStore.issues().length);
protected readonly statusCounts = computed(() => ({
draft: this.issuesStore.issues().filter((i) => i.status === 'draft').length,
todo: this.issuesStore.issues().filter((i) => i.status === 'todo').length,
inProgress: this.issuesStore.issues().filter((i) => i.status === 'in-progress').length,
done: this.issuesStore.issues().filter((i) => i.status === 'done').length,
}));
protected readonly completionRate = computed(() => {
const total = this.totalIssues();
if (total === 0) return 0;
return Math.round((this.statusCounts().done / total) * 100);
});
protected readonly totalMilestones = computed(() => this.milestonesStore.milestones().length);
protected readonly statusItems = computed(() =>
(['todo', 'in-progress', 'draft', 'done'] as IssueEntity['status'][]).map((status) => ({
status,
count: this.issuesStore.issues().filter((i) => i.status === status).length,
})),
);
protected readonly issuesByType = computed(() => {
const types: IssueEntity['type'][] = ['Epic', 'Bug', 'Study', 'Story', 'Task', 'Technical Story'];
return types
.map((type) => ({
type,
count: this.issuesStore.issues().filter((i) => i.type === type).length,
icon: this.typeIcon(type),
}))
.filter((t) => t.count > 0);
});
protected readonly highPriorityIssues = computed(() =>
this.issuesStore
.issues()
.filter((i) => (i.priority === 'HAUTE' || i.priority === 'TRES_HAUTE') && i.status !== 'done')
.slice(0, 6),
);
protected readonly activeMilestones = computed(() =>
this.milestonesStore
.milestones()
.map((m) => ({ ...m, progress: this.getMilestoneProgress(m) }))
.filter((m) => m.progress < 100)
.slice(0, 5),
);
protected readonly upcomingIssues = computed(() => {
const now = new Date();
const twoWeeks = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
return this.issuesStore
.issues()
.filter((i) => {
if (!i.dueDate || i.status === 'done') return false;
const due = new Date(i.dueDate);
return due >= now && due <= twoWeeks;
})
.sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime())
.slice(0, 6);
});
protected readonly overdueCount = computed(() => {
const now = new Date();
return this.issuesStore.issues().filter((i) => {
if (!i.dueDate || i.status === 'done') return false;
return new Date(i.dueDate) < now;
}).length;
});
private getMilestoneProgress(milestone: MilestoneEntity): number {
if (milestone.issueIds.length === 0) return 0;
const linked = this.issuesStore.issues().filter((i) => milestone.issueIds.includes(i.id));
if (linked.length === 0) return 0;
return Math.round((linked.filter((i) => i.status === 'done').length / linked.length) * 100);
}
protected formatDate(iso: string): string {
if (!iso) return '—';
return new Date(iso).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
protected typeIcon(type: IssueEntity['type']): { letter: string; bg: string } {
const map: Record<IssueEntity['type'], { letter: string; bg: string }> = {
Epic: { letter: 'E', bg: '#7c3aed' },
Bug: { letter: 'B', bg: '#dc2626' },
Story: { letter: 'S', bg: '#16a34a' },
Task: { letter: 'T', bg: '#2563eb' },
Study: { letter: 'St', bg: '#6b7280' },
'Technical Story': { letter: 'TS', bg: '#d97706' },
};
return map[type] ?? { letter: '?', bg: '#6b7280' };
}
protected priorityDisplay(priority: IssueEntity['priority']): {
symbol: string;
color: string;
label: string;
} {
const map: Record<IssueEntity['priority'], { symbol: string; color: string; label: string }> = {
TRES_HAUTE: { symbol: '↑↑', color: '#dc3545', label: 'Très haute' },
HAUTE: { symbol: '↑', color: '#fd7e14', label: 'Haute' },
MOYENNE: { symbol: '', color: '#ffc107', label: 'Moyenne' },
BASSE: { symbol: '↓', color: '#0d6efd', label: 'Basse' },
TRES_FAIBLE: { symbol: '↓↓', color: '#6c757d', label: 'Très faible' },
};
return map[priority] ?? { symbol: '?', color: '#6c757d', label: priority };
}
protected statusBadge(status: IssueEntity['status']): {
label: string;
bg: string;
color: string;
} {
const map: Record<IssueEntity['status'], { label: string; bg: string; color: string }> = {
draft: { label: 'BROUILLON', bg: '#e2e8f0', color: '#475569' },
todo: { label: 'À FAIRE', bg: '#dbeafe', color: '#1d4ed8' },
'in-progress': { label: 'EN COURS', bg: '#ffedd5', color: '#9a3412' },
done: { label: 'TERMINÉ', bg: '#dcfce7', color: '#166534' },
};
return map[status] ?? { label: status, bg: '#e2e8f0', color: '#475569' };
}
protected openIssue(id: number): void {
this.router.navigate(['/issues', id]);
}
protected openMilestone(id: number): void {
this.router.navigate(['/milestones', id]);
}
protected navigateToIssues(): void {
this.router.navigate(['/issues']);
}
protected navigateToMilestones(): void {
this.router.navigate(['/milestones']);
}
}