Files
ms1inscription-v5/ajax_inscr_gestion.php

335 lines
15 KiB
PHP

<?php
/**
* Actions AJAX reservees a la fiche de gestion des inscriptions (v2).
* N'altere pas ajax_promoteur.php ni enregistrer.php (legacy promoteur).
*/
session_start();
// MSIN-4401 — Traductions toujours au nom de compte.php (jamais ajax_inscr_gestion.php).
// Evite les doublons info_prg=ajax_*.php et les libelles manquants selon la page appelante
// (hub, compte, /mobile). Meme pattern que ajax_promoteur_hub / ajax_bib_range.
define('MS1_INSCR_GESTION_AJAX', true);
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
header('Content-Type: application/json; charset=utf-8');
// Filet de securite : transforme tout fatal (include manquant, fonction
// indefinie, parse error...) en reponse JSON exploitable au lieu d'un 500 muet.
$GLOBALS['__inscr_gestion_done'] = false;
register_shutdown_function(function () {
if (!empty($GLOBALS['__inscr_gestion_done'])) {
return;
}
$err = error_get_last();
$strDetail = ($err && in_array($err['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR), true))
? ($err['message'] . ' @ ' . $err['file'] . ':' . $err['line'])
: 'fatal inconnu';
error_log('[ajax_inscr_gestion] ' . $strDetail);
if (!headers_sent()) {
http_response_code(200);
}
echo json_encode(array('state' => 'error', 'message' => 'fatal', 'detail' => $strDetail));
});
require_once 'php/inc_fonctions.php';
require_once 'php/inc_fx_promoteur.php';
require_once 'php/inc_fx_inscriptions_gestion.php';
// Necessaire pour fxEveAccesResolveEveIdFromParId() appelee dans le case
// 'par_statut_course' : ce fichier n'est sinon charge que paresseusement
// (fxInscrGestionLoadEveAcces) APRES ce premier appel -> fatal undefined function.
require_once 'php/inc_fx_eve_acces.php';
$intComId = intval($_SESSION['com_id'] ?? 0);
$strAction = trim((string)($_REQUEST['a'] ?? ''));
$strLangue = trim((string)($_REQUEST['lang'] ?? 'fr'));
if ($strLangue === '' || !in_array($strLangue, array('fr', 'en'), true)) {
$strLangue = 'fr';
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
fxInscrGestionInitTextes($strLangue);
if ($intComId <= 0) {
$GLOBALS['__inscr_gestion_done'] = true;
// MSIN-4401 — code session : le client recharge pour afficher le login.
echo json_encode(array('state' => 'error', 'code' => 'session', 'message' => 'session'));
exit;
}
$tabRetour = array('state' => 'error');
try {
switch ($strAction) {
case 'par_statut_course':
$intParId = intval($_REQUEST['par_id'] ?? 0);
$strStatut = trim((string)($_REQUEST['par_statut_course'] ?? ''));
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.statut_edit')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
$tabRetour = fxSetParStatutCourse($intParId, $strStatut, $strLangue);
break;
case 'refund_form':
// Charge le formulaire de remboursement mutualise pour la fiche de gestion (panneau repliable).
$strNoPanier = trim((string)($_REQUEST['no_panier'] ?? ''));
if ($strNoPanier === '') {
$tabRetour = array('state' => 'error', 'message' => 'no_panier_manquant');
break;
}
// Meme source que le Super Admin : la commande vient de inscriptions_panier_acheteurs.
$sqlCmd = "SELECT *, IF(a.com_id <> 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager"
. " FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s"
. " WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "'";
$recCommandes = $objDatabase->fxGetRow($sqlCmd);
if (!$recCommandes) {
$tabRetour = array('state' => 'error', 'message' => 'commande_introuvable');
break;
}
// L'eve_id de contexte = evenement de la fiche (participant), pas seulement celui du checkout.
require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php';
$intEveContext = intval($_REQUEST['eve_id'] ?? 0);
if ($intEveContext <= 0) {
$intEveContext = intval($recCommandes['eve_id']);
}
if ($intEveContext <= 0 || !fxInscrGestionCanDo($intComId, $intEveContext, 'inscriptions_gestion.refund')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
if (!fxRefundPanierLinkedToEvent($recCommandes['no_panier'], $intEveContext, intval($recCommandes['eve_id']))) {
$tabRetour = array('state' => 'error', 'message' => 'panier_evenement_incompatible');
break;
}
$strRefundToken = fxRefundEnsureToken();
$arrRefundCtx = fxRefundComputeContext($recCommandes, $intEveContext);
// Auteur = promoteur connecte (alimente l'encart "Ajoute par" du formulaire).
$strAuteur = trim((string)($_SESSION['com_info']['com_prenom'] ?? '') . ' '
. (string)($_SESSION['com_info']['com_nom'] ?? ''));
// Markup seul (sans <script>, dernier param false) car injecte via innerHTML :
// le comportement (submit AJAX) est gere par le handler delegue dans inc_footer_scripts.php.
// NB : le resume de la transaction n'est plus inclus ici (MSIN) ; il est desormais
// accessible via le bouton "Afficher la transaction" de la barre Gestion (action transaction_resume).
$strDocAside = '';
if (function_exists('fxDocRenderRefundPromoteurAside')) {
global $vPage;
$memPage = $vPage ?? null;
$vPage = 'compte.php';
$strDocAside = fxDocRenderRefundPromoteurAside();
$vPage = $memPage;
}
$strForm = fxRenderRefundForm($recCommandes, $strRefundToken, $arrRefundCtx, $vDomaine, $strAuteur, null, false, $strDocAside);
$tabRetour = array('state' => 'ok', 'html' => $strForm);
break;
case 'bib_ocr':
// MSIN-4401 — Lecture numero dossard via Google Cloud Vision.
// Image : fichier multipart (prefere) ou base64 POST (legacy).
if (!fxInscrGestionBibOcrCloudEnabled()) {
$tabRetour = array('state' => 'error', 'message' => 'ocr_not_configured');
break;
}
$strImage = '';
if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
if ((int)$_FILES['image']['error'] === UPLOAD_ERR_OK && (int)$_FILES['image']['size'] > 0
&& (int)$_FILES['image']['size'] <= 4 * 1024 * 1024) {
$strBin = (string)file_get_contents($_FILES['image']['tmp_name']);
$strImage = base64_encode($strBin);
} else {
$tabRetour = array('state' => 'error', 'message' => 'image_invalid');
break;
}
} else {
$strImage = (string)($_POST['image'] ?? '');
}
if ($strImage === '') {
$tabRetour = array('state' => 'error', 'message' => 'image_invalid');
break;
}
$arrOcr = fxInscrGestionVisionReadBibNumber($strImage);
if (!empty($arrOcr['ok']) && !empty($arrOcr['number'])) {
$tabRetour = array(
'state' => 'ok',
'number' => (string)$arrOcr['number'],
);
break;
}
$strSeen = '';
if (!empty($arrOcr['text'])) {
$strSeen = preg_replace('/\D+/', '', (string)$arrOcr['text']);
}
$tabRetour = array(
'state' => 'error',
'message' => (string)($arrOcr['message'] ?? 'no_number'),
'seen' => $strSeen,
'detail' => (string)($arrOcr['detail'] ?? ''),
);
break;
case 'bib_duplicate_check':
// Controle "dossard en double" dans l'evenement (avant enregistrement legacy).
// Ne modifie rien : renvoie qui porte deja ce dossard + la politique a appliquer.
$intParId = intval($_REQUEST['par_id'] ?? 0);
$strNoBib = trim((string)($_REQUEST['no_bib'] ?? ''));
$blnTeam = (trim((string)($_REQUEST['type'] ?? '')) === 'no_equipe');
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.bib_edit')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
$strPolicy = fxInscrGestionBibDuplicatePolicy($intEveId);
if ($strNoBib === '') {
$tabRetour = array('state' => 'ok', 'duplicate' => false, 'policy' => $strPolicy, 'who' => array());
break;
}
// Commande courante a exclure du controle (un participant ne se "double" pas lui-meme).
$intPecOrig = intval($objDatabase->fxGetVar(
'SELECT e.pec_id_original FROM resultats_epreuves_commandees e, resultats_participants p'
. ' WHERE p.par_id = ' . $intParId . ' AND p.pec_id = e.pec_id_original LIMIT 1'
));
$intCurrentEprId = (int)$objDatabase->fxGetVar(
'SELECT epr_id FROM resultats_participants WHERE par_id = ' . $intParId . ' LIMIT 1'
);
$arrDup = fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intPecOrig, $blnTeam, $intCurrentEprId, $intParId);
$arrWho = array();
foreach ($arrDup as $rowDup) {
$arrWho[] = array(
'name' => fxInscrGestionEsc(fxUnescape($rowDup['par_nom']) . ', ' . fxUnescape($rowDup['par_prenom'])),
'epreuve' => fxInscrGestionEsc(fxInscrGestionEpreuveLabel($rowDup['epr_id'], $strLangue)),
);
}
$tabRetour = array(
'state' => 'ok',
'duplicate' => count($arrWho) > 0,
'policy' => $strPolicy,
'who' => $arrWho,
);
break;
case 'transaction_resume':
// Resume de la transaction (meme rendu que facture.php cote promoteur) + detail des
// remboursements PayPal, affichable a tout moment via le panneau repliable de la barre Gestion.
$strNoPanier = trim((string)($_REQUEST['no_panier'] ?? ''));
if ($strNoPanier === '') {
$tabRetour = array('state' => 'error', 'message' => 'no_panier_manquant');
break;
}
// Commande complete (meme source que refund_form / Super Admin) : alimente fxRefundComputeContext.
$sqlCmd = "SELECT *, IF(a.com_id <> 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager"
. " FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s"
. " WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "'";
$recCommandes = $objDatabase->fxGetRow($sqlCmd);
if (!$recCommandes) {
$tabRetour = array('state' => 'error', 'message' => 'commande_introuvable');
break;
}
// L'eve_id fait foi cote serveur (jamais le client). Droit granulaire transaction.
$intEveId = intval($recCommandes['eve_id']);
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.transaction')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/inc_fx_panier.php';
$strResume = fxShowPanier($strNoPanier, 0, 'web', 1, $strLangue);
// Sommaire des remboursements PayPal (vue compacte, pas la grande table Super Admin).
// N'apparait que s'il existe une capture PayPal exploitable ; sinon rien (les cas
// gratuits/non-PayPal sont deja couverts par la facture ci-dessus).
$strRefunds = '';
require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php';
$arrRefundCtx = fxRefundComputeContext($recCommandes);
if (!empty($arrRefundCtx['ok'])) {
// renderPayPalRefundSummaryHtml() renvoie '' s'il n'y a aucun remboursement :
// dans ce cas on n'ajoute meme pas le separateur/encadre (rien a montrer).
$strSummary = renderPayPalRefundSummaryHtml($arrRefundCtx['paypaldetails']);
if (trim($strSummary) !== '') {
$strRefunds = '<div style="margin-top:14px;border-top:1px solid #cfe0ff;padding-top:12px;">'
. $strSummary
. '</div>';
}
}
// Conteneur encadre (sans bascule interne : le bouton de la barre Gestion fait office de toggle).
$strHtml =
'<div class="ms1-transaction-resume"'
. ' style="border:1px solid #2d6cdf;border-radius:10px;background:#f4f7fc;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.12);">'
. '<div style="padding:14px;">' . $strResume . $strRefunds . '</div>'
. '</div>';
$tabRetour = array('state' => 'ok', 'html' => $strHtml);
break;
case 'fiche_audit_history':
// MSIN-4464 — derniers changements d'un champ whitelist (popup « ! » fiche).
require_once 'php/inc_fx_fiche_audit.php';
$intParId = intval($_REQUEST['par_id'] ?? 0);
$strField = preg_replace('/[^a-z0-9_]/i', '', (string)($_REQUEST['field'] ?? ''));
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
if ($intParId <= 0 || $strField === '' || $intEveId <= 0
|| !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.audit_view')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
$arrItems = fxFicheAuditGetRecentForField($intParId, $strField);
$arrOut = array();
if (is_array($arrItems)) {
foreach ($arrItems as $row) {
if (!is_array($row)) {
continue;
}
$arrOut[] = array(
'when' => (string)($row['created_at'] ?? ''),
'who' => (string)($row['changed_by_label'] ?? ''),
'old' => (string)($row['old_value'] ?? ''),
'new' => (string)($row['new_value'] ?? ''),
'source' => (string)($row['source'] ?? ''),
);
}
}
$tabRetour = array(
'state' => 'success',
'field' => $strField,
'items' => $arrOut,
);
break;
default:
$tabRetour = array('state' => 'error', 'message' => 'invalid_action');
break;
}
} catch (\Throwable $e) {
error_log('[ajax_inscr_gestion] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
$tabRetour = array(
'state' => 'error',
'message' => 'exception',
'detail' => $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine(),
);
}
$GLOBALS['__inscr_gestion_done'] = true;
echo json_encode($tabRetour);