<?php
/**
* TP FUN — VERSION "funsite" (Shortcode + Mémoire + ROI combos + Admin) — FULL
* Shortcode : [tp_funsite_analyzer bankroll="500" stake_pct="1" bookmaker="4" retention="32"]
*
* ✅ FREEZE “cartes” : dès qu’il y a ≥ 3 matchs affichés, on fige la liste du jour (les matchs ne bougent plus)
* ✅ FREEZE Top3 & Cote10 : basés sur les cartes figées, et conservés même si matchs terminés
* ✅ ROI J-1 / J-7 / J-32 : calculé PAR COMBINÉ (Top3 = 1 bet, Cote10 = 1 bet)
* ✅ Cote10 : si total < 10 => on n’affiche rien (et on stocke vide)
* ✅ Mode admin : override statut + cote, save AJAX
* ✅ UI : Bilan ROI + Admin en accordéon FERMÉ par défaut
* ✅ FIX CSS : noms d’équipes toujours en BLANC (override thème)
*/
if (!defined('ABSPATH')) exit;
/* Polyfills temps (au cas où) */
if (!defined('MINUTE_IN_SECONDS')) define('MINUTE_IN_SECONDS', 60);
if (!defined('HOUR_IN_SECONDS')) define('HOUR_IN_SECONDS', 3600);
/* ───────────────── CONFIG (funsite) ───────────────── */
if (!defined('TP_FUNSITE_APISPORTS_KEY')) define('TP_FUNSITE_APISPORTS_KEY', '54064c5d72f6a5f59487a5309de88efe');
if (!defined('TP_FUNSITE_APISPORTS_HOST')) define('TP_FUNSITE_APISPORTS_HOST', 'v3.football.api-sports.io');
if (!defined('TP_FUNSITE_SEASON')) define('TP_FUNSITE_SEASON', '2025');
if (!defined('TP_FUNSITE_TZ')) define('TP_FUNSITE_TZ', 'Europe/Paris');
if (!defined('TP_FUNSITE_DEFAULT_BOOKMAKER')) define('TP_FUNSITE_DEFAULT_BOOKMAKER', 4);
if (!defined('TP_FUNSITE_DEFAULT_BANKROLL')) define('TP_FUNSITE_DEFAULT_BANKROLL', 500);
if (!defined('TP_FUNSITE_DEFAULT_STAKE_PCT')) define('TP_FUNSITE_DEFAULT_STAKE_PCT', 1);
if (!defined('TP_FUNSITE_DEFAULT_RETENTION')) define('TP_FUNSITE_DEFAULT_RETENTION', 32);
/* IMPORTANT : ligues */
if (!defined('TP_FUNSITE_LEAGUE_IDS')) define('TP_FUNSITE_LEAGUE_IDS',
'2, 3, 39, 40, 61, 62, 78, 79,
88, 94, 95, 106, 119, 135, 136, 140, 141, 144, 172, 179, 188, 197, 203, 207, 210, 218, 271, 283, 286, 307, 318, 332, 345, 373, 848'
);
if (!defined('TP_FUNSITE_CACHE_15MIN')) define('TP_FUNSITE_CACHE_15MIN', 15 * MINUTE_IN_SECONDS);
if (!defined('TP_FUNSITE_CACHE_12H')) define('TP_FUNSITE_CACHE_12H', 12 * HOUR_IN_SECONDS);
if (!defined('TP_FUNSITE_BTTTS_LIVE_VALIDATE')) define('TP_FUNSITE_BTTTS_LIVE_VALIDATE', true);
if (!defined('TP_FUNSITE_VICTORY_LIVE_VALIDATE')) define('TP_FUNSITE_VICTORY_LIVE_VALIDATE', true);
/* ───────────────── API helper (funsite_) ───────────────── */
if (!function_exists('funsite_api_get')) {
function funsite_api_get(string $endpoint, array $args = [], string $cache_key = null, int $ttl = 0) {
// Si ton helper global existe, on l’utilise (en gardant NOS caches "funsite_")
if (function_exists('tp_foot_api_get')) {
return tp_foot_api_get($endpoint, $args, $cache_key, $ttl);
}
if ($cache_key) {
$cached = get_transient($cache_key);
if ($cached !== false) return $cached;
}
$url = add_query_arg($args, 'https://' . TP_FUNSITE_APISPORTS_HOST . '/' . ltrim($endpoint, '/'));
$headers = ['x-apisports-key' => TP_FUNSITE_APISPORTS_KEY];
$res = wp_remote_get($url, ['headers'=>$headers,'timeout'=>20]);
if (is_wp_error($res)) return $res;
$code = (int) wp_remote_retrieve_response_code($res);
if ($code < 200 || $code >= 300) return new WP_Error('apisports_http', 'HTTP '.$code.' for '.$url);
$data = json_decode(wp_remote_retrieve_body($res), true);
if (json_last_error() !== JSON_ERROR_NONE) return new WP_Error('apisports_json', 'Invalid JSON for '.$url);
if ($cache_key && $ttl > 0) set_transient($cache_key, $data, $ttl);
return $data;
}
}
/* ───────────────── Time helpers (funsite_) ───────────────── */
if (!function_exists('funsite_tz')) {
function funsite_tz(): DateTimeZone { return new DateTimeZone(TP_FUNSITE_TZ); }
}
if (!function_exists('funsite_today_ymd')) {
function funsite_today_ymd(): string { return (new DateTime('now', funsite_tz()))->format('Ymd'); }
}
if (!function_exists('funsite_ymd_days_ago')) {
function funsite_ymd_days_ago(int $days): string {
$dt = new DateTime('now', funsite_tz());
if ($days > 0) $dt->modify('-'.$days.' day');
return $dt->format('Ymd');
}
}
/* ───────────────── Snapshots fixture (funsite_) ───────────────── */
if (!function_exists('funsite_status_short')) {
function funsite_status_short(array $m): string {
return strtoupper(trim((string)($m['fixture']['status']['short'] ?? '')));
}
}
if (!function_exists('funsite_snap_key')) {
function funsite_snap_key(int $fixture_id): string { return 'funsite_snap_'.$fixture_id; }
}
if (!function_exists('funsite_snap_load')) {
function funsite_snap_load(int $fixture_id) {
$snap = get_option(funsite_snap_key($fixture_id), null);
if (!is_array($snap)) return null;
// TTL 16h
$ttl = 16 * HOUR_IN_SECONDS;
$created = (int)($snap['created_at'] ?? 0);
if ($created && (time() - $created) > $ttl) {
delete_option(funsite_snap_key($fixture_id));
return null;
}
return $snap;
}
}
if (!function_exists('funsite_snap_save')) {
function funsite_snap_save(int $fixture_id, array $payload) {
$payload['created_at'] = time();
$k = funsite_snap_key($fixture_id);
if (get_option($k, null) === null) add_option($k, $payload, '', false);
else update_option($k, $payload, false);
}
}
/* ───────────────── Mémoire quotidienne (funsite_) ───────────────── */
if (!function_exists('funsite_day_key')) {
function funsite_day_key(string $ymd): string { return 'funsite_day_'.$ymd; }
}
if (!function_exists('funsite_day_index_key')) {
function funsite_day_index_key(): string { return 'funsite_day_index'; }
}
if (!function_exists('funsite_day_load')) {
function funsite_day_load(string $ymd) {
$v = get_option(funsite_day_key($ymd), null);
return is_array($v) ? $v : null;
}
}
if (!function_exists('funsite_day_save')) {
function funsite_day_save(string $ymd, array $payload) {
$payload['ymd'] = $ymd;
$payload['created_at'] = time();
$k = funsite_day_key($ymd);
if (get_option($k, null) === null) add_option($k, $payload, '', false);
else update_option($k, $payload, false);
$idx_k = funsite_day_index_key();
$idx = get_option($idx_k, []);
if (!is_array($idx)) $idx = [];
if (!in_array($ymd, $idx, true)) {
$idx[] = $ymd;
sort($idx);
update_option($idx_k, $idx, false);
}
}
}
if (!function_exists('funsite_cleanup_history')) {
function funsite_cleanup_history(int $keep_days) {
$keep_days = max(1, (int)$keep_days);
$idx_k = funsite_day_index_key();
$idx = get_option($idx_k, []);
if (!is_array($idx) || empty($idx)) return;
$tz = funsite_tz();
$cut = new DateTime('now', $tz);
$cut->modify('-'.$keep_days.' day');
$cut_ts = $cut->getTimestamp();
$new = [];
foreach ($idx as $ymd) {
$dt = DateTime::createFromFormat('Ymd', (string)$ymd, $tz);
$ts = $dt ? $dt->getTimestamp() : 0;
if ($ts && $ts >= $cut_ts) $new[] = $ymd;
else delete_option(funsite_day_key((string)$ymd));
}
update_option($idx_k, $new, false);
}
}
/* ───────────────── Fetch fixture by id (funsite_) ───────────────── */
if (!function_exists('funsite_get_fixture_by_id')) {
function funsite_get_fixture_by_id(int $fixture_id) {
if ($fixture_id <= 0) return null;
$ck = "funsite_fix_byid_{$fixture_id}";
$res = funsite_api_get('fixtures', ['id'=>$fixture_id], $ck, 6 * HOUR_IN_SECONDS);
if (is_wp_error($res)) return null;
$arr = $res['response'] ?? [];
return (is_array($arr) && !empty($arr)) ? ($arr[0] ?? null) : null;
}
}
if (!function_exists('funsite_is_void_status')) {
function funsite_is_void_status(string $short): bool {
$short = strtoupper(trim($short));
return in_array($short, ['PST','CANC','ABD','SUSP','INT','TBD','WO','AWD'], true);
}
}
/* ───────────────── Fixtures window (funsite_) ───────────────── */
if (!function_exists('funsite_get_matches_window')) {
function funsite_get_matches_window($league_ids, string $season) {
if (is_string($league_ids)) $league_ids = array_filter(array_map('trim', explode(',', $league_ids)));
elseif (!is_array($league_ids)) $league_ids = [$league_ids];
$tz = funsite_tz();
$window_start = (new DateTime('today 00:00:00', $tz))->getTimestamp();
$window_end = (new DateTime('tomorrow 10:00:00', $tz))->getTimestamp();
$dates = [
wp_date('Y-m-d', $window_start),
wp_date('Y-m-d', (new DateTime('tomorrow', $tz))->getTimestamp()),
];
$by_fixture = [];
foreach ($dates as $date) {
foreach ($league_ids as $lg) {
$ck = "funsite_fix_{$lg}_{$season}_{$date}";
$res = funsite_api_get('fixtures', ['league'=>$lg,'season'=>$season,'date'=>$date], $ck, TP_FUNSITE_CACHE_15MIN);
if (is_wp_error($res)) continue;
foreach (($res['response'] ?? []) as $m) {
$ts = isset($m['fixture']['timestamp']) ? (int)$m['fixture']['timestamp'] : strtotime($m['fixture']['date'] ?? '');
if (!$ts) continue;
if ($ts < $window_start || $ts >= $window_end) continue;
$fid = (int)($m['fixture']['id'] ?? 0);
if ($fid) $by_fixture[$fid] = $m;
}
}
}
$list = array_values($by_fixture);
usort($list, function($a, $b){
$tsa = $a['fixture']['timestamp'] ?? strtotime($a['fixture']['date'] ?? '0');
$tsb = $b['fixture']['timestamp'] ?? strtotime($b['fixture']['date'] ?? '0');
if ($tsa === $tsb) return ($a['fixture']['id'] ?? 0) <=> ($b['fixture']['id'] ?? 0);
return $tsa <=> $tsb;
});
return $list;
}
}
/* ───────────────── Stats & model (funsite_) ───────────────── */
if (!function_exists('funsite_get_team_stats')) {
function funsite_get_team_stats(int $team_id, string $season, int $league_id) {
$ck = "funsite_teamstats_{$league_id}_{$season}_{$team_id}";
$data = funsite_api_get('teams/statistics', ['season'=>$season,'team'=>$team_id,'league'=>$league_id], $ck, TP_FUNSITE_CACHE_12H);
return is_wp_error($data) ? $data : $data;
}
}
if (!function_exists('funsite_get_last5_goals')) {
function funsite_get_last5_goals(int $team_id, int $league_id, string $season) {
$ck = "funsite_last5_goals_{$league_id}_{$season}_{$team_id}";
$res = funsite_api_get('fixtures', ['team'=>$team_id,'league'=>$league_id,'season'=>$season,'last'=>5], $ck, TP_FUNSITE_CACHE_12H);
if (is_wp_error($res)) return ['goals_for'=>0,'goals_against'=>0,'matches_with_goals'=>0,'matches_conceded_goals'=>0,'response'=>[]];
$fixtures = $res['response'] ?? [];
$gf=$ga=$scored=$conceded=0;
foreach ($fixtures as $m) {
$isHome = (($m['teams']['home']['id'] ?? null) == $team_id);
$for = $isHome ? (int)($m['goals']['home'] ?? 0) : (int)($m['goals']['away'] ?? 0);
$agt = $isHome ? (int)($m['goals']['away'] ?? 0) : (int)($m['goals']['home'] ?? 0);
$gf += $for; $ga += $agt;
if ($for>0) $scored++;
if ($agt>0) $conceded++;
}
return ['goals_for'=>$gf,'goals_against'=>$ga,'matches_with_goals'=>$scored,'matches_conceded_goals'=>$conceded,'response'=>$fixtures];
}
}
if (!function_exists('funsite_get_last5_points')) {
function funsite_get_last5_points(int $team_id, int $league_id, string $season) {
$ck = "funsite_last5_pts_{$league_id}_{$season}_{$team_id}";
$res = funsite_api_get('fixtures', ['team'=>$team_id,'league'=>$league_id,'season'=>$season,'last'=>5], $ck, TP_FUNSITE_CACHE_12H);
if (is_wp_error($res)) return 0;
$pts = 0;
foreach (($res['response'] ?? []) as $m) {
$isHome = (($m['teams']['home']['id'] ?? null) == $team_id);
$homeW = (bool)($m['teams']['home']['winner'] ?? false);
$awayW = (bool)($m['teams']['away']['winner'] ?? false);
if ($isHome) $pts += $homeW ? 3 : ((!$awayW) ? 1 : 0);
else $pts += $awayW ? 3 : ((!$homeW) ? 1 : 0);
}
return $pts;
}
}
if (!function_exists('funsite_get_standings')) {
function funsite_get_standings(int $league_id, string $season) {
$ck = "funsite_standings_{$league_id}_{$season}";
$data = funsite_api_get('standings', ['league'=>$league_id,'season'=>$season], $ck, TP_FUNSITE_CACHE_12H);
return is_wp_error($data) ? [] : ($data['response'] ?? []);
}
}
if (!function_exists('funsite_get_team_category')) {
function funsite_get_team_category(int $team_id, array $standings) {
$rows = $standings[0]['league']['standings'][0] ?? [];
if (empty($rows)) return 'Non classé';
$n = count($rows);
$q = max(1, (int)ceil($n/4));
foreach ($rows as $row) {
if (($row['team']['id'] ?? null) == $team_id) {
$rank = (int)($row['rank'] ?? 0);
if ($rank <= $q) return '1';
if ($rank <= 2*$q) return '2';
if ($rank <= 3*$q) return '3';
return '4';
}
}
return 'Non classé';
}
}
if (!function_exists('funsite_cat_multiplier')) {
function funsite_cat_multiplier($self, $opp) {
$table = [
'1' => ['1'=>1.00,'2'=>1.10,'3'=>1.20,'4'=>1.30],
'2' => ['1'=>0.90,'2'=>1.00,'3'=>1.05,'4'=>1.10],
'3' => ['1'=>0.80,'2'=>0.95,'3'=>1.00,'4'=>1.05],
'4' => ['1'=>0.70,'2'=>0.90,'3'=>0.95,'4'=>1.00],
];
return $table[$self][$opp] ?? 1.00;
}
}
if (!function_exists('funsite_margin_over_threshold')) {
function funsite_margin_over_threshold(float $diff, bool $is_home_winner): float {
return $is_home_winner ? ($diff - 1.50) : ((-1.0 * $diff) - 1.60);
}
}
if (!function_exists('funsite_conf_from_margin')) {
function funsite_conf_from_margin(float $m, float $cap): int {
if ($m < 0.00) return 0;
if ($m >= $cap) return 95;
if ($m >= 1.00) return 90;
if ($m >= 0.80) return 88;
if ($m >= 0.60) return 85;
if ($m >= 0.40) return 81;
if ($m >= 0.20) return 75;
return 60;
}
}
/* ───────────────── Build prediction (funsite_) ───────────────── */
if (!function_exists('funsite_build_stats_and_predictions')) {
function funsite_build_stats_and_predictions(array $match, string $season) {
$league_id = (int)($match['league']['id'] ?? 0);
$team1_id = (int)($match['teams']['home']['id'] ?? 0);
$team2_id = (int)($match['teams']['away']['id'] ?? 0);
$team1_name = (string)($match['teams']['home']['name'] ?? '');
$team2_name = (string)($match['teams']['away']['name'] ?? '');
if (!$league_id || !$team1_id || !$team2_id) return [];
$t1 = funsite_get_team_stats($team1_id, $season, $league_id);
$t2 = funsite_get_team_stats($team2_id, $season, $league_id);
$stand = funsite_get_standings($league_id, $season);
if (is_wp_error($t1) || empty($t1['response'])) return [];
if (is_wp_error($t2) || empty($t2['response'])) return [];
$cat1 = funsite_get_team_category($team1_id, $stand);
$cat2 = funsite_get_team_category($team2_id, $stand);
$p1 = (int)($t1['response']['fixtures']['played']['total'] ?? 0);
$p2 = (int)($t2['response']['fixtures']['played']['total'] ?? 0);
$hp1 = (int)($t1['response']['fixtures']['played']['home'] ?? 0);
$ap2 = (int)($t2['response']['fixtures']['played']['away'] ?? 0);
$hw1 = (int)($t1['response']['fixtures']['wins']['home'] ?? 0);
$hd1 = (int)($t1['response']['fixtures']['draws']['home'] ?? 0);
$aw2 = (int)($t2['response']['fixtures']['wins']['away'] ?? 0);
$ad2 = (int)($t2['response']['fixtures']['draws']['away'] ?? 0);
$ppg1_home = $hp1 ? (($hw1*3 + $hd1)/$hp1) : 0;
$ppg2_away = $ap2 ? (($aw2*3 + $ad2)/$ap2) : 0;
if ($ppg1_home == 0 || $ppg2_away == 0) return [];
$l5_1g = funsite_get_last5_goals($team1_id, $league_id, $season);
$l5_2g = funsite_get_last5_goals($team2_id, $league_id, $season);
$l5_1p = funsite_get_last5_points($team1_id, $league_id, $season)/5;
$l5_2p = funsite_get_last5_points($team2_id, $league_id, $season)/5;
$g1_season = $p1 ? ((int)($t1['response']['goals']['for']['total']['total'] ?? 0) / $p1) : 0;
$g2_season = $p2 ? ((int)($t2['response']['goals']['for']['total']['total'] ?? 0) / $p2) : 0;
$g1_home = $hp1 ? ((int)($t1['response']['goals']['for']['total']['home'] ?? 0) / $hp1) : 0;
$g2_away = $ap2 ? ((int)($t2['response']['goals']['for']['total']['away'] ?? 0) / $ap2) : 0;
$ga1_season= $p1 ? ((int)($t1['response']['goals']['against']['total']['total'] ?? 0) / $p1) : 0;
$ga2_season= $p2 ? ((int)($t2['response']['goals']['against']['total']['total'] ?? 0) / $p2) : 0;
$ga1_home = $hp1 ? ((int)($t1['response']['goals']['against']['total']['home'] ?? 0) / $hp1) : 0;
$ga2_away = $ap2 ? ((int)($t2['response']['goals']['against']['total']['away'] ?? 0) / $ap2) : 0;
$t1_total = ((($g1_season + $g1_home + ($l5_1g['goals_for']/5))/3)
+ (($ga2_season + $ga2_away + ($l5_2g['goals_against']/5))/3)) / 2;
$t2_total = ((($g2_season + $g2_away + ($l5_2g['goals_for']/5))/3)
+ (($ga1_season + $ga1_home + ($l5_1g['goals_against']/5))/3)) / 2;
$t1_adj = $t1_total * funsite_cat_multiplier($cat1, $cat2);
$t2_adj = $t2_total * funsite_cat_multiplier($cat2, $cat1);
// BTTS paliers
$btts_eval = (function($t1, $t2, $l5_1g, $l5_2g, $t1_total, $t2_total){
$paliers = [
['conf'=>91,'min_goals'=>1.55,'sc'=>[90,90,80],'conc'=>[65,65,60]],
['conf'=>90,'min_goals'=>1.50,'sc'=>[90,90,80],'conc'=>[65,65,60]],
['conf'=>89,'min_goals'=>1.45,'sc'=>[84,88,80],'conc'=>[65,65,60]],
['conf'=>88,'min_goals'=>1.40,'sc'=>[78,85,80],'conc'=>[65,65,60]],
['conf'=>87,'min_goals'=>1.30,'sc'=>[78,82,80],'conc'=>[63,63,60]],
['conf'=>86,'min_goals'=>1.20,'sc'=>[75,80,80],'conc'=>[60,60,60]],
];
$hp1 = max(1,(int)($t1['response']['fixtures']['played']['home'] ?? 0));
$ap2 = max(1,(int)($t2['response']['fixtures']['played']['away'] ?? 0));
$p1t = max(1,(int)($t1['response']['fixtures']['played']['total'] ?? 0));
$p2t = max(1,(int)($t2['response']['fixtures']['played']['total'] ?? 0));
$failed_home = (int)($t1['response']['failed_to_score']['home'] ?? 0);
$failed_away = (int)($t2['response']['failed_to_score']['away'] ?? 0);
$clean_home = (int)($t1['response']['clean_sheet']['home'] ?? 0);
$clean_away = (int)($t2['response']['clean_sheet']['away'] ?? 0);
$clean_total_home = (int)($t1['response']['clean_sheet']['total'] ?? 0);
$clean_total_away = (int)($t2['response']['clean_sheet']['total'] ?? 0);
$pct_home_sc_saison = round((($p1t - $failed_home) / $p1t) * 100, 0);
$pct_away_sc_saison = round((($p2t - $failed_away) / $p2t) * 100, 0);
$pct_home_sc_dom = round((($hp1 - $failed_home) / $hp1) * 100, 0);
$pct_away_sc_ext = round((($ap2 - $failed_away) / $ap2) * 100, 0);
$pct_home_conc_saison = round((($p1t - $clean_total_home) / $p1t) * 100, 0);
$pct_away_conc_saison = round((($p2t - $clean_total_away) / $p2t) * 100, 0);
$pct_home_conc_dom = round((($hp1 - $clean_home) / $hp1) * 100, 0);
$pct_away_conc_ext = round((($ap2 - $clean_away) / $ap2) * 100, 0);
$cnt1 = max(1, count($l5_1g['response'] ?? []));
$cnt2 = max(1, count($l5_2g['response'] ?? []));
$pct_l5_home_sc = round(($l5_1g['matches_with_goals'] / $cnt1) * 100, 0);
$pct_l5_away_sc = round(($l5_2g['matches_with_goals'] / $cnt2) * 100, 0);
$pct_l5_home_conc = round(($l5_1g['matches_conceded_goals'] / $cnt1) * 100, 0);
$pct_l5_away_conc = round(($l5_2g['matches_conceded_goals'] / $cnt2) * 100, 0);
foreach ($paliers as $p) {
$mg = $p['min_goals'];
if ($t1_total >= $mg && $t2_total >= $mg) {
$ok_sc =
($pct_home_sc_saison >= $p['sc'][0] && $pct_away_sc_saison >= $p['sc'][0]) &&
($pct_home_sc_dom >= $p['sc'][1] && $pct_away_sc_ext >= $p['sc'][1]) &&
($pct_l5_home_sc >= $p['sc'][2] && $pct_l5_away_sc >= $p['sc'][2]);
$ok_conc =
($pct_home_conc_saison >= $p['conc'][0] && $pct_away_conc_saison >= $p['conc'][0]) &&
($pct_home_conc_dom >= $p['conc'][1] && $pct_away_conc_ext >= $p['conc'][1]) &&
($pct_l5_home_conc >= $p['conc'][2] && $pct_l5_away_conc >= $p['conc'][2]);
if ($ok_sc && $ok_conc) return ['btts'=>'OUI','conf'=>$p['conf']];
}
}
return ['btts'=>'NON','conf'=>70];
})($t1,$t2,$l5_1g,$l5_2g,$t1_total,$t2_total);
$btts = $btts_eval['btts'];
$conf_btts = (int)$btts_eval['conf'];
// Victoire
$home_adv = ($ppg1_home + ($l5_1p ?? 0))/2;
$away_dis = ($ppg2_away + ($l5_2p ?? 0))/2;
$diff = $home_adv - $away_dis;
$victory = '';
if ($diff >= 1.5 && $t1_adj > $t2_adj) $victory = $team1_name;
elseif ($diff <= -1.6 && $t2_adj > $t1_adj) $victory = $team2_name;
$vic_conf = 0;
if ($victory !== '') {
$is_home_w = (strcasecmp($victory, $team1_name) === 0);
$margin = funsite_margin_over_threshold($diff, $is_home_w);
$cap = $is_home_w ? 1.40 : 1.50;
$base = funsite_conf_from_margin($margin, $cap);
if ($base !== 0) {
$adj_edge = $is_home_w ? max(0.0, $t1_adj - $t2_adj) : max(0.0, $t2_adj - $t1_adj);
$edge_bonus = ($adj_edge >= 0.4) ? 5 : (($adj_edge >= 0.25) ? 3 : (($adj_edge >= 0.1) ? 1 : 0));
$c_self = (int)(($is_home_w ? $cat1 : $cat2) ?: 0);
$c_opp = (int)(($is_home_w ? $cat2 : $cat1) ?: 0);
$cat_bonus = ($c_self && $c_opp && ($c_opp - $c_self) >= 2) ? 4 : (($c_opp - $c_self) === 1 ? 2 : 0);
$venue_bonus = ($is_home_w && ($home_adv > $away_dis)) ? 2 : 0;
$safety_malus = (abs($home_adv - $away_dis) < 0.10) ? 3 : 0;
$vic_conf = max(60, min(95, $base + $edge_bonus + $cat_bonus + $venue_bonus - $safety_malus));
}
}
return [
'teams' => ['home'=>$team1_name,'away'=>$team2_name],
'league_id' => $league_id,
'pred' => [
'diff' => $diff,
'btts' => ['conseil'=>$btts,'confiance'=>$conf_btts],
'victoire' => $victory,
'victoire_conf' => (int)$vic_conf,
],
];
}
}
/* ───────────────── Verdict (funsite_) ───────────────── */
if (!function_exists('funsite_eval_verdict')) {
function funsite_eval_verdict(array $m, bool $has_btts, bool $has_victory, string $vict_name) : array {
$stat = strtoupper($m['fixture']['status']['short'] ?? '');
$is_final = in_array($stat, ['FT','AET','PEN','AWD','WO'], true);
$gh = isset($m['goals']['home']) ? (int)$m['goals']['home'] : null;
$ga = isset($m['goals']['away']) ? (int)$m['goals']['away'] : null;
$score_txt = ($gh!==null && $ga!==null) ? ($gh.' - '.$ga) : '—';
$live_statuses = ['1H','2H','HT','ET','LIVE','BT','INT','P'];
$is_live = in_array($stat, $live_statuses, true);
if (!$is_final || $gh===null || $ga===null) {
if ($has_btts && !$has_victory && TP_FUNSITE_BTTTS_LIVE_VALIDATE && $is_live && $gh>0 && $ga>0) {
return ['state'=>'won','score'=>$score_txt,'reason'=>'BTTS (live)'];
}
if ($has_victory && !$has_btts && TP_FUNSITE_VICTORY_LIVE_VALIDATE && $is_live) {
$home = $m['teams']['home']['name'] ?? '';
$away = $m['teams']['away']['name'] ?? '';
$diff = $gh - $ga;
$lead = null;
if (strcasecmp($vict_name, $home) === 0) $lead = $diff;
elseif (strcasecmp($vict_name, $away) === 0) $lead = -$diff;
if ($lead !== null && $lead >= 2) return ['state'=>'won','score'=>$score_txt,'reason'=>'Victoire (live +2)'];
}
return ['state'=>'pending','score'=>$score_txt,'reason'=>'Match en cours/à venir'];
}
if (funsite_is_void_status($stat)) {
return ['state'=>'void','score'=>$score_txt,'reason'=>'Annulé/Interrompu'];
}
$btts_ok = ($gh > 0 && $ga > 0);
$home = $m['teams']['home']['name'] ?? '';
$away = $m['teams']['away']['name'] ?? '';
$homeW = (bool)($m['teams']['home']['winner'] ?? false);
$awayW = (bool)($m['teams']['away']['winner'] ?? false);
$vict_ok = true;
if ($has_victory && $vict_name !== '') {
if (strcasecmp($vict_name, $home) === 0) $vict_ok = $homeW;
elseif (strcasecmp($vict_name, $away) === 0) $vict_ok = $awayW;
else $vict_ok = false;
}
$won = false;
if ($has_btts && $has_victory) $won = ($btts_ok && $vict_ok);
elseif ($has_victory && !$has_btts) $won = $vict_ok;
elseif ($has_btts && !$has_victory) $won = $btts_ok;
return [
'state' => $won ? 'won' : 'lost',
'score' => $score_txt,
'reason' => ($has_btts && $has_victory ? 'BTTS+Win' : ($has_victory ? 'Win' : 'BTTS')),
];
}
}
/* ───────────────── Odds bundle (funsite_) + FALLBACK ───────────────── */
if (!function_exists('funsite_get_odds_bundle')) {
function funsite_get_odds_bundle(int $fixture_id, string $home_name, string $away_name, int $bookmaker_id) {
$bundle_key = "funsite_odds_bundle_FT_v3_{$fixture_id}_{$bookmaker_id}";
if (($cached = get_transient($bundle_key)) !== false) return $cached;
$raw_key = "funsite_odds_raw_{$fixture_id}_{$bookmaker_id}";
$data = funsite_api_get('odds', ['fixture'=>$fixture_id,'bookmaker'=>$bookmaker_id], $raw_key, 10 * MINUTE_IN_SECONDS);
if (is_wp_error($data)) return [];
$is_half_time = function(string $s): bool {
$s = strtolower($s);
return (strpos($s,'1st half')!==false || strpos($s,'first half')!==false ||
strpos($s,'2nd half')!==false || strpos($s,'second half')!==false ||
strpos($s,'half time')!==false || strpos($s,'halftime')!==false ||
preg_match('/\b(ht|1st|2nd)\b/i', $s) === 1 ||
strpos($s,'period')!==false || strpos($s,'half')!==false);
};
$is_full_time_1x2 = function(string $name) use ($is_half_time): bool {
$n = strtolower($name);
if ($is_half_time($n)) return false;
return (strpos($n,'match winner')!==false || strpos($n,'full time result')!==false || strpos($n,'1x2')!==false ||
(strpos($n,'result')!==false && strpos($n,'both')===false && strpos($n,'btts')===false));
};
$is_full_time_btts = function(string $name) use ($is_half_time): bool {
$n = strtolower($name);
if ($is_half_time($n)) return false;
return (strpos($n,'both teams')!==false || strpos($n,'btts')!==false);
};
$is_full_time_result_btts = function(string $name) use ($is_half_time, $is_full_time_1x2): bool {
$n = strtolower($name);
if ($is_half_time($n)) return false;
return (( $is_full_time_1x2($n) || strpos($n,'winner')!==false || strpos($n,'result')!==false || strpos($n,'win')!==false )
&& (strpos($n,'both teams')!==false || strpos($n,'btts')!==false));
};
$norm = function($s) { return strtolower(trim(preg_replace('/\s+/', ' ', (string)$s))); };
$out = ['btts_yes'=>null,'win_home'=>null,'win_away'=>null,'win_btts_home'=>null,'win_btts_away'=>null];
// 1) bookmaker demandé
foreach (($data['response'] ?? []) as $row) {
foreach (($row['bookmakers'] ?? []) as $bm) {
if ((int)$bm['id'] !== (int)$bookmaker_id) continue;
foreach (($bm['bets'] ?? []) as $bet) {
$bet_name = (string)($bet['name'] ?? '');
if ($is_full_time_1x2($bet_name)) {
foreach (($bet['values'] ?? []) as $v) {
$val_n = $norm($v['value'] ?? ''); $odd = $v['odd'] ?? null;
if ($odd === null) continue;
if ($val_n==='home' || $val_n==='1' || strpos($val_n,'home team')!==false ||
($home_name && stripos($v['value'],$home_name)!==false)) $out['win_home'] = $odd;
if ($val_n==='away' || $val_n==='2' || strpos($val_n,'away team')!==false ||
($away_name && stripos($v['value'],$away_name)!==false)) $out['win_away'] = $odd;
}
}
if ($is_full_time_btts($bet_name)) {
foreach (($bet['values'] ?? []) as $v) {
if ($norm($v['value'] ?? '') === 'yes') $out['btts_yes'] = $v['odd'] ?? null;
}
}
if ($is_full_time_result_btts($bet_name)) {
foreach (($bet['values'] ?? []) as $v) {
$val = $norm($v['value'] ?? ''); $odd = $v['odd'] ?? null;
if ($odd === null) continue;
if (strpos($val, 'yes') === false) continue;
$is_home = (strpos($val,'home')!==false || preg_match('#(^|[ /])1($|[ /])#', $val) || ($home_name && stripos($val, $home_name) !== false));
$is_away = (strpos($val,'away')!==false || preg_match('#(^|[ /])2($|[ /])#', $val) || ($away_name && stripos($val, $away_name) !== false));
if ($is_home) $out['win_btts_home'] = $odd;
elseif ($is_away) $out['win_btts_away'] = $odd;
}
}
}
}
}
// 2) FALLBACK : si rien trouvé, on tente tous les bookmakers présents
$empty = ($out['btts_yes']===null && $out['win_home']===null && $out['win_away']===null && $out['win_btts_home']===null && $out['win_btts_away']===null);
if ($empty) {
foreach (($data['response'] ?? []) as $row) {
foreach (($row['bookmakers'] ?? []) as $bm) {
foreach (($bm['bets'] ?? []) as $bet) {
$bet_name = (string)($bet['name'] ?? '');
if ($is_full_time_1x2($bet_name)) {
foreach (($bet['values'] ?? []) as $v) {
$val_n = $norm($v['value'] ?? ''); $odd = $v['odd'] ?? null;
if ($odd === null) continue;
if ($out['win_home']===null && ($val_n==='home' || $val_n==='1' || ($home_name && stripos($v['value'],$home_name)!==false))) $out['win_home'] = $odd;
if ($out['win_away']===null && ($val_n==='away' || $val_n==='2' || ($away_name && stripos($v['value'],$away_name)!==false))) $out['win_away'] = $odd;
}
}
if ($out['btts_yes']===null && $is_full_time_btts($bet_name)) {
foreach (($bet['values'] ?? []) as $v) {
if ($norm($v['value'] ?? '') === 'yes') $out['btts_yes'] = $v['odd'] ?? null;
}
}
if (($out['win_btts_home']===null || $out['win_btts_away']===null) && $is_full_time_result_btts($bet_name)) {
foreach (($bet['values'] ?? []) as $v) {
$val = $norm($v['value'] ?? ''); $odd = $v['odd'] ?? null;
if ($odd === null) continue;
if (strpos($val, 'yes') === false) continue;
$is_home = (strpos($val,'home')!==false || preg_match('#(^|[ /])1($|[ /])#', $val) || ($home_name && stripos($val, $home_name) !== false));
$is_away = (strpos($val,'away')!==false || preg_match('#(^|[ /])2($|[ /])#', $val) || ($away_name && stripos($val, $away_name) !== false));
if ($is_home && $out['win_btts_home']===null) $out['win_btts_home'] = $odd;
if ($is_away && $out['win_btts_away']===null) $out['win_btts_away'] = $odd;
}
}
}
}
}
}
set_transient($bundle_key, $out, 10 * MINUTE_IN_SECONDS);
return $out;
}
}
/* ───────────────── CSS (funsite_) ───────────────── */
if (!function_exists('funsite_render_styles')) {
function funsite_render_styles() { ?>
<style>
.funsite-wrap{max-width:1150px;margin:0 auto;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
.funsite-grid{display:grid;grid-template-columns:1fr;gap:16px}
@media(min-width:980px){.funsite-grid{grid-template-columns:1fr 1fr}}
.funsite-card{background:linear-gradient(180deg,#0b1830,#071225);color:#fff;border:1px solid rgba(255,255,255,.08);
padding:18px;border-radius:16px;box-shadow:0 12px 30px rgba(0,0,0,.25)}
.funsite-row{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
.funsite-teams{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.funsite-logo{width:28px;height:28px;border-radius:6px;object-fit:contain;background:rgba(255,255,255,.06);padding:3px}
.funsite-title{margin:0;font-size:15px;font-weight:900;letter-spacing:.2px}
.funsite-meta{margin:6px 0 0;color:rgba(255,255,255,.72);font-size:12.5px}
.funsite-tag{display:inline-flex;align-items:center;gap:6px;background:rgba(0,195,255,.12);border:1px solid rgba(0,195,255,.30);
color:#bfefff;border-radius:999px;padding:6px 12px;font-size:12px;margin-top:10px;font-weight:800}
.funsite-odd{margin:10px 0 0;font-size:14px}
.funsite-odd strong{color:#e7f7ff}
.funsite-badges{display:flex;gap:8px;flex-wrap:wrap}
.funsite-badge{display:inline-flex;align-items:center;gap:8px;border-radius:999px;padding:7px 12px;font-size:12px;border:1px solid transparent;font-weight:900}
.funsite-badge--won{background:rgba(22,163,74,.16);border-color:rgba(22,163,74,.45);color:#d8ffe6}
.funsite-badge--lost{background:rgba(220,38,38,.16);border-color:rgba(220,38,38,.45);color:#ffe0e0}
.funsite-badge--pend{background:rgba(59,130,246,.14);border-color:rgba(59,130,246,.35);color:#dbeafe}
.funsite-badge--void{background:rgba(148,163,184,.16);border-color:rgba(148,163,184,.35);color:#f1f5f9}
.funsite-block{margin-top:18px;border-radius:16px;padding:16px;border:1px solid rgba(255,255,255,.10);box-shadow:0 10px 24px rgba(0,0,0,.22)}
.funsite-block--gold{background:linear-gradient(135deg,#ffd86b,#ffb703);color:#0a0a0a}
.funsite-block--blue{background:linear-gradient(135deg,#0b2a55,#0a1b3a);color:#fff}
.funsite-block h3{margin:0 0 8px;font-size:17px;font-weight:950}
.funsite-block p{margin:0 0 10px;font-size:14px;opacity:.92}
.funsite-block ul{margin:0;padding-left:18px}
.funsite-block li{margin:6px 0;font-size:14px}
.funsite-pill{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:5px 12px;font-size:12px;border:1px solid transparent;font-weight:950}
.funsite-pill--pos{background:rgba(22,163,74,.16);border-color:rgba(22,163,74,.45);color:#d8ffe6}
.funsite-pill--neg{background:rgba(220,38,38,.16);border-color:rgba(220,38,38,.45);color:#ffe0e0}
.funsite-pill--neu{background:rgba(59,130,246,.14);border-color:rgba(59,130,246,.35);color:#dbeafe}
.funsite-table{width:100%;border-collapse:collapse;margin-top:8px;font-size:13.5px}
.funsite-table th,.funsite-table td{padding:10px;border-bottom:1px solid rgba(255,255,255,.08);text-align:left;vertical-align:middle}
.funsite-table th{color:#bfefff;font-weight:950}
.funsite-admin{margin-top:0;background:transparent;border:0;border-radius:0;padding:0;color:#fff;box-shadow:none}
.funsite-admin h3{margin:0 0 10px;font-size:16px;color:#ffd86b}
.funsite-admin .funsite-controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.funsite-admin select,.funsite-admin input{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);color:#fff;
border-radius:10px;padding:8px 10px;font-size:13px;outline:none}
.funsite-btn{background:#00c3ff;color:#041019;border:0;border-radius:12px;padding:9px 12px;font-weight:950;cursor:pointer}
.funsite-btn:hover{filter:brightness(1.05)}
.funsite-save{white-space:nowrap}
.funsite-note{margin-top:10px;color:rgba(255,255,255,.75);font-size:12.5px}
.funsite-toast{margin-top:10px;padding:10px 12px;border-radius:12px;display:none}
.funsite-toast.ok{display:block;background:rgba(22,163,74,.14);border:1px solid rgba(22,163,74,.35)}
.funsite-toast.err{display:block;background:rgba(220,38,38,.14);border:1px solid rgba(220,38,38,.35)}
.funsite-badge-inline{font-size:12px;opacity:.9}
/* Accordéons (fermés par défaut) */
.funsite-fold{margin-top:16px;border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,.10);
background:linear-gradient(180deg,#091428,#071225);box-shadow:0 12px 30px rgba(0,0,0,.25)}
.funsite-fold > summary{cursor:pointer;list-style:none;padding:14px 16px;font-weight:950;color:#7de3ff}
.funsite-fold > summary::-webkit-details-marker{display:none}
.funsite-fold[open] > summary{border-bottom:1px solid rgba(255,255,255,.08)}
.funsite-fold .funsite-fold-inner{padding:16px}
/* FIX OVERRIDE THÈME (titres équipes en noir) */
.funsite-card .funsite-title{color:#fff !important;}
.funsite-card .funsite-meta{color:rgba(255,255,255,.72) !important;}
.funsite-card .funsite-odd{color:#fff !important;}
.funsite-card .funsite-tag{color:#bfefff !important;}
</style>
<?php }
}
/* ───────────────── Render one card (funsite_) ───────────────── */
if (!function_exists('funsite_display_card')) {
function funsite_display_card(array $m, string $label, array $show_lines, array $verdict) {
$utc = $m['fixture']['date'] ?? null;
if (!$utc) return;
$dt = new DateTime($utc, new DateTimeZone('UTC'));
$dt->setTimezone(funsite_tz());
$when = $dt->format('d/m/Y, H:i');
$home = $m['teams']['home']['name'] ?? '';
$away = $m['teams']['away']['name'] ?? '';
$hlogo= $m['teams']['home']['logo'] ?? '';
$alogo= $m['teams']['away']['logo'] ?? '';
$league= $m['league']['name'] ?? '';
$country=$m['league']['country'] ?? '';
$cls = 'funsite-badge funsite-badge--pend';
$icon = '⏳';
if (($verdict['state'] ?? '') === 'won') { $cls='funsite-badge funsite-badge--won'; $icon='✅'; }
elseif (($verdict['state'] ?? '') === 'lost') { $cls='funsite-badge funsite-badge--lost'; $icon='❌'; }
elseif (($verdict['state'] ?? '') === 'void') { $cls='funsite-badge funsite-badge--void'; $icon='↩️'; }
echo '<div class="funsite-card">';
echo '<div class="funsite-row">';
echo '<div class="funsite-teams">';
if ($hlogo) echo '<img class="funsite-logo" src="'.esc_url($hlogo).'" alt="">';
echo '<h4 class="funsite-title">'.esc_html($home).'</h4>';
echo '<span style="opacity:.9;font-weight:950">–</span>';
if ($alogo) echo '<img class="funsite-logo" src="'.esc_url($alogo).'" alt="">';
echo '<h4 class="funsite-title">'.esc_html($away).'</h4>';
echo '</div>';
echo '<div class="funsite-badges">';
echo '<span class="'.$cls.'"><strong>'.$icon.' Score</strong> : '.esc_html($verdict['score'] ?? '—').'</span>';
if (!empty($verdict['reason']) && stripos((string)$verdict['reason'], 'live') !== false) {
echo '<span class="funsite-badge funsite-badge--won"><strong>LIVE</strong> payé</span>';
}
echo '</div>';
echo '</div>';
echo '<div class="funsite-meta">📅 '.esc_html($when).' • 🏆 '.esc_html($league).($country ? ' — '.esc_html($country) : '').'</div>';
echo '<div class="funsite-tag">🎯 '.esc_html($label).'</div>';
foreach ($show_lines as $line) {
if (!$line) continue;
[$caption, $odd] = $line;
if ($odd === null || $odd === '') continue;
$odd_fmt = number_format((float)$odd, 2, ',', ' ');
echo '<div class="funsite-odd"><strong>'.$caption.'</strong> : '.esc_html($odd_fmt).'</div>';
}
echo '</div>';
}
}
/* ───────────────── Pick evaluation with admin override (funsite_) ───────────────── */
if (!function_exists('funsite_pick_eval_state')) {
function funsite_pick_eval_state(array $pick): array {
$manual = strtolower(trim((string)($pick['manual_status'] ?? 'auto')));
if ($manual && $manual !== 'auto') {
if ($manual === 'win') return ['state'=>'won','score'=>'—','reason'=>'Admin'];
if ($manual === 'loss') return ['state'=>'lost','score'=>'—','reason'=>'Admin'];
if ($manual === 'void') return ['state'=>'void','score'=>'—','reason'=>'Admin'];
if ($manual === 'pending') return ['state'=>'pending','score'=>'—','reason'=>'Admin'];
}
$fid = (int)($pick['fixture_id'] ?? 0);
if ($fid <= 0) return ['state'=>'pending','score'=>'—','reason'=>'ID manquant'];
$m = funsite_get_fixture_by_id($fid);
if (!is_array($m)) return ['state'=>'pending','score'=>'—','reason'=>'Fixture indisponible'];
$flags = (array)($pick['flags'] ?? []);
$has_btts = !empty($flags['has_btts']);
$has_vic = !empty($flags['has_victory']);
$vict_name= (string)($flags['vict_name'] ?? '');
return funsite_eval_verdict($m, (bool)$has_btts, (bool)$has_vic, $vict_name);
}
}
/* ───────────────── Combo evaluation & ROI (funsite_) ─────────────────
- Void sur une sélection : l’odd de cette sélection est neutralisée (=1),
et le combiné peut quand même gagner.
- Si tout est void => combiné void (remboursé)
- Si au moins 1 pending => combiné pending
*/
if (!function_exists('funsite_combo_result_and_odd')) {
function funsite_combo_result_and_odd(array $picks): array {
$hasPending = false;
$hasLoss = false;
$anyNonVoid = false;
$odd = 1.0;
foreach ($picks as $p) {
$state = (string)(funsite_pick_eval_state($p)['state'] ?? 'pending');
if ($state === 'pending') { $hasPending = true; continue; }
if ($state === 'lost') { $hasLoss = true; continue; }
if ($state === 'void') { continue; }
$anyNonVoid = true;
$useOdd = 0.0;
if (isset($p['manual_odd']) && $p['manual_odd'] !== '' && is_numeric($p['manual_odd'])) $useOdd = (float)$p['manual_odd'];
else $useOdd = (float)($p['odd'] ?? 0);
if ($useOdd > 1.01) $odd *= $useOdd;
}
if ($hasPending) return ['state'=>'pending','odd'=>1.0];
if ($hasLoss) return ['state'=>'lost','odd'=>max(1.0,$odd)];
if (!$anyNonVoid) return ['state'=>'void','odd'=>1.0];
return ['state'=>'won','odd'=>max(1.0,$odd)];
}
}
if (!function_exists('funsite_calc_roi_period')) {
function funsite_calc_roi_period(array $days, float $bankroll, float $stake_pct): array {
$stake = round($bankroll * ($stake_pct/100), 2);
$profit = 0.0;
$stake_used = 0.0;
$w=$l=$v=$p=0;
foreach ($days as $day) {
if (!is_array($day)) continue;
// Top3 combiné (1 bet)
if (!empty($day['top3']) && is_array($day['top3'])) {
$r = funsite_combo_result_and_odd($day['top3']);
if ($r['state']==='pending') $p++;
elseif ($r['state']==='void') $v++;
else {
$stake_used += $stake;
if ($r['state']==='won') { $w++; $profit += $stake * ((float)$r['odd'] - 1.0); }
else { $l++; $profit -= $stake; }
}
}
// Cote10 combiné (1 bet) — uniquement si validé >=10
if (!empty($day['combo10']) && is_array($day['combo10'] ?? null) && !empty($day['combo_total']) && (float)$day['combo_total'] >= 10.0) {
$r = funsite_combo_result_and_odd($day['combo10']);
if ($r['state']==='pending') $p++;
elseif ($r['state']==='void') $v++;
else {
$stake_used += $stake;
if ($r['state']==='won') { $w++; $profit += $stake * ((float)$r['odd'] - 1.0); }
else { $l++; $profit -= $stake; }
}
}
}
$roi = ($stake_used > 0) ? round(($profit / $stake_used) * 100, 2) : 0.0;
return [
'stake_unit'=>$stake,
'stake_used'=>$stake_used,
'profit'=>$profit,
'roi'=>$roi,
'w'=>$w,'l'=>$l,'v'=>$v,'p'=>$p
];
}
}
/* ───────────────── Admin AJAX (funsite_) ───────────────── */
if (!function_exists('funsite_admin_ajax_save')) {
function funsite_admin_ajax_save() {
if (!current_user_can('manage_options')) wp_send_json_error(['msg'=>'forbidden'], 403);
$nonce = (string)($_POST['nonce'] ?? '');
if (!wp_verify_nonce($nonce, 'funsite_admin_nonce')) wp_send_json_error(['msg'=>'bad_nonce'], 403);
$ymd = preg_replace('/[^0-9]/', '', (string)($_POST['ymd'] ?? ''));
$block = (string)($_POST['block'] ?? 'top3'); // top3|combo10
$fixture_id = (int)($_POST['fixture_id'] ?? 0);
$type = (string)($_POST['type'] ?? '');
$manual_status = strtolower(trim((string)($_POST['manual_status'] ?? 'auto')));
$manual_odd_raw = (string)($_POST['manual_odd'] ?? '');
if (!$ymd || !$fixture_id || !in_array($block, ['top3','combo10'], true)) {
wp_send_json_error(['msg'=>'bad_params'], 400);
}
if (!in_array($manual_status, ['auto','win','loss','void','pending'], true)) $manual_status = 'auto';
$manual_odd = '';
if ($manual_odd_raw !== '' && is_numeric($manual_odd_raw)) {
$manual_odd = (string)round((float)$manual_odd_raw, 3);
}
$day = funsite_day_load($ymd);
if (!is_array($day)) wp_send_json_error(['msg'=>'day_not_found'], 404);
$arr = $day[$block] ?? [];
if (!is_array($arr)) $arr = [];
$found = false;
foreach ($arr as &$p) {
if ((int)($p['fixture_id'] ?? 0) === $fixture_id && ((string)($p['type'] ?? '') === $type)) {
$p['manual_status'] = $manual_status;
$p['manual_odd'] = $manual_odd;
$found = true;
break;
}
}
unset($p);
if (!$found) wp_send_json_error(['msg'=>'pick_not_found'], 404);
$day[$block] = $arr;
// recalcul totals (visuel)
$recalc_total = function(array $picks): float {
$tot = 1.0;
foreach ($picks as $p) {
$useOdd = 0.0;
if (isset($p['manual_odd']) && $p['manual_odd'] !== '' && is_numeric($p['manual_odd'])) $useOdd = (float)$p['manual_odd'];
else $useOdd = (float)($p['odd'] ?? 0);
if ($useOdd > 1.01) $tot *= $useOdd;
}
return $tot;
};
if ($block === 'top3') $day['top3_total'] = $recalc_total($day['top3'] ?? []);
if ($block === 'combo10') $day['combo_total'] = $recalc_total($day['combo10'] ?? []);
funsite_day_save($ymd, $day);
wp_send_json_success(['msg'=>'saved','ymd'=>$ymd,'block'=>$block]);
}
}
if (!function_exists('funsite_register_admin_ajax')) {
function funsite_register_admin_ajax() {
add_action('wp_ajax_funsite_save_pick', 'funsite_admin_ajax_save');
}
funsite_register_admin_ajax();
}
/* ───────────────── Main render (funsite_) ───────────────── */
if (!function_exists('funsite_render_analyzer')) {
function funsite_render_analyzer($atts = []) {
$atts = shortcode_atts([
'bankroll' => (string)TP_FUNSITE_DEFAULT_BANKROLL,
'stake_pct' => (string)TP_FUNSITE_DEFAULT_STAKE_PCT,
'bookmaker' => (string)TP_FUNSITE_DEFAULT_BOOKMAKER,
'retention' => (string)TP_FUNSITE_DEFAULT_RETENTION,
'league_ids'=> (string)TP_FUNSITE_LEAGUE_IDS,
'season' => (string)TP_FUNSITE_SEASON,
], $atts, 'tp_funsite_analyzer');
$bankroll = max(1, (float)$atts['bankroll']);
$stake_pct = max(0.1, (float)$atts['stake_pct']);
$bookmaker = max(1, (int)$atts['bookmaker']);
$retention = max(7, (int)$atts['retention']);
$season = (string)$atts['season'];
$league_ids = (string)$atts['league_ids'];
funsite_cleanup_history($retention);
$today = funsite_today_ymd();
$stored = funsite_day_load($today);
ob_start();
echo '<div class="funsite-wrap">';
funsite_render_styles();
/* ───────── 1) CARTES FIGÉES DU JOUR (si existantes et >=3) ───────── */
$cards = [];
$frozen_cards = (is_array($stored) && !empty($stored['cards']) && is_array($stored['cards']) && count($stored['cards']) >= 3);
if ($frozen_cards) {
foreach ($stored['cards'] as $sc) {
$fid = (int)($sc['fixture_id'] ?? 0);
if (!$fid) continue;
$m = funsite_get_fixture_by_id($fid);
if (!is_array($m)) continue;
$flags = (array)($sc['flags'] ?? []);
$verdict = funsite_eval_verdict($m, !empty($flags['has_btts']), !empty($flags['has_victory']), (string)($flags['vict_name'] ?? ''));
$cards[] = [
'match' => $m,
'label' => (string)($sc['label'] ?? ''),
'show_lines'=> (array)($sc['show_lines'] ?? []),
'verdict' => $verdict,
'conf' => (int)($sc['conf'] ?? 0),
'type' => (string)($sc['type'] ?? 'S'),
'flags' => $flags,
'picked_odd'=> (float)($sc['picked_odd'] ?? 0),
];
}
}
/* ───────── 2) Sinon, on calcule (et on FREEZE dès qu’il y a ≥3 cartes) ───────── */
if (!$frozen_cards) {
$matches = funsite_get_matches_window($league_ids, $season);
if (empty($matches)) {
echo '<div class="funsite-fold"><summary>Analyseur Fun</summary><div class="funsite-fold-inner"><p>Aucun match trouvé.</p></div></div>';
echo '</div>';
return ob_get_clean();
}
foreach ($matches as $m) {
$fid = (int)($m['fixture']['id'] ?? 0);
if (!$fid) continue;
$stat = funsite_status_short($m);
$is_started = !in_array($stat, ['NS','TBD','PST','CANC'], true);
// si match démarré : snapshot fixture si présent
if ($is_started) {
$snap = funsite_snap_load($fid);
if (is_array($snap) && !empty($snap['label'])) {
$flags = (array)($snap['flags'] ?? []);
$verdict = funsite_eval_verdict($m, !empty($flags['has_btts']), !empty($flags['has_victory']), (string)($flags['vict_name'] ?? ''));
$cards[] = [
'match'=>$m,
'label'=>(string)$snap['label'],
'show_lines'=>(array)($snap['show_lines'] ?? []),
'verdict'=>$verdict,
'conf'=>(int)($snap['conf'] ?? 0),
'type'=>(string)($snap['type'] ?? 'S'),
'flags'=>$flags,
'picked_odd'=>(float)($snap['picked_odd'] ?? 0),
];
continue;
}
}
// avant match : on calcule
$data = funsite_build_stats_and_predictions($m, $season);
if (empty($data)) continue;
$btts_ok = (($data['pred']['btts']['conseil'] ?? 'NON') === 'OUI') && ((int)($data['pred']['btts']['confiance'] ?? 0) >= 85);
$vict_name = trim((string)($data['pred']['victoire'] ?? ''));
$diff_ok = (abs((float)($data['pred']['diff'] ?? 0)) >= 0.6);
$vict_ok = ($vict_name !== '' && $diff_ok);
if (!$btts_ok && !$vict_ok) continue;
$home = (string)($m['teams']['home']['name'] ?? '');
$away = (string)($m['teams']['away']['name'] ?? '');
$vic_pct = (int)($data['pred']['victoire_conf'] ?? 0);
$btts_pct = (int)($data['pred']['btts']['confiance'] ?? 0);
$vic_txt = $vict_name . ($vic_pct ? ' ('.$vic_pct.'%)' : '');
$btts_txt = 'Les 2 équipes marquent ('.$btts_pct.'%)';
$label = ($btts_ok && $vict_ok) ? ('Victoire : '.$vic_txt.' + '.$btts_txt) : ($vict_ok ? ('Victoire : '.$vic_txt) : $btts_txt);
$odds = funsite_get_odds_bundle($fid, $home, $away, $bookmaker);
$show_lines = [];
$picked_odd = 0.0;
if ($btts_ok && $vict_ok) {
$is_home_w = (strcasecmp($vict_name, $home) === 0);
$combined = $is_home_w ? ($odds['win_btts_home'] ?? null) : ($odds['win_btts_away'] ?? null);
if ($combined) { $show_lines[]=['💰 Cote Victoire + BTTS', $combined]; $picked_odd=(float)$combined; }
else {
$win = $is_home_w ? ($odds['win_home'] ?? null) : ($odds['win_away'] ?? null);
$btts = $odds['btts_yes'] ?? null;
if ($win) $show_lines[]=['💰 Cote Victoire', $win];
if ($btts) $show_lines[]=['💰 Cote BTTS', $btts];
if ($win && $btts) { $picked_odd=(float)$win*(float)$btts; $show_lines[]=['🧮 Cote combinée (V×BTTS)', $picked_odd]; }
else $picked_odd=(float)($win ?? $btts ?? 0);
}
} elseif ($vict_ok) {
$is_home_w = (strcasecmp($vict_name, $home) === 0);
$win = $is_home_w ? ($odds['win_home'] ?? null) : ($odds['win_away'] ?? null);
$show_lines[]=['💰 Cote Victoire', $win];
$picked_odd = (float)($win ?? 0);
} else {
$btts = $odds['btts_yes'] ?? null;
$show_lines[]=['💰 Cote BTTS', $btts];
$picked_odd = (float)($btts ?? 0);
}
$flags = [
'has_btts' => $btts_ok,
'has_victory' => $vict_ok,
'vict_name' => $vict_name,
];
$type = ($btts_ok && $vict_ok) ? 'VB' : ($vict_ok ? 'V' : 'B');
$conf = ($btts_ok && $vict_ok) ? max($vic_pct,$btts_pct) : ($vict_ok ? $vic_pct : $btts_pct);
$verdict = funsite_eval_verdict($m, $btts_ok, $vict_ok, $vict_name);
// snapshot fixture si match “started”
if ($is_started && empty(funsite_snap_load($fid))) {
funsite_snap_save($fid, [
'label'=>$label,'show_lines'=>$show_lines,'flags'=>$flags,'conf'=>$conf,'type'=>$type,'picked_odd'=>$picked_odd,
]);
}
$cards[] = [
'match'=>$m,'label'=>$label,'show_lines'=>$show_lines,'verdict'=>$verdict,
'conf'=>$conf,'type'=>$type,'flags'=>$flags,'picked_odd'=>$picked_odd
];
}
if (count($cards) >= 3) {
// FREEZE du jour : on stocke les cartes affichées (et elles ne bougeront plus)
$cards_store = [];
foreach ($cards as $c) {
$fid = (int)($c['match']['fixture']['id'] ?? 0);
if (!$fid) continue;
$cards_store[] = [
'fixture_id' => $fid,
'label' => (string)($c['label'] ?? ''),
'show_lines' => (array)($c['show_lines'] ?? []),
'flags' => (array)($c['flags'] ?? []),
'conf' => (int)($c['conf'] ?? 0),
'type' => (string)($c['type'] ?? 'S'),
'picked_odd' => (float)($c['picked_odd'] ?? 0),
];
}
$stored = is_array($stored) ? $stored : [];
$stored['cards'] = $cards_store;
$stored['cards_frozen'] = true;
funsite_day_save($today, $stored);
}
}
if (empty($cards)) {
echo '<div class="funsite-fold"><summary>Analyseur Fun</summary><div class="funsite-fold-inner"><p>Aucun match ne correspond aux critères.</p></div></div>';
echo '</div>';
return ob_get_clean();
}
echo '<div class="funsite-grid">';
foreach ($cards as $c) funsite_display_card($c['match'],$c['label'],$c['show_lines'],$c['verdict']);
echo '</div>';
/* ───────── 3) TOP3 + COTE10 : sur cartes FIGÉES (et on mémorise une seule fois) ───────── */
$stored = funsite_day_load($today);
if (!is_array($stored)) $stored = [];
$best = [];
foreach ($cards as $c) {
$stat = strtoupper(trim((string)($c['match']['fixture']['status']['short'] ?? '')));
if (funsite_is_void_status($stat)) continue; // on exclut juste annulé/abandonné
$odd = (float)($c['picked_odd'] ?? 0);
if ($odd >= 1.20) {
$best[] = ['card'=>$c, 'conf'=>(int)$c['conf'], 'odd'=>$odd, 'type'=>$c['type']];
}
}
usort($best, function($a,$b){
if ($a['conf'] === $b['conf']) {
$prio = ['VB'=>3,'V'=>2,'B'=>1];
$ta = $prio[$a['type']] ?? 0;
$tb = $prio[$b['type']] ?? 0;
if ($ta === $tb) return $b['odd'] <=> $a['odd'];
return $tb <=> $ta;
}
return $b['conf'] <=> $a['conf'];
});
$norm_pick = function(array $row){
$m = (array)($row['card']['match'] ?? []);
$fid = (int)($m['fixture']['id'] ?? 0);
$home = (string)($m['teams']['home']['name'] ?? '');
$away = (string)($m['teams']['away']['name'] ?? '');
$label = (string)($row['card']['label'] ?? '');
$flags = (array)($row['card']['flags'] ?? []);
return [
'fixture_id'=>$fid,'home'=>$home,'away'=>$away,'label'=>$label,
'odd'=>(float)($row['odd'] ?? 0),'conf'=>(int)($row['conf'] ?? 0),'type'=>(string)($row['type'] ?? ''),
'flags'=>$flags,
'manual_status'=>'auto','manual_odd'=>''
];
};
// On ne (re)construit que si pas déjà stocké aujourd’hui
if (empty($stored['top3']) && empty($stored['combo10'])) {
$top3_raw = array_slice($best, 0, 3);
$top3 = []; $top3_total = 1.0;
foreach ($top3_raw as $r) {
$p = $norm_pick($r);
if ($p['fixture_id'] && $p['odd'] > 1.01) { $top3[] = $p; $top3_total *= $p['odd']; }
}
// Cote10 : on vise >= 10 sinon on stocke vide
$combo_target = 10.0;
$combo = []; $combo_total = 1.0;
foreach ($best as $r) {
if ($combo_total >= $combo_target) break;
$p = $norm_pick($r);
if ($p['fixture_id'] && $p['odd'] > 1.01) { $combo[] = $p; $combo_total *= $p['odd']; }
}
if ($combo_total < 10.0) { $combo = []; $combo_total = 1.0; }
$stored['top3'] = $top3;
$stored['top3_total'] = $top3_total;
$stored['combo10'] = $combo;
$stored['combo_total'] = $combo_total;
funsite_day_save($today, $stored);
$stored = funsite_day_load($today);
if (!is_array($stored)) $stored = [];
}
$top3 = (array)($stored['top3'] ?? []);
$top3_total = (float)($stored['top3_total'] ?? 1.0);
$combo10 = (array)($stored['combo10'] ?? []);
$combo_total = (float)($stored['combo_total'] ?? 1.0);
$render_pick_line = function(array $p){
$v = funsite_pick_eval_state($p);
$st = (string)($v['state'] ?? 'pending');
$score = (string)($v['score'] ?? '—');
$ico = '⏳';
if ($st==='won') $ico='✅';
elseif ($st==='lost') $ico='❌';
elseif ($st==='void') $ico='↩️';
$odd = (isset($p['manual_odd']) && $p['manual_odd']!=='' && is_numeric($p['manual_odd'])) ? (float)$p['manual_odd'] : (float)($p['odd'] ?? 0);
$odd_fmt = $odd ? number_format($odd,2,',',' ') : '—';
echo '<li>'.$ico.' <strong>'.esc_html(($p['home'] ?? '').' – '.($p['away'] ?? '')).'</strong> → '.esc_html($p['label'] ?? '').' <span class="funsite-badge-inline">(cote '.$odd_fmt.')</span>';
if ($score && $score !== '—') echo ' <span class="funsite-badge-inline">• Score '.$score.'</span>';
echo '</li>';
};
if (!empty($top3)) {
echo '<div class="funsite-block funsite-block--gold">';
echo '<h3>💎 Sélection Top 3 Confiances</h3>';
echo '<p>'.esc_html(wp_date('d/m')).' – cote totale ≈ <strong>'.esc_html(number_format($top3_total,2,',',' ')).'</strong></p>';
echo '<ul>';
foreach ($top3 as $p) $render_pick_line($p);
echo '</ul>';
echo '</div>';
}
if (!empty($combo10) && $combo_total >= 10.0) {
echo '<div class="funsite-block funsite-block--blue">';
echo '<h3>💥 Combiné Fun du Jour – Objectif Cote 10+</h3>';
echo '<p>'.esc_html(wp_date('d/m')).' – cote totale <strong>'.esc_html(number_format($combo_total,2,',',' ')).'</strong></p>';
echo '<ul>';
foreach ($combo10 as $p) $render_pick_line($p);
echo '</ul>';
echo '</div>';
}
/* ───────── 4) ROI (accordéon fermé) ───────── */
$load_days = function(int $days){
$out = [];
for ($i=1; $i<=$days; $i++) {
$ymd = funsite_ymd_days_ago($i);
$d = funsite_day_load($ymd);
if (is_array($d)) $out[] = $d;
}
return $out;
};
$r1 = funsite_calc_roi_period($load_days(1), $bankroll, $stake_pct);
$r7 = funsite_calc_roi_period($load_days(7), $bankroll, $stake_pct);
$r32 = funsite_calc_roi_period($load_days(32), $bankroll, $stake_pct);
$render_roi_row = function(string $label, array $r){
$profit = (float)$r['profit'];
$roi = (float)$r['roi'];
$profit_fmt = number_format($profit,2,',',' ').' €';
$roi_fmt = number_format($roi,2,',',' ').'%';
$pcls = ($profit>0)?'funsite-pill funsite-pill--pos':(($profit<0)?'funsite-pill funsite-pill--neg':'funsite-pill funsite-pill--neu');
$rcls = ($roi>0)?'funsite-pill funsite-pill--pos':(($roi<0)?'funsite-pill funsite-pill--neg':'funsite-pill funsite-pill--neu');
echo '<tr>';
echo '<td><strong>'.esc_html($label).'</strong></td>';
echo '<td><span class="'.$pcls.'">'.esc_html($profit_fmt).'</span></td>';
echo '<td><span class="'.$rcls.'">'.esc_html($roi_fmt).'</span></td>';
echo '<td>'.esc_html((int)$r['w']).' / '.esc_html((int)$r['l']).'</td>';
echo '<td>'.esc_html((int)$r['v']).'</td>';
echo '<td>'.esc_html((int)$r['p']).'</td>';
echo '</tr>';
};
echo '<details class="funsite-fold">';
echo '<summary>📈 Bilans ROI (combinés) — J-1 / J-7 / J-32</summary>';
echo '<div class="funsite-fold-inner">';
echo '<p style="margin:0 0 10px;color:rgba(255,255,255,.72);font-size:12.5px">Mise fixe par combiné : <strong>'.esc_html(number_format((float)$r1['stake_unit'],2,',',' ')).'€</strong> ('.esc_html($stake_pct).'% de '.esc_html($bankroll).'€). Void/pending exclus du ROI.</p>';
echo '<table class="funsite-table"><thead><tr><th>Période</th><th>Profit</th><th>ROI</th><th>W / L</th><th>Void</th><th>Pending</th></tr></thead><tbody>';
$render_roi_row('J-1', $r1);
$render_roi_row('J-7', $r7);
$render_roi_row('J-32', $r32);
echo '</tbody></table>';
echo '</div>';
echo '</details>';
/* ───────── 5) ADMIN (accordéon fermé) ───────── */
if (current_user_can('manage_options')) {
$idx = get_option(funsite_day_index_key(), []);
if (!is_array($idx)) $idx = [];
$idx = array_reverse($idx);
$sel = (string)($_GET['funsite_day'] ?? $today);
$sel = preg_replace('/[^0-9]/', '', $sel);
if (!$sel) $sel = $today;
$day = funsite_day_load($sel);
$nonce = wp_create_nonce('funsite_admin_nonce');
echo '<details class="funsite-fold">';
echo '<summary>🛠️ Mode ADMIN — Overrides (statut / cote)</summary>';
echo '<div class="funsite-fold-inner">';
echo '<div class="funsite-admin" id="funsite-admin">';
echo '<div class="funsite-controls">';
echo '<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">';
foreach ($_GET as $k=>$v) {
if ($k==='funsite_day') continue;
echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr($v).'">';
}
echo '<label style="font-size:13px;opacity:.9">Journée :</label>';
echo '<select name="funsite_day">';
echo '<option value="'.esc_attr($today).'" '.selected($sel,$today,false).'>Aujourd’hui ('.$today.')</option>';
foreach ($idx as $ymd) {
echo '<option value="'.esc_attr($ymd).'" '.selected($sel,(string)$ymd,false).'>'.$ymd.'</option>';
}
echo '</select>';
echo '<button class="funsite-btn" type="submit">Charger</button>';
echo '</form>';
echo '</div>';
if (!is_array($day)) {
echo '<div class="funsite-note">Aucune mémoire trouvée pour cette journée.</div>';
} else {
$render_admin_table = function(string $block, string $title, array $picks) use ($sel, $nonce) {
if (empty($picks)) return;
echo '<div style="margin-top:14px">';
echo '<h4 style="margin:0 0 8px;font-size:14px;color:#bfefff;font-weight:950">'.esc_html($title).'</h4>';
echo '<table class="funsite-table">';
echo '<thead><tr><th>Match</th><th>Pick</th><th>Cote</th><th>Statut</th><th>Action</th></tr></thead><tbody>';
foreach ($picks as $p) {
$fid = (int)($p['fixture_id'] ?? 0);
$type = (string)($p['type'] ?? '');
$match = (string)($p['home'] ?? '').' – '.(string)($p['away'] ?? '');
$label = (string)($p['label'] ?? '');
$odd = (isset($p['manual_odd']) && $p['manual_odd']!=='') ? $p['manual_odd'] : (string)($p['odd'] ?? '');
$st = (string)($p['manual_status'] ?? 'auto');
echo '<tr data-block="'.esc_attr($block).'" data-ymd="'.esc_attr($sel).'" data-fid="'.esc_attr($fid).'" data-type="'.esc_attr($type).'">';
echo '<td><strong>'.esc_html($match).'</strong><div style="opacity:.75;font-size:12px">#'.$fid.' • '.$type.'</div></td>';
echo '<td>'.esc_html($label).'</td>';
echo '<td><input class="funsite-inp-odd" type="text" value="'.esc_attr($odd).'" style="width:90px"></td>';
echo '<td>';
echo '<select class="funsite-inp-status">';
$opts = ['auto'=>'Auto','win'=>'Gagné','loss'=>'Perdu','void'=>'Remboursé','pending'=>'En attente'];
foreach ($opts as $k=>$lab) echo '<option value="'.esc_attr($k).'" '.selected($st,$k,false).'>'.esc_html($lab).'</option>';
echo '</select>';
echo '</td>';
echo '<td><button class="funsite-btn funsite-save" type="button">Enregistrer</button></td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</div>';
echo '<div class="funsite-note">Astuce : mets "Auto" pour revenir au verdict API. "Remboursé" neutralise la sélection dans le combiné (odd = 1).</div>';
echo '<div class="funsite-toast" id="funsite-toast"></div>';
static $js_done = false;
if (!$js_done) {
$js_done = true;
?>
<script>
(function(){
const root = document.getElementById('funsite-admin');
if(!root) return;
const toast = document.getElementById('funsite-toast');
const showToast = (ok, msg) => {
if(!toast) return;
toast.className = 'funsite-toast ' + (ok ? 'ok' : 'err');
toast.textContent = msg;
};
root.addEventListener('click', async (e) => {
const btn = e.target.closest('.funsite-save');
if(!btn) return;
const tr = btn.closest('tr');
if(!tr) return;
btn.disabled = true;
btn.textContent = '...';
const block = tr.getAttribute('data-block');
const ymd = tr.getAttribute('data-ymd');
const fid = tr.getAttribute('data-fid');
const type = tr.getAttribute('data-type');
const odd = tr.querySelector('.funsite-inp-odd')?.value ?? '';
const st = tr.querySelector('.funsite-inp-status')?.value ?? 'auto';
const fd = new FormData();
fd.append('action','funsite_save_pick');
fd.append('nonce', <?php echo json_encode($nonce); ?>);
fd.append('block', block);
fd.append('ymd', ymd);
fd.append('fixture_id', fid);
fd.append('type', type);
fd.append('manual_odd', odd);
fd.append('manual_status', st);
try{
const res = await fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', { method:'POST', body: fd });
const json = await res.json();
if(json && json.success){
showToast(true, '✅ Sauvegardé. Recharge la page pour voir le ROI mis à jour.');
} else {
showToast(false, '❌ Erreur : ' + ((json && json.data && json.data.msg) ? json.data.msg : 'unknown'));
}
} catch(err){
showToast(false, '❌ Erreur réseau');
} finally {
btn.disabled = false;
btn.textContent = 'Enregistrer';
}
});
})();
</script>
<?php
}
};
$render_admin_table('top3', '💎 Top 3 (override)', (array)($day['top3'] ?? []));
$render_admin_table('combo10', '💥 Cote 10+ (override)', (array)($day['combo10'] ?? []));
}
echo '</div>'; // admin
echo '</div>'; // fold inner
echo '</details>';
}
echo '</div>'; // wrap
return ob_get_clean();
}
}
/* ───────────────── Shortcode registration (funsite_) ───────────────── */
if (!function_exists('funsite_register_shortcode')) {
function funsite_register_shortcode() {
add_shortcode('tp_funsite_analyzer', 'funsite_render_analyzer');
}
add_action('init', 'funsite_register_shortcode');
}
Vues : 680
J’aime ça :
J’aime chargement…
Similaire