Description
Découvrez l’ELITE 9 en vidéo
<?php
/**
* ============================================================
* MOOVIKA — Système de prise de rendez-vous (EDR Auto)
* ============================================================
* À coller dans le plugin "Code Snippets" (PHP, "Exécuter partout").
* >>> UN SEUL snippet RDV actif à la fois (sinon erreur fatale). <<<
*
* Shortcode :
* [rdv_calendrier] (formulaire + calendrier)
* [rdv_calendrier product_id="123"] (+ ajout panier WooCommerce)
*
* Admin (menu "Rendez-vous") :
* - liste, modification et création de RDV (dates passées OK)
* - mode vacances / congés
* E-mails : confirmation au client + notification admin + rappel J-1.
*
* >>> CONFIG (horaires, durée, jours) : moovika_rdv_config() <<<
* ============================================================
*/
if (!defined('ABSPATH')) exit;
/* ------------------------------------------------------------
* 0) CONFIGURATION CENTRALE + HELPERS
* ---------------------------------------------------------- */
function moovika_rdv_config() {
return array(
'open' => '08:00',
'close' => '13:00',
'duration' => 180, // minutes (3 h)
'step' => 60, // pas entre deux débuts
'open_days' => array(3, 4, 5, 6), // mer..sam (date('N'))
);
}
function moovika_rdv_to_min($hhmm) { $p = explode(':', $hhmm); return ((int)$p[0]) * 60 + ((int)($p[1] ?? 0)); }
function moovika_rdv_to_hhmm($min) { return sprintf('%02d:%02d', floor($min / 60), $min % 60); }
function moovika_rdv_slots() {
$c = moovika_rdv_config();
$open = moovika_rdv_to_min($c['open']);
$close = moovika_rdv_to_min($c['close']);
$slots = array();
for ($t = $open; $t + $c['duration'] <= $close; $t += $c['step']) {
$slots[] = moovika_rdv_to_hhmm($t);
}
return $slots;
}
function moovika_rdv_duration_label() {
$d = moovika_rdv_config()['duration'];
$h = floor($d / 60); $m = $d % 60;
return $m ? ($h . ' h ' . $m) : ($h . ' h');
}
function moovika_rdv_format_fr($date) {
$ts = strtotime($date);
$jours = array('Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi');
$mois = array('', 'janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre');
return $jours[(int)date('w', $ts)] . ' ' . (int)date('j', $ts) . ' ' . $mois[(int)date('n', $ts)] . ' ' . date('Y', $ts);
}
function moovika_rdv_mail_headers() {
return array(
'Content-Type: text/plain; charset=UTF-8',
'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>',
'Reply-To: ' . get_option('admin_email'),
);
}
/* Périodes de congés */
function moovika_rdv_get_vacances() {
$v = get_option('moovika_rdv_vacances', array());
return is_array($v) ? array_values($v) : array();
}
function moovika_rdv_in_vacances($date) {
foreach (moovika_rdv_get_vacances() as $p) {
if ($date >= $p['start'] && $date <= $p['end']) return true;
}
return false;
}
/* ------------------------------------------------------------
* 1) Table (v1.2 : ajout email + reminder_sent)
* ---------------------------------------------------------- */
add_action('init', 'moovika_rdv_create_table');
function moovika_rdv_create_table() {
if (get_option('moovika_rdv_db_version') === '1.2') return;
global $wpdb;
$table = $wpdb->prefix . 'moovika_rdv';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL DEFAULT '',
adresse VARCHAR(255) NOT NULL,
telephone VARCHAR(30) NOT NULL,
vehicule_marque VARCHAR(100) NOT NULL,
vehicule_modele VARCHAR(100) NOT NULL,
motorisation VARCHAR(20) NOT NULL,
date_rdv DATE NOT NULL,
heure_rdv VARCHAR(10) NOT NULL,
reminder_sent TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slot (date_rdv, heure_rdv)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
update_option('moovika_rdv_db_version', '1.2');
}
/* ------------------------------------------------------------
* 2) AJAX public : lecture (3 lettres) + congés
* ---------------------------------------------------------- */
add_action('wp_ajax_moovika_get_rdv', 'moovika_get_rdv');
add_action('wp_ajax_nopriv_moovika_get_rdv', 'moovika_get_rdv');
function moovika_get_rdv() {
global $wpdb;
$table = $wpdb->prefix . 'moovika_rdv';
$rows = $wpdb->get_results("SELECT date_rdv, heure_rdv, nom FROM $table ORDER BY date_rdv, heure_rdv", ARRAY_A);
$appts = array();
foreach ((array) $rows as $r) {
$appts[] = array(
'date' => $r['date_rdv'],
'heure' => $r['heure_rdv'],
'label' => mb_strtoupper(mb_substr($r['nom'], 0, 3)),
);
}
wp_send_json_success(array('appts' => $appts, 'vacances' => moovika_rdv_get_vacances()));
}
/* 2bis) AJAX détails complets (ADMIN UNIQUEMENT) */
add_action('wp_ajax_moovika_rdv_detail', 'moovika_rdv_detail');
function moovika_rdv_detail() {
check_ajax_referer('moovika_rdv_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Accès refusé.'));
}
global $wpdb;
$table = $wpdb->prefix . 'moovika_rdv';
$date = sanitize_text_field($_POST['date'] ?? '');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
wp_send_json_error(array('message' => 'Date invalide.'));
}
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT nom, prenom, email, adresse, telephone, vehicule_marque, vehicule_modele, motorisation, heure_rdv
FROM $table WHERE date_rdv = %s ORDER BY heure_rdv", $date
), ARRAY_A);
wp_send_json_success($rows ? $rows : array());
}
/* ------------------------------------------------------------
* 3) AJAX public : enregistrement + e-mails
* ---------------------------------------------------------- */
add_action('wp_ajax_moovika_save_rdv', 'moovika_save_rdv');
add_action('wp_ajax_nopriv_moovika_save_rdv', 'moovika_save_rdv');
function moovika_save_rdv() {
check_ajax_referer('moovika_rdv_nonce', 'nonce');
global $wpdb;
$table = $wpdb->prefix . 'moovika_rdv';
$cfg = moovika_rdv_config();
$nom = sanitize_text_field($_POST['nom'] ?? '');
$prenom = sanitize_text_field($_POST['prenom'] ?? '');
$email = sanitize_email($_POST['email'] ?? '');
$adresse = sanitize_text_field($_POST['adresse'] ?? '');
$tel = sanitize_text_field($_POST['telephone'] ?? '');
$marque = sanitize_text_field($_POST['marque'] ?? '');
$modele = sanitize_text_field($_POST['modele'] ?? '');
$motor = sanitize_text_field($_POST['motorisation'] ?? '');
$date = sanitize_text_field($_POST['date'] ?? '');
$heure = sanitize_text_field($_POST['heure'] ?? '');
$product = intval($_POST['product_id'] ?? 0);
if (!$nom || !$prenom || !$email || !$adresse || !$tel || !$marque || !$modele || !$motor || !$date || !$heure) {
wp_send_json_error(array('message' => 'Tous les champs sont obligatoires.'));
}
if (!is_email($email)) {
wp_send_json_error(array('message' => 'Adresse e-mail invalide.'));
}
if (!in_array($motor, array('thermique', 'hybride', 'electrique'), true)) {
wp_send_json_error(array('message' => 'Motorisation invalide.'));
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
wp_send_json_error(array('message' => 'Date invalide.'));
}
if (strtotime($date) < strtotime(date('Y-m-d'))) {
wp_send_json_error(array('message' => 'Impossible de réserver une date passée.'));
}
if (!in_array((int) date('N', strtotime($date)), $cfg['open_days'], true)) {
wp_send_json_error(array('message' => 'Les installations ont lieu du mercredi au samedi uniquement.'));
}
if (moovika_rdv_in_vacances($date)) {
wp_send_json_error(array('message' => 'Cette date correspond à une période de congés.'));
}
if (!in_array($heure, moovika_rdv_slots(), true)) {
wp_send_json_error(array('message' => 'Créneau horaire invalide.'));
}
// Chevauchement
$dur = (int) $cfg['duration'];
$reqStart = moovika_rdv_to_min($heure);
$existing = $wpdb->get_col($wpdb->prepare("SELECT heure_rdv FROM $table WHERE date_rdv = %s", $date));
foreach ((array) $existing as $e) {
$es = moovika_rdv_to_min($e);
if ($reqStart < $es + $dur && $es < $reqStart + $dur) {
wp_send_json_error(array('message' => 'Ce créneau chevauche un rendez-vous existant.'));
}
}
$row = array(
'nom' => $nom,
'prenom' => $prenom,
'email' => $email,
'adresse' => $adresse,
'telephone' => $tel,
'vehicule_marque' => $marque,
'vehicule_modele' => $modele,
'motorisation' => $motor,
'date_rdv' => $date,
'heure_rdv' => $heure,
'reminder_sent' => 0,
);
$ok = $wpdb->insert($table, $row, array('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%d'));
if ($ok === false) {
wp_send_json_error(array('message' => 'Erreur lors de l\'enregistrement.'));
}
// E-mail de confirmation au client
moovika_rdv_send_client_mail($row, 'confirmation');
// Notification à l'administrateur
$admin_email = get_option('admin_email');
if ($admin_email) {
$subject = 'Nouveau RDV installation — ' . $date . ' ' . $heure;
$body = "Nouveau rendez-vous d'installation :\n\n";
$body .= "Date : $date à $heure (durée " . moovika_rdv_duration_label() . ")\n";
$body .= "Client : $prenom $nom\n";
$body .= "E-mail : $email\n";
$body .= "Téléphone: $tel\n";
$body .= "Adresse : $adresse\n";
$body .= "Véhicule : $marque $modele (" . ucfirst($motor) . ")\n";
wp_mail($admin_email, $subject, $body, moovika_rdv_mail_headers());
}
// WooCommerce (optionnel)
$redirect = '';
if ($product > 0 && function_exists('WC') && WC()->cart) {
WC()->cart->add_to_cart($product);
$redirect = wc_get_checkout_url();
}
wp_send_json_success(array(
'message' => 'Rendez-vous confirmé ! Un e-mail de confirmation vous a été envoyé.',
'redirect' => $redirect,
));
}
/* E-mail client : type = 'confirmation' ou 'rappel' */
function moovika_rdv_send_client_mail($r, $type) {
if (empty($r['email']) || !is_email($r['email'])) return;
$dateFr = moovika_rdv_format_fr($r['date_rdv']);
if ($type === 'rappel') {
$subject = 'Rappel — votre rendez-vous demain à ' . $r['heure_rdv'];
$intro = "Petit rappel : votre rendez-vous a lieu demain.";
} else {
$subject = 'Confirmation de votre rendez-vous — ' . $r['date_rdv'] . ' à ' . $r['heure_rdv'];
$intro = "Votre rendez-vous est bien confirmé.";
}
$body = "Bonjour " . $r['prenom'] . ",\n\n";
$body .= $intro . "\n\n";
$body .= "Date : " . $dateFr . " à " . $r['heure_rdv'] . "\n";
$body .= "Durée : " . moovika_rdv_duration_label() . "\n";
$body .= "Véhicule : " . $r['vehicule_marque'] . " " . $r['vehicule_modele'] . "\n\n";
$body .= "Lieu de l'installation :\n";
$body .= "MOOVIKA SARL\n";
$body .= "6 Allée Rodolphe Piguet, 77400 Lagny-sur-Marne\n\n";
$body .= "Pour toute modification, répondez simplement à cet e-mail.\n\n";
$body .= "À bientôt,\n" . get_bloginfo('name');
$headers = moovika_rdv_mail_headers();
if ($type === 'confirmation') {
$headers[] = 'Cc: contact@moovika.fr';
}
wp_mail($r['email'], $subject, $body, $headers);
}
/* ------------------------------------------------------------
* 3bis) Rappel automatique J-1 (WP-Cron quotidien)
* ---------------------------------------------------------- */
add_action('init', function () {
if (!wp_next_scheduled('moovika_rdv_reminder_cron')) {
wp_schedule_event(time(), 'daily', 'moovika_rdv_reminder_cron');
}
});
add_action('moovika_rdv_reminder_cron', 'moovika_rdv_send_reminders');
function moovika_rdv_send_reminders() {
global $wpdb;
$table = $wpdb->prefix . 'moovika_rdv';
$tomorrow = date('Y-m-d', strtotime('+1 day'));
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table WHERE date_rdv = %s AND reminder_sent = 0 AND email <> ''", $tomorrow
), ARRAY_A);
foreach ((array) $rows as $r) {
moovika_rdv_send_client_mail($r, 'rappel');
$wpdb->update($table, array('reminder_sent' => 1), array('id' => $r['id']), array('%d'), array('%d'));
}
}
/* ------------------------------------------------------------
* 4) Shortcode [rdv_calendrier]
* ---------------------------------------------------------- */
add_shortcode('rdv_calendrier', 'moovika_rdv_shortcode');
function moovika_rdv_shortcode($atts) {
$atts = shortcode_atts(array('product_id' => 0), $atts);
$cfg = moovika_rdv_config();
$config = '<script>window.MOOVIKA_RDV = ' . wp_json_encode(array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('moovika_rdv_nonce'),
'product_id' => intval($atts['product_id']),
'isAdmin' => current_user_can('manage_options'),
'slots' => moovika_rdv_slots(),
'durationMin' => intval($cfg['duration']),
'openDays' => array_values($cfg['open_days']),
'durationLabel' => moovika_rdv_duration_label(),
)) . ';</script>';
$html = <<<'HTML'
<style>
#mvk-rdv-app{max-width:760px;margin:0 auto;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#1f2933;}
#mvk-rdv-app *{box-sizing:border-box;}
#mvk-rdv-app .mvk-intro{background:#eef0f7;border:1px solid #d7dcef;border-left:4px solid #374084;border-radius:10px;padding:14px 18px;margin-bottom:22px;font-size:.92rem;line-height:1.55;}
#mvk-rdv-app .mvk-intro h4{margin:0 0 8px;color:#374084;font-size:1.05rem;}
#mvk-rdv-app .mvk-intro ul{margin:0;padding-left:20px;}
#mvk-rdv-app .mvk-intro li{margin-bottom:6px;}
#mvk-rdv-app .mvk-intro strong{color:#374084;}
#mvk-rdv-app .mvk-intro .mvk-warn{color:#b25b00;font-weight:600;}
#mvk-rdv-app .mvk-cal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;}
#mvk-rdv-app .mvk-title{font-size:1.25rem;font-weight:700;text-transform:capitalize;}
#mvk-rdv-app .mvk-nav{background:linear-gradient(135deg,#3f4992,#374084);color:#fff;border:none;width:38px;height:38px;border-radius:8px;font-size:1.3rem;line-height:1;cursor:pointer;}
#mvk-rdv-app .mvk-nav:hover{background:#2c3468;}
#mvk-rdv-app .mvk-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;}
#mvk-rdv-app .mvk-dow-cell{text-align:center;font-weight:600;font-size:.8rem;color:#7b8794;padding:4px 0;}
#mvk-rdv-app .mvk-day{min-height:84px;border:1px solid #e4e7eb;border-radius:8px;padding:5px;background:#fff;position:relative;transition:.12s;}
#mvk-rdv-app .mvk-day[data-date]{cursor:pointer;}
#mvk-rdv-app .mvk-day[data-date]:hover{border-color:#374084;box-shadow:0 2px 8px rgba(55,64,132,.15);}
#mvk-rdv-app .mvk-empty{background:transparent;border:none;}
#mvk-rdv-app .mvk-past{background:#f5f7fa;color:#cbd2d9;}
#mvk-rdv-app .mvk-closed{background:#f5f7fa;color:#cbd2d9;cursor:not-allowed;}
#mvk-rdv-app .mvk-vacday{background:#fff8e1;border-color:#f3d27a;cursor:not-allowed;}
#mvk-rdv-app .mvk-full{cursor:not-allowed;background:#f5f7fa;}
#mvk-rdv-app .mvk-today .mvk-daynum{background:#374084;color:#fff;border-radius:50%;width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;}
#mvk-rdv-app .mvk-daynum{font-size:.8rem;font-weight:600;}
#mvk-rdv-app .mvk-badges{margin-top:4px;display:flex;flex-direction:column;gap:3px;}
#mvk-rdv-app .mvk-badge{background:linear-gradient(135deg,#404a93,#374084);color:#fff;font-size:.68rem;font-weight:600;border-radius:4px;padding:2px 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
#mvk-rdv-app .mvk-vac{display:inline-block;background:#e0a800;color:#fff;font-size:.62rem;font-weight:600;border-radius:4px;padding:2px 4px;}
#mvk-rdv-app .mvk-modal{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:99999;padding:16px;}
#mvk-rdv-app .mvk-modal[hidden]{display:none;}
#mvk-rdv-app .mvk-modal-box{background:#fff;border-radius:14px;padding:24px;max-width:440px;width:100%;max-height:90vh;overflow:auto;position:relative;}
#mvk-rdv-app .mvk-modal-box h3{margin:0 0 4px;font-size:1.2rem;}
#mvk-rdv-app .mvk-modal-date{margin:0 0 2px;color:#374084;font-weight:600;text-transform:capitalize;}
#mvk-rdv-app .mvk-duree{margin:0 0 16px;font-size:.8rem;color:#7b8794;}
#mvk-rdv-app .mvk-close,#mvk-rdv-app .mvk-close-detail{position:absolute;top:12px;right:14px;border:none;background:none;font-size:1.6rem;line-height:1;cursor:pointer;color:#7b8794;}
#mvk-rdv-app .mvk-field{margin-bottom:12px;}
#mvk-rdv-app .mvk-field label{display:block;font-size:.82rem;font-weight:600;margin-bottom:4px;}
#mvk-rdv-app .mvk-field input,#mvk-rdv-app .mvk-field select{width:100%;padding:9px 11px;border:1px solid #cbd2d9;border-radius:8px;font-size:.92rem;}
#mvk-rdv-app .mvk-field input:focus,#mvk-rdv-app .mvk-field select:focus{outline:none;border-color:#374084;}
#mvk-rdv-app .mvk-row{display:flex;gap:12px;}
#mvk-rdv-app .mvk-row .mvk-field{flex:1;}
#mvk-rdv-app .mvk-submit{width:100%;background:linear-gradient(135deg,#3f4992,#374084);color:#fff;border:none;padding:12px;border-radius:8px;font-size:1rem;font-weight:700;cursor:pointer;margin-top:4px;}
#mvk-rdv-app .mvk-submit:hover{background:#2c3468;}
#mvk-rdv-app .mvk-submit:disabled{opacity:.6;cursor:not-allowed;}
#mvk-rdv-app .mvk-msg{padding:9px 11px;border-radius:8px;font-size:.85rem;margin-bottom:12px;}
#mvk-rdv-app .mvk-msg.ok{background:#e3f9e5;color:#207227;}
#mvk-rdv-app .mvk-msg.err{background:#ffeeee;color:#374084;}
#mvk-rdv-app .mvk-detail-card{border:1px solid #e4e7eb;border-radius:10px;padding:14px 16px;margin-bottom:12px;font-size:.92rem;line-height:1.7;}
#mvk-rdv-app .mvk-detail-card strong{color:#52606d;font-weight:600;}
#mvk-rdv-app .mvk-detail-card a{color:#374084;font-weight:600;text-decoration:none;}
@media(max-width:520px){#mvk-rdv-app .mvk-day{min-height:62px;}#mvk-rdv-app .mvk-badge{font-size:.6rem;}}
</style>
<div id="mvk-rdv-app">
<div class="mvk-intro">
<h4>Réservez votre créneau d'installation</h4>
<ul>
<li>Sélectionnez un jour disponible dans le calendrier, puis remplissez le formulaire pour réserver votre rendez-vous.</li>
<li>Le <strong>paiement de l'installation</strong> peut se faire directement en ligne via la boutique, ou sur place par carte bancaire après l'intervention.</li>
<li class="mvk-warn">⚠️ L'achat de votre dashcam BlackVue doit être effectué <u>avant</u> la prise de rendez-vous.</li>
</ul>
</div>
<div class="mvk-cal"></div>
<div class="mvk-modal" hidden>
<div class="mvk-modal-box">
<button type="button" class="mvk-close" aria-label="Fermer">×</button>
<h3>Prendre rendez-vous</h3>
<p class="mvk-modal-date"></p>
<p class="mvk-duree"></p>
<div class="mvk-msg" hidden></div>
<div class="mvk-row">
<div class="mvk-field"><label>Nom</label><input type="text" id="mvk-nom" autocomplete="family-name"></div>
<div class="mvk-field"><label>Prénom</label><input type="text" id="mvk-prenom" autocomplete="given-name"></div>
</div>
<div class="mvk-field"><label>E-mail</label><input type="email" id="mvk-email" autocomplete="email" inputmode="email"></div>
<div class="mvk-field"><label>Téléphone</label><input type="tel" id="mvk-telephone" autocomplete="tel" inputmode="tel"></div>
<div class="mvk-field"><label>Adresse</label><input type="text" id="mvk-adresse" autocomplete="street-address"></div>
<div class="mvk-row">
<div class="mvk-field"><label>Marque</label><input type="text" id="mvk-marque"></div>
<div class="mvk-field"><label>Modèle</label><input type="text" id="mvk-modele"></div>
</div>
<div class="mvk-field">
<label>Motorisation</label>
<select id="mvk-motor">
<option value="">— Choisir —</option>
<option value="thermique">Thermique</option>
<option value="hybride">Hybride</option>
<option value="electrique">Électrique</option>
</select>
</div>
<div class="mvk-field">
<label>Créneau horaire</label>
<select id="mvk-heure"></select>
</div>
<button type="button" class="mvk-submit">Confirmer le rendez-vous</button>
</div>
</div>
<div class="mvk-modal mvk-detail" hidden>
<div class="mvk-modal-box">
<button type="button" class="mvk-close-detail" aria-label="Fermer">×</button>
<h3>Détails du rendez-vous</h3>
<p class="mvk-modal-date mvk-detail-date"></p>
<div class="mvk-detail-body"></div>
</div>
</div>
</div>
<script>
(function(){
var CFG = window.MOOVIKA_RDV || {};
var SLOTS = CFG.slots || [];
var DUR = CFG.durationMin || 180;
var ODAYS = CFG.openDays || [3,4,5,6];
var MOIS = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
var JOURS = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'];
var app = document.getElementById('mvk-rdv-app');
if(!app) return;
var calBox = app.querySelector('.mvk-cal');
var modal = app.querySelector('.mvk-modal');
var detailModal = app.querySelector('.mvk-detail');
var view = new Date(); view.setDate(1); view.setHours(0,0,0,0);
var appts = [];
var vacances = [];
function pad(n){return (n<10?'0':'')+n;}
function ymd(d){return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate());}
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});}
function escAttr(s){return String(s==null?'':s).replace(/"/g,'"');}
function val(sel){return (app.querySelector(sel).value||'').trim();}
function toMin(t){var p=t.split(':');return (+p[0])*60+(+p[1]);}
function toHHMM(m){return pad(Math.floor(m/60))+':'+pad(m%60);}
function addMin(t,m){return toHHMM(toMin(t)+m);}
function validEmail(e){return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e);}
function apptsFor(ds){return appts.filter(function(a){return a.date===ds;});}
function isVacation(ds){return vacances.some(function(p){return ds>=p.start && ds<=p.end;});}
function frDate(ds){var p=ds.split('-');var d=new Date(p[0],p[1]-1,p[2]);return JOURS[(d.getDay()+6)%7]+' '+parseInt(p[2],10)+' '+MOIS[p[1]-1]+' '+p[0];}
function load(){
var fd=new FormData(); fd.append('action','moovika_get_rdv');
fetch(CFG.ajaxurl,{method:'POST',body:fd})
.then(function(r){return r.json();})
.then(function(j){
if(j&&j.success){ appts=j.data.appts||[]; vacances=j.data.vacances||[]; }
else { appts=[]; vacances=[]; }
render();
})
.catch(function(){ appts=[]; vacances=[]; render(); });
}
function render(){
var y=view.getFullYear(), m=view.getMonth();
var today=new Date(); today.setHours(0,0,0,0);
var h='';
h+='<div class="mvk-cal-head">';
h+='<button type="button" class="mvk-nav" data-nav="-1">‹</button>';
h+='<span class="mvk-title">'+MOIS[m]+' '+y+'</span>';
h+='<button type="button" class="mvk-nav" data-nav="1">›</button>';
h+='</div>';
h+='<div class="mvk-grid mvk-dow">';
JOURS.forEach(function(j){h+='<div class="mvk-dow-cell">'+j+'</div>';});
h+='</div>';
h+='<div class="mvk-grid mvk-days">';
var first=new Date(y,m,1);
var lead=(first.getDay()+6)%7;
for(var i=0;i<lead;i++) h+='<div class="mvk-day mvk-empty"></div>';
var dim=new Date(y,m+1,0).getDate();
for(var d=1;d<=dim;d++){
var date=new Date(y,m,d); date.setHours(0,0,0,0);
var ds=ymd(date);
var isPast=date<today;
var isOpen=ODAYS.indexOf(date.getDay())!==-1;
var isVac=isVacation(ds);
var list=apptsFor(ds);
var cls='mvk-day';
if(isPast) cls+=' mvk-past';
if(isVac){ cls+=' mvk-vacday'; } else if(!isOpen){ cls+=' mvk-closed'; }
if(ds===ymd(today)) cls+=' mvk-today';
var hasAppt=list.length>0;
var freeFuture=!isPast && isOpen && !isVac;
if(hasAppt && !CFG.isAdmin) cls+=' mvk-full';
var clickable = CFG.isAdmin ? (freeFuture || hasAppt) : (freeFuture && !hasAppt);
h+='<div class="'+cls+'"'+(clickable?' data-date="'+ds+'"':'')+'>';
h+='<span class="mvk-daynum">'+d+'</span>';
if(isVac && isOpen && !isPast){
h+='<div class="mvk-badges"><span class="mvk-vac">Congés</span></div>';
} else if(list.length){
h+='<div class="mvk-badges">';
list.forEach(function(a){ h+='<span class="mvk-badge">'+a.heure+' '+esc(a.label)+'</span>'; });
h+='</div>';
}
h+='</div>';
}
h+='</div>';
calBox.innerHTML=h;
}
function openDay(ds){
app.dataset.selDate=ds;
app.querySelector('.mvk-modal-date').textContent=frDate(ds);
var booked=apptsFor(ds).map(function(a){return toMin(a.heure);});
var free=SLOTS.filter(function(s){
var st=toMin(s);
return !booked.some(function(b){ return st < b+DUR && b < st+DUR; });
});
var sel=app.querySelector('#mvk-heure');
var submit=app.querySelector('.mvk-submit');
if(free.length===0){
sel.innerHTML='<option value="">— Complet —</option>';
submit.disabled=true;
} else {
sel.innerHTML=free.map(function(s){return '<option value="'+s+'">'+s+' → '+addMin(s,DUR)+'</option>';}).join('');
submit.disabled=false;
}
showMsg('');
modal.hidden=false;
}
function showMsg(txt,type){
var m=app.querySelector('.mvk-msg');
if(!txt){ m.hidden=true; m.textContent=''; m.className='mvk-msg'; return; }
m.hidden=false; m.textContent=txt; m.className='mvk-msg '+(type||'');
}
function clearForm(){
['#mvk-nom','#mvk-prenom','#mvk-email','#mvk-telephone','#mvk-adresse','#mvk-marque','#mvk-modele'].forEach(function(s){app.querySelector(s).value='';});
app.querySelector('#mvk-motor').value='';
}
// Détails (admin)
function renderDetail(r){
var tel=r.telephone||'', mail=r.email||'';
var s='<div class="mvk-detail-card">';
s+='<div><strong>Créneau :</strong> '+esc(r.heure_rdv)+'</div>';
s+='<div><strong>Client :</strong> '+esc(r.nom)+' '+esc(r.prenom)+'</div>';
if(mail) s+='<div><strong>E-mail :</strong> <a href="mailto:'+escAttr(mail)+'">'+esc(mail)+'</a></div>';
s+='<div><strong>Téléphone :</strong> <a href="tel:'+escAttr(tel)+'">'+esc(tel)+'</a></div>';
s+='<div><strong>Adresse :</strong> '+esc(r.adresse)+'</div>';
s+='<div><strong>Véhicule :</strong> '+esc(r.vehicule_marque)+' '+esc(r.vehicule_modele)+'</div>';
s+='<div><strong>Motorisation :</strong> '+esc(r.motorisation)+'</div>';
s+='</div>';
return s;
}
function openDetail(ds){
app.querySelector('.mvk-detail-date').textContent=frDate(ds);
var body=app.querySelector('.mvk-detail-body');
body.innerHTML='<p>Chargement…</p>';
detailModal.hidden=false;
var fd=new FormData();
fd.append('action','moovika_rdv_detail');
fd.append('nonce',CFG.nonce);
fd.append('date',ds);
fetch(CFG.ajaxurl,{method:'POST',body:fd})
.then(function(r){return r.json();})
.then(function(j){
if(j&&j.success&&j.data&&j.data.length){ body.innerHTML=j.data.map(renderDetail).join(''); }
else if(j&&j.success){ body.innerHTML='<p>Aucun détail pour cette date.</p>'; }
else { body.innerHTML='<p>'+((j&&j.data&&j.data.message)||'Accès refusé.')+'</p>'; }
})
.catch(function(){ body.innerHTML='<p>Erreur de chargement.</p>'; });
}
// Durée affichée
var dn=app.querySelector('.mvk-duree');
if(dn) dn.textContent='Durée de l\'intervention : '+(CFG.durationLabel||'');
app.addEventListener('click',function(e){
var nav=e.target.closest('[data-nav]');
if(nav){ view.setMonth(view.getMonth()+parseInt(nav.dataset.nav,10)); render(); return; }
var day=e.target.closest('[data-date]');
if(day){
var ds=day.dataset.date;
if(CFG.isAdmin && apptsFor(ds).length>0){ openDetail(ds); }
else { openDay(ds); }
return;
}
});
app.querySelector('.mvk-close-detail').addEventListener('click',function(){detailModal.hidden=true;});
detailModal.addEventListener('click',function(e){ if(e.target===detailModal) detailModal.hidden=true; });
app.querySelector('.mvk-close').addEventListener('click',function(){modal.hidden=true;});
modal.addEventListener('click',function(e){ if(e.target===modal) modal.hidden=true; });
app.querySelector('.mvk-submit').addEventListener('click',function(){
var btn=this;
var ds=app.dataset.selDate;
var nom=val('#mvk-nom'), prenom=val('#mvk-prenom'), email=val('#mvk-email'), tel=val('#mvk-telephone'), adresse=val('#mvk-adresse');
var marque=val('#mvk-marque'), modele=val('#mvk-modele'), motor=val('#mvk-motor'), heure=val('#mvk-heure');
if(!nom||!prenom||!email||!tel||!adresse||!marque||!modele||!motor||!heure){
showMsg('Merci de remplir tous les champs.','err'); return;
}
if(!validEmail(email)){ showMsg('Adresse e-mail invalide.','err'); return; }
var fd=new FormData();
fd.append('action','moovika_save_rdv');
fd.append('nonce',CFG.nonce);
fd.append('date',ds);
fd.append('nom',nom); fd.append('prenom',prenom); fd.append('email',email);
fd.append('telephone',tel); fd.append('adresse',adresse);
fd.append('marque',marque); fd.append('modele',modele);
fd.append('motorisation',motor); fd.append('heure',heure);
fd.append('product_id',CFG.product_id||0);
btn.disabled=true; showMsg('Envoi en cours…','');
fetch(CFG.ajaxurl,{method:'POST',body:fd})
.then(function(r){return r.json();})
.then(function(j){
if(j&&j.success){
showMsg(j.data.message||'Rendez-vous confirmé !','ok');
if(j.data.redirect){ setTimeout(function(){window.location=j.data.redirect;},1100); }
else { setTimeout(function(){ modal.hidden=true; clearForm(); btn.disabled=false; load(); },1100); }
} else {
showMsg((j&&j.data&&j.data.message)||'Une erreur est survenue.','err');
btn.disabled=false;
}
})
.catch(function(){ showMsg('Erreur réseau.','err'); btn.disabled=false; });
});
load();
})();
</script>
HTML;
return $config . $html;
}
/* ------------------------------------------------------------
* 5) Page admin : RDV (liste / créer / modifier) + vacances
* ---------------------------------------------------------- */
add_action('admin_menu', function () {
add_menu_page('Rendez-vous', 'Rendez-vous', 'manage_options', 'moovika-rdv', 'moovika_rdv_admin_page', 'dashicons-calendar-alt', 26);
});
function moovika_rdv_admin_page() {
if (!current_user_can('manage_options')) return;
global $wpdb;
$table = $wpdb->prefix . 'moovika_rdv';
/* Suppression d'un RDV */
if (isset($_GET['del']) && check_admin_referer('mvk_del_' . intval($_GET['del']))) {
$wpdb->delete($table, array('id' => intval($_GET['del'])), array('%d'));
echo '<div class="notice notice-success is-dismissible"><p>Rendez-vous supprimé.</p></div>';
}
/* Ajout congés */
if (isset($_POST['mvk_vac_action']) && $_POST['mvk_vac_action'] === 'add' && check_admin_referer('mvk_vacances')) {
$s = sanitize_text_field($_POST['mvk_vac_start'] ?? '');
$e = sanitize_text_field($_POST['mvk_vac_end'] ?? '');
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $s) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $e) && $e >= $s) {
$vac = moovika_rdv_get_vacances();
$vac[] = array('start' => $s, 'end' => $e);
update_option('moovika_rdv_vacances', array_values($vac));
echo '<div class="notice notice-success is-dismissible"><p>Période de congés ajoutée.</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>Dates invalides (fin ≥ début).</p></div>';
}
}
/* Suppression congés */
if (isset($_GET['delvac']) && check_admin_referer('mvk_delvac_' . intval($_GET['delvac']))) {
$vac = moovika_rdv_get_vacances();
$i = intval($_GET['delvac']);
if (isset($vac[$i])) { unset($vac[$i]); update_option('moovika_rdv_vacances', array_values($vac)); }
echo '<div class="notice notice-success is-dismissible"><p>Période supprimée.</p></div>';
}
/* Création / modification d'un RDV */
if (isset($_POST['mvk_rdv_save']) && check_admin_referer('mvk_rdv_save')) {
$id = intval($_POST['mvk_rdv_id'] ?? 0);
$data = array(
'nom' => sanitize_text_field($_POST['nom'] ?? ''),
'prenom' => sanitize_text_field($_POST['prenom'] ?? ''),
'email' => sanitize_email($_POST['email'] ?? ''),
'adresse' => sanitize_text_field($_POST['adresse'] ?? ''),
'telephone' => sanitize_text_field($_POST['telephone'] ?? ''),
'vehicule_marque' => sanitize_text_field($_POST['marque'] ?? ''),
'vehicule_modele' => sanitize_text_field($_POST['modele'] ?? ''),
'motorisation' => sanitize_text_field($_POST['motorisation'] ?? ''),
'date_rdv' => sanitize_text_field($_POST['date_rdv'] ?? ''),
'heure_rdv' => substr(sanitize_text_field($_POST['heure_rdv'] ?? ''), 0, 5),
'reminder_sent' => 0,
);
$fmt = array('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%d');
if (!$data['nom'] || !$data['date_rdv'] || !$data['heure_rdv']) {
echo '<div class="notice notice-error is-dismissible"><p>Nom, date et heure sont obligatoires.</p></div>';
} elseif (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['date_rdv'])) {
echo '<div class="notice notice-error is-dismissible"><p>Date invalide.</p></div>';
} elseif ($id > 0) {
$res = $wpdb->update($table, $data, array('id' => $id), $fmt, array('%d'));
echo ($res === false)
? '<div class="notice notice-error is-dismissible"><p>Échec : un autre RDV occupe peut-être déjà ce créneau (date + heure).</p></div>'
: '<div class="notice notice-success is-dismissible"><p>Fiche mise à jour.</p></div>';
} else {
$res = $wpdb->insert($table, $data, $fmt);
echo ($res === false)
? '<div class="notice notice-error is-dismissible"><p>Échec : un RDV existe déjà sur ce créneau (date + heure).</p></div>'
: '<div class="notice notice-success is-dismissible"><p>Rendez-vous créé.</p></div>';
}
}
/* Pré-remplissage en mode édition */
$edit = null;
if (isset($_GET['edit'])) {
$edit = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", intval($_GET['edit'])), ARRAY_A);
}
$v = function ($k) use ($edit) { return $edit && isset($edit[$k]) ? esc_attr($edit[$k]) : ''; };
$mot = $edit ? $edit['motorisation'] : '';
echo '<div class="wrap"><h1>Rendez-vous Moovika</h1>';
/* ---- Créer / modifier ---- */
echo '<h2 style="margin-top:24px;">➕ ' . ($edit ? 'Modifier le rendez-vous #' . intval($edit['id']) : 'Créer un rendez-vous') . '</h2>';
echo '<p>Créez une intervention (y compris une <strong>date passée</strong>, pour compléter l\'historique) ou modifiez une fiche existante. Un RDV passé n\'envoie ni confirmation ni rappel.</p>';
echo '<form method="post" style="background:#fff;border:1px solid #dcdcde;border-radius:8px;padding:16px 18px;max-width:780px;margin-bottom:34px;">';
wp_nonce_field('mvk_rdv_save');
echo '<input type="hidden" name="mvk_rdv_save" value="1">';
echo '<input type="hidden" name="mvk_rdv_id" value="' . ($edit ? intval($edit['id']) : 0) . '">';
echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px 16px;">';
echo '<p><label><strong>Nom *</strong><br><input type="text" name="nom" value="' . $v('nom') . '" class="regular-text" required></label></p>';
echo '<p><label><strong>Prénom</strong><br><input type="text" name="prenom" value="' . $v('prenom') . '" class="regular-text"></label></p>';
echo '<p><label>E-mail<br><input type="email" name="email" value="' . $v('email') . '" class="regular-text"></label></p>';
echo '<p><label>Téléphone<br><input type="text" name="telephone" value="' . $v('telephone') . '" class="regular-text"></label></p>';
echo '<p style="grid-column:1/3;"><label>Adresse<br><input type="text" name="adresse" value="' . $v('adresse') . '" style="width:100%;"></label></p>';
echo '<p><label>Marque<br><input type="text" name="marque" value="' . ($edit ? esc_attr($edit['vehicule_marque']) : '') . '" class="regular-text"></label></p>';
echo '<p><label>Modèle<br><input type="text" name="modele" value="' . ($edit ? esc_attr($edit['vehicule_modele']) : '') . '" class="regular-text"></label></p>';
echo '<p><label>Motorisation<br><select name="motorisation">';
echo '<option value="thermique"' . selected($mot, 'thermique', false) . '>Thermique</option>';
echo '<option value="hybride"' . selected($mot, 'hybride', false) . '>Hybride</option>';
echo '<option value="electrique"' . selected($mot, 'electrique', false) . '>Électrique</option>';
echo '</select></label></p>';
echo '<p></p>';
echo '<p><label><strong>Date *</strong><br><input type="date" name="date_rdv" value="' . $v('date_rdv') . '" required></label></p>';
echo '<p><label><strong>Heure *</strong><br><input type="time" name="heure_rdv" value="' . ($edit ? esc_attr(substr($edit['heure_rdv'], 0, 5)) : '') . '" required></label></p>';
echo '</div>';
echo '<p style="margin-bottom:0;"><button class="button button-primary">' . ($edit ? 'Mettre à jour la fiche' : 'Créer le rendez-vous') . '</button>';
if ($edit) echo ' <a href="' . esc_url(admin_url('admin.php?page=moovika-rdv')) . '" class="button">Annuler</a>';
echo '</p></form>';
/* ---- Vacances ---- */
echo '<h2>🏖️ Mode vacances / congés</h2>';
echo '<p>Les jours de ces périodes sont affichés en jaune et non réservables.</p>';
echo '<form method="post" style="margin-bottom:14px;">';
wp_nonce_field('mvk_vacances');
echo '<input type="hidden" name="mvk_vac_action" value="add">';
echo 'Du <input type="date" name="mvk_vac_start" required> au <input type="date" name="mvk_vac_end" required> ';
echo '<button class="button button-primary">Ajouter la période</button></form>';
$vac = moovika_rdv_get_vacances();
if ($vac) {
echo '<table class="wp-list-table widefat fixed striped" style="max-width:520px;margin-bottom:34px;"><thead><tr><th>Début</th><th>Fin</th><th></th></tr></thead><tbody>';
foreach ($vac as $i => $p) {
$delv = wp_nonce_url(admin_url('admin.php?page=moovika-rdv&delvac=' . $i), 'mvk_delvac_' . $i);
echo '<tr><td>' . esc_html($p['start']) . '</td><td>' . esc_html($p['end']) . '</td>';
echo '<td><a href="' . esc_url($delv) . '" onclick="return confirm(\'Supprimer cette période ?\')">Supprimer</a></td></tr>';
}
echo '</tbody></table>';
} else {
echo '<p style="color:#7b8794;margin-bottom:34px;"><em>Aucune période de congés.</em></p>';
}
/* ---- Liste des RDV ---- */
echo '<h2>📅 Liste des rendez-vous</h2>';
$rows = $wpdb->get_results("SELECT * FROM $table ORDER BY date_rdv DESC, heure_rdv DESC", ARRAY_A);
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>Date</th><th>Heure</th><th>Nom</th><th>Prénom</th><th>E-mail</th><th>Téléphone</th><th>Adresse</th><th>Véhicule</th><th>Moteur</th><th>Actions</th>';
echo '</tr></thead><tbody>';
if ($rows) {
foreach ($rows as $r) {
$del = wp_nonce_url(admin_url('admin.php?page=moovika-rdv&del=' . $r['id']), 'mvk_del_' . $r['id']);
$edt = admin_url('admin.php?page=moovika-rdv&edit=' . $r['id']);
echo '<tr>';
echo '<td>' . esc_html($r['date_rdv']) . '</td>';
echo '<td>' . esc_html($r['heure_rdv']) . '</td>';
echo '<td>' . esc_html($r['nom']) . '</td>';
echo '<td>' . esc_html($r['prenom']) . '</td>';
echo '<td>' . esc_html($r['email']) . '</td>';
echo '<td>' . esc_html($r['telephone']) . '</td>';
echo '<td>' . esc_html($r['adresse']) . '</td>';
echo '<td>' . esc_html($r['vehicule_marque'] . ' ' . $r['vehicule_modele']) . '</td>';
echo '<td>' . esc_html(ucfirst($r['motorisation'])) . '</td>';
echo '<td><a href="' . esc_url($edt) . '">Modifier</a> | <a href="' . esc_url($del) . '" onclick="return confirm(\'Supprimer ce rendez-vous ?\')" style="color:#b32d2e;">Supprimer</a></td>';
echo '</tr>';
}
} else {
echo '<tr><td colspan="10">Aucun rendez-vous pour le moment.</td></tr>';
}
echo '</tbody></table></div>';
}
Fiche produit
Comparaison Avant / Après
[bafg id= »8241″]













Avis
Il n’y a pas encore d’avis.