# User Story: Dashboard & Consultation des Prédictions **ID** : US-002 **Epic** : Epic 5 - Dashboard & Core Visualizations + Epic 3 - Prediction System **Status** : Draft **Priorité** : P0 - Critique (c'est le cœur de l'application) --- ## 📋 Description En tant qu'utilisateur connecté, je veux consulter les prédictions de matchs sur un dashboard clair et intuitif avec des indicateurs de confiance pour pouvoir prendre des décisions éclairées. ## 🎯 Objectifs d'Utilisateur 1. Voir une liste de matchs à venir avec leurs prédictions 2. Comprendre le niveau de confiance de chaque prédiction (Confidence Meter) 3. Voir mon classement personnel et les statistiques globales 4. Accéder aux détails de chaque match (équipes, date, ligue) 5. Récupérer l'historique de mes prédictions consultées ## ✅ Critères de Succès ### Scénario 1 : Consultation du Dashboard Principal **Given** : Je suis un utilisateur connecté sur le dashboard principal `http://localhost:3000/dashboard` **When** : La page se charge **Then** : - ✅ Je vois 3 cartes de statistiques personnelles en haut : - "Votre Classement : #42" - "Précision : 65%" - "Prédictions : 15" - ✅ Je vois une carte "Prédictions Récentes" avec les 5 derniers matchs - ✅ Chaque match affiche : équipes, date/heure, ligue, Confidence Meter - ✅ Le Confidence Meter utilise le bon code couleur : - Vert (>70%) : Match très fiable - Jaune (50-70%) : Match moyennement fiable - Rouge (<50%) : Match peu fiable ### Scénario 2 : Filtrage des Prédictions **Given** : Je suis sur le dashboard avec plusieurs prédictions **When** : Je clique sur "Filtrer" **Then** : - ✅ Je peux filtrer par ligue (ex: Ligue 1, Premier League) - ✅ Je peux filtrer par date (ex: Aujourd'hui, Demain, Cette semaine) - ✅ Je peux filtrer par niveau de confiance (ex: >70% seulement) - ✅ Les résultats se mettent à jour en temps réel sans rechargement de page ### Scénario 3 : Détails d'une Prédiction **Given** : Je clique sur un match dans la liste **When** : La page de détails s'ouvre **Then** : - ✅ Je vois les informations complètes du match : - Équipes : PSG vs Marseille - Date et heure : 2026-01-18 20:00 - Ligue : Ligue 1 - Statut : Scheduled / In Progress / Completed - ✅ Je vois les détails de la prédiction : - Vainqueur prédit : PSG - Niveau de confiance : 70% - Score d'énergie collective : Haute (72/68) - ✅ Je vois l'évolution de l'énergie sur 24h (graphique) ### Scénario 4 : Historique Personnel **Given** : Je clique sur l'onglet "Historique" **When** : La page d'historique s'affiche **Then** : - ✅ Je vois la liste chronologique de toutes mes prédictions consultées - ✅ Chaque prédiction indique : - Date de consultation - Match consulté - Résultat (si disponible) : Correct ✅ / Incorrect ❌ - ✅ Je vois mon taux de précision personnel global - ✅ Je vois mon ROI (Return on Investment) si applicable ### Scénario 5 : Classement Gamifié **Given** : Je clique sur l'onglet "Classement" **When** : La page de classement s'affiche **Then** : - ✅ Je vois le Top 100 utilisateurs classés par précision - ✅ Mon rang personnel est mis en évidence si je suis dans le Top 100 - ✅ Chaque utilisateur affiche : - Rang - Nom d'utilisateur (ou anonyme) - Taux de précision (ex: 85%) - Nombre de prédictions (ex: 100) --- ## 📱 Composants Frontend Requis ### Page Dashboard Principal (`src/app/dashboard/page.tsx`) ```tsx "use client"; import { useEffect, useState } from 'react'; import { useAuth } from '@/hooks/useAuth'; import { usePredictions } from '@/hooks/usePredictions'; import { useLeaderboard } from '@/hooks/useLeaderboard'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; export default function DashboardPage() { const { user } = useAuth(); const { predictions, isLoading, error } = usePredictions(); const { personalRank, topUsers } = useLeaderboard(); const [filter, setFilter] = useState({ league: 'all', confidence: 'all' }); // Chargement initial useEffect(() => { if (!user) return; // Charger les données du dashboard // usePredictions et useLeaderboard feront les appels API }, [user]); if (isLoading) { return ; } if (error) { return ; } // Filtrer les prédictions const filteredPredictions = predictions.filter(p => { if (filter.league !== 'all' && p.match.league !== filter.league) return false; if (filter.confidence !== 'all') { const confidenceValue = parseFloat(p.confidence); if (filter.confidence === 'high' && confidenceValue < 70) return false; if (filter.confidence === 'medium' && (confidenceValue < 50 || confidenceValue >= 70)) return false; if (filter.confidence === 'low' && confidenceValue >= 50) return false; } } return true; }); return (
{/* En-tête utilisateur */}

Bonjour, {user?.name || 'Sportif'} ! 👋

Bienvenue sur votre dashboard Chartbastan

{/* Statistiques personnelles */} {personalRank && ( )} {/* Filtres */} {/* Liste des prédictions */}
{filteredPredictions.map(prediction => ( ))}
{/* Top Classement (aperçu) */}
); } // Composant de squelette de chargement function DashboardSkeleton() { return (
{/* Statistiques personnelles */}
{[1, 2, 3].map(i => ( ))}
{/* Filtres */} {/* Cartes de prédictions */}
{[1, 2, 3, 4, 5, 6].map(i => ( ))}
); } // Composant d'état d'erreur function ErrorState({ message }: { message: string }) { return (

{message}

Veuillez rafraîchir la page ou réessayer ultérieurement.

); } ``` ### Composant Carte de Statistiques Personnelles ```tsx import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; interface PersonalStatsCardProps { personalRank: { rank: number; accuracy: number; predictions_count: number; }; } export function PersonalStatsCard({ personalRank }: PersonalStatsCardProps) { return ( 📊 Vos Statistiques
{/* Classement */}

Votre Classement

#{personalRank.rank}

{/* Précision */}

Précision

{personalRank.accuracy}%

{/* Prédictions */}

Prédictions

{personalRank.predictions_count}

); } ``` ### Composant Barre de Filtres ```tsx import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; interface FilterBarProps { filter: { league: string; confidence: string }; setFilter: (filter: { league: string; confidence: string }) => void; } export function FilterBar({ filter, setFilter }: FilterBarProps) { return (
); } ``` ### Composant Carte de Prédiction ```tsx import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { useNavigate } from 'react-router-dom'; import Link from 'next/link'; interface PredictionCardProps { prediction: { id: number; match_id: number; match: { id: number; home_team: string; away_team: string; date: string; league: string; status: string; }; energy_score: string; confidence: string; predicted_winner: string; created_at: string; }; } export function PredictionCard({ prediction }: PredictionCardProps) { const navigate = useNavigate(); // Extraire la valeur numérique de confiance const confidenceValue = parseFloat(prediction.confidence); // Déterminer le niveau de confiance pour le code couleur const getConfidenceLevel = () => { if (confidenceValue >= 70) return 'high'; if (confidenceValue >= 50) return 'medium'; return 'low'; }; const getConfidenceColor = () => { if (confidenceValue >= 70) return 'bg-green-500 text-white'; if (confidenceValue >= 50) return 'bg-yellow-500 text-black'; return 'bg-red-500 text-white'; }; const getConfidenceEmoji = () => { if (confidenceValue >= 70) return '🟢'; if (confidenceValue >= 50) return '🟡'; return '🔴'; }; const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); }; return (
{prediction.match.home_team} vs {prediction.match.away_team}
{prediction.match.league} {formatDate(prediction.match.date)} {prediction.match.status}
{getConfidenceEmoji()}
{/* Vainqueur prédit */}

Vainqueur prédit

{prediction.predicted_winner}

{/* Score d'énergie */}

Score d'énergie collective

{prediction.match.home_team}
{prediction.match.away_team}
{new Date(prediction.created_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
); } ``` ### Composant Confidence Meter ```tsx import { Card, CardContent } from '@/components/ui/card'; interface ConfidenceMeterProps { confidence: string; // Ex: "70.5%" } export function ConfidenceMeter({ confidence }: ConfidenceMeterProps) { const value = parseFloat(confidence); const getLevel = () => { if (value >= 70) return { level: 'Élevée', color: 'text-green-600', bg: 'bg-green-100' }; if (value >= 50) return { level: 'Moyenne', color: 'text-yellow-600', bg: 'bg-yellow-100' }; return { level: 'Faible', color: 'text-red-600', bg: 'bg-red-100' }; }; const level = getLevel(); return (
Confiance :
{confidence} {level.level}
); } ``` ### Hook usePredictions ```typescript // src/hooks/usePredictions.ts import { useState, useEffect } from 'react'; import { apiRequest } from '@/lib/api'; export interface Prediction { id: number; match_id: number; match: { id: number; home_team: string; away_team: string; date: string; league: string; status: string; }; energy_score: string; confidence: string; predicted_winner: string; created_at: string; } export interface PredictionsResponse { data: Prediction[]; meta: { total: number; limit: number; offset: number; timestamp: string; version: string; }; } export function usePredictions() { const [state, setState] = useState<{ predictions: Prediction[]; isLoading: boolean; error: string | null; meta: PredictionsResponse['meta'] | null; }>({ predictions: [], isLoading: false, error: null, meta: null }); const fetchPredictions = async () => { setState(prev => ({ ...prev, isLoading: true, error: null })); try { const response = await apiRequest('/api/v1/predictions?limit=20&offset=0'); if (!response.ok) { throw new Error('Erreur lors du chargement des prédictions'); } const data: PredictionsResponse = await response.json(); setState({ predictions: data.data, isLoading: false, error: null, meta: data.meta }); return data.data; } catch (error) { setState(prev => ({ ...prev, isLoading: false, error: error instanceof Error ? error.message : 'Erreur inconnue' })); return []; } }; // Charger au montage useEffect(() => { fetchPredictions(); }, []); return { ...state, fetchPredictions, // Exposer pour rafraîchissement manuel refresh: fetchPredictions }; } ``` --- ## 🔌 API Backend Requis ### Endpoints à Créer dans `backend/app/api/v1/predictions.py` ```python """ Enhancement des endpoints de prédictions avec statistiques personnelles et optimisations. """ from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from app.database import get_db from app.services.prediction_service import PredictionService from app.services.leaderboard_service import LeaderboardService from app.schemas.prediction import PredictionListResponse, PredictionResponse router = APIRouter(prefix="/api/v1/predictions", tags=["predictions"]) # Service instances prediction_service = PredictionService leaderboard_service = LeaderboardService @router.get("", response_model=PredictionListResponse) def get_predictions( limit: int = Query(20, ge=1, le=100, description="Maximum 20 prédictions par défaut"), offset: int = Query(0, ge=0, description="Pagination offset"), league_filter: Optional[str] = Query(None, description="Filtre par ligue"), confidence_filter: Optional[str] = Query(None, description="Filtre par niveau de confiance"), db: Session = Depends(get_db) ): """ Récupérer les prédictions avec filtres avancés. Améliorations: - Filtre par ligue - Filtre par niveau de confiance (high/medium/low) - Optimisation des requêtes avec indexes - Tri par date de match (matchs à venir d'abord) """ # Récupérer les prédictions predictions, total = prediction_service.get_predictions_with_pagination( limit=limit, offset=offset, league=league_filter ) # Filtrer par niveau de confiance côté backend pour optimiser if confidence_filter: filtered_predictions = [] for prediction in predictions: conf_value = float(prediction.confidence.rstrip('%')) if confidence_filter == 'high' and conf_value < 70: continue elif confidence_filter == 'medium' and (conf_value < 50 or conf_value >= 70): continue elif confidence_filter == 'low' and conf_value >= 50: continue filtered_predictions.append(prediction) predictions = filtered_predictions total = len(filtered_predictions) # Construire la réponse prediction_responses = [] for prediction in predictions: match = prediction.match prediction_responses.append({ "id": prediction.id, "match_id": prediction.match_id, "match": { "id": match.id, "home_team": match.home_team, "away_team": match.away_team, "date": match.date.isoformat() if match.date else None, "league": match.league, "status": match.status, "actual_winner": match.actual_winner }, "energy_score": prediction.energy_score, "confidence": prediction.confidence, "predicted_winner": prediction.predicted_winner, "created_at": prediction.created_at.isoformat() if prediction.created_at else None }) return { "data": prediction_responses, "meta": { "total": total, "limit": limit, "offset": offset, "timestamp": datetime.utcnow().isoformat() + "Z", "version": "v1" } } @router.get("/{prediction_id}", response_model=PredictionResponse) def get_prediction_details( prediction_id: int, db: Session = Depends(get_db) ): """ Récupérer les détails complets d'une prédiction avec historique. Améliorations: - Inclut l'historique des prédictions pour ce match - Inclut les scores d'énergie détaillés """ prediction = prediction_service.get_prediction_with_details(prediction_id) if not prediction: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Prédiction {prediction_id} non trouvée" ) return prediction @router.post("/regenerate/{prediction_id}", status_code=status.HTTP_200_OK) def regenerate_prediction( prediction_id: int, db: Session = Depends(get_db) ): """ Régénérer une prédiction pour un match existant. Cette fonctionnalité permet de recalculer une prédiction si de nouvelles données d'énergie sont disponibles. """ prediction = prediction_service.get_prediction_by_id(prediction_id) if not prediction: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Prédiction {prediction_id} non trouvée" ) # Récupérer les scores d'énergie actuels pour ce match # TODO: Récupérer depuis energy_scores table # Pour l'instant, on simule home_energy = 65.0 # Exemple away_energy = 45.0 # Exemple # Créer une nouvelle prédiction new_prediction = prediction_service.create_prediction_for_match( match_id=prediction.match_id, home_energy=home_energy, away_energy=away_energy ) # Archiver l'ancienne prédiction # TODO: Marquer comme archivée return { "message": "Prédiction régénérée avec succès", "prediction": new_prediction.to_dict(), "previous_prediction": prediction.to_dict() } @router.delete("/{prediction_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_prediction( prediction_id: int, db: Session = Depends(get_db) ): """ Supprimer une prédiction (utilisateur peut vouloir supprimer ses consultations). """ success = prediction_service.delete_prediction(prediction_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Prédiction {prediction_id} non trouvée" ) return None @router.get("/dashboard/summary", status_code=status.HTTP_200_OK) def get_dashboard_summary( user_id: int = Query(..., description="ID de l'utilisateur"), db: Session = Depends(get_db) ): """ Récupérer un résumé pour le dashboard de l'utilisateur. Inclut: - Statistiques personnelles - Prédictions récentes - Classement personnel """ # Statistiques personnelles personal_rank = leaderboard_service.get_personal_rank(user_id) # Prédictions récentes (5 dernières) recent_predictions, _ = prediction_service.get_predictions_with_pagination(limit=5, offset=0) return { "personal_rank": personal_rank, "recent_predictions": recent_predictions[:5], "total_predictions": personal_rank.predictions_count if personal_rank else 0, "accuracy": personal_rank.accuracy if personal_rank else 0 } ``` --- ## 📊 Tests Acceptation ### Tests Frontend ```typescript // tests/dashboard.test.tsx import { renderHook, waitFor } from '@testing-library/react'; import { usePredictions } from '@/hooks/usePredictions'; describe('Dashboard Page', () => { it('devrait charger les prédictions au montage', async () => { const { result, waitForNextUpdate } = renderHook(() => usePredictions()); global.fetch = jest.fn(() => Promise.resolve({ ok: true, json: async () => ({ data: [ { id: 1, match_id: 1, match: { id: 1, home_team: "PSG", away_team: "Marseille", date: "2026-01-18T20:00:00Z", league: "Ligue 1", status: "scheduled" }, energy_score: "high", confidence: "70.5%", predicted_winner: "PSG", created_at: "2026-01-17T12:00:00Z" } ], meta: { total: 1, limit: 20, offset: 0, timestamp: "2026-01-17T14:30:00Z", version: "v1" } }) }) ); await waitForNextUpdate(); expect(result.current.predictions).toHaveLength(1); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBeNull(); }); it('devrait afficher une erreur si le chargement échoue', async () => { const { result, waitForNextUpdate } = renderHook(() => usePredictions()); global.fetch = jest.fn(() => Promise.reject(new Error('Erreur API')) ); await act(async () => { await result.current.fetchPredictions(); }); await waitForNextUpdate(); expect(result.current.error).toBe('Erreur API'); expect(result.current.predictions).toEqual([]); }); it('devrait rafraîchir les prédictions manuellement', async () => { const { result, waitForNextUpdate } = renderHook(() => usePredictions()); global.fetch = jest.fn(() => Promise.resolve({ ok: true, json: async () => ({ data: [], meta: { total: 0, limit: 20, offset: 0, timestamp: '2026-01-17T14:30:00Z', version: 'v1' } }) }) }); await act(async () => { await result.current.fetchPredictions(); }); await waitForNextUpdate(); expect(global.fetch).toHaveBeenCalledTimes(2); }); }); ``` ### Tests d'Intégration Backend ```python # tests/test_dashboard_api.py import pytest from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.database import Base, get_db from app.models.prediction import Prediction from app.models.match import Match # Setup base de données de test TEST_DATABASE_URL = "sqlite:///./test_chartbastan.db" engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False}) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def override_get_db(): try: db = TestingSessionLocal() yield db finally: db.close() client = TestClient(app) def test_get_predictions_success(): """Test récupération réussie des prédictions.""" # Setup: Créer des données de test db = next(override_get_db()) # Créer un match match = Match( home_team="PSG", away_team="Marseille", date=datetime.now(), league="Ligue 1", status="scheduled" ) db.add(match) db.commit() # Créer une prédiction prediction = Prediction( match_id=match.id, energy_score="high", confidence="70.5%", predicted_winner="PSG", created_at=datetime.now() ) db.add(prediction) db.commit() # Test response = client.get("/api/v1/predictions?limit=20") assert response.status_code == 200 data = response.json() assert "data" in data assert len(data["data"]) == 1 assert data["data"][0]["confidence"] == "70.5%" assert data["data"][0]["predicted_winner"] == "PSG" def test_get_predictions_with_filters(): """Test récupération avec filtres.""" # Créer plusieurs prédictions db = next(override_get_db()) matches = [ Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled"), Match(home_team="Real Madrid", away_team="Barcelona", date=datetime.now(), league="La Liga", status="scheduled") ] db.add_all(matches) db.commit() predictions = [ Prediction(match_id=matches[0].id, energy_score="high", confidence="75.0%", predicted_winner="PSG", created_at=datetime.now()), Prediction(match_id=matches[0].id, energy_score="medium", confidence="55.0%", predicted_winner="PSG", created_at=datetime.now()), Prediction(match_id=matches[1].id, energy_score="high", confidence="78.0%", predicted_winner="Real Madrid", created_at=datetime.now()), ] db.add_all(predictions) db.commit() # Test filtre ligue response = client.get("/api/v1/predictions?limit=20&league_filter=Ligue 1") assert response.status_code == 200 data = response.json() assert len(data["data"]) == 2 # Seulement les 2 prédictions Ligue 1 # Test filtre confiance high response = client.get("/api/v1/predictions?limit=20&confidence_filter=high") assert response.status_code == 200 data = response.json() assert len(data["data"]) == 3 # 3 prédictions avec confiance >70% def test_regenerate_prediction(): """Test la régénération d'une prédiction.""" db = next(override_get_db()) # Créer match et prédiction match = Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled") db.add(match) db.commit() prediction = Prediction(match_id=match.id, energy_score="high", confidence="70.5%", predicted_winner="PSG", created_at=datetime.now()) db.add(prediction) db.commit() # Régénérer response = client.post(f"/api/v1/predictions/regenerate/{prediction.id}") assert response.status_code == 200 data = response.json() assert "message" in data assert "prediction" in data assert data["prediction"]["confidence"] != prediction.confidence # Nouvelle prédiction def test_delete_prediction(): """Test la suppression d'une prédiction.""" db = next(override_get_db()) match = Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled") db.add(match) db.commit() prediction = Prediction(match_id=match.id, energy_score="high", confidence="70.5%", predicted_winner="PSG", created_at=datetime.now()) db.add(prediction) db.commit() # Supprimer response = client.delete(f"/api/v1/predictions/{prediction.id}") assert response.status_code == 204 # Vérifier que la prédiction n'existe plus db = next(override_get_db()) deleted_prediction = db.query(Prediction).filter(Prediction.id == prediction.id).first() assert deleted_prediction is None def test_get_prediction_details(): """Test la récupération des détails d'une prédiction.""" db = next(override_get_db()) match = Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled") db.add(match) db.commit() prediction = Prediction(match_id=match.id, energy_score="high", confidence="70.5%", predicted_winner="PSG", created_at=datetime.now()) db.add(prediction) db.commit() # Récupérer les détails response = client.get(f"/api/v1/predictions/{prediction.id}") assert response.status_code == 200 data = response.json() assert data["id"] == prediction.id assert data["predicted_winner"] == "PSG" assert "history" in data assert len(data["history"]) >= 1 ``` --- ## 📝 Définitions de Succès - [ ] Un utilisateur peut voir une liste de prédictions sur le dashboard - [ ] Le Confidence Meter utilise le bon code couleur (vert >70%, jaune 50-70%, rouge <50%) - [ ] Les prédictions sont triées par date de match (plus proches d'abord) - [ ] Les prédictions peuvent être filtrées par ligue - [ ] Les prédictions peuvent être filtrées par niveau de confiance - [ ] Les utilisateurs peuvent voir leurs statistiques personnelles (classement, précision, nombre) - [ ] Les utilisateurs peuvent voir le Top 100 du classement - [ ] Les utilisateurs peuvent accéder aux détails d'une prédiction - [ ] Le dashboard est responsive (mobile et desktop) --- ## 🔗 Dépendances **Frontend** : - `@/components/ui/card`, `skeleton`, `badge`, `button`, `select` - `@/hooks/usePredictions`, `useLeaderboard` - `react-router-dom` (ou Next.js Link) - `lucide-react` (animations) **Backend** : - `app/services/prediction_service.py` (existant) - `app/services/leaderboard_service.py` (existant) - `app/models/prediction.py` (existant) - `app/models/match.py` (existant) --- ## 🎯 KPIs de Mesure - **Temps de chargement dashboard** : < 2s - **Temps de réponse API prédictions** : < 500ms - **Nombre de prédictions par page** : 20 (pagination) - **Taux de conversion dashboard → détails** : À suivre - **Utilisateurs actifs** : Mesure avec GA (Phase 2+) --- **Note** : Cette User Story est au cœur de l'application Chartbastan. Une fois implémentée, les utilisateurs pourront consulter les prédictions sportives basées sur l'énergie collective avec une interface claire et intuitive. Les filtres avancés (ligue, confiance) permettront aux utilisateurs de trouver rapidement les prédictions les plus pertinentes.