Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
// /public/choisir_abonnement.php
|
||||
require_once __DIR__ . '/includes/init_praticien.php';
|
||||
|
||||
$praticien_id = $_SESSION['reference_id'] ?? 0;
|
||||
|
||||
// CSRF
|
||||
if (empty($_SESSION['csrf_abon'])) {
|
||||
$_SESSION['csrf_abon'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
$CSRF = $_SESSION['csrf_abon'];
|
||||
|
||||
// Abonnement gratuit (id=1)
|
||||
$FREE_PLAN_ID = 1;
|
||||
|
||||
// Abonnements éligibles au renouvellement automatique (Stripe subscription)
|
||||
$AUTO_ELIGIBLE_IDS = [3, 4, 5]; // mensuel, annuel, pack pro (id=5)
|
||||
|
||||
// Déjà utilisé ?
|
||||
$hasUsedFree = false;
|
||||
if ($praticien_id) {
|
||||
$sql = "SELECT 1
|
||||
FROM paiement
|
||||
WHERE praticien_id = ?
|
||||
AND abonnement_id = ?
|
||||
AND (statut IS NULL OR statut <> 'échoué')
|
||||
LIMIT 1";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([$praticien_id, $FREE_PLAN_ID]);
|
||||
$hasUsedFree = (bool)$st->fetchColumn();
|
||||
}
|
||||
|
||||
// Récupérer les abonnements actifs (sauf plan 2 promo si actif=0)
|
||||
$stmt = $pdo->query("SELECT * FROM abonnement WHERE actif = 1 ORDER BY prix ASC, duree_jours ASC");
|
||||
$abonnements = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Si essai gratuit utilisé → retirer l'id=1
|
||||
if ($hasUsedFree) {
|
||||
$abonnements = array_values(array_filter($abonnements, function($ab){
|
||||
return (int)$ab['id'] !== 1;
|
||||
}));
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title><?= htmlspecialchars($translations['seo_choisir_abonnement_title'] ?? 'Тарифы для практикующего — Услуги.фр') ?></title>
|
||||
<meta name="description" content="<?= htmlspecialchars($translations['seo_choisir_abonnement_desc'] ?? '') ?>">
|
||||
<meta name="keywords" content="<?= htmlspecialchars($translations['seo_choisir_abonnement_keywords'] ?? '') ?>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--brand-blue:#38378B;
|
||||
--brand-red:#E03C3F;
|
||||
--page-blue:#8CA9FF;
|
||||
--border:#e5e7eb;
|
||||
--radius:18px;
|
||||
--shadow:0 8px 20px rgba(0,0,0,.06);
|
||||
}
|
||||
body {
|
||||
background:var(--page-blue);
|
||||
font-family:'Montserrat',sans-serif;
|
||||
color:#111;
|
||||
}
|
||||
.card { border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
|
||||
.card-title { color:var(--brand-blue); font-weight:700; }
|
||||
.btn-brand { background:var(--brand-red); border-color:var(--brand-red); color:#fff; border-radius:999px; font-weight:600; }
|
||||
.btn-brand:hover { background:var(--brand-red); border-color:var(--brand-red); color:#fff; border-radius:999px; font-weight:600; }
|
||||
|
||||
/* Bouton principal (CONFIRMER) */
|
||||
.swal2-confirm {
|
||||
background-color: #E03C3F !important;
|
||||
border-radius: 999px !important;
|
||||
padding: 10px 28px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
}
|
||||
.swal2-confirm:hover {
|
||||
background-color: #E03C3F !important;
|
||||
}
|
||||
|
||||
/* Bouton secondaire (ANNULER) */
|
||||
.swal2-cancel {
|
||||
background-color: #38378B !important; /* bleu foncé */
|
||||
border-radius: 999px !important;
|
||||
padding: 10px 28px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
}
|
||||
.swal2-cancel:hover {
|
||||
background-color: #38378B !important;
|
||||
}
|
||||
|
||||
/* Champ texte */
|
||||
.swal2-input {
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid #d1d5db !important;
|
||||
padding: 10px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
/* Fenêtre principale */
|
||||
.swal2-popup {
|
||||
border-radius: 18px !important;
|
||||
padding: 24px !important;
|
||||
font-family: 'Montserrat', sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
|
||||
<h2 class="text-center mb-4" style="color:#fff;">
|
||||
<?= htmlspecialchars($translations['choisissez_votre_abonnement'] ?? 'Выберите ваш тариф') ?>
|
||||
</h2>
|
||||
|
||||
<!-- Choix mode de paiement -->
|
||||
<div class="d-flex justify-content-center mb-4">
|
||||
<div class="btn-group" role="group" aria-label="<?= htmlspecialchars($translations['mode_paiement'] ?? 'Mode de paiement') ?>">
|
||||
<input type="radio" class="btn-check" name="billing_mode_ui" id="bm_manual" value="manual">
|
||||
<!--<label class="btn btn-outline-light" for="bm_manual">
|
||||
<?= htmlspecialchars($translations['paiement_ponctuel'] ?? 'Paiement ponctuel') ?>
|
||||
</label>-->
|
||||
|
||||
<input type="radio" class="btn-check" name="billing_mode_ui" id="bm_auto" value="auto" checked>
|
||||
<label class="btn btn-outline-light" for="bm_auto">
|
||||
<?= htmlspecialchars($translations['renouvellement_auto'] ?? 'Renouvellement automatique') ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$abonnements): ?>
|
||||
<div class="alert alert-warning text-center">Нет доступных тарифов.</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-4 justify-content-center">
|
||||
|
||||
<?php foreach ($abonnements as $ab): ?>
|
||||
<?php
|
||||
$isFree = ((float)$ab['prix'] == 0.0);
|
||||
$btnLabel = $isFree
|
||||
? ($translations['essai_gratuit'] ?? 'Бесплатно')
|
||||
: ($translations['btn_choisir_abonnement'] ?? 'Выбрать');
|
||||
|
||||
$prixTxt = $isFree
|
||||
? ($translations['gratuit'] ?? 'Бесплатно')
|
||||
: number_format((float)$ab['prix'], 2, ',', ' ') . ' €';
|
||||
|
||||
$nomKey = trim((string)($ab['nom'] ?? ''));
|
||||
$descKey = trim((string)($ab['description'] ?? ''));
|
||||
|
||||
// Si la clé existe dans $translations → on affiche la traduction, sinon on affiche brut (fallback)
|
||||
$nomTxt = $translations[$nomKey] ?? $nomKey;
|
||||
$descTxt = $translations[$descKey] ?? $descKey;
|
||||
|
||||
$isAutoEligible = in_array((int)$ab['id'], $AUTO_ELIGIBLE_IDS, true);
|
||||
?>
|
||||
<div class="col-12 col-sm-10 col-md-6 col-lg-4 col-xl-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title"><?= htmlspecialchars($nomTxt) ?></h5>
|
||||
|
||||
<?php if ($descKey !== ''): ?>
|
||||
<p class="card-text"><?= nl2br(htmlspecialchars($descTxt)) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="fw-bold mb-1">
|
||||
<?= $translations['col_duree'] ?? 'Длительность:' ?>
|
||||
<?= (int)$ab['duree_jours'] ?>
|
||||
<?= $translations['dnei'] ?? 'дней' ?>
|
||||
</p>
|
||||
|
||||
<p class="fw-bold mb-3"><?= $prixTxt ?></p>
|
||||
|
||||
<form method="POST"
|
||||
action="/public/souscrire_abonnement.php"
|
||||
class="mt-auto js-abo-form"
|
||||
data-abo-id="<?= (int)$ab['id'] ?>"
|
||||
data-auto-eligible="<?= $isAutoEligible ? '1' : '0' ?>">
|
||||
|
||||
<input type="hidden" name="csrf" value="<?= htmlspecialchars($CSRF) ?>">
|
||||
<input type="hidden" name="abonnement_id" value="<?= (int)$ab['id'] ?>">
|
||||
<input type="hidden" name="billing_mode" value="manual">
|
||||
<input type="hidden" name="promo_code" value="">
|
||||
|
||||
<button type="submit" class="btn btn-brand w-100"><?= htmlspecialchars($btnLabel) ?></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const I18N_PROMO = {
|
||||
title: <?= json_encode($translations['promo_title'] ?? 'Promo') ?>,
|
||||
placeholder: <?= json_encode($translations['promo_placeholder'] ?? '') ?>,
|
||||
confirm: <?= json_encode($translations['promo_confirm'] ?? 'OK') ?>,
|
||||
cancel: <?= json_encode($translations['promo_cancel'] ?? 'Cancel') ?>,
|
||||
|
||||
autoIndispoTitle: <?= json_encode($translations['auto_indispo_title'] ?? 'Non disponible') ?>,
|
||||
autoIndispoText: <?= json_encode($translations['auto_indispo_text'] ?? "Le renouvellement automatique n'est disponible que pour les offres mensuelle/annuelle.") ?>,
|
||||
promoAutoNotSupported: <?= json_encode($translations['promo_auto_not_supported'] ?? "La promo n'est pas disponible en renouvellement automatique. Utilisez le paiement ponctuel pour cette offre.") ?>
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// ------------------------
|
||||
// SweetAlert PROMO LOGIC + Billing mode
|
||||
// ------------------------
|
||||
|
||||
function getSelectedBillingMode() {
|
||||
const el = document.querySelector('input[name="billing_mode_ui"]:checked');
|
||||
return el ? el.value : 'manual';
|
||||
}
|
||||
|
||||
document.querySelectorAll(".js-abo-form").forEach(function(form){
|
||||
form.addEventListener("submit", function(e){
|
||||
const aboId = parseInt(form.dataset.aboId, 10);
|
||||
const selectedMode = getSelectedBillingMode();
|
||||
const eligibleAuto = (form.dataset.autoEligible === '1');
|
||||
|
||||
// Pousser le mode choisi dans le champ hidden (important avant form.submit())
|
||||
const bmInput = form.querySelector('input[name="billing_mode"]');
|
||||
if (bmInput) bmInput.value = selectedMode;
|
||||
|
||||
// Si auto choisi mais offre non éligible -> bloquer
|
||||
if (selectedMode === 'auto' && !eligibleAuto) {
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: I18N_PROMO.autoIndispoTitle,
|
||||
text: I18N_PROMO.autoIndispoText,
|
||||
confirmButtonText: I18N_PROMO.confirm
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Promo uniquement pour l’abonnement PROFESSIONNEL (ID = 5)
|
||||
if (aboId !== 5) return; // laisser fonctionner normalement
|
||||
|
||||
// Si l'utilisateur est en "auto", on bloque la promo (car promo bascule sur ID=2)
|
||||
// et ID=2 n'est pas (encore) configuré avec un Price récurrent.
|
||||
if (selectedMode === 'auto') {
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
icon: 'info',
|
||||
title: I18N_PROMO.title,
|
||||
text: I18N_PROMO.promoAutoNotSupported,
|
||||
confirmButtonText: I18N_PROMO.confirm
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
Swal.fire({
|
||||
title: I18N_PROMO.title,
|
||||
input: 'text',
|
||||
inputPlaceholder: I18N_PROMO.placeholder,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: I18N_PROMO.confirm,
|
||||
cancelButtonText: I18N_PROMO.cancel
|
||||
}).then(function(result){
|
||||
|
||||
// Annulé -> pas de code → abonnement 5
|
||||
if (result.isDismissed) {
|
||||
form.submit();
|
||||
return;
|
||||
}
|
||||
|
||||
const code = (result.value || '').trim();
|
||||
|
||||
// Le code correct
|
||||
const CODE_VALID = "uslugiW@W2026";
|
||||
|
||||
if (code === CODE_VALID) {
|
||||
// On remplace l’abonnement → on envoie ID=2 (promo)
|
||||
form.querySelector('input[name="abonnement_id"]').value = 2;
|
||||
// On garde en mémoire le code (optionnel)
|
||||
form.querySelector('input[name="promo_code"]').value = code;
|
||||
|
||||
// Sécurité : on force le mode à "manual" car ID=2 n'a pas de price récurrent configuré
|
||||
const bmInput2 = form.querySelector('input[name="billing_mode"]');
|
||||
if (bmInput2) bmInput2.value = 'manual';
|
||||
|
||||
form.submit();
|
||||
} else {
|
||||
// Code faux → abonnement normal
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user