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
+217
View File
@@ -0,0 +1,217 @@
<!-- suppress HtmlUnknownAttribute -->
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h2 mb-2">Tableau de bord</h1>
<p class="text-secondary mb-0">Vue d'ensemble du projet.</p>
</div>
</div>
<!-- KPI cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-lg-3">
<div class="card shadow-sm kpi-card" role="button" tabindex="0" (click)="navigateToIssues()" (keydown.enter)="navigateToIssues()">
<div class="card-body">
<div class="kpi-label">Issues totales</div>
<div class="kpi-value">{{ totalIssues() }}</div>
<div class="kpi-sub text-secondary">{{ completionRate() }}% terminées</div>
<div class="progress mt-2" style="height: 4px;">
<div class="progress-bar bg-success" role="progressbar" [style.width.%]="completionRate()"></div>
</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card shadow-sm kpi-card">
<div class="card-body">
<div class="kpi-label">En cours</div>
<div class="kpi-value" style="color: #9a3412;">{{ statusCounts().inProgress }}</div>
<div class="kpi-sub text-secondary">{{ statusCounts().todo }} à faire</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card shadow-sm kpi-card" [class.kpi-card--alert]="overdueCount() > 0">
<div class="card-body">
<div class="kpi-label">En retard</div>
<div class="kpi-value" [class.text-danger]="overdueCount() > 0" [class.text-secondary]="overdueCount() === 0">
{{ overdueCount() }}
</div>
<div class="kpi-sub text-secondary">issues dépassées</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card shadow-sm kpi-card" role="button" tabindex="0" (click)="navigateToMilestones()" (keydown.enter)="navigateToMilestones()">
<div class="card-body">
<div class="kpi-label">Milestones</div>
<div class="kpi-value">{{ totalMilestones() }}</div>
<div class="kpi-sub text-secondary">{{ activeMilestones().length }} en cours</div>
</div>
</div>
</div>
</div>
<!-- Statuts & Types -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white fw-semibold small">Répartition par statut</div>
<div class="card-body">
@for (item of statusItems(); track item.status) {
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span
class="status-badge"
[style.background]="statusBadge(item.status).bg"
[style.color]="statusBadge(item.status).color"
>{{ statusBadge(item.status).label }}</span>
<span class="text-secondary small">{{ item.count }}</span>
</div>
<div class="progress" style="height: 5px;">
<div
class="progress-bar"
role="progressbar"
[style.width.%]="totalIssues() > 0 ? (item.count / totalIssues() * 100) : 0"
[style.background]="statusBadge(item.status).color"
></div>
</div>
</div>
}
@if (totalIssues() === 0) {
<p class="text-secondary text-center py-2 mb-0">Aucune issue.</p>
}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white fw-semibold small">Répartition par type</div>
<div class="card-body">
@for (item of issuesByType(); track item.type) {
<div class="d-flex align-items-center gap-2 mb-2">
<span class="type-icon" [style.background]="item.icon.bg">{{ item.icon.letter }}</span>
<span class="flex-grow-1 small">{{ item.type }}</span>
<span class="badge bg-secondary rounded-pill">{{ item.count }}</span>
</div>
}
@if (issuesByType().length === 0) {
<p class="text-secondary text-center py-2 mb-0">Aucune issue.</p>
}
</div>
</div>
</div>
</div>
<!-- Haute priorité & Milestones en cours -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span class="fw-semibold small">Haute priorité — ouvertes</span>
@if (highPriorityIssues().length > 0) {
<span class="badge bg-danger rounded-pill">{{ highPriorityIssues().length }}</span>
}
</div>
<div class="card-body p-0">
@for (issue of highPriorityIssues(); track issue.id) {
<div
class="issue-row"
tabindex="0"
(click)="openIssue(issue.id)"
(keydown.enter)="openIssue(issue.id)"
>
<span class="type-icon" [style.background]="typeIcon(issue.type).bg">{{ typeIcon(issue.type).letter }}</span>
<span class="flex-grow-1 small text-truncate">{{ issue.name }}</span>
<span
class="priority-symbol"
[style.color]="priorityDisplay(issue.priority).color"
[title]="priorityDisplay(issue.priority).label"
>{{ priorityDisplay(issue.priority).symbol }}</span>
<span
class="status-badge-sm"
[style.background]="statusBadge(issue.status).bg"
[style.color]="statusBadge(issue.status).color"
>{{ statusBadge(issue.status).label }}</span>
</div>
}
@if (highPriorityIssues().length === 0) {
<p class="text-secondary text-center py-4 mb-0">Aucune issue haute priorité ouverte.</p>
}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white fw-semibold small">Milestones en cours</div>
<div class="card-body p-0">
@for (m of activeMilestones(); track m.id) {
<div
class="milestone-row"
tabindex="0"
(click)="openMilestone(m.id)"
(keydown.enter)="openMilestone(m.id)"
>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-truncate flex-grow-1 me-2">{{ m.name }}</span>
<span class="small text-secondary text-nowrap">{{ m.progress }}%</span>
</div>
<div class="d-flex align-items-center gap-2">
<div class="progress flex-grow-1" style="height: 5px;">
<div
class="progress-bar"
role="progressbar"
[style.width.%]="m.progress"
[attr.aria-valuenow]="m.progress"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
<span class="small text-secondary text-nowrap">{{ formatDate(m.dueDate) }}</span>
</div>
</div>
}
@if (activeMilestones().length === 0) {
<p class="text-secondary text-center py-4 mb-0">Tous les milestones sont terminés.</p>
}
</div>
</div>
</div>
</div>
<!-- Échéances proches -->
@if (upcomingIssues().length > 0) {
<div class="row g-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span class="fw-semibold small">Échéances dans les 14 prochains jours</span>
<span class="badge bg-warning text-dark rounded-pill">{{ upcomingIssues().length }}</span>
</div>
<div class="card-body p-0">
@for (issue of upcomingIssues(); track issue.id) {
<div
class="issue-row"
tabindex="0"
(click)="openIssue(issue.id)"
(keydown.enter)="openIssue(issue.id)"
>
<span class="type-icon" [style.background]="typeIcon(issue.type).bg">{{ typeIcon(issue.type).letter }}</span>
<span class="flex-grow-1 small text-truncate">{{ issue.name }}</span>
<span class="small text-secondary text-nowrap">{{ formatDate(issue.dueDate) }}</span>
<span
class="status-badge-sm"
[style.background]="statusBadge(issue.status).bg"
[style.color]="statusBadge(issue.status).color"
>{{ statusBadge(issue.status).label }}</span>
</div>
}
</div>
</div>
</div>
</div>
}