Ajoute du tableau de bord
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
@@ -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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user