- tablesort.js : parseFloat lisait l'année des dates ISO (2026) et mettait toutes les lignes à égalité — comparaison numérique seulement pour les valeurs entièrement numériques. - dashboard.js : pinned-grid rejoint les grilles triées et regroupées (son exclusion rendait les deux outils invisibles là où l'œil se pose) ; dates comparées via Date.parse. - data-first-dir=desc sur Dernière activité / Écart / Zombies.
121 lines
6.3 KiB
HTML
121 lines
6.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token }}">
|
|
<title>Prévisions — Kanbante</title>
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
</head>
|
|
<body>
|
|
{% include "_header.html" %}
|
|
|
|
<main class="focus-main">
|
|
<section class="briefing-card">
|
|
<h1>🔮 Prévisions — « on finit quand ? »</h1>
|
|
<p>Le tableau de bord des jalons, <strong>tous projets confondus</strong>, trié par risque : l'écart entre la date estimée (au débit réel du journal) et l'échéance. Réponse honnête — fourchettes plutôt que faux chiffres précis, et aucune prévision sans historique de fermetures.</p>
|
|
<p class="forecast-summary">
|
|
<span class="chip chip-total">🎯 {{ summary.total }} jalon(s) ouvert(s)</span>
|
|
{% if summary.late %}<span class="chip chip-late">⚠️ {{ summary.late }} en retard à ce rythme</span>{% endif %}
|
|
{% if summary.ontime %}<span class="chip chip-ok">✅ {{ summary.ontime }} dans les temps</span>{% endif %}
|
|
</p>
|
|
<div class="search-bar forecast-filter">
|
|
<input type="text" id="ms-filter" placeholder="🔍 Filtrer par projet ou jalon..." autocomplete="off">
|
|
</div>
|
|
</section>
|
|
|
|
{% if rows %}
|
|
<table class="proj-table pilot-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-sort="project">Projet</th>
|
|
<th data-sort="title">Jalon</th>
|
|
<th data-sort="due">Échéance</th>
|
|
<th>Progression</th>
|
|
<th>ETA</th>
|
|
<th data-sort="gap" data-first-dir="desc">Écart</th>
|
|
<th data-sort="throughput">Débit / sem.</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in rows %}
|
|
<tr{% if row.anchor %} id="ms-{{ row.owner }}-{{ row.repo }}"{% endif %}
|
|
data-project="{{ row.project|lower }}" data-title="{{ row.title|lower }}"
|
|
data-due="{{ row.due_sort }}" data-gap="{{ row.gap_days if row.gap_days is not none else '' }}"
|
|
data-throughput="{{ row.throughput if row.throughput is not none else '' }}"
|
|
data-search="{{ (row.project ~ ' ' ~ row.title)|lower }}">
|
|
<td><a href="/project/{{ row.project }}">{{ row.project }}</a></td>
|
|
<td><a href="/board/{{ row.project }}?milestone={{ row.title|urlencode }}"><strong>{{ row.title }}</strong></a></td>
|
|
<td>{{ row.due_label }}</td>
|
|
<td>
|
|
{% if row.total %}
|
|
<div class="progress ms-progress pilot-progress"><span style="width: {{ row.pct }}%"></span></div>
|
|
<span class="ms-meta">{{ row.closed }}/{{ row.total }}</span>
|
|
{% else %}—{% endif %}
|
|
</td>
|
|
<td>{{ row.eta or '—' }}</td>
|
|
<td>
|
|
{% if row.verdict == 'late' %}
|
|
<span class="verdict verdict-late">⚠️ +{{ row.gap_days }} j</span>
|
|
{% elif row.verdict == 'ontime' %}
|
|
<span class="verdict verdict-ok">✅ dans les temps{% if row.gap_days is not none and row.gap_days < 0 %} ({{ -row.gap_days }} j d'avance){% endif %}</span>
|
|
{% elif row.verdict == 'nodue' %}
|
|
<span class="verdict">sans échéance</span>
|
|
{% else %}
|
|
<span class="verdict">pas d'ETA</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ row.throughput if row.throughput is not none else '—' }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<section class="briefing-card"><p>Aucun jalon ouvert dans les projets actifs. Crée des jalons dans Gitea pour piloter par échéances.</p></section>
|
|
{% endif %}
|
|
|
|
<section class="briefing-card">
|
|
<h2>📈 Débit par projet</h2>
|
|
{% for project in projects %}
|
|
<div class="forecast-flex">
|
|
<div class="forecast-proj-name">📂 <a href="/project/{{ project.full_name }}">{{ project.full_name }}</a></div>
|
|
<svg class="sparkline" viewBox="0 0 168 48" role="img" aria-label="Fermetures par semaine">
|
|
{% for bar in project.bars %}
|
|
{% set px = (bar.h * 0.42)|round|int %}
|
|
<rect x="{{ loop.index0 * 21 }}" y="{{ 44 - px }}" width="15" height="{{ px if px > 0 else 1 }}" rx="2">
|
|
<title>{{ bar.label }} : {{ bar.count }} fermée(s)</title>
|
|
</rect>
|
|
{% endfor %}
|
|
</svg>
|
|
<div class="forecast-nums">
|
|
{% if project.stats %}
|
|
<p>Débit : <strong>{{ project.stats.avg }} / semaine</strong> (min {{ project.stats.min }}, max {{ project.stats.max }})</p>
|
|
{% else %}
|
|
<p>Débit : <strong>pas assez d'historique</strong>.</p>
|
|
{% endif %}
|
|
<p>Restant ouvert : <strong>{{ project.remaining }}</strong>{% if project.eta_label %} · 🔮 backlog {{ project.eta_label }}{% endif %}</p>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<p>Aucun projet actif.</p>
|
|
{% endfor %}
|
|
</section>
|
|
</main>
|
|
<footer>
|
|
<span>Kanbante v{{ version }} · prévisions = journal des fermetures, pas boule de cristal</span>
|
|
</footer>
|
|
{% include "_palette.html" %}
|
|
<script src="/static/js/palette.js" defer></script>
|
|
<script src="/static/js/live.js" defer></script>
|
|
<script src="/static/js/tablesort.js" defer></script>
|
|
<script>
|
|
document.getElementById('ms-filter')?.addEventListener('input', function (e) {
|
|
var q = e.target.value.toLowerCase();
|
|
document.querySelectorAll('.pilot-table tbody tr').forEach(function (row) {
|
|
row.style.display = !q || (row.dataset.search || '').includes(q) ? '' : 'none';
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|