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'; import { StatusesStore } from '../statuses/statuses.store'; import { ProjectContextService } from '../projects/project-context.service'; @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); private readonly statusesStore = inject(StatusesStore); private readonly projectContext = inject(ProjectContextService); constructor() { const projectId = this.projectContext.projectId()!; this.issuesStore.load(projectId); this.milestonesStore.load(projectId); } 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; const done = this.issuesStore.issues().filter((i) => this.statusesStore.isCompleted(i.status)).length; return Math.round((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') && !this.statusesStore.isCompleted(i.status)) .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 || this.statusesStore.isCompleted(i.status)) 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 || this.statusesStore.isCompleted(i.status)) 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) => this.statusesStore.isCompleted(i.status)).length / linked.length) * 100); } protected formatDate(iso: string): string { if (!iso) return '—'; const d = new Date(iso); const day = String(d.getDate()).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0'); const year = d.getFullYear(); return `${day}/${month}/${year}`; } protected typeIcon(type: IssueEntity['type']): { letter: string; bg: string } { const map: Record = { 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 = { 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 = { 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 { const pid = this.projectContext.projectId(); this.router.navigate(['/projects', pid, 'issues', id]); } protected openMilestone(id: number): void { const pid = this.projectContext.projectId(); this.router.navigate(['/projects', pid, 'milestones', id]); } protected navigateToIssues(): void { const pid = this.projectContext.projectId(); this.router.navigate(['/projects', pid, 'issues']); } protected navigateToMilestones(): void { const pid = this.projectContext.projectId(); this.router.navigate(['/projects', pid, 'milestones']); } }