Files
Bonsai-webapp/src/app/dashboard/dashboard.ts
T
2026-05-31 10:00:36 +02:00

177 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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 {
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']);
}
}