diff --git a/css/style.css b/css/style.css index 2cc360a..f1d04d9 100644 --- a/css/style.css +++ b/css/style.css @@ -3183,3 +3183,214 @@ a.ms1-trad-link.btn-aide-trad{ border-top: 1px solid #e9ecef; padding-top: 1rem; } + +/* ===== Hub promoteur : stats par epreuve (panneau de droite) ===== */ +.promoteur-hub-card-row { + align-items: stretch; +} + +@media (min-width: 992px) { + .promoteur-hub-card-stats { + border-left: 1px dashed #e3c4a6; + } +} + +.promoteur-hub-card-stats { + margin-top: 0.5rem; +} + +@media (min-width: 992px) { + .promoteur-hub-card-stats { + margin-top: 0; + } +} + +/* --- Bande flash --- */ +.promoteur-hub-flash { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + background: linear-gradient(135deg, #fff7f0 0%, #fdeede 100%); + border: 1px solid #f3d6b8; + border-radius: 12px; + padding: 0.6rem 0.75rem; + margin-bottom: 0.6rem; +} + +.phub-flash-item { + flex: 1 1 auto; + min-width: 72px; + text-align: center; + line-height: 1.1; +} + +.phub-flash-num { + display: block; + font-size: 1.5rem; + font-weight: 700; + color: #1f3b63; +} + +.phub-flash-lbl { + display: block; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.03em; + color: #8a6d52; +} + +.phub-flash-main .phub-flash-num { + color: #e8821e; + font-size: 1.85rem; +} + +/* --- Badges d'alerte --- */ +.promoteur-hub-badges { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-bottom: 0.6rem; +} + +.phub-badge { + font-size: 0.72rem; + font-weight: 600; + padding: 0.2rem 0.55rem; + border-radius: 999px; + white-space: nowrap; +} + +.phub-badge-danger { background: #fde2e1; color: #b3261e; } +.phub-badge-warn { background: #fdecc8; color: #9a6700; } +.phub-badge-info { background: #dceafd; color: #1f5fb3; } +.phub-badge-soft { background: #eceff3; color: #4a5568; } + +/* --- Grille de tuiles par epreuve --- */ +.promoteur-hub-epreuves-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.6rem; +} + +.phub-epr-tile { + background: #fff; + border: 1px solid #e7ebf0; + border-left: 4px solid #adb5bd; + border-radius: 10px; + padding: 0.6rem 0.7rem; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; +} + +.phub-epr-head { + margin-bottom: 0.35rem; +} + +.phub-epr-titre { + display: block; + font-weight: 700; + color: #1f3b63; + font-size: 0.9rem; + line-height: 1.15; +} + +.phub-epr-cat { + display: inline-block; + font-size: 0.68rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; + color: #8a6d52; +} + +.phub-epr-sub { + display: block; + font-size: 0.72rem; + color: #6b7785; +} + +.phub-epr-big { + line-height: 1; + margin: 0.15rem 0 0.4rem; +} + +.phub-epr-bignum { + font-size: 2.3rem; + font-weight: 800; + color: #1f3b63; +} + +.phub-epr-max { + font-size: 1rem; + font-weight: 600; + color: #9aa6b2; + margin-left: 0.15rem; +} + +.phub-epr-biglbl { + display: block; + font-size: 0.66rem; + text-transform: uppercase; + letter-spacing: 0.03em; + color: #8a93a0; + margin-top: 0.1rem; +} + +.phub-epr-bar { + height: 8px; + background: #eef1f4; + border-radius: 999px; + overflow: hidden; +} + +.phub-epr-bar > span { + display: block; + height: 100%; + border-radius: 999px; + background: #2f9e44; + transition: width 0.4s ease; +} + +.phub-epr-barlbl { + font-size: 0.7rem; + color: #6b7785; + margin-top: 0.2rem; +} + +.phub-epr-illimite { + color: #6b7785; + font-style: italic; +} + +.phub-epr-chips { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin-top: 0.45rem; +} + +.phub-chip { + font-size: 0.68rem; + font-weight: 600; + padding: 0.15rem 0.45rem; + border-radius: 6px; + background: #f1f4f7; + color: #45525f; + white-space: nowrap; +} + +.phub-chip-prix { background: #e7f3ea; color: #1d6b32; } +.phub-chip-next { background: #fdecc8; color: #8a5a00; } +.phub-chip-checkin { background: #e2eefb; color: #1f5fb3; } +.phub-chip-attente { background: #efe6fb; color: #6b3fb3; } + +/* --- Etats de remplissage (bordure + barre) --- */ +.phub-etat-ok { border-left-color: #2f9e44; } +.phub-etat-attention { border-left-color: #f59f00; } +.phub-etat-attention .phub-epr-bar > span { background: #f59f00; } +.phub-etat-presque { border-left-color: #e8590c; } +.phub-etat-presque .phub-epr-bar > span { background: #e8590c; } +.phub-etat-complete { border-left-color: #b3261e; } +.phub-etat-complete .phub-epr-bar > span { background: #b3261e; } +.phub-etat-illimitee { border-left-color: #868e96; } diff --git a/php/inc_fx_promoteur_hub.php b/php/inc_fx_promoteur_hub.php index 6b8eb80..5d23f68 100644 --- a/php/inc_fx_promoteur_hub.php +++ b/php/inc_fx_promoteur_hub.php @@ -487,12 +487,453 @@ function fxPromoteurHubRenderEventPanel($arrItem, $strLangue) return $html; } +/** + * Statistiques par epreuve pour un evenement (vendus / restant / max, + * check-in, liste d'attente, prix courant + date du prochain palier). + * Requetes agregees (1 par metrique) plutot qu'une boucle fxGetQuantites, + * pour rester rapide dans le hub. + * + * Retourne array('epreuves' => [...], 'flash' => [...]) ou null si aucune epreuve. + */ +function fxPromoteurHubGetEpreuveStats($intEveId, $strLangue = 'fr') +{ + global $objDatabase; + + $intEveId = intval($intEveId); + if ($intEveId <= 0 || !isset($objDatabase)) { + return null; + } + + $strLang = in_array($strLangue, array('fr', 'en'), true) ? $strLangue : 'fr'; + $strAlt = ($strLang === 'fr') ? 'en' : 'fr'; + + // ---- Liste des epreuves ---- + $sqlEpreuves = " + SELECT epr_id, epr_categorie_fr, epr_categorie_en, epr_type_fr, epr_type_en, + epr_nom_fr, epr_nom_en, epr_date, epr_heure, epr_date_limite, + epr_qte, epr_qte_origine, epr_qte_limitee, epr_equipe, epr_nb_participants + FROM inscriptions_epreuves + WHERE eve_id = " . $intEveId . " AND epr_actif = 1 + ORDER BY epr_tri, epr_type_" . $strLang . ", epr_nom_" . $strLang . " + "; + $tabEpreuves = $objDatabase->fxGetResults($sqlEpreuves); + + if (!is_array($tabEpreuves) || count($tabEpreuves) === 0) { + return null; + } + + $arrIds = array(); + for ($i = 1; $i <= count($tabEpreuves); $i++) { + $arrIds[] = intval($tabEpreuves[$i]['epr_id']); + } + $strIds = implode(',', $arrIds); + + // ---- Vendus / inscrits (confirmes, non annules) ---- + $arrVendus = array(); + $sqlVendus = " + SELECT p.epr_id AS epr_id, COUNT(DISTINCT e.pec_id_original) AS nb + FROM resultats_epreuves_commandees e + JOIN resultats_participants p + ON p.epr_id = e.epr_id AND p.pec_id = e.pec_id_original + WHERE p.is_cancelled = 0 AND p.epr_id IN (" . $strIds . ") + GROUP BY p.epr_id + "; + $tabVendus = $objDatabase->fxGetResults($sqlVendus); + if (is_array($tabVendus)) { + for ($i = 1; $i <= count($tabVendus); $i++) { + $arrVendus[intval($tabVendus[$i]['epr_id'])] = intval($tabVendus[$i]['nb']); + } + } + + // ---- Check-in (dossards remis) ---- + $arrCheckin = array(); + $sqlCheckin = " + SELECT epr_id, COUNT(*) AS nb + FROM resultats_participants + WHERE no_bib_remis = 1 AND epr_id IN (" . $strIds . ") + GROUP BY epr_id + "; + $tabCheckin = $objDatabase->fxGetResults($sqlCheckin); + if (is_array($tabCheckin)) { + for ($i = 1; $i <= count($tabCheckin); $i++) { + $arrCheckin[intval($tabCheckin[$i]['epr_id'])] = intval($tabCheckin[$i]['nb']); + } + } + + // ---- Liste d'attente : nombre + statut actif ---- + $arrAttenteNb = array(); + $sqlAttente = " + SELECT epr_id, COUNT(1) AS nb + FROM inscriptions_liste_attente_participants + WHERE (lispar_statut = 1 OR lispar_statut = 2) AND epr_id IN (" . $strIds . ") + GROUP BY epr_id + "; + $tabAttente = $objDatabase->fxGetResults($sqlAttente); + if (is_array($tabAttente)) { + for ($i = 1; $i <= count($tabAttente); $i++) { + $arrAttenteNb[intval($tabAttente[$i]['epr_id'])] = intval($tabAttente[$i]['nb']); + } + } + + $arrAttenteActive = array(); + $sqlAttenteActive = " + SELECT epr_id, lis_active + FROM inscriptions_liste_attente + WHERE epr_id IN (" . $strIds . ") + "; + $tabAttenteActive = $objDatabase->fxGetResults($sqlAttenteActive); + if (is_array($tabAttenteActive)) { + for ($i = 1; $i <= count($tabAttenteActive); $i++) { + $arrAttenteActive[intval($tabAttenteActive[$i]['epr_id'])] = intval($tabAttenteActive[$i]['lis_active']); + } + } + + // ---- Prix (paliers publics) : prix courant + date du prochain palier ---- + $arrTiers = array(); + $sqlPrix = " + SELECT epr_id, ep_montant, ep_montant_affiche, ep_date_limite + FROM inscriptions_epreuves_prix + WHERE ep_prive = 0 AND epr_id IN (" . $strIds . ") + ORDER BY epr_id, ep_date_limite ASC + "; + $tabPrix = $objDatabase->fxGetResults($sqlPrix); + if (is_array($tabPrix)) { + for ($i = 1; $i <= count($tabPrix); $i++) { + $intEpr = intval($tabPrix[$i]['epr_id']); + $fltMontant = (floatval($tabPrix[$i]['ep_montant_affiche']) > 0) + ? floatval($tabPrix[$i]['ep_montant_affiche']) + : floatval($tabPrix[$i]['ep_montant']); + if (!isset($arrTiers[$intEpr])) { + $arrTiers[$intEpr] = array(); + } + $arrTiers[$intEpr][] = array( + 'montant' => $fltMontant, + 'date_limite' => trim((string)($tabPrix[$i]['ep_date_limite'] ?? '')), + ); + } + } + + $strToday = date('Y-m-d'); + + // ---- Assemblage par epreuve + agregat flash ---- + $arrEpreuves = array(); + $intFlashVendus = 0; + $intFlashMax = 0; + $intFlashAttente = 0; + $intFlashCompletes = 0; + $intFlashPresque = 0; + $fltFlashRevenus = 0.0; + $strFlashProchaineLimite = ''; + + for ($i = 1; $i <= count($tabEpreuves); $i++) { + $row = $tabEpreuves[$i]; + $intEpr = intval($row['epr_id']); + + $strTitre = trim((string)($row['epr_type_' . $strLang] ?? '')); + if ($strTitre === '') { + $strTitre = trim((string)($row['epr_type_' . $strAlt] ?? '')); + } + $strNom = trim((string)($row['epr_nom_' . $strLang] ?? '')); + if ($strNom === '') { + $strNom = trim((string)($row['epr_nom_' . $strAlt] ?? '')); + } + $strCategorie = trim((string)($row['epr_categorie_' . $strLang] ?? '')); + if ($strTitre === '') { + $strTitre = ($strNom !== '') ? $strNom : ('#' . $intEpr); + } + + $intVendus = $arrVendus[$intEpr] ?? 0; + $intRestant = intval($row['epr_qte']); + $blnIllimitee = (intval($row['epr_qte_limitee']) !== 1); + // Max courant = places vendues + restantes ; origine = max initial fixe. + $intMax = $intVendus + $intRestant; + $intOrigine = intval($row['epr_qte_origine']); + + $fltPct = ($blnIllimitee || $intMax <= 0) ? 0 : round(($intVendus / $intMax) * 100); + if ($fltPct > 100) { + $fltPct = 100; + } + + // Etat de remplissage : complete / presque / ok / illimitee + if ($blnIllimitee) { + $strEtat = 'illimitee'; + } elseif ($intRestant <= 0) { + $strEtat = 'complete'; + $intFlashCompletes++; + } elseif ($fltPct >= 90) { + $strEtat = 'presque'; + $intFlashPresque++; + } elseif ($fltPct >= 75) { + $strEtat = 'attention'; + } else { + $strEtat = 'ok'; + } + + // Prix courant + date du prochain palier + $fltPrixCourant = null; + $strPrixDateFin = ''; + $fltPrixProchain = null; + if (!empty($arrTiers[$intEpr])) { + $arrEprTiers = $arrTiers[$intEpr]; + $intIdxCourant = -1; + foreach ($arrEprTiers as $intIdx => $arrTier) { + if ($arrTier['date_limite'] !== '' && $arrTier['date_limite'] >= $strToday) { + $intIdxCourant = $intIdx; + break; + } + } + if ($intIdxCourant === -1) { + // Tous les paliers sont passes : on prend le dernier (prix final), pas de prochain. + $intIdxCourant = count($arrEprTiers) - 1; + } + $fltPrixCourant = $arrEprTiers[$intIdxCourant]['montant']; + $strPrixDateFin = $arrEprTiers[$intIdxCourant]['date_limite']; + if (isset($arrEprTiers[$intIdxCourant + 1])) { + $fltPrixProchain = $arrEprTiers[$intIdxCourant + 1]['montant']; + } + } + + $intAttenteNb = $arrAttenteNb[$intEpr] ?? 0; + $blnAttenteActive = !empty($arrAttenteActive[$intEpr]); + + // Agregat flash + $intFlashVendus += $intVendus; + if (!$blnIllimitee) { + $intFlashMax += $intMax; + } + $intFlashAttente += $intAttenteNb; + if ($fltPrixCourant !== null) { + $fltFlashRevenus += $fltPrixCourant * $intVendus; + } + // Prochaine date limite d'inscription (la plus proche, dans le futur) + $strLimiteInscr = trim((string)($row['epr_date_limite'] ?? '')); + if ($strLimiteInscr !== '' && strpos($strLimiteInscr, '0000') !== 0 && $strLimiteInscr >= $strToday) { + if ($strFlashProchaineLimite === '' || $strLimiteInscr < $strFlashProchaineLimite) { + $strFlashProchaineLimite = $strLimiteInscr; + } + } + + $arrEpreuves[] = array( + 'epr_id' => $intEpr, + 'titre' => $strTitre, + 'nom' => $strNom, + 'categorie' => $strCategorie, + 'date' => trim((string)($row['epr_date'] ?? '')), + 'heure' => trim((string)($row['epr_heure'] ?? '')), + 'date_limite_inscr' => $strLimiteInscr, + 'equipe' => (intval($row['epr_equipe']) === 1), + 'nb_participants' => intval($row['epr_nb_participants']), + 'vendus' => $intVendus, + 'restant' => $intRestant, + 'max' => $intMax, + 'origine' => $intOrigine, + 'illimitee' => $blnIllimitee, + 'pct' => $fltPct, + 'etat' => $strEtat, + 'checkin' => $arrCheckin[$intEpr] ?? 0, + 'attente_nb' => $intAttenteNb, + 'attente_active' => $blnAttenteActive, + 'prix_courant' => $fltPrixCourant, + 'prix_date_fin' => $strPrixDateFin, + 'prix_prochain' => $fltPrixProchain, + ); + } + + $intPctGlobal = ($intFlashMax > 0) ? round(($intFlashVendus / $intFlashMax) * 100) : 0; + if ($intPctGlobal > 100) { + $intPctGlobal = 100; + } + + $arrFlash = array( + 'total_vendus' => $intFlashVendus, + 'total_max' => $intFlashMax, + 'total_restant' => max(0, $intFlashMax - $intFlashVendus), + 'pct_global' => $intPctGlobal, + 'revenus' => $fltFlashRevenus, + 'nb_epreuves' => count($arrEpreuves), + 'nb_completes' => $intFlashCompletes, + 'nb_presque' => $intFlashPresque, + 'total_attente' => $intFlashAttente, + 'prochaine_date_limite' => $strFlashProchaineLimite, + ); + + return array('epreuves' => $arrEpreuves, 'flash' => $arrFlash); +} + +/** + * Rendu visuel du panneau de stats (bande flash + tuiles par epreuve). + * Toujours visible (pas de collapse), s'affiche a droite de la carte. + */ +function fxPromoteurHubRenderStatsPanel($arrStats, $strLangue = 'fr') +{ + if (empty($arrStats) || empty($arrStats['epreuves'])) { + return ''; + } + + $blnFr = ($strLangue !== 'en'); + $arrFlash = $arrStats['flash']; + + $lbl = function ($fr, $en) use ($blnFr) { + return $blnFr ? $fr : $en; + }; + + $html = '