401da09f8f
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
170 lines
6.2 KiB
TypeScript
170 lines
6.2 KiB
TypeScript
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';
|
||
|
||
@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);
|
||
|
||
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;
|
||
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<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']);
|
||
}
|
||
}
|