Files
TicketTracker/tickettracker/web/templates/product.html
laurent 30e4b3e144 feat: dashboard web FastAPI Sprint 4
Ajout d'un dashboard lecture seule par-dessus la DB SQLite existante.

Fichiers créés :
  - tickettracker/web/queries.py   : 7 fonctions SQL (stats, compare, historique...)
  - tickettracker/web/api.py       : router /api/* JSON (FastAPI)
  - tickettracker/web/app.py       : routes HTML + Jinja2 + point d'entrée uvicorn
  - tickettracker/web/templates/   : base.html, index.html, compare.html, product.html, receipt.html
  - tickettracker/web/static/style.css : personnalisations Pico CSS
  - tests/test_web.py              : 19 tests (96 passent, 1 xfail OCR)

Fichiers modifiés :
  - requirements.txt : +fastapi, uvicorn[standard], jinja2, python-multipart, httpx
  - config.py        : +DB_PATH (lu depuis TICKETTRACKER_DB_PATH)

Lancement : python -m tickettracker.web.app

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 20:04:55 +01:00

119 lines
3.4 KiB
HTML

{% extends "base.html" %}
{% block title %}
{% if data %}{{ data.name }}{% else %}Produit inconnu{% endif %} — TicketTracker
{% endblock %}
{% block content %}
<h1>Historique produit</h1>
<!-- Sélecteur de produit : navigation JS vers /product/<nom encodé> -->
<label for="product-select">Choisir un produit :</label>
<select id="product-select" onchange="window.location='/product/' + encodeURIComponent(this.value)">
<option value="">— sélectionner —</option>
{% for p in all_products %}
<option value="{{ p }}" {% if p == name %}selected{% endif %}>{{ p }}</option>
{% endfor %}
</select>
{% if empty %}
<article>
<p>Produit <strong>{{ name }}</strong> introuvable dans la base.</p>
<p>Le nom doit correspondre exactement à un <code>name_normalized</code> existant.</p>
</article>
{% else %}
<!-- Cartes statistiques -->
<div class="stat-grid">
<article class="stat-card">
<h3>{{ "%.2f"|format(data.min_price) }} €</h3>
<p>Prix minimum</p>
</article>
<article class="stat-card">
<h3>{{ "%.2f"|format(data.avg_price) }} €</h3>
<p>Prix moyen</p>
</article>
<article class="stat-card">
<h3>{{ "%.2f"|format(data.max_price) }} €</h3>
<p>Prix maximum</p>
</article>
</div>
<!-- Graphique d'historique des prix -->
<article>
<h2>Évolution du prix unitaire</h2>
<div class="chart-container">
<canvas id="priceChart"></canvas>
</div>
</article>
<script>
(function () {
const history = {{ data.history | tojson }};
// Couleurs par enseigne
const colors = { picnic: "#4a9eff", leclerc: "#ff6b35" };
const stores = [...new Set(history.map(h => h.store))].sort();
const datasets = stores.map(store => {
const points = history.filter(h => h.store === store);
return {
label: store,
data: points.map(h => ({ x: h.date, y: h.unit_price })),
borderColor: colors[store] || "#888",
backgroundColor: (colors[store] || "#888") + "33",
tension: 0.2,
fill: false,
};
});
new Chart(document.getElementById("priceChart").getContext("2d"), {
type: "line",
data: { datasets },
options: {
responsive: true,
scales: {
x: { type: "category", title: { display: true, text: "Date" } },
y: {
title: { display: true, text: "Prix unitaire (€)" },
ticks: { callback: v => v + " €" }
}
},
plugins: { legend: { position: "top" } }
}
});
})();
</script>
<!-- Tableau des occurrences -->
<article>
<h2>Toutes les occurrences</h2>
<div class="overflow-auto">
<table>
<thead>
<tr>
<th>Date</th>
<th>Enseigne</th>
<th>Prix unitaire</th>
<th>Quantité</th>
<th>Unité</th>
</tr>
</thead>
<tbody>
{% for h in data.history %}
<tr>
<td>{{ h.date }}</td>
<td>{{ h.store }}</td>
<td>{{ "%.2f"|format(h.unit_price) }} €</td>
<td>{{ h.quantity }}</td>
<td>{{ h.unit }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
{% endif %}
{% endblock %}