🎯 Prédictions Analyseur Basket Mi-Temps & Quarts pour chaque match
Vous cherchez des prévisions fiables sur les vainqueurs par mi-temps et par quart-temps ? Avec l’Analyseur Mi-Temps & Quarts de Tedam’s Pronos, accédez à une sélection pointue de pronostics à forte probabilité, pensés pour maximiser vos chances de succès sur les paris partiels.
Notre approche se concentre sur les dynamiques de match : entame de rencontre, domination intermédiaire, ou relâchement en fin de période. Grâce à une analyse complète des statistiques détaillées par quart-temps ou mi-temps, vous saurez quand et sur quelle période miser intelligemment.
<?php
/**
* ===========================================================
* ✅ TP NBA — SPOT FUSION ANALYZER + 2 TICKET BUILDERS (V6 + UI)
* Shortcode : [tp_nba_spot_fusion]
*
* ✅ AJOUTS :
* - Bouton 🔄 Rafraîchir (rebuild cache HTML via nonce)
* - Filtre Marché (liste déroulante)
* - Filtre Match (liste déroulante)
* - Bouton ➕ Charger plus (par match) => AJAX chunking
* - ✅ TRI colonnes (clic sur entêtes) : Ligne/Cote/Confiance/Niveau/Palier(moy)/Matchs/Min/IA/ΔIA/Palier1/Edge/Value/Score
* - ✅ Jusqu’à 10 legs (manuel + auto)
*
* ✅ FIX :
* - NAMESPACE complet en tpnbaf6_*
* - remove_shortcode + add_shortcode
* - Polyfills MINUTE/HOUR
* - ✅ TICKET BUILDERS + ✅ AUTO BUILDERS (Main & P1)
* - ✅ CSS/JS imprimés même quand HTML est servi depuis le cache
* - ✅ PRP : ajout paliers 44.5 et 49.5 (fallback dash)
* ===========================================================
*/
if (!defined('ABSPATH')) exit;
/* Polyfills temps */
if (!defined('MINUTE_IN_SECONDS')) define('MINUTE_IN_SECONDS', 60);
if (!defined('HOUR_IN_SECONDS')) define('HOUR_IN_SECONDS', 3600);
/* Force override shortcode (évite collision ancienne version) */
add_action('init', function(){
remove_shortcode('tp_nba_spot_fusion');
add_shortcode('tp_nba_spot_fusion', 'tpnbaf6_render_shortcode');
}, 99);
/* =========================
AJAX: Load more rows (par match)
========================= */
add_action('wp_ajax_tpnbaf6_more', 'tpnbaf6_ajax_more');
add_action('wp_ajax_nopriv_tpnbaf6_more', 'tpnbaf6_ajax_more');
if (!function_exists('tpnbaf6_ajax_more')) {
function tpnbaf6_ajax_more(){
if (!defined('DOING_AJAX') || !DOING_AJAX) wp_die();
$nonce = sanitize_text_field($_POST['nonce'] ?? '');
if (!wp_verify_nonce($nonce, 'tpnbaf6_more')) {
wp_send_json(['ok'=>0,'msg'=>'Nonce invalide'], 403);
}
if (!function_exists('tpnbap_get_player_props_ou') || !function_exists('tpnbap_player_props_rows')) {
wp_send_json(['ok'=>0,'msg'=>'tp_nba_props non détecté'], 400);
}
$gid = (int)($_POST['gid'] ?? 0);
$league = (int)($_POST['league'] ?? 0);
$season = sanitize_text_field((string)($_POST['season'] ?? ''));
$bk = (int)($_POST['bookmaker'] ?? 0);
$offset = max(0, (int)($_POST['offset'] ?? 0));
$limit = (int)($_POST['limit'] ?? 200);
if ($limit < 50) $limit = 50;
if ($limit > 500) $limit = 500;
$minMinutes = (float)($_POST['min_minutes'] ?? 25);
$minConf = (int)($_POST['min_conf'] ?? 60);
$minGames = (int)($_POST['min_games'] ?? 12);
$matchLabel = sanitize_text_field((string)($_POST['match'] ?? ''));
$timeLabel = sanitize_text_field((string)($_POST['time'] ?? ''));
if (!$gid || !$season || !$bk) {
wp_send_json(['ok'=>0,'msg'=>'Paramètres manquants'], 400);
}
// Récup props
$propsOut = tpnbap_get_player_props_ou($gid, $season, $bk);
if (empty($propsOut)) {
wp_send_json([
'ok'=>1,'rows_html'=>[],'next_offset'=>$offset,'done'=>true,'raw_total'=>0,'kept'=>0
]);
}
// On récupère un gros batch (mais sur 1 match => maîtrisé)
$allRows = tpnbap_player_props_rows($propsOut, 5000);
if (empty($allRows) || !is_array($allRows)) {
wp_send_json([
'ok'=>1,'rows_html'=>[],'next_offset'=>$offset,'done'=>true,'raw_total'=>0,'kept'=>0
]);
}
$rawTotal = count($allRows);
if ($offset >= $rawTotal) {
wp_send_json([
'ok'=>1,'rows_html'=>[],'next_offset'=>$offset,'done'=>true,'raw_total'=>$rawTotal,'kept'=>0
]);
}
$slice = array_slice($allRows, $offset, $limit);
// IA snapshot joueurs (cache identique au rendu principal)
$predCacheKey = 'tpnbaf6_playerpred_'.$gid;
$playerPredMap = get_transient($predCacheKey);
if (!is_array($playerPredMap)) {
$snapPlayers = tpnbaf6_get_player_analyzer_snapshot($gid);
if (is_array($snapPlayers) && !empty($snapPlayers['combined']) && is_array($snapPlayers['combined'])) {
$playerPredMap = tpnbaf6_build_player_pred_map($snapPlayers['combined']);
set_transient($predCacheKey, $playerPredMap, 6 * HOUR_IN_SECONDS);
} else {
$playerPredMap = [];
set_transient($predCacheKey, $playerPredMap, 10 * MINUTE_IN_SECONDS);
}
}
$rowsHtml = [];
$kept = 0;
foreach ($slice as $j => $r){
$player = (string)($r['player'] ?? '');
$market = tpnbaf6_norm_market((string)($r['market'] ?? ''));
$line = (float)($r['line'] ?? 0);
if ($player === '' || $market === '' || $line <= 0) continue;
$oddOver = tpnbaf6_parse_odd($r['over'] ?? 0);
$oddUnder = tpnbaf6_parse_odd($r['under'] ?? 0);
$st = tpnbaf6_conf_stats($player, $market, $line);
$co = $st['over'];
$cu = $st['under'];
$n = (int)($st['n'] ?? 0);
$avg= $st['avg'];
if ($co === null && $cu === null) continue;
// side recommandé
$side = 'Over';
$conf = $co;
$odd = $oddOver;
if ($co === null) { $side='Under'; $conf=$cu; $odd=$oddUnder; }
elseif ($cu !== null && $cu > $co) { $side='Under'; $conf=$cu; $odd=$oddUnder; }
// fallback cote
if (($odd <= 0) && (($side==='Over' && $oddUnder>0) || ($side==='Under' && $oddOver>0))) {
$side = ($side==='Over') ? 'Under' : 'Over';
$conf = ($side==='Over') ? $co : $cu;
$odd = ($side==='Over') ? $oddOver : $oddUnder;
}
if ($conf === null || $odd <= 0) continue;
// seuils obligatoires
if ((int)$conf < $minConf) continue;
if ($n < $minGames) continue;
$lvl = tpnbaf6_level((int)$conf);
// ✅ Palier 1 (Dash) = palier inférieur réel (liste du dashboard)
$pal1Txt = '—';
$pal1Conf = null;
$pal1Line = null;
if ($side === 'Over') {
$lowerLine = tpnbaf6_dash_lower_line($market, $line);
if ($lowerLine !== null) {
$st2 = tpnbaf6_conf_stats($player, $market, (float)$lowerLine);
$pal1Conf = $st2['over'];
$pal1Line = (float)$lowerLine;
if ($pal1Conf !== null) $pal1Txt = number_format((float)$lowerLine, 1, '.', '').' • '.$pal1Conf.'%';
}
}
// Minutes: hist sinon snapshot joueurs
$minHist = tpnbaf6_minutes_avg($player);
$minPred = (!empty($playerPredMap) ? tpnbaf6_player_minutes_pred($playerPredMap, $player) : null);
$minUse = ($minHist !== null) ? $minHist : $minPred;
if ($minUse === null || (float)$minUse < $minMinutes) continue;
// IA
$predIA = null;
if (is_array($playerPredMap) && !empty($playerPredMap)) {
$predIA = tpnbaf6_player_pred_value($playerPredMap, $player, $market);
}
$deltaIA = null;
if ($predIA !== null) $deltaIA = (float)$predIA - (float)$line;
// Edge + EV
$implied = (100.0 / (float)$odd);
$edgePP = ((float)$conf - (float)$implied);
$evPct = (((float)$conf/100.0) * (float)$odd - 1.0) * 100.0;
$hay = strtolower(trim($matchLabel.' '.$timeLabel.' '.$player.' '.$market.' '.$side));
$row = [
'gid'=>$gid,'match'=>$matchLabel,'time'=>$timeLabel,'player'=>$player,
'player_norm'=>tpnbaf6_norm_player($player),
'market'=>$market,'line'=>$line,'side'=>$side,'odd'=>$odd,'conf'=>(int)$conf,
'lvl_key'=>$lvl['key'],'lvl_label'=>$lvl['label'],'lvl_cls'=>$lvl['cls'],
'co'=>$co,'cu'=>$cu,'avg'=>$avg,'n'=>$n,
'min_hist'=>$minHist,'min_pred'=>$minPred,'min_use'=>$minUse,
'pred_ia'=>$predIA,'delta_ia'=>$deltaIA,
'pal1_txt'=>$pal1Txt,'pal1_conf'=>$pal1Conf,'pal1_line'=>$pal1Line,
'edge_pp'=>$edgePP,'ev_pct'=>$evPct,'hay'=>$hay,
];
$row['score'] = tpnbaf6_compute_score($row);
// render <tr>
$id = $gid.'_more_'.$offset.'_'.$j.'_'.substr(md5($row['player'].$row['market'].$row['line'].$row['side']),0,6);
$mktLabel = $row['market'];
$labels = ['AST'=>'PAS','PTS+AST'=>'PTS+PAS','RA'=>'RP','PRP'=>'PRP'];
if (isset($labels[$mktLabel])) $mktLabel = $labels[$mktLabel];
$oddTxt = ($row['odd']>0) ? number_format((float)$row['odd'], 2, '.', '') : '-';
$details = [];
if ($row['co'] !== null) $details[] = 'Over '.$row['co'].'%';
if ($row['cu'] !== null) $details[] = 'Under '.$row['cu'].'%';
$avgTxt = ($row['avg'] !== null) ? number_format((float)$row['avg'], 1, '.', '') : '—';
$nTxt = ($row['n'] ?? 0) ? (int)$row['n'] : '—';
$minTxt = '—';
if ($row['min_hist'] !== null) $minTxt = number_format((float)$row['min_hist'], 1, '.', '');
elseif ($row['min_pred'] !== null) $minTxt = number_format((float)$row['min_pred'], 1, '.', '').'*';
$iaTxt = ($row['pred_ia'] !== null) ? number_format((float)$row['pred_ia'], 1, '.', '') : '—';
$hasIA = ($row['pred_ia'] !== null) ? 1 : 0;
$dTxt = '—';
if ($row['delta_ia'] !== null) {
$d = (float)$row['delta_ia'];
$dTxt = ($d >= 0 ? '+' : '').number_format($d, 1, '.', '');
}
$pal1Badge = '';
$pal85 = 0;
$pal1LineTxt = '';
$pal1ConfTxt = '';
if ($row['pal1_conf'] !== null) $pal1ConfTxt = (string)((int)$row['pal1_conf']);
if ($row['pal1_line'] !== null) $pal1LineTxt = number_format((float)$row['pal1_line'], 1, '.', '');
if ($row['pal1_conf'] !== null && (int)$row['pal1_conf'] >= 85) {
$pal1Badge = ' <span class="tpnbaf-badge85">≥85%</span>';
$pal85 = 1;
}
$edgeTxt = '<span class="tpnbaf-chipblue">'.(($row['edge_pp']>=0)?'+':'').number_format((float)$row['edge_pp'], 1, '.', '').'pp</span>';
$valTxt = '<span class="tpnbaf-chipnum">'.(($row['ev_pct']>=0)?'+':'').number_format((float)$row['ev_pct'], 1, '.', '').'%</span>';
// baseline eligibility at 80 (JS applies selected threshold live)
$p1Eligible = ($pal1ConfTxt !== '' && (int)$pal1ConfTxt >= 80 && $pal1LineTxt !== '');
ob_start();
?>
<tr
data-gid="<?php echo esc_attr((string)$row['gid']); ?>"
data-id="<?php echo esc_attr($id); ?>"
data-match="<?php echo esc_attr($row['match']); ?>"
data-player="<?php echo esc_attr($row['player']); ?>"
data-playernorm="<?php echo esc_attr($row['player_norm']); ?>"
data-market="<?php echo esc_attr($row['market']); ?>"
data-side="<?php echo esc_attr($row['side']); ?>"
data-line="<?php echo esc_attr(number_format((float)$row['line'], 1, '.', '')); ?>"
data-odd="<?php echo esc_attr($oddTxt); ?>"
data-conf="<?php echo esc_attr((int)$row['conf']); ?>"
data-level="<?php echo esc_attr($row['lvl_key']); ?>"
data-avg="<?php echo esc_attr($row['avg'] !== null ? number_format((float)$row['avg'], 1, '.', '') : ''); ?>"
data-hasia="<?php echo esc_attr($hasIA); ?>"
data-score="<?php echo esc_attr(number_format((float)$row['score'], 1, '.', '')); ?>"
data-n="<?php echo esc_attr((int)$row['n']); ?>"
data-min="<?php echo esc_attr(number_format((float)$row['min_use'], 1, '.', '')); ?>"
data-edge="<?php echo esc_attr(number_format((float)$row['edge_pp'], 1, '.', '')); ?>"
data-ev="<?php echo esc_attr(number_format((float)$row['ev_pct'], 1, '.', '')); ?>"
data-ia="<?php echo esc_attr($row['pred_ia'] !== null ? number_format((float)$row['pred_ia'], 1, '.', '') : ''); ?>"
data-delta="<?php echo esc_attr($row['delta_ia'] !== null ? number_format((float)$row['delta_ia'], 1, '.', '') : ''); ?>"
data-pal85="<?php echo esc_attr($pal85); ?>"
data-pal1line="<?php echo esc_attr($pal1LineTxt); ?>"
data-pal1conf="<?php echo esc_attr($pal1ConfTxt); ?>"
data-hay="<?php echo esc_attr($row['hay']); ?>"
>
<td><b><?php echo esc_html($row['match']); ?></b><div class="tpnbaf-muted" style="font-size:11px"><?php echo esc_html($row['time']); ?></div></td>
<td><?php echo esc_html($row['player']); ?></td>
<td><?php echo esc_html($mktLabel); ?></td>
<td><b><?php echo esc_html($row['side']); ?></b><div class="tpnbaf-muted" style="font-size:11px"><?php echo esc_html(implode(' • ', $details)); ?></div></td>
<td><?php echo esc_html(number_format((float)$row['line'], 1, '.', '')); ?></td>
<td><b><?php echo $oddTxt; ?></b></td>
<td><b><?php echo (int)$row['conf']; ?>%</b></td>
<td><span class="<?php echo esc_attr($row['lvl_cls']); ?>"><?php echo esc_html($row['lvl_label']); ?></span></td>
<td><span class="tpnbaf-chipgray"><?php echo $avgTxt; ?></span></td>
<td><span class="tpnbaf-chipgray"><?php echo $nTxt; ?></span></td>
<td><span class="tpnbaf-chipgray"><?php echo $minTxt; ?></span></td>
<td><span class="tpnbaf-chipgray"><?php echo $iaTxt; ?></span></td>
<td><span class="tpnbaf-chipgray"><?php echo esc_html($dTxt); ?></span></td>
<td><?php echo esc_html($row['pal1_txt']).$pal1Badge; ?></td>
<td><?php echo $edgeTxt; ?></td>
<td><?php echo $valTxt; ?></td>
<td><span class="tpnbaf-chipblue"><?php echo esc_html(number_format((float)$row['score'], 1, '.', '')); ?></span></td>
<td>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
<button type="button" class="tpnbaf-btn tpnbaf-btn-add tpnbaf-add-main">➕ Main</button>
<button type="button" class="tpnbaf-btn tpnbaf-btn-p1 tpnbaf-add-p1" <?php echo ($p1Eligible ? '' : 'disabled'); ?>>🛡️ P1</button>
</div>
<div class="tpnbaf-muted" style="font-size:11px;margin-top:4px">
P1 piloté par le filtre “Palier 1 Dash”
</div>
</td>
</tr>
<?php
$rowsHtml[] = trim(ob_get_clean());
$kept++;
}
$nextOffset = min($rawTotal, $offset + $limit);
$done = ($nextOffset >= $rawTotal);
wp_send_json([
'ok'=>1,
'rows_html'=>$rowsHtml,
'next_offset'=>$nextOffset,
'done'=>$done,
'raw_total'=>$rawTotal,
'kept'=>$kept
]);
}
}
/* =========================
CSS (once)
========================= */
if (!function_exists('tpnbaf6_css_once')) {
function tpnbaf6_css_once(){
static $done = false; if ($done) return; $done = true;
$css = <<<'CSS'
.tpnbaf-wrap{margin:14px 0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
.tpnbaf-head{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin-bottom:10px}
.tpnbaf-title{font-weight:900;font-size:18px}
.tpnbaf-sub{opacity:.7;font-size:12px}
.tpnbaf-filters{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.tpnbaf-filters input,.tpnbaf-filters select{padding:7px 10px;border-radius:999px;border:1px solid #e5e7eb;font-size:12px;background:#fff}
.tpnbaf-card{background:#fff;border:1px solid #e5e7eb;border-radius:16px;box-shadow:0 8px 18px rgba(0,0,0,.06);padding:12px}
.tpnbaf-tablewrap{width:100%;overflow:auto;border-radius:14px;border:1px solid #e5e7eb}
.tpnbaf-table{width:100%;border-collapse:collapse;min-width:2050px}
.tpnbaf-table th{position:sticky;top:0;z-index:2;background:linear-gradient(90deg,#ff8c00,#ffcc33);color:#111;font-weight:900;font-size:12px;text-transform:uppercase;letter-spacing:.3px;padding:10px;border-bottom:2px solid #ffd37a;text-align:left;white-space:nowrap}
.tpnbaf-table td{padding:9px 10px;border-bottom:1px solid #f1f5f9;font-size:13px;vertical-align:middle;white-space:nowrap}
.tpnbaf-table tr:hover td{background:#fff7ed}
.tpnbaf-chip{display:inline-block;padding:3px 9px;border-radius:999px;font-weight:900;font-size:12px}
.tpnbaf-low{background:#e2e8f0;color:#0f172a}
.tpnbaf-med{background:#16a34a;color:#fff}
.tpnbaf-high{background:linear-gradient(90deg,#8b5cf6,#f97316);color:#fff}
.tpnbaf-muted{opacity:.72}
.tpnbaf-btn{border:none;border-radius:12px;padding:8px 10px;font-weight:900;font-size:12px;cursor:pointer}
.tpnbaf-btn-add{background:#111827;color:#fff}
.tpnbaf-btn-add:hover{filter:brightness(1.08)}
.tpnbaf-btn-copy{background:#2563eb;color:#fff}
.tpnbaf-btn-clear{background:#ef4444;color:#fff}
.tpnbaf-btn-auto{background:linear-gradient(90deg,#2563eb,#22c55e);color:#fff}
.tpnbaf-btn-p1{background:linear-gradient(90deg,#22c55e,#10b981);color:#fff}
.tpnbaf-btn-p1:hover{filter:brightness(1.06)}
.tpnbaf-btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
.tpnbaf-btn-refresh{background:#0f172a;color:#fff}
.tpnbaf-btn-more{background:linear-gradient(90deg,#f59e0b,#ef4444);color:#fff}
.tpnbaf-kpis{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
.tpnbaf-kpi{background:#f8fafc;border:1px solid #e5e7eb;border-radius:14px;padding:10px 12px;font-size:12px;min-width:120px}
.tpnbaf-kpi b{font-weight:900}
.tpnbaf-kpi div{font-weight:900;font-size:16px;margin-top:2px}
.tpnbaf-textarea{width:100%;min-height:110px;border:1px solid #e5e7eb;border-radius:14px;padding:10px;font-size:12px}
.tpnbaf-textarea.small{min-height:130px}
.tpnbaf-badge85{display:inline-block;margin-left:6px;padding:3px 8px;border-radius:999px;font-weight:900;font-size:11px;background:linear-gradient(90deg,#22c55e,#10b981);color:#fff}
.tpnbaf-chipnum{display:inline-block;padding:3px 8px;border-radius:999px;font-weight:900;font-size:12px;background:#111827;color:#fff}
.tpnbaf-chipblue{display:inline-block;padding:3px 8px;border-radius:999px;font-weight:900;font-size:12px;background:#2563eb;color:#fff}
.tpnbaf-chipgray{display:inline-block;padding:3px 8px;border-radius:999px;font-weight:900;font-size:12px;background:#e2e8f0;color:#0f172a}
.tpnbaf-star{display:inline-block;margin-left:6px;padding:3px 8px;border-radius:999px;font-weight:900;font-size:11px;background:linear-gradient(90deg,#2563eb,#22c55e);color:#fff}
.tpnbaf-builders{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start;margin-top:12px}
@media(max-width:1100px){.tpnbaf-builders{grid-template-columns:1fr}}
.tpnbaf-ticketbox{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.tpnbaf-leg{border:1px solid #e5e7eb;border-radius:14px;padding:10px;background:linear-gradient(180deg,#ffffff,#f8fafc)}
.tpnbaf-leg-top{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
.tpnbaf-leg-main{min-width:0}
.tpnbaf-leg-main .player{font-weight:950;font-size:13px;line-height:1.2}
.tpnbaf-leg-main .pick{font-weight:950;font-size:12px;margin-top:4px}
.tpnbaf-leg-main .sub{font-size:11px;opacity:.72;margin-top:3px;white-space:normal}
.tpnbaf-leg-right{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:0 0 auto}
.tpnbaf-pill{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:4px 10px;font-weight:900;font-size:11px;border:1px solid #e5e7eb;background:#fff}
.tpnbaf-pill strong{font-size:11px}
.tpnbaf-x{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:999px;border:1px solid #fecaca;background:#fff1f2;color:#b91c1c;font-weight:950;cursor:pointer}
.tpnbaf-x:hover{filter:brightness(0.97)}
/* ✅ TRI colonnes */
.tpnbaf-sortable{cursor:pointer;user-select:none}
.tpnbaf-sortable:after{content:" ↕";opacity:.45;font-weight:900}
.tpnbaf-sortable[data-dir="asc"]:after{content:" ↑";opacity:.95}
.tpnbaf-sortable[data-dir="desc"]:after{content:" ↓";opacity:.95}
CSS;
echo "<style>{$css}</style>";
}
}
/* =========================
JS (once)
========================= */
if (!function_exists('tpnbaf6_js_once')) {
function tpnbaf6_js_once(){
static $done = false; if ($done) return; $done = true;
$js = <<<'JS'
(function(){
const LS_MAIN = "tpnbaf6_ticket_main_v1";
const LS_P1 = "tpnbaf6_ticket_p1_v1";
const AJAX = (window.tpnbaf6_ajax || {});
const AJAX_URL = AJAX.url || "";
const AJAX_NONCE = AJAX.nonce || "";
const CFG = AJAX.cfg || {};
const MAX_ROWS_GAME = parseInt(CFG.maxRowsGame || "240", 10) || 240;
// ✅ Jusqu’à 10 legs (manuel + auto)
const MAX_LEGS = parseInt(CFG.maxLegs || "10", 10) || 10;
const rawOffsets = {};
function readTicket(key){
try { const x = JSON.parse(localStorage.getItem(key) || "[]"); return Array.isArray(x) ? x : []; }
catch(e){ return []; }
}
function saveTicket(key, arr){ localStorage.setItem(key, JSON.stringify(arr || [])); }
function fmtOdd(x){ const n = parseFloat(x); if (!isFinite(n) || n<=0) return "-"; return n.toFixed(2); }
function toNum(x){ const n = parseFloat(x); return isFinite(n) ? n : null; }
function calcTotalOdds(items){ let t = 1; items.forEach(it=>{ const o = parseFloat(it.odd); if (isFinite(o) && o>0) t *= o; }); return t; }
function fairOddFromConf(conf){ const c = toNum(conf); if (c===null || c<=0) return null; return (100.0 / c); }
function getP1SelectValue(){ return (document.querySelector("#tpnbafP1Min")?.value || "ALL"); }
function getP1Threshold(){
const v = getP1SelectValue();
const n = parseInt(v, 10);
if ([80,85,90,95].includes(n)) return n;
return 85;
}
function getSelectedMatchGid(){ return (document.querySelector("#tpnbafMatch")?.value || "ALL"); }
function updateLoadMoreBtn(){
const btn = document.querySelector("#tpnbafLoadMore");
if (!btn) return;
const gid = getSelectedMatchGid();
const ok = (gid !== "ALL" && !!AJAX_URL && !!AJAX_NONCE);
btn.disabled = !ok;
if (ok) btn.textContent = "➕ Charger plus (ce match)";
else btn.textContent = "➕ Charger plus (sélectionne un match)";
}
function buildWhyForItem(it){
const parts = [];
const score = toNum(it.score);
const conf = toNum(it.conf);
const n = toNum(it.n);
const min = toNum(it.min);
const edge = toNum(it.edge_pp);
const ev = toNum(it.ev_pct);
const ia = toNum(it.pred_ia);
const dIA = toNum(it.delta_ia);
const pal85 = (String(it.pal85||"0") === "1");
if (score!==null) parts.push("Score " + score.toFixed(1));
if (conf!==null) parts.push("Confiance " + Math.round(conf) + "%");
if (min!==null) parts.push("Minutes " + min.toFixed(1) + "/match");
if (n!==null) parts.push("Échantillon " + Math.round(n) + " matchs");
if (edge!==null) parts.push("Edge " + (edge>=0?"+":"") + edge.toFixed(1) + "pp");
if (ev!==null) parts.push("Value " + (ev>=0?"+":"") + ev.toFixed(1) + "%");
if (ia!==null && dIA!==null) parts.push("IA " + ia.toFixed(1) + " (Δ " + (dIA>=0?"+":"") + dIA.toFixed(1) + ")");
else if (ia!==null) parts.push("IA " + ia.toFixed(1));
if (pal85) parts.push("Palier ≥85% (SAFE)");
return "• " + (it.player||"") + " — " + (it.market||"") + " " + (it.side||"") + " " + (it.line||"") + " : " + parts.join(" • ");
}
function renderWhy(textareaId, title, items){
const why = document.querySelector("#"+textareaId);
if (!why) return;
if (!items.length){ why.value = ""; return; }
const lines = [];
lines.push(title);
lines.push("");
items.forEach(it => lines.push(buildWhyForItem(it)));
why.value = lines.join("\n");
}
function flashBtn(btn, okText, revertText){
if (!btn) return;
const old = btn.textContent;
btn.textContent = okText;
setTimeout(()=>btn.textContent=(revertText||old), 1200);
}
function getVisibleRows(){
return Array.from(document.querySelectorAll(".tpnbaf-table tbody tr"))
.filter(tr => tr.style.display !== "none");
}
function mainItemFromTr(tr){
return {
gid: tr.getAttribute("data-gid") || "",
id: tr.getAttribute("data-id") || (Date.now()+""),
match: tr.getAttribute("data-match") || "",
player: tr.getAttribute("data-player") || "",
player_norm: tr.getAttribute("data-playernorm") || "",
market: tr.getAttribute("data-market") || "",
side: tr.getAttribute("data-side") || "",
line: tr.getAttribute("data-line") || "",
odd: tr.getAttribute("data-odd") || "",
conf: tr.getAttribute("data-conf") || "",
score: tr.getAttribute("data-score") || "",
n: tr.getAttribute("data-n") || "",
min: tr.getAttribute("data-min") || "",
edge_pp: tr.getAttribute("data-edge") || "",
ev_pct: tr.getAttribute("data-ev") || "",
pred_ia: tr.getAttribute("data-ia") || "",
delta_ia: tr.getAttribute("data-delta") || "",
pal85: tr.getAttribute("data-pal85") || "0",
mode: "MAIN"
};
}
function p1ItemFromTr(tr){
const p1line = tr.getAttribute("data-pal1line") || "";
const p1conf = tr.getAttribute("data-pal1conf") || "";
const oddN = fairOddFromConf(p1conf);
const oddS = (oddN!==null) ? oddN.toFixed(2) : "";
return {
gid: tr.getAttribute("data-gid") || "",
id: (tr.getAttribute("data-id") || (Date.now()+"")) + "_p1",
match: tr.getAttribute("data-match") || "",
player: tr.getAttribute("data-player") || "",
player_norm: tr.getAttribute("data-playernorm") || "",
market: tr.getAttribute("data-market") || "",
side: "Over",
line: p1line,
odd: oddS,
conf: p1conf,
score: tr.getAttribute("data-score") || "",
n: tr.getAttribute("data-n") || "",
min: tr.getAttribute("data-min") || "",
pal85: (toNum(p1conf)!==null && toNum(p1conf) >= 85) ? "1" : "0",
mode: "P1"
};
}
function renderTicketBox(containerId, key){
const items = readTicket(key);
const box = document.querySelector("#"+containerId);
if (!box) return;
box.innerHTML = "";
if (!items.length){
box.innerHTML = "<div class='tpnbaf-muted'>Aucune sélection. Utilise les boutons du tableau ou l’Auto Builder.</div>";
return;
}
const wrap = document.createElement("div");
wrap.className = "tpnbaf-ticketbox";
items.forEach((it, idx)=>{
const sc = toNum(it.score);
const conf = toNum(it.conf);
const min = toNum(it.min);
const n = toNum(it.n);
const leg = document.createElement("div");
leg.className = "tpnbaf-leg";
const top = document.createElement("div");
top.className = "tpnbaf-leg-top";
const main = document.createElement("div");
main.className = "tpnbaf-leg-main";
main.innerHTML =
"<div class='player'>"+(it.player||"-")+"</div>" +
"<div class='pick'>"+(it.market||"-")+" <span style='font-weight:950'>"+(it.side||"-")+"</span> "+(it.line||"-")+"</div>" +
"<div class='sub'>"+(it.match||"")+"</div>";
const right = document.createElement("div");
right.className = "tpnbaf-leg-right";
const x = document.createElement("div");
x.className = "tpnbaf-x";
x.textContent = "×";
x.setAttribute("data-x", String(idx));
x.setAttribute("data-key", key);
const p1 = document.createElement("div");
p1.className = "tpnbaf-pill";
p1.innerHTML = "<strong>@"+fmtOdd(it.odd)+"</strong> • "+(conf!==null?Math.round(conf)+"%":"-");
const p2 = document.createElement("div");
p2.className = "tpnbaf-pill";
p2.innerHTML = "score <strong>"+(sc!==null?sc.toFixed(1):"-")+"</strong>";
right.appendChild(x);
right.appendChild(p1);
right.appendChild(p2);
if (min!==null || n!==null){
const p3 = document.createElement("div");
p3.className = "tpnbaf-pill";
p3.innerHTML = (min!==null ? ("min <strong>"+min.toFixed(1)+"</strong>") : "min -") + (n!==null ? (" • n <strong>"+Math.round(n)+"</strong>") : "");
right.appendChild(p3);
}
top.appendChild(main);
top.appendChild(right);
leg.appendChild(top);
wrap.appendChild(leg);
});
box.appendChild(wrap);
}
function renderBuilder(key, cfg){
const items = readTicket(key);
renderTicketBox(cfg.boxId, key);
const total = calcTotalOdds(items);
const kpiOdds = document.querySelector("#"+cfg.kpiOddsId);
const kpiN = document.querySelector("#"+cfg.kpiNId);
const kpiRet = document.querySelector("#"+cfg.kpiRetId);
const stakeEl = document.querySelector("#"+cfg.stakeId);
if (kpiOdds) kpiOdds.textContent = items.length ? total.toFixed(2) : "-";
if (kpiN) kpiN.textContent = items.length ? String(items.length) : "0";
const stake = parseFloat(stakeEl ? stakeEl.value : 0) || 0;
const ret = (stake>0 && items.length) ? (stake * total) : 0;
if (kpiRet) kpiRet.textContent = (ret>0) ? ret.toFixed(2) + "€" : "-";
const txt = document.querySelector("#"+cfg.textId);
if (txt){
if (!items.length){ txt.value = ""; }
else {
const lines = items.map((it,i)=>(
(i+1)+". " + it.match + " — " + it.player + " — " + it.market + " " + it.side + " " + it.line +
" @"+fmtOdd(it.odd) + " (" + (it.conf||"-") + "% | score " + (toNum(it.score)!==null?toNum(it.score).toFixed(1):"-") + ")"
));
txt.value = cfg.title + "\n" + lines.join("\n") + "\n\nTotal cotes: " + (items.length ? total.toFixed(2) : "-");
}
}
renderWhy(cfg.whyId, cfg.whyTitle, items);
}
function renderAll(){
const p1Min = getP1Threshold();
const t1 = document.querySelector("#tpnbafP1Title");
if (t1) t1.textContent = "🛡️ Ticket SAFE (Palier 1 Dash ≥"+p1Min+"%)";
const hint = document.querySelector("#tpnbafP1Hint");
if (hint) hint.textContent =
"SAFE P1 = uniquement Over Palier 1 (Dash) ≥"+p1Min+"% • cotes = estimation (100/conf) • 1 match = 1 spot • max "+MAX_LEGS+" legs.";
renderBuilder(LS_MAIN, {
boxId:"tpnbafTicketBoxMain",
textId:"tpnbafTicketTextMain",
whyId:"tpnbafWhyTextMain",
kpiOddsId:"tpnbafKpiOddsMain",
kpiNId:"tpnbafKpiNMain",
kpiRetId:"tpnbafKpiRetMain",
stakeId:"tpnbafStakeMain",
title:"🎫 Ticket NBA (Fusion Spots) — max "+MAX_LEGS+" legs",
whyTitle:"Pourquoi ces joueurs ?"
});
renderBuilder(LS_P1, {
boxId:"tpnbafTicketBoxP1",
textId:"tpnbafTicketTextP1",
whyId:"tpnbafWhyTextP1",
kpiOddsId:"tpnbafKpiOddsP1",
kpiNId:"tpnbafKpiNP1",
kpiRetId:"tpnbafKpiRetP1",
stakeId:"tpnbafStakeP1",
title:"🛡️ Ticket SAFE (Palier 1 Dash ≥"+p1Min+"%) — cotes théoriques — max "+MAX_LEGS+" legs",
whyTitle:"Pourquoi SAFE Palier 1 ?"
});
}
// Remove item
document.addEventListener("click", (e)=>{
const x = e.target.closest("[data-x][data-key]");
if (!x) return;
const key = x.getAttribute("data-key");
const idx = parseInt(x.getAttribute("data-x"),10);
if (![LS_MAIN, LS_P1].includes(key)) return;
const items = readTicket(key);
if (!isNaN(idx)) items.splice(idx,1);
saveTicket(key, items);
renderAll();
});
// Add MAIN
document.addEventListener("click", (e)=>{
const btn = e.target.closest(".tpnbaf-add-main");
if (!btn) return;
const tr = btn.closest("tr");
if (!tr) return;
const it = mainItemFromTr(tr);
const items = readTicket(LS_MAIN);
if (items.length >= MAX_LEGS){
flashBtn(btn, "⛔ Max "+MAX_LEGS, "➕ Main");
return;
}
// 1 spot par match
if (it.gid && items.some(x => String(x.gid||"") === String(it.gid))) {
flashBtn(btn, "⛔ Même match", "➕ Main");
return;
}
const sig = [it.match,it.player,it.market,it.side,it.line].join("|");
if (!items.some(x=>[x.match,x.player,x.market,x.side,x.line].join("|")===sig)){
items.push(it);
saveTicket(LS_MAIN, items);
renderAll();
flashBtn(btn, "✅ Ajouté", "➕ Main");
} else {
flashBtn(btn, "⚠️ Déjà", "➕ Main");
}
});
// Add P1
document.addEventListener("click", (e)=>{
const btn = e.target.closest(".tpnbaf-add-p1");
if (!btn || btn.disabled) return;
const tr = btn.closest("tr");
if (!tr) return;
const items = readTicket(LS_P1);
if (items.length >= MAX_LEGS){
flashBtn(btn, "⛔ Max "+MAX_LEGS, "🛡️ P1");
return;
}
const p1Min = getP1Threshold();
const p1conf = toNum(tr.getAttribute("data-pal1conf"));
const p1line = tr.getAttribute("data-pal1line") || "";
if (p1conf===null || p1conf < p1Min || !p1line){
flashBtn(btn, "⛔ P1 <"+p1Min, "🛡️ P1");
return;
}
const it = p1ItemFromTr(tr);
// 1 spot par match
if (it.gid && items.some(x => String(x.gid||"") === String(it.gid))) {
flashBtn(btn, "⛔ Même match", "🛡️ P1");
return;
}
const sig = [it.match,it.player,it.market,it.side,it.line].join("|");
if (!items.some(x=>[x.match,x.player,x.market,x.side,x.line].join("|")===sig)){
items.push(it);
saveTicket(LS_P1, items);
renderAll();
flashBtn(btn, "✅ Ajouté", "🛡️ P1");
} else {
flashBtn(btn, "⚠️ Déjà", "🛡️ P1");
}
});
// ✅ Auto builders
function autoBuildMain(btn){
const rows = getVisibleRows()
.sort((a,b)=> (parseFloat(b.getAttribute("data-score")||"0") - parseFloat(a.getAttribute("data-score")||"0")));
const out = [];
const usedGid = new Set();
for (const tr of rows){
if (out.length >= MAX_LEGS) break;
const gid = tr.getAttribute("data-gid") || "";
if (!gid || usedGid.has(gid)) continue;
out.push(mainItemFromTr(tr));
usedGid.add(gid);
}
saveTicket(LS_MAIN, out);
renderAll();
flashBtn(btn, "✅ Généré", "🤖 Auto");
}
function autoBuildP1(btn){
const thr = getP1Threshold();
const rows = getVisibleRows()
.filter(tr=>{
const p1c = toNum(tr.getAttribute("data-pal1conf"));
const p1l = tr.getAttribute("data-pal1line") || "";
return (p1c !== null && p1c >= thr && !!p1l);
})
.sort((a,b)=> (toNum(b.getAttribute("data-pal1conf"))||0) - (toNum(a.getAttribute("data-pal1conf"))||0));
const out = [];
const usedGid = new Set();
for (const tr of rows){
if (out.length >= MAX_LEGS) break;
const gid = tr.getAttribute("data-gid") || "";
if (!gid || usedGid.has(gid)) continue;
out.push(p1ItemFromTr(tr));
usedGid.add(gid);
}
saveTicket(LS_P1, out);
renderAll();
flashBtn(btn, "✅ Généré", "🤖 Auto");
}
// Copy / Clear / Auto
document.addEventListener("click", async (e)=>{
if (e.target.closest("#tpnbafClearMain")){ saveTicket(LS_MAIN, []); renderAll(); return; }
if (e.target.closest("#tpnbafClearP1")){ saveTicket(LS_P1, []); renderAll(); return; }
const aM = e.target.closest("#tpnbafAutoMain");
if (aM){ autoBuildMain(aM); return; }
const aP = e.target.closest("#tpnbafAutoP1");
if (aP){ autoBuildP1(aP); return; }
const cpM = e.target.closest("#tpnbafCopyMain");
if (cpM){
const txt = document.querySelector("#tpnbafTicketTextMain");
try{ await navigator.clipboard.writeText(txt?.value || ""); cpM.textContent="✅ Copié"; }
catch(err){ cpM.textContent="❌"; }
setTimeout(()=>cpM.textContent="📋 Copier ticket", 1200);
return;
}
const cpP = e.target.closest("#tpnbafCopyP1");
if (cpP){
const txt = document.querySelector("#tpnbafTicketTextP1");
try{ await navigator.clipboard.writeText(txt?.value || ""); cpP.textContent="✅ Copié"; }
catch(err){ cpP.textContent="❌"; }
setTimeout(()=>cpP.textContent="📋 Copier ticket", 1200);
return;
}
const cpWM = e.target.closest("#tpnbafCopyWhyMain");
if (cpWM){
const why = document.querySelector("#tpnbafWhyTextMain");
try{ await navigator.clipboard.writeText(why?.value || ""); cpWM.textContent="✅ Copié"; }
catch(err){ cpWM.textContent="❌"; }
setTimeout(()=>cpWM.textContent="📋 Copier pourquoi", 1200);
return;
}
const cpWP = e.target.closest("#tpnbafCopyWhyP1");
if (cpWP){
const why = document.querySelector("#tpnbafWhyTextP1");
try{ await navigator.clipboard.writeText(why?.value || ""); cpWP.textContent="✅ Copié"; }
catch(err){ cpWP.textContent="❌"; }
setTimeout(()=>cpWP.textContent="📋 Copier pourquoi", 1200);
return;
}
});
document.addEventListener("input",(e)=>{
if (e.target && (e.target.id==="tpnbafStakeMain" || e.target.id==="tpnbafStakeP1")) renderAll();
});
// ==========================
// ✅ TRI TABLE (asc/desc)
// ==========================
let currentSort = { key: null, dir: null }; // dir: "asc" | "desc"
function levelRank(v){
const x = String(v || "").toUpperCase();
if (x === "HIGH") return 3;
if (x === "MED") return 2;
return 1; // LOW ou vide
}
function numOrNull(v){
if (v === null || v === undefined) return null;
const s = String(v).trim();
if (!s || s === "-" || s === "—") return null;
const n = parseFloat(s.replace(",", "."));
return Number.isFinite(n) ? n : null;
}
function getSortVal(tr, key){
switch(key){
case "line": return numOrNull(tr.getAttribute("data-line"));
case "odd": return numOrNull(tr.getAttribute("data-odd"));
case "conf": return numOrNull(tr.getAttribute("data-conf"));
case "level": return levelRank(tr.getAttribute("data-level"));
case "avg": return numOrNull(tr.getAttribute("data-avg"));
case "n": return numOrNull(tr.getAttribute("data-n"));
case "min": return numOrNull(tr.getAttribute("data-min"));
case "ia": return numOrNull(tr.getAttribute("data-ia"));
case "delta": return numOrNull(tr.getAttribute("data-delta"));
case "edge": return numOrNull(tr.getAttribute("data-edge"));
case "ev": return numOrNull(tr.getAttribute("data-ev"));
case "score": return numOrNull(tr.getAttribute("data-score"));
case "pal1": {
const c = numOrNull(tr.getAttribute("data-pal1conf"));
const l = numOrNull(tr.getAttribute("data-pal1line"));
return (c === null ? null : (c * 1000 + (l || 0)));
}
default:
return null;
}
}
function applySort(){
const tbody = document.querySelector(".tpnbaf-table tbody");
if (!tbody) return;
if (!currentSort.key || !currentSort.dir) return;
const rows = Array.from(tbody.querySelectorAll("tr"));
const dirMul = (currentSort.dir === "asc") ? 1 : -1;
const withIdx = rows.map((tr, idx)=>({tr, idx}));
withIdx.sort((a,b)=>{
const va = getSortVal(a.tr, currentSort.key);
const vb = getSortVal(b.tr, currentSort.key);
const aNull = (va === null);
const bNull = (vb === null);
if (aNull && bNull) return a.idx - b.idx;
if (aNull) return 1;
if (bNull) return -1;
if (va < vb) return -1 * dirMul;
if (va > vb) return 1 * dirMul;
return a.idx - b.idx;
});
withIdx.forEach(o => tbody.appendChild(o.tr));
}
document.addEventListener("click", (e)=>{
const th = e.target.closest("th.tpnbaf-sortable[data-sort]");
if (!th) return;
const key = th.getAttribute("data-sort");
document.querySelectorAll("th.tpnbaf-sortable[data-sort]").forEach(x=>{
if (x !== th) x.removeAttribute("data-dir");
});
if (currentSort.key !== key){
currentSort.key = key;
currentSort.dir = "desc"; // 1er clic => décroissant
} else {
currentSort.dir = (currentSort.dir === "desc") ? "asc" : "desc";
}
th.setAttribute("data-dir", currentSort.dir);
applySort();
});
function applyFilters(){
const q = (document.querySelector("#tpnbafQ")?.value || "").toLowerCase().trim();
const lvl = (document.querySelector("#tpnbafLvl")?.value || "ALL");
const side = (document.querySelector("#tpnbafSide")?.value || "ALL");
const iaf = (document.querySelector("#tpnbafIA")?.value || "ALL");
const mktF = (document.querySelector("#tpnbafMarket")?.value || "ALL");
const gidF = (document.querySelector("#tpnbafMatch")?.value || "ALL");
const p1Sel = getP1SelectValue();
const p1Min = getP1Threshold();
document.querySelectorAll(".tpnbaf-table tbody tr").forEach(tr=>{
const hay = (tr.getAttribute("data-hay") || "").toLowerCase();
const tl = tr.getAttribute("data-level") || "";
const sd = tr.getAttribute("data-side") || "";
const hasIA = tr.getAttribute("data-hasia") || "0";
const mk = tr.getAttribute("data-market") || "";
const gid = tr.getAttribute("data-gid") || "";
const p1c = toNum(tr.getAttribute("data-pal1conf"));
const p1l = (tr.getAttribute("data-pal1line") || "");
let ok = true;
if (q && hay.indexOf(q) === -1) ok = false;
if (lvl !== "ALL" && tl !== lvl) ok = false;
if (side !== "ALL" && sd !== side) ok = false;
if (iaf === "YES" && hasIA !== "1") ok = false;
if (iaf === "NO" && hasIA !== "0") ok = false;
if (mktF !== "ALL" && mk !== mktF) ok = false;
if (gidF !== "ALL" && gid !== gidF) ok = false;
if (p1Sel !== "ALL"){
if (p1c===null || p1c < p1Min || !p1l) ok = false;
}
tr.style.display = ok ? "" : "none";
const p1btn = tr.querySelector(".tpnbaf-add-p1");
if (p1btn){
const eligibleNow = (p1c!==null && p1c >= p1Min && !!p1l);
p1btn.disabled = !eligibleNow;
}
});
updateLoadMoreBtn();
renderAll();
applySort(); // ✅ conserve le tri après filtre / load more
}
document.addEventListener("input",(e)=>{ if (e.target && (e.target.id==="tpnbafQ")) applyFilters(); });
document.addEventListener("change",(e)=>{
if (e.target && (
e.target.id==="tpnbafLvl" || e.target.id==="tpnbafSide" || e.target.id==="tpnbafIA" ||
e.target.id==="tpnbafP1Min" || e.target.id==="tpnbafMarket" || e.target.id==="tpnbafMatch"
)) applyFilters();
});
document.addEventListener("click",(e)=>{
const b = e.target.closest("#tpnbafRefresh");
if (!b) return;
const url = b.getAttribute("data-url") || "";
if (url) window.location.href = url;
else window.location.reload();
});
document.addEventListener("click", async (e)=>{
const b = e.target.closest("#tpnbafLoadMore");
if (!b || b.disabled) return;
const gid = getSelectedMatchGid();
if (gid === "ALL") return;
if (rawOffsets[gid] === undefined) rawOffsets[gid] = MAX_ROWS_GAME;
const offset = rawOffsets[gid];
const limit = parseInt(b.getAttribute("data-limit") || "200", 10) || 200;
const sel = document.querySelector("#tpnbafMatch");
const opt = sel?.selectedOptions?.[0];
const matchText = opt ? (opt.textContent || "") : "";
const time = opt ? (opt.getAttribute("data-time") || "") : "";
b.disabled = true;
const old = b.textContent;
b.textContent = "⏳ Chargement…";
try{
const params = new URLSearchParams();
params.set("action","tpnbaf6_more");
params.set("nonce", AJAX_NONCE);
params.set("gid", gid);
params.set("league", String(CFG.league||""));
params.set("season", String(CFG.season||""));
params.set("bookmaker", String(CFG.bookmaker||""));
params.set("offset", String(offset));
params.set("limit", String(limit));
params.set("min_minutes", String(CFG.minMinutes||""));
params.set("min_conf", String(CFG.minConf||""));
params.set("min_games", String(CFG.minGames||""));
params.set("match", matchText);
params.set("time", time);
const res = await fetch(AJAX_URL, {
method: "POST",
headers: {"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},
body: params.toString()
});
const data = await res.json();
if (!data || !data.ok) throw new Error((data && data.msg) ? data.msg : "Erreur AJAX");
const tbody = document.querySelector(".tpnbaf-table tbody");
const rows = (data.rows_html || []);
if (tbody && rows.length){
rows.forEach(html=>{
const tmp = document.createElement("tbody");
tmp.innerHTML = html.trim();
const tr = tmp.querySelector("tr");
if (tr) tbody.appendChild(tr);
const mk = tr ? (tr.getAttribute("data-market")||"") : "";
if (mk){
const msel = document.querySelector("#tpnbafMarket");
if (msel && !Array.from(msel.options).some(o=>o.value===mk)){
const o = document.createElement("option");
o.value = mk;
o.textContent = mk;
msel.appendChild(o);
}
}
});
}
rawOffsets[gid] = (data.next_offset !== undefined) ? data.next_offset : (offset + limit);
if (data.done){
b.textContent = "✅ Plus rien à charger";
b.disabled = true;
} else {
b.textContent = old;
b.disabled = false;
}
applyFilters();
} catch(err){
b.textContent = "❌ Erreur";
setTimeout(()=>{
b.textContent = old;
b.disabled = false;
}, 1200);
}
});
window.addEventListener("DOMContentLoaded", ()=>{
renderAll();
applyFilters();
updateLoadMoreBtn();
});
})();
JS;
echo "<script>{$js}</script>";
}
}
/* =========================
Helpers: normalisation
========================= */
if (!function_exists('tpnbaf6_lower')) {
function tpnbaf6_lower(string $s): string {
$s = trim($s);
if (function_exists('mb_strtolower')) return mb_strtolower($s, 'UTF-8');
return strtolower($s);
}
}
if (!function_exists('tpnbaf6_norm_player')) {
function tpnbaf6_norm_player(string $name): string {
$n = tpnbaf6_lower($name);
$n = str_replace(['’','ʼ','`'], "'", $n);
$n = str_replace('.', '', $n);
$n = preg_replace('~\s+~',' ', $n);
return trim((string)$n);
}
}
if (!function_exists('tpnbaf6_norm_market')) {
function tpnbaf6_norm_market(string $m): string {
$x = strtoupper(trim($m));
$x = str_replace(['PTS+PAS','PTS+AST'], 'PTS+AST', $x);
if ($x === 'PAS') $x = 'AST';
if ($x === 'RP') $x = 'RA';
if ($x === 'PRA') $x = 'PRP';
return $x;
}
}
/* =========================
✅ Dash lines map + palier inférieur réel (PRP 44.5/49.5)
========================= */
if (!function_exists('tpnbaf6_dash_lines_map')) {
function tpnbaf6_dash_lines_map(): array {
// 1) depuis dashboard si dispo
$map = [];
if (function_exists('tp_nba_get_lines_map')) {
$m = tp_nba_get_lines_map();
if (is_array($m)) $map = $m;
}
// 2) fallback interne (copie “dashboard” + PRP 44.5/49.5)
if (empty($map)) {
$map = [
'PTS' => [4.5,9.5,14.5,19.5,24.5,29.5],
'REB' => [0.5,2.5,4.5,6.5,8.5,10.5,12.5],
'AST' => [0.5,2.5,4.5,6.5,8.5,10.5,12.5],
'3PM' => [0.5,1.5,2.5,3.5,4.5],
'PRP' => [9.5,14.5,19.5,24.5,29.5,34.5,39.5,44.5,49.5],
'PR' => [9.5,14.5,19.5,24.5,29.5,34.5],
'PA' => [9.5,14.5,19.5,24.5,29.5],
'RA' => [4.5,6.5,8.5,10.5,12.5,14.5],
'STL' => [0.5,1.5,2.5,3.5,4.5],
'BLK' => [0.5,1.5,2.5,3.5,4.5],
];
}
// Aliases utiles
if (!empty($map['PA']) && empty($map['PTS+AST'])) $map['PTS+AST'] = $map['PA'];
if (!empty($map['PTS+AST']) && empty($map['PA'])) $map['PA'] = $map['PTS+AST'];
return $map;
}
}
if (!function_exists('tpnbaf6_dash_lower_line')) {
function tpnbaf6_dash_lower_line(string $market, float $line): ?float {
$m = tpnbaf6_norm_market($market);
$map = tpnbaf6_dash_lines_map();
$list = $map[$m] ?? null;
if (!is_array($list) || empty($list)) return null;
$vals = array_map('floatval', $list);
sort($vals, SORT_NUMERIC);
$best = null;
foreach ($vals as $v){
if ($v < $line) $best = $v;
else break;
}
return ($best !== null && $best > 0) ? round((float)$best, 1) : null;
}
}
if (!function_exists('tpnbaf6_level')) {
function tpnbaf6_level(?int $pct): array {
if ($pct === null) return ['key'=>'LOW','label'=>'Faible','cls'=>'tpnbaf-chip tpnbaf-low'];
if ($pct >= 75) return ['key'=>'HIGH','label'=>'Forte','cls'=>'tpnbaf-chip tpnbaf-high'];
if ($pct >= 60) return ['key'=>'MED','label'=>'Moyenne','cls'=>'tpnbaf-chip tpnbaf-med'];
return ['key'=>'LOW','label'=>'Faible','cls'=>'tpnbaf-chip tpnbaf-low'];
}
}
/* =========================
Dashboard hist global (souple)
========================= */
if (!function_exists('tpnbaf6_get_hist_global')) {
function tpnbaf6_get_hist_global(): array {
if (function_exists('tp_nba_get_hist_global')) {
$h = tp_nba_get_hist_global();
return is_array($h) ? $h : [];
}
$h = get_transient('tp_hist_global_cache_2h');
if ($h !== false && is_array($h)) return $h;
$o = get_option('tp_hist_global_cache_2h');
return is_array($o) ? $o : [];
}
}
if (!function_exists('tpnbaf6_find_hist_key')) {
function tpnbaf6_find_hist_key(array $hist, string $player, string $market): ?string {
$key = tpnbaf6_norm_player($player);
$candidates = [];
if ($key !== '') $candidates[] = $key;
$parts = preg_split('/\s+/', $key);
if (count($parts) >= 2) {
$rev = trim(implode(' ', array_reverse($parts)));
if ($rev && $rev !== $key) $candidates[] = $rev;
}
foreach ($candidates as $k) {
if (!empty($hist[$k][$market]) && is_array($hist[$k][$market])) return $k;
}
if (count($parts) >= 2) {
$first = $parts[0];
$last = $parts[count($parts)-1];
foreach ($hist as $k => $per) {
if (empty($per[$market]) || !is_array($per[$market])) continue;
$kp = preg_split('/\s+/', (string)$k);
if (in_array($first,$kp,true) && in_array($last,$kp,true)) return $k;
}
}
return null;
}
}
if (!function_exists('tpnbaf6_get_series')) {
function tpnbaf6_get_series(string $player, string $market): array {
$hist = tpnbaf6_get_hist_global();
if (empty($hist)) return [];
$mkt = tpnbaf6_norm_market($market);
$k = tpnbaf6_find_hist_key($hist, $player, $mkt);
if ($k && !empty($hist[$k][$mkt]) && is_array($hist[$k][$mkt])) {
return array_map('floatval', array_values($hist[$k][$mkt]));
}
$kPts = tpnbaf6_find_hist_key($hist, $player, 'PTS');
$kReb = tpnbaf6_find_hist_key($hist, $player, 'REB');
$kAst = tpnbaf6_find_hist_key($hist, $player, 'AST');
if ($mkt === 'PTS+AST' && $kPts && $kAst) {
$a = array_values($hist[$kPts]['PTS'] ?? []);
$b = array_values($hist[$kAst]['AST'] ?? []);
$n = min(count($a), count($b));
if ($n >= 3){
$series = [];
for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i];
return $series;
}
}
if ($mkt === 'PRP' && $kPts && $kReb && $kAst) {
$a = array_values($hist[$kPts]['PTS'] ?? []);
$b = array_values($hist[$kReb]['REB'] ?? []);
$c = array_values($hist[$kAst]['AST'] ?? []);
$n = min(count($a), count($b), count($c));
if ($n >= 3){
$series = [];
for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i] + (float)$c[$i];
return $series;
}
}
if ($mkt === 'PR' && $kPts && $kReb) {
$a = array_values($hist[$kPts]['PTS'] ?? []);
$b = array_values($hist[$kReb]['REB'] ?? []);
$n = min(count($a), count($b));
if ($n >= 3){
$series = [];
for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i];
return $series;
}
}
if ($mkt === 'RA' && $kReb && $kAst) {
$a = array_values($hist[$kReb]['REB'] ?? []);
$b = array_values($hist[$kAst]['AST'] ?? []);
$n = min(count($a), count($b));
if ($n >= 3){
$series = [];
for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i];
return $series;
}
}
return [];
}
}
if (!function_exists('tpnbaf6_conf_stats')) {
function tpnbaf6_conf_stats(string $player, string $market, float $line): array {
$series = tpnbaf6_get_series($player, $market);
if (!is_array($series) || count($series) < 3) {
return ['over'=>null,'under'=>null,'n'=>0,'avg'=>null,'ho'=>0,'hu'=>0];
}
$n = count($series);
$sum = 0.0; $ho = 0; $hu = 0;
foreach ($series as $v){
$v = (float)$v;
$sum += $v;
if ($v > $line) $ho++;
if ($v < $line) $hu++;
}
$avg = $n ? round($sum / $n, 1) : null;
$over = $n ? (int)round(100 * $ho / $n) : null;
$under= $n ? (int)round(100 * $hu / $n) : null;
return ['over'=>$over,'under'=>$under,'n'=>$n,'avg'=>$avg,'ho'=>$ho,'hu'=>$hu];
}
}
if (!function_exists('tpnbaf6_minutes_avg')) {
function tpnbaf6_minutes_avg(string $player): ?float {
$series = tpnbaf6_get_series($player, 'MIN');
if (!is_array($series) || count($series) < 3) {
$series = tpnbaf6_get_series($player, 'MINUTES');
}
if (!is_array($series) || count($series) < 3) return null;
$n= count($series); $sum=0.0;
foreach ($series as $v) $sum += (float)$v;
return $n ? round($sum/$n, 1) : null;
}
}
if (!function_exists('tpnbaf6_parse_odd')) {
function tpnbaf6_parse_odd($s): float {
if (is_float($s) || is_int($s)) return (float)$s;
$x = (string)$s;
$x = preg_replace('~[^0-9,\.]~','', $x);
$x = str_replace(',', '.', $x);
return (float)$x;
}
}
/* Snapshots joueurs IA */
if (!function_exists('tpnbaf6_get_any_cache_array')) {
function tpnbaf6_get_any_cache_array(string $key): ?array {
$t = get_transient($key);
if (is_array($t)) return $t;
$o = get_option($key);
return is_array($o) ? $o : null;
}
}
if (!function_exists('tpnbaf6_get_player_analyzer_snapshot')) {
function tpnbaf6_get_player_analyzer_snapshot(int $game_id): ?array {
if (!$game_id) return null;
$arr = null;
if (function_exists('tp_pre_cache_get')) {
$s = tp_pre_cache_get($game_id);
if (is_array($s) && isset($s['combined']) && is_array($s['combined'])) $arr = $s;
}
if (!$arr) {
$arr = tpnbaf6_get_any_cache_array('tp_pre_'.$game_id);
if (!(is_array($arr) && isset($arr['combined']) && is_array($arr['combined']))) $arr = null;
}
return $arr;
}
}
if (!function_exists('tpnbaf6_build_player_pred_map')) {
function tpnbaf6_build_player_pred_map(array $combined): array {
$map = [
'_by_last' => [],
'_by_init_last' => [],
];
foreach ($combined as $k => $pl){
if (!is_array($pl)) continue;
$name = (string)($pl['name'] ?? $k);
$norm = tpnbaf6_norm_player($name);
if ($norm === '') continue;
$g = (int)($pl['games'] ?? 0);
if ($g <= 0) continue;
$tp = (float)($pl['total_points'] ?? 0);
$tr = (float)($pl['total_rebounds'] ?? 0);
$ta = (float)($pl['total_assists'] ?? 0);
$pts = (int)floor($tp / $g);
$reb = (int)floor($tr / $g);
$ast = (int)floor($ta / $g);
$prp = $pts + $reb + $ast;
$a3 = null;
if (!empty($pl['avg_3pts'])) {
$a3 = (float)$pl['avg_3pts'];
} elseif (!empty($pl['avg_3pts_count']) && !empty($pl['total_avg_3pts']) && (int)$pl['avg_3pts_count'] > 0) {
$a3 = round(((float)$pl['total_avg_3pts']) / (int)$pl['avg_3pts_count'], 1);
}
$min = null;
if (isset($pl['avg_minutes']) && is_numeric($pl['avg_minutes'])) {
$min = (float)$pl['avg_minutes'];
}
$map[$norm] = [
'name' => $name,
'PTS' => $pts,
'REB' => $reb,
'AST' => $ast,
'PRP' => $prp,
'3PM' => $a3,
'MIN' => $min,
'G' => $g,
];
$parts = preg_split('/\s+/', $norm);
$first = $parts[0] ?? '';
$last = $parts[count($parts)-1] ?? '';
if ($last){
$map['_by_last'][$last][] = $norm;
if ($first){
$init = substr($first, 0, 1);
$key = $init.' '.$last;
$map['_by_init_last'][$key][] = $norm;
}
}
}
return $map;
}
}
if (!function_exists('tpnbaf6_find_player_pred_key')) {
function tpnbaf6_find_player_pred_key(array $predMap, string $player): ?string {
$p = tpnbaf6_norm_player($player);
if ($p && isset($predMap[$p])) return $p;
$parts = preg_split('/\s+/', $p);
$first = $parts[0] ?? '';
$last = $parts[count($parts)-1] ?? '';
if ($first && $last){
$key = substr($first,0,1).' '.$last;
$list = $predMap['_by_init_last'][$key] ?? [];
if (count($list) === 1) return $list[0];
}
if ($last){
$list = $predMap['_by_last'][$last] ?? [];
if (count($list) === 1) return $list[0];
}
if ($first && $last){
foreach ($predMap as $k => $_){
if ($k === '_by_last' || $k === '_by_init_last') continue;
if (strpos($k, $first) !== false && strpos($k, $last) !== false) return $k;
}
}
return null;
}
}
if (!function_exists('tpnbaf6_player_pred_value')) {
function tpnbaf6_player_pred_value(array $predMap, string $player, string $market): ?float {
$k = tpnbaf6_find_player_pred_key($predMap, $player);
if (!$k || empty($predMap[$k]) || !is_array($predMap[$k])) return null;
$m = tpnbaf6_norm_market($market);
$pts = $predMap[$k]['PTS'] ?? null;
$reb = $predMap[$k]['REB'] ?? null;
$ast = $predMap[$k]['AST'] ?? null;
switch ($m){
case 'PTS': return is_numeric($pts) ? (float)$pts : null;
case 'REB': return is_numeric($reb) ? (float)$reb : null;
case 'AST': return is_numeric($ast) ? (float)$ast : null;
case '3PM': return isset($predMap[$k]['3PM']) && $predMap[$k]['3PM'] !== null ? (float)$predMap[$k]['3PM'] : null;
case 'PRP':
if (is_numeric($predMap[$k]['PRP'] ?? null)) return (float)$predMap[$k]['PRP'];
if (is_numeric($pts) && is_numeric($reb) && is_numeric($ast)) return (float)$pts + (float)$reb + (float)$ast;
return null;
case 'PTS+AST':
if (is_numeric($pts) && is_numeric($ast)) return (float)$pts + (float)$ast;
return null;
case 'PR':
if (is_numeric($pts) && is_numeric($reb)) return (float)$pts + (float)$reb;
return null;
case 'RA':
if (is_numeric($reb) && is_numeric($ast)) return (float)$reb + (float)$ast;
return null;
default:
return null;
}
}
}
if (!function_exists('tpnbaf6_player_minutes_pred')) {
function tpnbaf6_player_minutes_pred(array $predMap, string $player): ?float {
$k = tpnbaf6_find_player_pred_key($predMap, $player);
if (!$k || empty($predMap[$k]) || !is_array($predMap[$k])) return null;
$min = $predMap[$k]['MIN'] ?? null;
return (is_numeric($min) ? (float)$min : null);
}
}
/* Scoring */
if (!function_exists('tpnbaf6_market_weight')) {
function tpnbaf6_market_weight(string $market): float {
$m = tpnbaf6_norm_market($market);
switch ($m){
case 'PTS':
case 'REB':
case 'AST':
return 1.00;
case 'PR':
case 'RA':
case 'PTS+AST':
return 0.92;
case 'PRP':
return 0.85;
case '3PM':
return 0.75;
default:
return 0.90;
}
}
}
if (!function_exists('tpnbaf6_align_bonus')) {
function tpnbaf6_align_bonus(string $side, ?float $deltaIA): float {
if ($deltaIA === null) return 0.0;
$d = (float)$deltaIA;
if ($side === 'Over'){
if ($d >= 0.0) return 8.0;
if ($d >= -1.0) return 4.0;
return -8.0;
} else {
if ($d <= 0.0) return 8.0;
if ($d <= 1.0) return 4.0;
return -8.0;
}
}
}
if (!function_exists('tpnbaf6_compute_score')) {
function tpnbaf6_compute_score(array $r): float {
$conf = (float)($r['conf'] ?? 0);
$edge = ($r['edge_pp'] !== null) ? (float)$r['edge_pp'] : 0.0;
$ev = ($r['ev_pct'] !== null) ? (float)$r['ev_pct'] : 0.0;
$n = (int)($r['n'] ?? 0);
$min = ($r['min_use'] !== null) ? (float)$r['min_use'] : 0.0;
$edge = max(-10.0, min(20.0, $edge));
$ev = max(-15.0, min(60.0, $ev));
$sampleBonus = 0.0;
if ($n >= 12) $sampleBonus = min(6.0, ($n - 12) * 0.30);
$minBonus = 0.0;
if ($min >= 25) $minBonus = min(4.0, ($min - 25) * 0.25);
$align = tpnbaf6_align_bonus((string)($r['side'] ?? ''), $r['delta_ia'] ?? null);
$palBonus = (!empty($r['pal1_conf']) && (int)$r['pal1_conf'] >= 85) ? 3.0 : 0.0;
$base = $conf
+ (1.2 * $edge)
+ (0.35 * $ev)
+ $align
+ $sampleBonus
+ $minBonus
+ $palBonus;
$w = tpnbaf6_market_weight((string)($r['market'] ?? ''));
return round($base * $w, 1);
}
}
/* =========================
Render Shortcode
========================= */
if (!function_exists('tpnbaf6_render_shortcode')) {
function tpnbaf6_render_shortcode($atts = []){
$atts = shortcode_atts([
'season' => defined('TP_BASK_SEASON') ? TP_BASK_SEASON : '2025-2026',
'league' => defined('TP_BASK_LEAGUE') ? TP_BASK_LEAGUE : 12,
'bookmaker' => 4,
'ttl' => 600,
'max_rows_per_game' => 240,
'player_pred_ttl' => 6 * HOUR_IN_SECONDS,
'min_minutes' => 25,
'min_conf' => 10,
'min_games' => 12,
'max_legs' => 10, // ✅ jusqu’à 10 legs
], $atts, 'tp_nba_spot_fusion');
$league = (int)$atts['league'];
$season = (string)$atts['season'];
$bk = (int)$atts['bookmaker'];
$ttl = max(60, (int)$atts['ttl']);
$maxRowsGame = max(60, (int)$atts['max_rows_per_game']);
$playerPredTtl = max(300, (int)$atts['player_pred_ttl']);
$minMinutes = (float)$atts['min_minutes'];
$minConf = (int)$atts['min_conf'];
$minGames = (int)$atts['min_games'];
$maxLegs = max(1, (int)$atts['max_legs']);
if ($maxLegs > 20) $maxLegs = 20; // garde-fou
if (!function_exists('tpnbap_get_player_props_ou') || !function_exists('tpnbap_player_props_rows')) {
return '<div class="tpnbaf-wrap"><div class="tpnbaf-card"><b>⚠️ tp_nba_props non détecté.</b><br>Il faut que ton analyseur [tp_nba_props] soit actif.</div></div>';
}
// matches
if (function_exists('tpnbap_get_matches_next_days')) {
$matches = tpnbap_get_matches_next_days($league, $season);
} elseif (function_exists('get_nba_matches_on_next_2_days')) {
$matches = get_nba_matches_on_next_2_days((string)$league, $season);
$matches = array_map(function($m){
return [
'id' => (int)($m['id'] ?? 0),
'ts' => (int)($m['date_ts'] ?? 0),
'teams' => $m['teams'] ?? [],
'status' => $m['status'] ?? [],
];
}, $matches);
} else {
return '<div class="tpnbaf-wrap"><div class="tpnbaf-card"><b>⚠️ Impossible de récupérer les matchs.</b></div></div>';
}
if (empty($matches)) {
return '<div class="tpnbaf-wrap"><div class="tpnbaf-card">Aucun match NBA à venir trouvé.</div></div>';
}
$ck = 'tpnbaf6_html_'.md5(wp_json_encode([$league,$season,$bk,$minMinutes,$minConf,$minGames,$maxLegs,date_i18n('Ymd')]));
$force = (!empty($_GET['tpnbaf6_refresh']) && !empty($_GET['_tpnbaf6_nonce']) && wp_verify_nonce((string)$_GET['_tpnbaf6_nonce'], 'tpnbaf6_refresh'));
if ($force) delete_transient($ck);
// ✅ IMPORTANT: CSS/JS doivent être imprimés même si HTML vient du cache
tpnbaf6_css_once();
tpnbaf6_js_once();
$cached = get_transient($ck);
if (!$force && $cached !== false) return $cached;
$rowsAll = [];
$matchMap = []; // gid => ['label'=>..., 'time'=>...]
$marketSet = []; // market => true
foreach ($matches as $m){
$gid = (int)($m['id'] ?? 0);
if (!$gid) continue;
$status = strtoupper($m['status']['short'] ?? '');
if ($status && $status !== 'NS') continue;
$home = $m['teams']['home'] ?? [];
$away = $m['teams']['away'] ?? [];
$homeN = (string)($home['name'] ?? 'Home');
$awayN = (string)($away['name'] ?? 'Away');
$ts = (int)($m['ts'] ?? 0);
$timeLabel = '';
if ($ts > 0 && function_exists('tpnbap_format_fr_datetime')) {
$fr = tpnbap_format_fr_datetime($ts);
$timeLabel = trim(($fr['time'] ?? '').' '.($fr['date'] ?? ''));
}
$matchLabel = $homeN.' vs '.$awayN;
$matchMap[(string)$gid] = ['label'=>$matchLabel, 'time'=>$timeLabel];
$propsOut = tpnbap_get_player_props_ou($gid, $season, $bk);
if (empty($propsOut)) continue;
$rows = tpnbap_player_props_rows($propsOut, (int)$maxRowsGame);
if (empty($rows)) continue;
$predCacheKey = 'tpnbaf6_playerpred_'.$gid;
$playerPredMap = get_transient($predCacheKey);
if (!is_array($playerPredMap)) {
$snapPlayers = tpnbaf6_get_player_analyzer_snapshot($gid);
if (is_array($snapPlayers) && !empty($snapPlayers['combined']) && is_array($snapPlayers['combined'])) {
$playerPredMap = tpnbaf6_build_player_pred_map($snapPlayers['combined']);
set_transient($predCacheKey, $playerPredMap, $playerPredTtl);
} else {
$playerPredMap = [];
set_transient($predCacheKey, $playerPredMap, 10 * MINUTE_IN_SECONDS);
}
}
foreach ($rows as $r){
$player = (string)($r['player'] ?? '');
$market = tpnbaf6_norm_market((string)($r['market'] ?? ''));
$line = (float)($r['line'] ?? 0);
if ($player === '' || $market === '' || $line <= 0) continue;
$oddOver = tpnbaf6_parse_odd($r['over'] ?? 0);
$oddUnder = tpnbaf6_parse_odd($r['under'] ?? 0);
$st = tpnbaf6_conf_stats($player, $market, $line);
$co = $st['over'];
$cu = $st['under'];
$n = (int)($st['n'] ?? 0);
$avg= $st['avg'];
if ($co === null && $cu === null) continue;
$side = 'Over';
$conf = $co;
$odd = $oddOver;
if ($co === null) { $side='Under'; $conf=$cu; $odd=$oddUnder; }
elseif ($cu !== null && $cu > $co) { $side='Under'; $conf=$cu; $odd=$oddUnder; }
if (($odd <= 0) && (($side==='Over' && $oddUnder>0) || ($side==='Under' && $oddOver>0))) {
$side = ($side==='Over') ? 'Under' : 'Over';
$conf = ($side==='Over') ? $co : $cu;
$odd = ($side==='Over') ? $oddOver : $oddUnder;
}
if ($conf === null || $odd <= 0) continue;
if ((int)$conf < $minConf) continue;
if ($n < $minGames) continue;
$lvl = tpnbaf6_level((int)$conf);
// ✅ Palier 1 (Dash) = palier inférieur réel
$pal1Txt = '—';
$pal1Conf = null;
$pal1Line = null;
if ($side === 'Over') {
$lowerLine = tpnbaf6_dash_lower_line($market, $line);
if ($lowerLine !== null) {
$st2 = tpnbaf6_conf_stats($player, $market, (float)$lowerLine);
$pal1Conf = $st2['over'];
$pal1Line = (float)$lowerLine;
if ($pal1Conf !== null) $pal1Txt = number_format((float)$lowerLine, 1, '.', '').' • '.$pal1Conf.'%';
}
}
$minHist = tpnbaf6_minutes_avg($player);
$minPred = (!empty($playerPredMap) ? tpnbaf6_player_minutes_pred($playerPredMap, $player) : null);
$minUse = ($minHist !== null) ? $minHist : $minPred;
if ($minUse === null || (float)$minUse < $minMinutes) continue;
$predIA = null;
if (is_array($playerPredMap) && !empty($playerPredMap)) {
$predIA = tpnbaf6_player_pred_value($playerPredMap, $player, $market);
}
$deltaIA = null;
if ($predIA !== null) $deltaIA = (float)$predIA - (float)$line;
$implied = (100.0 / (float)$odd);
$edgePP = ((float)$conf - (float)$implied);
$evPct = (((float)$conf/100.0) * (float)$odd - 1.0) * 100.0;
$hay = strtolower($matchLabel.' '.$timeLabel.' '.$player.' '.$market.' '.$side);
$row = [
'gid'=>$gid,'match'=>$matchLabel,'time'=>$timeLabel,'player'=>$player,
'player_norm'=>tpnbaf6_norm_player($player),
'market'=>$market,'line'=>$line,'side'=>$side,'odd'=>$odd,'conf'=>(int)$conf,
'lvl_key'=>$lvl['key'],'lvl_label'=>$lvl['label'],'lvl_cls'=>$lvl['cls'],
'co'=>$co,'cu'=>$cu,'avg'=>$avg,'n'=>$n,
'min_hist'=>$minHist,'min_pred'=>$minPred,'min_use'=>$minUse,
'pred_ia'=>$predIA,'delta_ia'=>$deltaIA,
'pal1_txt'=>$pal1Txt,'pal1_conf'=>$pal1Conf,'pal1_line'=>$pal1Line,
'edge_pp'=>$edgePP,'ev_pct'=>$evPct,'hay'=>$hay,
];
$row['score'] = tpnbaf6_compute_score($row);
$rowsAll[] = $row;
$marketSet[$market] = true;
}
}
if (empty($rowsAll)) {
$html = '<div class="tpnbaf-wrap"><div class="tpnbaf-card">Aucun spot éligible (min '.$minMinutes.' min, conf ≥ '.$minConf.'%, matchs ≥ '.$minGames.').</div></div>';
set_transient($ck, $html, $ttl);
return $html;
}
usort($rowsAll, function($a,$b){
if ($b['score'] !== $a['score']) return $b['score'] <=> $a['score'];
if ($b['conf'] !== $a['conf']) return $b['conf'] <=> $a['conf'];
return strcmp($a['match'], $b['match']);
});
$marketOptions = array_keys($marketSet);
sort($marketOptions);
$matchOptions = $matchMap;
$current_url = (is_ssl() ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? '') . ($_SERVER['REQUEST_URI'] ?? '');
$base_url = remove_query_arg(['tpnbaf6_refresh','_tpnbaf6_nonce'], $current_url);
$refresh_url = add_query_arg([
'tpnbaf6_refresh' => 1,
'_tpnbaf6_nonce' => wp_create_nonce('tpnbaf6_refresh'),
], $base_url);
$ajaxData = [
'url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('tpnbaf6_more'),
'cfg' => [
'league' => $league,
'season' => $season,
'bookmaker' => $bk,
'maxRowsGame' => $maxRowsGame,
'minMinutes' => $minMinutes,
'minConf' => $minConf,
'minGames' => $minGames,
'maxLegs' => $maxLegs, // ✅
]
];
ob_start();
echo '<div class="tpnbaf-wrap">';
echo '<script>window.tpnbaf6_ajax = '.wp_json_encode($ajaxData).';</script>';
echo '<div class="tpnbaf-head">';
echo '<div>';
echo '<div class="tpnbaf-title">🏀 Fusion Spots NBA</div>';
echo '<div class="tpnbaf-sub">'.count($rowsAll).' spots éligibles (min '.$minMinutes.'m • conf ≥ '.$minConf.'% • matchs ≥ '.$minGames.' • max '.$maxLegs.' legs)</div>';
echo '</div>';
echo '<div class="tpnbaf-filters">';
echo '<button id="tpnbafRefresh" class="tpnbaf-btn tpnbaf-btn-refresh" type="button" data-url="'.esc_url($refresh_url).'">🔄 Rafraîchir</button>';
echo '<input id="tpnbafQ" type="search" placeholder="Recherche (joueur, match, marché...)">';
echo '<select id="tpnbafMatch">';
echo '<option value="ALL">Tous les matchs</option>';
foreach ($matchOptions as $gidStr => $mm){
$label = (string)($mm['label'] ?? '');
$t = (string)($mm['time'] ?? '');
$optLabel = trim($label . ($t ? (' • '.$t) : ''));
echo '<option value="'.esc_attr((string)$gidStr).'" data-time="'.esc_attr($t).'">'.esc_html($optLabel).'</option>';
}
echo '</select>';
echo '<button id="tpnbafLoadMore" class="tpnbaf-btn tpnbaf-btn-more" type="button" data-limit="200" disabled>➕ Charger plus (sélectionne un match)</button>';
echo '<select id="tpnbafMarket">';
echo '<option value="ALL">Tous marchés</option>';
foreach ($marketOptions as $mk){
echo '<option value="'.esc_attr($mk).'">'.esc_html($mk).'</option>';
}
echo '</select>';
echo '<select id="tpnbafLvl">
<option value="ALL">Tous niveaux</option>
<option value="HIGH">Confiance forte (≥75%)</option>
<option value="MED">Confiance moyenne (60–74%)</option>
<option value="LOW">Confiance faible (≤59%)</option>
</select>';
echo '<select id="tpnbafSide">
<option value="ALL">Over+Under</option>
<option value="Over">Over</option>
<option value="Under">Under</option>
</select>';
echo '<select id="tpnbafIA">
<option value="ALL">Prédiction IA : Tout</option>
<option value="YES">Prédiction IA : Oui</option>
<option value="NO">Prédiction IA : Non</option>
</select>';
echo '<select id="tpnbafP1Min">
<option value="ALL">Palier 1 Dash : Tous</option>
<option value="80">Palier 1 Dash ≥80%</option>
<option value="85" selected>Palier 1 Dash ≥85%</option>
<option value="90">Palier 1 Dash ≥90%</option>
<option value="95">Palier 1 Dash ≥95%</option>
</select>';
echo '</div>';
echo '</div>';
echo '<div class="tpnbaf-card">';
echo '<div class="tpnbaf-tablewrap">';
echo '<table class="tpnbaf-table">';
echo '<thead><tr>
<th>Match</th>
<th>Joueur</th>
<th>Marché</th>
<th>Pick</th>
<th class="tpnbaf-sortable" data-sort="line">Ligne</th>
<th class="tpnbaf-sortable" data-sort="odd">Cote</th>
<th class="tpnbaf-sortable" data-sort="conf">Confiance</th>
<th class="tpnbaf-sortable" data-sort="level">Niveau</th>
<th class="tpnbaf-sortable" data-sort="avg">Palier (moy)</th>
<th class="tpnbaf-sortable" data-sort="n">Matchs</th>
<th class="tpnbaf-sortable" data-sort="min">Min/m</th>
<th class="tpnbaf-sortable" data-sort="ia">Prédiction IA</th>
<th class="tpnbaf-sortable" data-sort="delta">Δ IA</th>
<th class="tpnbaf-sortable" data-sort="pal1">Palier 1 (Dash)</th>
<th class="tpnbaf-sortable" data-sort="edge">Edge</th>
<th class="tpnbaf-sortable" data-sort="ev">Value</th>
<th class="tpnbaf-sortable" data-sort="score">Score</th>
<th>Ticket</th>
</tr></thead><tbody>';
foreach ($rowsAll as $i => $r){
$id = $r['gid'].'_'.$i.'_'.substr(md5($r['player'].$r['market'].$r['line'].$r['side']),0,6);
$mktLabel = $r['market'];
$labels = ['AST'=>'PAS','PTS+AST'=>'PTS+PAS','RA'=>'RP','PRP'=>'PRP'];
if (isset($labels[$mktLabel])) $mktLabel = $labels[$mktLabel];
$oddTxt = ($r['odd']>0) ? number_format((float)$r['odd'], 2, '.', '') : '-';
$details = [];
if ($r['co'] !== null) $details[] = 'Over '.$r['co'].'%';
if ($r['cu'] !== null) $details[] = 'Under '.$r['cu'].'%';
$avgTxt = ($r['avg'] !== null) ? number_format((float)$r['avg'], 1, '.', '') : '—';
$nTxt = ($r['n'] ?? 0) ? (int)$r['n'] : '—';
$minTxt = '—';
if ($r['min_hist'] !== null) $minTxt = number_format((float)$r['min_hist'], 1, '.', '');
elseif ($r['min_pred'] !== null) $minTxt = number_format((float)$r['min_pred'], 1, '.', '').'*';
$iaTxt = ($r['pred_ia'] !== null) ? number_format((float)$r['pred_ia'], 1, '.', '') : '—';
$hasIA = ($r['pred_ia'] !== null) ? 1 : 0;
$dTxt = '—';
if ($r['delta_ia'] !== null) {
$d = (float)$r['delta_ia'];
$dTxt = ($d >= 0 ? '+' : '').number_format($d, 1, '.', '');
}
$pal1Badge = '';
$pal85 = 0;
$pal1LineTxt = '';
$pal1ConfTxt = '';
if ($r['pal1_conf'] !== null) $pal1ConfTxt = (string)((int)$r['pal1_conf']);
if ($r['pal1_line'] !== null) $pal1LineTxt = number_format((float)$r['pal1_line'], 1, '.', '');
if ($r['pal1_conf'] !== null && (int)$r['pal1_conf'] >= 85) {
$pal1Badge = ' <span class="tpnbaf-badge85">≥85%</span>';
$pal85 = 1;
}
$edgeTxt = '<span class="tpnbaf-chipblue">'.(($r['edge_pp']>=0)?'+':'').number_format((float)$r['edge_pp'], 1, '.', '').'pp</span>';
$valTxt = '<span class="tpnbaf-chipnum">'.(($r['ev_pct']>=0)?'+':'').number_format((float)$r['ev_pct'], 1, '.', '').'%</span>';
$bestBadge = ($i === 0) ? ' <span class="tpnbaf-star">BEST</span>' : '';
$p1Eligible = ($pal1ConfTxt !== '' && (int)$pal1ConfTxt >= 80 && $pal1LineTxt !== '');
echo '<tr
data-gid="'.esc_attr((string)$r['gid']).'"
data-id="'.esc_attr($id).'"
data-match="'.esc_attr($r['match']).'"
data-player="'.esc_attr($r['player']).'"
data-playernorm="'.esc_attr($r['player_norm']).'"
data-market="'.esc_attr($r['market']).'"
data-side="'.esc_attr($r['side']).'"
data-line="'.esc_attr(number_format((float)$r['line'], 1, '.', '')).'"
data-odd="'.esc_attr($oddTxt).'"
data-conf="'.esc_attr((int)$r['conf']).'"
data-level="'.esc_attr($r['lvl_key']).'"
data-avg="'.esc_attr($r['avg'] !== null ? number_format((float)$r['avg'], 1, '.', '') : '').'"
data-hasia="'.esc_attr($hasIA).'"
data-score="'.esc_attr(number_format((float)$r['score'], 1, '.', '')).'"
data-n="'.esc_attr((int)$r['n']).'"
data-min="'.esc_attr(number_format((float)$r['min_use'], 1, '.', '')).'"
data-edge="'.esc_attr(number_format((float)$r['edge_pp'], 1, '.', '')).'"
data-ev="'.esc_attr(number_format((float)$r['ev_pct'], 1, '.', '')).'"
data-ia="'.esc_attr($r['pred_ia'] !== null ? number_format((float)$r['pred_ia'], 1, '.', '') : '').'"
data-delta="'.esc_attr($r['delta_ia'] !== null ? number_format((float)$r['delta_ia'], 1, '.', '') : '').'"
data-pal85="'.esc_attr($pal85).'"
data-pal1line="'.esc_attr($pal1LineTxt).'"
data-pal1conf="'.esc_attr($pal1ConfTxt).'"
data-hay="'.esc_attr($r['hay']).'"
>';
echo '<td><b>'.esc_html($r['match']).'</b><div class="tpnbaf-muted" style="font-size:11px">'.esc_html($r['time']).'</div></td>';
echo '<td>'.esc_html($r['player']).$bestBadge.'</td>';
echo '<td>'.esc_html($mktLabel).'</td>';
echo '<td><b>'.esc_html($r['side']).'</b><div class="tpnbaf-muted" style="font-size:11px">'.esc_html(implode(' • ', $details)).'</div></td>';
echo '<td>'.esc_html(number_format((float)$r['line'], 1, '.', '')).'</td>';
echo '<td><b>'.$oddTxt.'</b></td>';
echo '<td><b>'.(int)$r['conf'].'%</b></td>';
echo '<td><span class="'.esc_attr($r['lvl_cls']).'">'.esc_html($r['lvl_label']).'</span></td>';
echo '<td><span class="tpnbaf-chipgray">'.$avgTxt.'</span></td>';
echo '<td><span class="tpnbaf-chipgray">'.$nTxt.'</span></td>';
echo '<td><span class="tpnbaf-chipgray">'.$minTxt.'</span></td>';
echo '<td><span class="tpnbaf-chipgray">'.$iaTxt.'</span></td>';
echo '<td><span class="tpnbaf-chipgray">'.esc_html($dTxt).'</span></td>';
echo '<td>'.esc_html($r['pal1_txt']).$pal1Badge.'</td>';
echo '<td>'.$edgeTxt.'</td>';
echo '<td>'.$valTxt.'</td>';
echo '<td><span class="tpnbaf-chipblue">'.esc_html(number_format((float)$r['score'], 1, '.', '')).'</span></td>';
echo '<td>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
<button type="button" class="tpnbaf-btn tpnbaf-btn-add tpnbaf-add-main">➕ Main</button>
<button type="button" class="tpnbaf-btn tpnbaf-btn-p1 tpnbaf-add-p1" '.($p1Eligible ? '' : 'disabled').'>🛡️ P1</button>
</div>
<div class="tpnbaf-muted" style="font-size:11px;margin-top:4px">
P1 piloté par le filtre “Palier 1 Dash”
</div>
</td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</div>';
echo '<div class="tpnbaf-muted" style="font-size:11px;margin-top:10px">
* minutes avec astérisque = minutes issues du snapshot joueurs (si hist minutes indisponible).
</div>';
echo '</div>'; // card table
/* =========================
✅ TICKET BUILDERS (UI)
========================= */
echo '<div class="tpnbaf-builders">';
// MAIN
echo '<div class="tpnbaf-card">';
echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">';
echo '<div>';
echo '<div style="font-weight:950;font-size:14px">🎫 Ticket MAIN</div>';
echo '<div class="tpnbaf-muted" style="font-size:11px">1 sélection par match max • max '.$maxLegs.' legs</div>';
echo '</div>';
echo '<div style="display:flex;gap:8px;flex-wrap:wrap">';
echo '<button id="tpnbafAutoMain" class="tpnbaf-btn tpnbaf-btn-auto" type="button">🤖 Auto</button>';
echo '<button id="tpnbafCopyMain" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier ticket</button>';
echo '<button id="tpnbafClearMain" class="tpnbaf-btn tpnbaf-btn-clear" type="button">🗑️ Vider</button>';
echo '</div>';
echo '</div>';
echo '<div class="tpnbaf-kpis">';
echo '<div class="tpnbaf-kpi"><b>Sélections</b><div id="tpnbafKpiNMain">0</div></div>';
echo '<div class="tpnbaf-kpi"><b>Cote totale</b><div id="tpnbafKpiOddsMain">-</div></div>';
echo '<div class="tpnbaf-kpi"><b>Mise</b><div><input id="tpnbafStakeMain" type="number" min="0" step="0.5" value="10" style="width:90px;padding:6px;border:1px solid #e5e7eb;border-radius:10px"></div></div>';
echo '<div class="tpnbaf-kpi"><b>Retour</b><div id="tpnbafKpiRetMain">-</div></div>';
echo '</div>';
echo '<div id="tpnbafTicketBoxMain"></div>';
echo '<textarea id="tpnbafTicketTextMain" class="tpnbaf-textarea" readonly></textarea>';
echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px">';
echo '<div style="font-weight:900">Pourquoi (explications)</div>';
echo '<button id="tpnbafCopyWhyMain" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier pourquoi</button>';
echo '</div>';
echo '<textarea id="tpnbafWhyTextMain" class="tpnbaf-textarea small" readonly></textarea>';
echo '</div>';
// SAFE P1
echo '<div class="tpnbaf-card">';
echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">';
echo '<div>';
echo '<div id="tpnbafP1Title" style="font-weight:950;font-size:14px">🛡️ Ticket SAFE (Palier 1 Dash)</div>';
echo '<div id="tpnbafP1Hint" class="tpnbaf-muted" style="font-size:11px"></div>';
echo '</div>';
echo '<div style="display:flex;gap:8px;flex-wrap:wrap">';
echo '<button id="tpnbafAutoP1" class="tpnbaf-btn tpnbaf-btn-auto" type="button">🤖 Auto</button>';
echo '<button id="tpnbafCopyP1" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier ticket</button>';
echo '<button id="tpnbafClearP1" class="tpnbaf-btn tpnbaf-btn-clear" type="button">🗑️ Vider</button>';
echo '</div>';
echo '</div>';
echo '<div class="tpnbaf-kpis">';
echo '<div class="tpnbaf-kpi"><b>Sélections</b><div id="tpnbafKpiNP1">0</div></div>';
echo '<div class="tpnbaf-kpi"><b>Cote totale</b><div id="tpnbafKpiOddsP1">-</div></div>';
echo '<div class="tpnbaf-kpi"><b>Mise</b><div><input id="tpnbafStakeP1" type="number" min="0" step="0.5" value="10" style="width:90px;padding:6px;border:1px solid #e5e7eb;border-radius:10px"></div></div>';
echo '<div class="tpnbaf-kpi"><b>Retour</b><div id="tpnbafKpiRetP1">-</div></div>';
echo '</div>';
echo '<div id="tpnbafTicketBoxP1"></div>';
echo '<textarea id="tpnbafTicketTextP1" class="tpnbaf-textarea" readonly></textarea>';
echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px">';
echo '<div style="font-weight:900">Pourquoi (SAFE)</div>';
echo '<button id="tpnbafCopyWhyP1" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier pourquoi</button>';
echo '</div>';
echo '<textarea id="tpnbafWhyTextP1" class="tpnbaf-textarea small" readonly></textarea>';
echo '</div>';
echo '</div>'; // builders
echo '</div>'; // wrap
$html = ob_get_clean();
set_transient($ck, $html, $ttl);
return $html;
}
}
✅ Pourquoi choisir l’Analyseur Basket Mi-Temps & Quarts ?
- 🎯 Prédictions ciblées et sécurisées : seulement les suggestions avec une probabilité élevée de succès.
- 📈 Stratégie fine : idéal pour ceux qui veulent profiter des variations de cotes par période.
- 🧩 Lisibilité totale : vous savez qui domine quand, sans devoir analyser vous-même.
- 🔄 Actualisations régulières : nos analyses sont mises à jour avec les dernières données disponibles.