Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+310
View File
@@ -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>