- Remplace la table trop large par une carte de révision centrée - Une paire à la fois : noms wrappés, score, prix moyens - Valider/Rejeter via fetch() sans rechargement de page - Passage automatique à la paire suivante après chaque action - Compteurs mis à jour en temps réel (en attente/validées/rejetées) - Message de fin avec lien vers /compare quand tout est traité - Ajout tests pytest pour la page /matches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
170 lines
5.9 KiB
HTML
170 lines
5.9 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Correspondances fuzzy — TicketTracker{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1>Correspondances Picnic ↔ Leclerc</h1>
|
|
|
|
<!-- Compteurs -->
|
|
<div class="stat-grid" id="stat-grid">
|
|
<article class="stat-card">
|
|
<h3 id="stat-pending">{{ pending | length }}</h3>
|
|
<p>En attente</p>
|
|
</article>
|
|
<article class="stat-card">
|
|
<h3 id="stat-validated">{{ validated_count }}</h3>
|
|
<p>Validées</p>
|
|
</article>
|
|
<article class="stat-card">
|
|
<h3 id="stat-rejected">{{ rejected_count }}</h3>
|
|
<p>Rejetées</p>
|
|
</article>
|
|
</div>
|
|
|
|
{% if pending %}
|
|
|
|
<!-- Carte de révision -->
|
|
<div id="review-zone">
|
|
<article id="match-card" class="match-card">
|
|
|
|
<!-- Barre de progression -->
|
|
<div class="match-progress">
|
|
<span id="progress-text"></span>
|
|
<span id="score-badge" class="match-score"></span>
|
|
</div>
|
|
|
|
<!-- Les deux noms face à face -->
|
|
<div class="match-sides">
|
|
<div class="match-side match-side-picnic">
|
|
<div class="match-store-label">🛍 Picnic</div>
|
|
<div class="match-name" id="name-picnic"></div>
|
|
<div class="match-price" id="price-picnic"></div>
|
|
</div>
|
|
<div class="match-vs">↔</div>
|
|
<div class="match-side match-side-leclerc">
|
|
<div class="match-store-label">🏪 Leclerc</div>
|
|
<div class="match-name" id="name-leclerc"></div>
|
|
<div class="match-price" id="price-leclerc"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Boutons d'action -->
|
|
<div class="match-buttons">
|
|
<button id="btn-reject" class="btn-reject secondary outline" onclick="decide('reject')">
|
|
✗ Rejeter
|
|
</button>
|
|
<button id="btn-validate" class="btn-validate" onclick="decide('validate')">
|
|
✓ Valider — c'est le même produit
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Message d'erreur éventuel -->
|
|
<p id="error-msg" style="color:var(--pico-color-red-500);display:none;text-align:center;margin-top:0.5rem"></p>
|
|
</article>
|
|
</div>
|
|
|
|
<!-- Message "tout fait" (caché au départ) -->
|
|
<article id="done-card" style="display:none;text-align:center;padding:2rem">
|
|
<h2>Tout traité ✓</h2>
|
|
<p id="done-summary"></p>
|
|
<a href="/matches" role="button" class="secondary outline">Recharger la page</a>
|
|
|
|
<a href="/compare" role="button">Voir la comparaison →</a>
|
|
</article>
|
|
|
|
<script>
|
|
/* Liste des paires en attente, injectée depuis le serveur */
|
|
const MATCHES = {{ pending_json | safe }};
|
|
|
|
let idx = 0;
|
|
let sessionValidated = 0;
|
|
let sessionRejected = 0;
|
|
|
|
function fmt_price(p) {
|
|
return p !== null && p !== undefined ? p.toFixed(2) + ' €' : '—';
|
|
}
|
|
|
|
function show_current() {
|
|
if (idx >= MATCHES.length) {
|
|
/* Plus rien à traiter */
|
|
document.getElementById('review-zone').style.display = 'none';
|
|
const done = document.getElementById('done-card');
|
|
done.style.display = '';
|
|
document.getElementById('stat-pending').textContent = '0';
|
|
document.getElementById('done-summary').textContent =
|
|
sessionValidated + ' validée(s), ' + sessionRejected + ' rejetée(s) cette session.';
|
|
return;
|
|
}
|
|
|
|
const m = MATCHES[idx];
|
|
const score = Math.round(m.score);
|
|
const scoreEl = document.getElementById('score-badge');
|
|
scoreEl.textContent = 'Score : ' + score + '%';
|
|
scoreEl.className = 'match-score ' +
|
|
(score >= 95 ? 'score-high' : score >= 85 ? 'score-medium' : 'score-low');
|
|
|
|
document.getElementById('progress-text').textContent =
|
|
'Paire ' + (idx + 1) + ' / ' + MATCHES.length;
|
|
|
|
document.getElementById('name-picnic').textContent = m.name_picnic;
|
|
document.getElementById('name-leclerc').textContent = m.name_leclerc;
|
|
document.getElementById('price-picnic').textContent = 'Prix moyen : ' + fmt_price(m.price_picnic);
|
|
document.getElementById('price-leclerc').textContent = 'Prix moyen : ' + fmt_price(m.price_leclerc);
|
|
|
|
document.getElementById('error-msg').style.display = 'none';
|
|
document.getElementById('btn-validate').disabled = false;
|
|
document.getElementById('btn-reject').disabled = false;
|
|
document.getElementById('stat-pending').textContent = MATCHES.length - idx;
|
|
}
|
|
|
|
async function decide(action) {
|
|
const m = MATCHES[idx];
|
|
document.getElementById('btn-validate').disabled = true;
|
|
document.getElementById('btn-reject').disabled = true;
|
|
|
|
try {
|
|
const resp = await fetch('/api/match/' + m.id + '/' + action, {method: 'POST'});
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
} catch (e) {
|
|
document.getElementById('error-msg').textContent = 'Erreur : ' + e.message;
|
|
document.getElementById('error-msg').style.display = '';
|
|
document.getElementById('btn-validate').disabled = false;
|
|
document.getElementById('btn-reject').disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (action === 'validate') {
|
|
sessionValidated++;
|
|
document.getElementById('stat-validated').textContent =
|
|
parseInt(document.getElementById('stat-validated').textContent) + 1;
|
|
} else {
|
|
sessionRejected++;
|
|
document.getElementById('stat-rejected').textContent =
|
|
parseInt(document.getElementById('stat-rejected').textContent) + 1;
|
|
}
|
|
|
|
idx++;
|
|
show_current();
|
|
}
|
|
|
|
/* Démarrage */
|
|
show_current();
|
|
</script>
|
|
|
|
{% else %}
|
|
<article>
|
|
<p>
|
|
Aucune paire en attente.
|
|
{% if validated_count == 0 and rejected_count == 0 %}
|
|
Lancez d'abord la commande de matching :
|
|
<pre><code>python -m tickettracker.cli match --threshold 85</code></pre>
|
|
{% else %}
|
|
Toutes les paires ont été traitées ({{ validated_count }} validées, {{ rejected_count }} rejetées).
|
|
<a href="/compare">Voir la comparaison →</a>
|
|
{% endif %}
|
|
</p>
|
|
</article>
|
|
{% endif %}
|
|
|
|
{% endblock %}
|