Spaces:
Sleeping
Sleeping
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>YELY | Corriger une lecture</title> | |
| <style> | |
| :root { | |
| --bg: #0f172a; | |
| --panel: #1e293b; | |
| --panel-2: #273449; | |
| --border: #334155; | |
| --text: #e2e8f0; | |
| --muted: #94a3b8; | |
| --accent: #f59e0b; | |
| --ok-bg: #052e1f; | |
| --ok-border: #10b981; | |
| --ok-text: #6ee7b7; | |
| --ko-bg: #3b0d0d; | |
| --ko-border: #ef4444; | |
| --ko-text: #fca5a5; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: -apple-system, "Segoe UI", Roboto, Arial, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| display: flex; | |
| justify-content: center; | |
| padding: 2rem 1rem; | |
| } | |
| .app { width: 100%; max-width: 480px; } | |
| .brand { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.25rem; } | |
| .brand-left { display: flex; align-items: center; gap: 0.6rem; } | |
| .brand .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); } | |
| .brand h1 { font-size: 1.2rem; margin: 0; } | |
| .brand a { color: var(--muted); font-size: 0.85rem; text-decoration: none; border: 1px solid var(--border); padding: 0.4rem 0.7rem; border-radius: 8px; } | |
| .brand a:hover { border-color: var(--accent); color: var(--text); } | |
| .subtitle { color: var(--muted); font-size: 0.9rem; margin: 0 0 1.5rem; } | |
| .card { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 1.25rem; margin-bottom: 1rem; } | |
| label { display: block; font-size: 0.85rem; color: var(--muted); margin: 0.9rem 0 0.3rem; } | |
| label:first-child { margin-top: 0; } | |
| input { | |
| width: 100%; | |
| padding: 0.65rem 0.75rem; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| background: var(--panel-2); | |
| color: var(--text); | |
| font-size: 1rem; | |
| } | |
| #photoPreview { | |
| display: none; | |
| width: 100%; | |
| max-height: 280px; | |
| object-fit: contain; | |
| border-radius: 10px; | |
| margin-top: 0.75rem; | |
| border: 1px solid var(--border); | |
| background: #000; | |
| } | |
| #photoMissing { display: none; color: var(--muted); font-size: 0.85rem; margin-top: 0.6rem; } | |
| button { | |
| width: 100%; | |
| margin-top: 1.25rem; | |
| padding: 0.85rem; | |
| border: none; | |
| border-radius: 10px; | |
| background: var(--accent); | |
| color: #1a1200; | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| } | |
| button:disabled { background: #4b5563; color: #9ca3af; cursor: not-allowed; } | |
| .status { font-size: 0.85rem; color: var(--muted); text-align: center; margin-top: 0.75rem; min-height: 1.2em; } | |
| .banner { display: none; border-radius: 12px; padding: 1rem 1.1rem; font-weight: 700; font-size: 1.05rem; margin-bottom: 1rem; border: 1px solid; } | |
| .banner.ok { display: block; background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); } | |
| .banner.ko { display: block; background: var(--ko-bg); border-color: var(--ko-border); color: var(--ko-text); } | |
| .hint { color: var(--muted); font-size: 0.78rem; margin-top: 0.4rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <div class="brand"> | |
| <div class="brand-left"><span class="dot"></span><h1>YELY | Corriger une lecture</h1></div> | |
| <a href="stats.html">← Tableau de bord</a> | |
| </div> | |
| <p class="subtitle">Renseigne les valeurs réellement affichées sur l'écran, sert à améliorer le modèle (voir docs/MONITORING.md).</p> | |
| <div id="banner" class="banner"></div> | |
| <form id="form" class="card"> | |
| <label>Référence de la photo</label> | |
| <input type="text" id="photoReference" placeholder="ex. 3f2a1c9e-....jpg" required /> | |
| <img id="photoPreview" alt="Photo de la transaction" /> | |
| <div id="photoMissing">Photo introuvable pour cette référence (vérifie l'URL de l'API ou la référence).</div> | |
| <label>Montant réellement affiché (FCFA)</label> | |
| <input type="number" id="correctedPrix" step="0.01" placeholder="ex. 10000" /> | |
| <label>Volume réellement affiché (litres)</label> | |
| <input type="number" id="correctedVolume" step="0.01" placeholder="ex. 14.28" /> | |
| <label>Prix du litre réellement affiché (FCFA)</label> | |
| <input type="number" id="correctedPrixLitre" step="0.01" placeholder="ex. 700" /> | |
| <label>Corrigé par (optionnel)</label> | |
| <input type="text" id="correctedBy" placeholder="pompiste-7" /> | |
| <details style="margin-top: 0.9rem;"> | |
| <summary style="cursor:pointer; color: var(--muted); font-size: 0.8rem;">Configuration (URL de l'API)</summary> | |
| <label>URL du service (base, sans /analyze)</label> | |
| <input type="text" id="apiUrl" /> | |
| </details> | |
| <button type="submit" id="submitBtn">Envoyer la correction</button> | |
| <div class="status" id="status"></div> | |
| <div class="hint">Au moins une valeur est requise ; laisse vide ce qui n'est pas lisible.</div> | |
| </form> | |
| </div> | |
| <script src="config.js"></script> | |
| <script> | |
| const form = document.getElementById('form'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const statusEl = document.getElementById('status'); | |
| const banner = document.getElementById('banner'); | |
| const photoRefInput = document.getElementById('photoReference'); | |
| const photoPreview = document.getElementById('photoPreview'); | |
| const photoMissing = document.getElementById('photoMissing'); | |
| const apiUrlInput = document.getElementById('apiUrl'); | |
| function defaultBaseUrl() { | |
| const stored = localStorage.getItem('yely_stats_base_url') | |
| || localStorage.getItem('yely_api_url') | |
| || window.YELY_API_URL || ''; | |
| return stored.replace(/\/analyze\/?$/, '').replace(/\/+$/, ''); | |
| } | |
| apiUrlInput.value = defaultBaseUrl(); | |
| const params = new URLSearchParams(window.location.search); | |
| const prefillRef = params.get('ref') || params.get('photo_reference'); | |
| if (prefillRef) photoRefInput.value = prefillRef; | |
| function loadPhotoPreview() { | |
| const base = apiUrlInput.value.trim().replace(/\/+$/, ''); | |
| const ref = photoRefInput.value.trim(); | |
| photoPreview.style.display = 'none'; | |
| photoMissing.style.display = 'none'; | |
| if (!base || !ref) return; | |
| const url = base + '/photos/' + encodeURIComponent(ref); | |
| photoPreview.onload = () => { photoPreview.style.display = 'block'; }; | |
| photoPreview.onerror = () => { photoMissing.style.display = 'block'; }; | |
| photoPreview.src = url + '?t=' + Date.now(); | |
| } | |
| photoRefInput.addEventListener('change', loadPhotoPreview); | |
| apiUrlInput.addEventListener('change', loadPhotoPreview); | |
| loadPhotoPreview(); | |
| form.addEventListener('submit', async (event) => { | |
| event.preventDefault(); | |
| const base = apiUrlInput.value.trim().replace(/\/+$/, ''); | |
| localStorage.setItem('yely_stats_base_url', base); | |
| const ref = photoRefInput.value.trim(); | |
| if (!base || !ref) { | |
| statusEl.textContent = "Renseigne l'URL de l'API et la référence de la photo."; | |
| return; | |
| } | |
| const prix = document.getElementById('correctedPrix').value; | |
| const volume = document.getElementById('correctedVolume').value; | |
| const prixLitre = document.getElementById('correctedPrixLitre').value; | |
| if (!prix && !volume && !prixLitre) { | |
| statusEl.textContent = "Renseigne au moins une valeur corrigée."; | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('photo_reference', ref); | |
| if (prix) formData.append('corrected_prix', prix); | |
| if (volume) formData.append('corrected_volume', volume); | |
| if (prixLitre) formData.append('corrected_prix_litre', prixLitre); | |
| const correctedBy = document.getElementById('correctedBy').value.trim(); | |
| if (correctedBy) formData.append('corrected_by', correctedBy); | |
| submitBtn.disabled = true; | |
| submitBtn.textContent = 'Envoi…'; | |
| banner.className = 'banner'; | |
| banner.textContent = ''; | |
| statusEl.textContent = 'Envoi de la correction…'; | |
| try { | |
| const res = await fetch(base + '/feedback', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| if (res.ok && data.success) { | |
| banner.className = 'banner ok'; | |
| banner.textContent = '✅ Correction enregistrée, merci.'; | |
| statusEl.textContent = ''; | |
| } else { | |
| banner.className = 'banner ko'; | |
| banner.textContent = '⛔ ' + (data.detail || 'Échec de l\'enregistrement.'); | |
| statusEl.textContent = ''; | |
| } | |
| } catch (err) { | |
| banner.className = 'banner ko'; | |
| banner.textContent = "⛔ Impossible de contacter l'API (" + err.message + ")."; | |
| statusEl.textContent = ''; | |
| } finally { | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Envoyer la correction'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |