ELITE 9 2CH

Plage de prix : 469,00 € à 549,00 €

UGS : ELITE 8 2 CH-1 Catégorie :

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;
}

/* ------------------------------------------------------------
 * 0bis) DEVIS — configuration + page HTML sécurisée par lien
 * ---------------------------------------------------------- */
function moovika_devis_company() {
    return array(
        'name'        => 'EDR Auto',
        'legal_name'  => 'MOOVIKA SARL',
        'logo'        => 'https://edrauto.fr/wp-content/uploads/2025/04/cropped-logo-EDR-HQ-330.jpg',
        'address'     => '6 Allée Rodolphe Piguet, 77400 Lagny-sur-Marne',
        'phone'       => '09 50 17 50 08',
        'siret'       => '484 686 365 00020',
        'tva'         => 'FR 07 484 686 365',
        'ape'         => '4690Z',
        'capital'     => '',   // ex : '5 000 €' — à compléter
        'rcs'         => '',   // ex : 'RCS Meaux 484 686 365' — à compléter
        'validite'    => 30,   // durée de validité du devis en jours
        'photos_url'  => 'https://edrauto.fr/product/installation-camera-embarquee-dashcam-dep-77-93-94-75/',
    );
}
function moovika_devis_prestations() {
    return array(
        "Passage soigné du câble coaxial dans la goulotte et les soufflets d'étanchéité",
        "Récupération du + contact et de la masse dans la boîte à fusibles (hardwiring), pour la BlackVue ou pour la batterie",
        "Passage discret des câbles dans les joints de porte et le ciel de toit",
        "Fixation définitive des caméras avant et arrière",
        "Tests complets avec l'app BlackVue en Wi-Fi : mode conduite et mode parking (enregistrement, détection de mouvement, etc.)",
    );
}
function moovika_devis_default_montant() { return 230.00; }
function moovika_devis_url($token) { return add_query_arg('mvk_devis', rawurlencode($token), home_url('/')); }

/* ------------------------------------------------------------
 * 0ter) COMMANDE WooCommerce depuis un RDV
 *   >>> Renseigne l'ID du produit "Installation caméra embarquée" <<<
 *   (Produits → ouvrir le produit → l'ID est dans l'URL : post=XXXX)
 * ---------------------------------------------------------- */
function moovika_rdv_product_id() { return 152; } // Produit "Installation caméra embarquée"

function moovika_rdv_create_order($r) {
    if (!function_exists('wc_create_order')) return new WP_Error('woo', "WooCommerce n'est pas actif.");
    if (empty($r['email']) || !is_email($r['email'])) return new WP_Error('mail', "Adresse e-mail client manquante ou invalide.");

    $pid     = (int) moovika_rdv_product_id();
    $montant = isset($r['montant']) ? (float) $r['montant'] : moovika_devis_default_montant();

    $order = wc_create_order();

    // Ligne unique au montant convenu (celui du devis). Le produit est associé
    // pour que l'avis CusRev se rattache à la fiche produit installation.
    $item = new WC_Order_Item_Product();
    $item->set_name('Installation caméra embarquée — RDV du ' . $r['date_rdv'] . ' à ' . $r['heure_rdv']);
    if ($pid > 0) $item->set_product_id($pid);
    $item->set_quantity(1);
    $item->set_subtotal($montant);
    $item->set_total($montant);
    $order->add_item($item);

    $order->set_billing_first_name($r['prenom']);
    $order->set_billing_last_name($r['nom']);
    $order->set_billing_email($r['email']);
    $order->set_billing_phone($r['telephone']);
    $order->set_billing_address_1($r['adresse']);
    $order->set_billing_postcode($r['code_postal'] ?? '');
    $order->set_billing_city($r['ville'] ?? '');
    $order->set_billing_country('FR');

    // Compte client : on rattache un compte existant, ou on en crée un
    // (WooCommerce envoie alors l'e-mail « nouveau compte » avec le lien mot de passe).
    $customer_id = 0;
    $existing = get_user_by('email', $r['email']);
    if ($existing) {
        $customer_id = $existing->ID;
    } elseif (function_exists('wc_create_new_customer')) {
        // On empêche l'e-mail « nouveau compte » à la création : il sera
        // envoyé plus tard, après le paiement (pour ne pas gêner la demande de paiement).
        add_filter('woocommerce_email_enabled_customer_new_account', '__return_false', 99);
        $new = wc_create_new_customer($r['email'], '', '', array(
            'first_name' => $r['prenom'],
            'last_name'  => $r['nom'],
        ));
        remove_filter('woocommerce_email_enabled_customer_new_account', '__return_false', 99);
        if (!is_wp_error($new)) {
            $customer_id = $new;
            update_user_meta($customer_id, '_mvk_needs_account_email', '1'); // à envoyer après paiement
        }
    }
    if ($customer_id) {
        $order->set_customer_id($customer_id);
        // Pré-remplissage du profil client (adresse de facturation)
        update_user_meta($customer_id, 'billing_first_name', $r['prenom']);
        update_user_meta($customer_id, 'billing_last_name',  $r['nom']);
        update_user_meta($customer_id, 'billing_email',      $r['email']);
        update_user_meta($customer_id, 'billing_phone',      $r['telephone']);
        update_user_meta($customer_id, 'billing_address_1',  $r['adresse']);
        update_user_meta($customer_id, 'billing_postcode',   $r['code_postal'] ?? '');
        update_user_meta($customer_id, 'billing_city',       $r['ville'] ?? '');
        update_user_meta($customer_id, 'billing_country',    'FR');
    }

    $order->add_order_note('Commande créée automatiquement depuis le RDV #' . $r['id'] . ' — montant du devis : ' . number_format($montant, 2, ',', ' ') . ' € TTC.');

    // Total = montant exact du devis (pas de recalcul de TVA qui gonflerait le total)
    $order->calculate_totals(false);
    $order->set_status('pending');
    $order->save();
    return $order;
}

/* Met à jour le total d'une commande existante au montant convenu */
function moovika_rdv_set_order_total($order, $montant) {
    foreach ($order->get_items() as $item) {
        $item->set_subtotal((float) $montant);
        $item->set_total((float) $montant);
        $item->save();
        break; // une seule ligne
    }
    $order->calculate_totals(false);
    $order->save();
}

/* Config de l'e-mail de fin d'installation */
function moovika_rdv_completion_config() {
    return array(
        'google_review' => 'https://g.page/r/CWXiISoC7zxxEAE/review',
        'signataire'    => 'David CUIGNET',
        'sites'         => array('www.blackvue.fr', 'www.blackvue-shop.fr'),
        'tel'           => '00 33 9 50 17 50 08',
    );
}

/* E-mail « installation terminée » (modèle + montant + paiement + avis Google) */
function moovika_rdv_send_completion($r, $pay_url, $invoice) {
    if (empty($r['email']) || !is_email($r['email'])) return false;
    $cfg     = moovika_rdv_completion_config();
    $montant = number_format((float) $r['montant'], 2, ',', ' ');
    $model   = trim((string) ($r['dashcam_model'] ?? '')) !== '' ? $r['dashcam_model'] : 'votre dashcam';

    $subject = 'Installation terminée — ' . $model;
    $b  = "Bonjour " . $r['prenom'] . ",\n\n";
    $b .= "Je suis ravi de vous confirmer que l'installation de votre caméra embarquée " . $model . " a été réalisée aujourd'hui avec succès. L'intervention a inclus :\n\n";
    $b .= "- Le passage soigné du câble coaxial dans la goulotte et les soufflets d'étanchéité\n";
    $b .= "- La récupération du + contact et de la masse dans la boîte à fusibles (hardwiring)\n";
    $b .= "- Le passage discret des câbles dans le joint de porte et le ciel de toit\n";
    $b .= "- La fixation définitive des caméras avant et arrière\n";
    $b .= "- Les tests complets en mode conduite et mode parking (enregistrement, détection de mouvement, etc.)\n";
    if (!empty($r['cloud_option'])) {
        $b .= "- Configuration et activation du Cloud pour l'accès à distance en direct (option)\n";
    }
    $b .= "\nL'installation est propre et discrète, comme convenu.\n\n";
    $inv = trim((string) $invoice) !== '' ? (' n° ' . $invoice) : '';
    $b .= "Vous trouverez ci-dessous le récapitulatif de votre facture" . $inv . " d'un montant de " . $montant . " € TTC correspondant à cette prestation.\n\n";
    if ($pay_url) {
        $b .= "Pour procéder au règlement, cliquez sur le lien sécurisé ci-dessous :\n" . $pay_url . "\n\n";
    } else {
        $b .= "Pour procéder au règlement, cliquez sur le lien reçu par e-mail précédemment.\n\n";
    }
    $b .= "Le paiement est immédiat et sécurisé. Une fois le règlement effectué, vous aurez accès à votre facture.\n\n";
    $b .= "N'hésitez pas à me contacter si vous avez la moindre question ou si vous rencontrez un souci avec le lien / le paiement. Je reste à votre disposition pour toute assistance supplémentaire (réglages de l'application, conseils d'utilisation, etc.).\n\n";
    $b .= "Je vous remercie pour votre confiance et vous souhaite d'excellentes routes avec votre nouvelle protection !\n\n";
    $b .= "Si vous êtes satisfait de la prestation, cela me ferait vraiment plaisir si vous pouviez laisser un petit avis sur notre fiche Google. Cela aide énormément les autres conducteurs à nous choisir en toute confiance !\n";
    $b .= "Voici le lien direct (ça prend moins d'une minute) :\n" . $cfg['google_review'] . "\n\n";
    $b .= "Cordialement,\n\n" . $cfg['signataire'] . "\n";
    foreach ($cfg['sites'] as $s) { $b .= $s . "\n"; }
    $b .= "Tél : " . $cfg['tel'] . "\n";

    $headers = moovika_rdv_mail_headers();
    $headers[] = 'Cc: contact@moovika.fr';
    return wp_mail($r['email'], $subject, $b, $headers);
}

add_action('template_redirect', 'moovika_devis_render');

/* ------------------------------------------------------------
 * 0quater) Après paiement : passage auto en « Installation terminée »
 *   + envoi de l'e-mail de définition du mot de passe (différé)
 * ---------------------------------------------------------- */

/* Résout le slug du statut personnalisé par son libellé exact (robuste). */
function moovika_rdv_completion_status_slug() {
    if (!function_exists('wc_get_order_statuses')) return '';
    $target = 'installation terminée';
    foreach (wc_get_order_statuses() as $slug => $label) {
        if (mb_strtolower(trim($label)) === $target) return $slug; // ex : wc-installation-terminee
    }
    return '';
}

add_action('woocommerce_payment_complete', 'moovika_rdv_order_paid', 20);
add_action('woocommerce_order_status_processing', 'moovika_rdv_order_paid', 20);
add_action('woocommerce_order_status_completed', 'moovika_rdv_order_paid', 20);
function moovika_rdv_order_paid($order_id) {
    if (!function_exists('wc_get_order')) return;
    $order = wc_get_order($order_id);
    if (!$order) return;

    global $wpdb;
    $table = $wpdb->prefix . 'moovika_rdv';
    $rdv = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE order_id = %d", $order_id), ARRAY_A);
    if (!$rdv) return; // commande non liée à un RDV → on ne touche à rien

    // Idempotence (évite les doublons si plusieurs hooks se déclenchent)
    if ($order->get_meta('_mvk_paid_handled') === 'yes') return;
    $order->update_meta_data('_mvk_paid_handled', 'yes');
    $order->save();

    // 1) Bascule vers le statut personnalisé « Installation terminée »
    $slug = moovika_rdv_completion_status_slug();
    if ($slug) {
        $clean = preg_replace('/^wc-/', '', $slug);
        if (!$order->has_status($clean)) {
            $order->update_status($clean, 'Paiement reçu : passage automatique en « Installation terminée ».');
        }
    }

    // 2) E-mail de définition du mot de passe (différé après paiement)
    if (!empty($rdv['email']) && is_email($rdv['email'])) {
        $user = get_user_by('email', $rdv['email']);
        if ($user && get_user_meta($user->ID, '_mvk_needs_account_email', true)) {
            if (function_exists('WC') && WC()->mailer()) {
                $emails = WC()->mailer()->get_emails();
                if (isset($emails['WC_Email_Customer_New_Account'])) {
                    $emails['WC_Email_Customer_New_Account']->trigger($user->ID, '', true);
                }
            }
            delete_user_meta($user->ID, '_mvk_needs_account_email');
        }
    }
}
function moovika_devis_render() {
    if (empty($_GET['mvk_devis'])) return;
    $token = sanitize_text_field(wp_unslash($_GET['mvk_devis']));
    global $wpdb;
    $table = $wpdb->prefix . 'moovika_rdv';
    $r = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE devis_token = %s", $token), ARRAY_A);
    if (!$r) { status_header(404); nocache_headers(); wp_die('Devis introuvable ou lien expiré.', 'Devis', array('response' => 404)); }

    $co      = moovika_devis_company();
    $montant = isset($r['montant']) ? (float) $r['montant'] : moovika_devis_default_montant();

    $e_name   = esc_html($co['name']);
    $e_logo   = esc_url($co['logo']);
    $e_addr   = esc_html($co['address']);
    $e_phone  = esc_html($co['phone']);
    $e_photos = esc_url($co['photos_url']);
    $e_num    = esc_html('EDR-' . str_pad($r['id'], 4, '0', STR_PAD_LEFT));
    $e_today  = esc_html(moovika_rdv_format_fr(date('Y-m-d')));
    $e_client = esc_html(trim($r['prenom'] . ' ' . $r['nom']));
    $e_cadr   = esc_html($r['adresse']);
    $e_cpville = esc_html(trim(((string) ($r['code_postal'] ?? '')) . ' ' . ((string) ($r['ville'] ?? ''))));
    $e_ctel   = esc_html($r['telephone']);
    $e_cmail  = esc_html($r['email']);
    $e_veh    = esc_html(trim($r['vehicule_marque'] . ' ' . $r['vehicule_modele']) . ' (' . ucfirst($r['motorisation']) . ')');
    $e_date   = esc_html(moovika_rdv_format_fr($r['date_rdv']) . ' à ' . $r['heure_rdv']);
    $tva_rate = 20;
    $ht       = $montant / (1 + $tva_rate / 100);
    $tva_amt  = $montant - $ht;
    $e_ht     = esc_html(number_format($ht, 2, ',', ' ') . ' €');
    $e_tva    = esc_html(number_format($tva_amt, 2, ',', ' ') . ' €');
    $e_ttc    = esc_html(number_format($montant, 2, ',', ' ') . ' €');
    $e_siret  = esc_html($co['siret']);
    $e_tvaid  = esc_html($co['tva']);
    $e_ape    = esc_html($co['ape']);
    $valid_days = !empty($co['validite']) ? (int) $co['validite'] : 30;
    $e_valid  = esc_html(moovika_rdv_format_fr(date('Y-m-d', strtotime('+' . $valid_days . ' days'))));
    $e_days   = esc_html($valid_days);
    $ent = $co['legal_name'];
    if (!empty($co['capital'])) $ent .= ' au capital de ' . $co['capital'];
    $legal_parts = array(
        $ent . ' — enseigne ' . $co['name'],
        $co['address'],
        'SIRET ' . $co['siret'],
        'TVA intracom. ' . $co['tva'],
        'APE ' . $co['ape'],
    );
    if (!empty($co['rcs'])) $legal_parts[] = $co['rcs'];
    $e_legal = esc_html(implode(' · ', $legal_parts));
    $presta = '';
    foreach (moovika_devis_prestations() as $p) { $presta .= '<li>' . esc_html($p) . '</li>'; }
    $note = trim((string) ($r['devis_note'] ?? ''));
    $e_note = $note !== '' ? '<p class="note"><strong>Note :</strong> ' . nl2br(esc_html($note)) . '</p>' : '';

    nocache_headers();
    header('Content-Type: text/html; charset=UTF-8');
    echo <<<HTML
<!DOCTYPE html>
<html lang="fr"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Devis $e_num — $e_name</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#1f2933;background:#eceef3;padding:24px;line-height:1.55;}
.sheet{max-width:780px;margin:0 auto;background:#fff;border-radius:10px;padding:38px 44px;box-shadow:0 4px 24px rgba(0,0,0,.08);}
.top{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;border-bottom:3px solid #374084;padding-bottom:18px;margin-bottom:22px;}
.top img{max-height:74px;width:auto;}
.co{text-align:right;font-size:.85rem;color:#52606d;}
.co .co-name{font-size:1.05rem;font-weight:700;color:#374084;margin-bottom:3px;}
h1{font-size:1.5rem;color:#374084;letter-spacing:1px;margin-bottom:4px;}
.meta{font-size:.85rem;color:#7b8794;margin-bottom:24px;}
.cols{display:flex;gap:24px;margin-bottom:26px;}
.box{flex:1;background:#f7f8fb;border:1px solid #e4e7eb;border-radius:8px;padding:14px 16px;font-size:.9rem;}
.box h3{font-size:.78rem;text-transform:uppercase;letter-spacing:.5px;color:#374084;margin-bottom:8px;}
.box div{margin-bottom:3px;}
.section-title{font-size:1rem;font-weight:700;color:#374084;margin:0 0 12px;}
ul.presta{list-style:none;margin:0 0 22px;}
ul.presta li{position:relative;padding:7px 0 7px 26px;border-bottom:1px solid #f0f1f4;font-size:.92rem;}
ul.presta li:before{content:"\\2713";position:absolute;left:0;top:7px;color:#374084;font-weight:700;}
.totals{margin-left:auto;width:280px;margin-bottom:24px;font-size:.92rem;}
.totals .trow{display:flex;justify-content:space-between;padding:7px 14px;}
.totals .trow span:first-child{color:#52606d;}
.totals .ttc{background:#374084;color:#fff;border-radius:8px;font-weight:700;font-size:1.05rem;padding:11px 14px;margin-top:4px;}
.totals .ttc span:first-child{color:#fff;}
.note{background:#fff8e1;border-left:4px solid #e0a800;border-radius:0 6px 6px 0;padding:10px 14px;font-size:.9rem;margin-bottom:20px;}
.foot{font-size:.9rem;color:#3e4c59;border-top:1px solid #e4e7eb;padding-top:18px;margin-top:6px;}
.conditions{background:#f7f8fb;border:1px solid #e4e7eb;border-radius:8px;padding:12px 16px;font-size:.84rem;color:#3e4c59;margin-bottom:20px;}
.conditions div{margin:3px 0;}
.signature{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:24px;}
.signature .cert{max-height:90px;width:auto;flex:0 0 auto;}
.sig-box{width:300px;flex:0 0 auto;border:1px dashed #aeb6c2;border-radius:8px;padding:12px 14px;min-height:110px;}
.sig-label{font-weight:700;color:#374084;font-size:.9rem;}
.sig-sub{font-size:.74rem;color:#7b8794;margin-top:4px;}
.foot a{color:#374084;}
.actions{max-width:780px;margin:18px auto 0;text-align:center;}
.btn{display:inline-block;background:#374084;color:#fff;text-decoration:none;border:none;padding:11px 26px;border-radius:8px;font-size:.95rem;font-weight:600;cursor:pointer;}
@media print{body{background:#fff;padding:0;}.sheet{box-shadow:none;border-radius:0;max-width:100%;padding:0;}.actions{display:none;}}
</style></head><body>
<div class="sheet">
  <div class="top">
    <img src="$e_logo" alt="$e_name">
    <div class="co"><div class="co-name">$e_name</div><div>$e_addr</div><div>Tél. $e_phone</div></div>
  </div>
  <h1>DEVIS</h1>
  <div class="meta">N° $e_num  ·  Émis le $e_today</div>
  <div class="cols">
    <div class="box"><h3>Client</h3><div><strong>$e_client</strong></div><div>$e_cadr</div><div>$e_cpville</div><div>$e_ctel</div><div>$e_cmail</div></div>
    <div class="box"><h3>Intervention</h3><div><strong>Véhicule :</strong> $e_veh</div><div><strong>Rendez-vous :</strong> $e_date</div><div><strong>Lieu :</strong> $e_addr</div></div>
  </div>
  <div class="section-title">Installation caméra embarquée (avant + arrière)</div>
  <ul class="presta">$presta</ul>
  <div class="totals">
    <div class="trow"><span>Total HT</span><span>$e_ht</span></div>
    <div class="trow"><span>TVA 20 %</span><span>$e_tva</span></div>
    <div class="trow ttc"><span>Total TTC</span><span>$e_ttc</span></div>
  </div>
  $e_note
  <div class="conditions">
    <div><strong>Devis gratuit.</strong> Valable $e_days jours, soit jusqu'au $e_valid.</div>
    <div><strong>Modalités de règlement :</strong> à l'issue de l'installation par carte bancaire, ou en ligne via la boutique edrauto.fr.</div>
    <div><strong>Prestation :</strong> installation réalisée dans nos locaux à Lagny-sur-Marne.</div>
  </div>
  <p style="font-size:.9rem;margin-bottom:18px;">L'installation est réalisée de façon propre et discrète. Vous pouvez voir des exemples d'installations déjà réalisées sur <a href="$e_photos">cette page</a>.</p>
  <div class="signature">
    <img class="cert" src="https://edrauto.fr/wp-content/uploads/2025/05/Certification-pro-blackvue.webp" alt="Installateur certifié BlackVue">
    <div class="sig-box">
      <div class="sig-label">Bon pour accord</div>
      <div class="sig-sub">Date et signature du client, précédées de la mention manuscrite « Bon pour accord ».</div>
    </div>
  </div>
  <div class="foot">
    <p>N'hésitez pas à me contacter au <strong>$e_phone</strong>. Je reste à votre disposition pour toute assistance supplémentaire (réglages de l'application, conseils d'utilisation, etc.).</p>
    <p style="margin-top:8px;">Je vous remercie pour votre confiance.</p>
    <p style="margin-top:8px;font-weight:600;color:#374084;">$e_name</p>
    <p style="margin-top:12px;font-size:.72rem;color:#7b8794;line-height:1.5;">$e_legal</p>
  </div>
</div>
<div class="actions"><button class="btn" onclick="window.print()">Imprimer / Enregistrer en PDF</button></div>
</body></html>
HTML;
    exit;
}

/* ------------------------------------------------------------
 * 1) Table (v1.3 : + montant, devis_token, devis_note)
 * ---------------------------------------------------------- */
add_action('init', 'moovika_rdv_create_table');
function moovika_rdv_create_table() {
    if (get_option('moovika_rdv_db_version') === '1.6') 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,
        code_postal VARCHAR(10) NOT NULL DEFAULT '',
        ville VARCHAR(120) NOT NULL DEFAULT '',
        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,
        montant DECIMAL(10,2) NOT NULL DEFAULT 230.00,
        devis_token VARCHAR(40) NOT NULL DEFAULT '',
        devis_note TEXT NULL,
        reminder_sent TINYINT(1) NOT NULL DEFAULT 0,
        order_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
        dashcam_model VARCHAR(200) NOT NULL DEFAULT '',
        cloud_option TINYINT(1) NOT NULL DEFAULT 0,
        invoice_number VARCHAR(50) NOT NULL DEFAULT '',
        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);

    // Générer un jeton de devis pour les RDV existants qui n'en ont pas
    $ids = $wpdb->get_col("SELECT id FROM $table WHERE devis_token = '' OR devis_token IS NULL");
    foreach ((array) $ids as $rid) {
        $wpdb->update($table, array('devis_token' => wp_generate_password(24, false)), array('id' => intval($rid)), array('%s'), array('%d'));
    }

    update_option('moovika_rdv_db_version', '1.6');
}

/* ------------------------------------------------------------
 * 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, code_postal, ville, 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());
}

/* 2ter) AJAX : statut admin de l'utilisateur courant (jamais mis en cache) */
add_action('wp_ajax_moovika_is_admin', 'moovika_is_admin_check');
add_action('wp_ajax_nopriv_moovika_is_admin', 'moovika_is_admin_check');
function moovika_is_admin_check() {
    wp_send_json_success(array('isAdmin' => current_user_can('manage_options')));
}

/* ------------------------------------------------------------
 * 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']      ?? '');
    $cp      = sanitize_text_field($_POST['code_postal']  ?? '');
    $ville   = sanitize_text_field($_POST['ville']        ?? '');
    $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 || !$cp || !$ville || !$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,
        'code_postal'     => $cp,
        'ville'           => $ville,
        'telephone'       => $tel,
        'vehicule_marque' => $marque,
        'vehicule_modele' => $modele,
        'motorisation'    => $motor,
        'date_rdv'        => $date,
        'heure_rdv'       => $heure,
        'montant'         => moovika_devis_default_montant(),
        'devis_token'     => wp_generate_password(24, false),
        'devis_note'      => '',
        'reminder_sent'   => 0,
    );
    $ok = $wpdb->insert($table, $row, array('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%f','%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";
    if (!empty($r['devis_token'])) {
        $body .= "Votre devis détaillé (montant et prestations) :\n";
        $body .= moovika_devis_url($r['devis_token']) . "\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']),
        '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>N° et rue</label><input type="text" id="mvk-adresse" autocomplete="address-line1"></div>
      <div class="mvk-row">
        <div class="mvk-field"><label>Code postal</label><input type="text" id="mvk-cp" autocomplete="postal-code" inputmode="numeric"></div>
        <div class="mvk-field"><label>Ville</label><input type="text" id="mvk-ville" autocomplete="address-level2"></div>
      </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 loadAppts(){
    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 load(){
    // Statut admin déterminé par AJAX (jamais mis en cache), puis chargement des RDV
    var fa=new FormData(); fa.append('action','moovika_is_admin');
    fetch(CFG.ajaxurl,{method:'POST',body:fa})
      .then(function(r){return r.json();})
      .then(function(j){ CFG.isAdmin = !!(j&&j.success&&j.data&&j.data.isAdmin); })
      .catch(function(){ CFG.isAdmin = false; })
      .then(function(){ loadAppts(); });
  }

  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-cp','#mvk-ville','#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)+((r.code_postal||r.ville)?(', '+esc(((r.code_postal||'')+' '+(r.ville||'')).trim())):'')+'</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'), cp=val('#mvk-cp'), ville=val('#mvk-ville');
    var marque=val('#mvk-marque'), modele=val('#mvk-modele'), motor=val('#mvk-motor'), heure=val('#mvk-heure');
    if(!nom||!prenom||!email||!tel||!adresse||!cp||!ville||!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('code_postal',cp); fd.append('ville',ville);
    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'] ?? ''),
            'code_postal'     => sanitize_text_field($_POST['code_postal'] ?? ''),
            'ville'           => sanitize_text_field($_POST['ville'] ?? ''),
            '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),
            'montant'         => (float) str_replace(',', '.', sanitize_text_field($_POST['montant'] ?? '230')),
            'devis_note'      => sanitize_textarea_field($_POST['devis_note'] ?? ''),
            'dashcam_model'   => sanitize_text_field($_POST['dashcam_model'] ?? ''),
            'cloud_option'    => isset($_POST['cloud_option']) ? 1 : 0,
            'invoice_number'  => sanitize_text_field($_POST['invoice_number'] ?? ''),
            'reminder_sent'   => 0,
        );
        $fmt = array('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%f','%s','%s','%d','%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'));
            // Si une commande existe déjà, on la remet au montant à jour
            $oid = (int) $wpdb->get_var($wpdb->prepare("SELECT order_id FROM $table WHERE id = %d", $id));
            if ($oid && function_exists('wc_get_order')) {
                $o = wc_get_order($oid);
                if ($o) moovika_rdv_set_order_total($o, $data['montant']);
            }
            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.' . ($oid ? ' Commande #' . $oid . ' synchronisée au nouveau montant.' : '') . '</p></div>';
        } else {
            $data['devis_token'] = wp_generate_password(24, false);
            $fmt[] = '%s';
            $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>';
        }
    }

    /* Envoi manuel du devis + confirmation au client */
    if (isset($_GET['senddevis']) && check_admin_referer('mvk_senddevis_' . intval($_GET['senddevis']))) {
        $rid  = intval($_GET['senddevis']);
        $rowd = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $rid), ARRAY_A);
        if (!$rowd) {
            echo '<div class="notice notice-error is-dismissible"><p>Rendez-vous introuvable.</p></div>';
        } elseif (empty($rowd['email']) || !is_email($rowd['email'])) {
            echo '<div class="notice notice-error is-dismissible"><p>Aucune adresse e-mail valide sur cette fiche : impossible d\'envoyer le devis.</p></div>';
        } else {
            moovika_rdv_send_client_mail($rowd, 'confirmation');
            echo '<div class="notice notice-success is-dismissible"><p>Devis et confirmation envoyés à ' . esc_html($rowd['email']) . '.</p></div>';
        }
    }

    /* Créer la commande WooCommerce + demande de paiement */
    if (isset($_GET['createorder']) && check_admin_referer('mvk_createorder_' . intval($_GET['createorder']))) {
        $rid  = intval($_GET['createorder']);
        $rowd = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $rid), ARRAY_A);
        if (!$rowd) {
            echo '<div class="notice notice-error is-dismissible"><p>Rendez-vous introuvable.</p></div>';
        } elseif (!empty($rowd['order_id']) && function_exists('wc_get_order') && wc_get_order($rowd['order_id'])) {
            $ord = wc_get_order($rowd['order_id']);
            echo '<div class="notice notice-warning is-dismissible"><p>Une commande existe déjà : <a href="' . esc_url($ord->get_edit_order_url()) . '">#' . intval($rowd['order_id']) . '</a>.</p></div>';
        } else {
            $order = moovika_rdv_create_order($rowd);
            if (is_wp_error($order)) {
                echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($order->get_error_message()) . '</p></div>';
            } else {
                $wpdb->update($table, array('order_id' => $order->get_id()), array('id' => $rid), array('%d'), array('%d'));
                if (function_exists('WC') && WC()->mailer()) {
                    WC()->mailer()->customer_invoice($order); // e-mail "facture / demande de paiement" (lien Stripe)
                }
                echo '<div class="notice notice-success is-dismissible"><p>Commande <a href="' . esc_url($order->get_edit_order_url()) . '">#' . $order->get_id() . '</a> créée et demande de paiement envoyée à ' . esc_html($rowd['email']) . '.<br>Lien de paiement : <a href="' . esc_url($order->get_checkout_payment_url()) . '" target="_blank">' . esc_html($order->get_checkout_payment_url()) . '</a></p></div>';
            }
        }
    }

    /* Installation terminée : commande à jour + e-mail de fin (paiement + avis Google) */
    if (isset($_GET['complete']) && check_admin_referer('mvk_complete_' . intval($_GET['complete']))) {
        $rid  = intval($_GET['complete']);
        $rowd = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $rid), ARRAY_A);
        if (!$rowd) {
            echo '<div class="notice notice-error is-dismissible"><p>Rendez-vous introuvable.</p></div>';
        } elseif (empty($rowd['email']) || !is_email($rowd['email'])) {
            echo '<div class="notice notice-error is-dismissible"><p>Aucune adresse e-mail valide sur cette fiche.</p></div>';
        } else {
            $pay_url = '';
            $invoice = (string) $rowd['invoice_number'];
            if (function_exists('wc_get_order')) {
                $order = !empty($rowd['order_id']) ? wc_get_order($rowd['order_id']) : false;
                if (!$order) {
                    $order = moovika_rdv_create_order($rowd);
                    if (!is_wp_error($order)) {
                        $wpdb->update($table, array('order_id' => $order->get_id()), array('id' => $rid), array('%d'), array('%d'));
                    } else { $order = false; }
                } else {
                    moovika_rdv_set_order_total($order, (float) $rowd['montant']); // resync au montant final
                }
                if ($order) {
                    $pay_url = $order->get_checkout_payment_url();
                    if ($invoice === '') $invoice = '#' . $order->get_order_number();
                }
            }
            moovika_rdv_send_completion($rowd, $pay_url, $invoice);
            echo '<div class="notice notice-success is-dismissible"><p>E-mail de fin d\'installation envoyé à ' . esc_html($rowd['email']) . ' (montant ' . esc_html(number_format((float) $rowd['montant'], 2, ',', ' ')) . ' € TTC).</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>N° et rue<br><input type="text" name="adresse" value="' . $v('adresse') . '" style="width:100%;"></label></p>';
    echo '<p><label>Code postal<br><input type="text" name="code_postal" value="' . ($edit ? esc_attr($edit['code_postal']) : '') . '" class="regular-text"></label></p>';
    echo '<p><label>Ville<br><input type="text" name="ville" value="' . ($edit ? esc_attr($edit['ville']) : '') . '" class="regular-text"></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 '<p><label><strong>Montant TTC (€)</strong><br><input type="number" step="0.01" min="0" name="montant" value="' . ($edit && $edit['montant'] !== null ? esc_attr($edit['montant']) : '230.00') . '" class="regular-text"></label></p>';
    echo '<p></p>';
    echo '<p style="grid-column:1/3;"><label>Note personnalisée sur le devis (optionnel)<br><textarea name="devis_note" rows="2" style="width:100%;">' . ($edit ? esc_textarea($edit['devis_note']) : '') . '</textarea></label></p>';
    echo '<p style="grid-column:1/3;"><label>Modèle de dashcam installé <span style="color:#7b8794;font-weight:400;">(pour l\'e-mail de fin d\'installation)</span><br><input type="text" name="dashcam_model" value="' . ($edit ? esc_attr($edit['dashcam_model']) : '') . '" style="width:100%;" placeholder="ex : BlackVue DR970X 2CH LTE PLUS + B130A II"></label></p>';
    echo '<p><label>N° de facture <span style="color:#7b8794;font-weight:400;">(optionnel)</span><br><input type="text" name="invoice_number" value="' . ($edit ? esc_attr($edit['invoice_number']) : '') . '" class="regular-text"></label></p>';
    echo '<p style="align-self:end;"><label><input type="checkbox" name="cloud_option" value="1" ' . ($edit ? checked($edit['cloud_option'], 1, false) : '') . '> Option Cloud installée</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 && !empty($edit['devis_token'])) echo ' <a href="' . esc_url(moovika_devis_url($edit['devis_token'])) . '" target="_blank" class="button">Voir le devis</a>';
    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>Montant</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(trim($r['adresse'] . ' ' . $r['code_postal'] . ' ' . $r['ville'])) . '</td>';
            echo '<td>' . esc_html($r['vehicule_marque'] . ' ' . $r['vehicule_modele']) . '</td>';
            echo '<td>' . esc_html(ucfirst($r['motorisation'])) . '</td>';
            echo '<td>' . esc_html(number_format((float) $r['montant'], 2, ',', ' ')) . ' €</td>';
            $devis_link = !empty($r['devis_token']) ? '  |  <a href="' . esc_url(moovika_devis_url($r['devis_token'])) . '" target="_blank">Devis</a>' : '';
            $send_link = '';
            if (!empty($r['email']) && is_email($r['email'])) {
                $send = wp_nonce_url(admin_url('admin.php?page=moovika-rdv&senddevis=' . $r['id']), 'mvk_senddevis_' . $r['id']);
                $send_link = '  |  <a href="' . esc_url($send) . '" onclick="return confirm(\'Envoyer le devis et la confirmation par e-mail au client ?\')">Envoyer le devis</a>';
            }
            $order_link = '';
            if (!empty($r['order_id']) && function_exists('wc_get_order') && wc_get_order($r['order_id'])) {
                $order_link = '  |  <a href="' . esc_url(wc_get_order($r['order_id'])->get_edit_order_url()) . '" target="_blank">Commande #' . intval($r['order_id']) . '</a>';
            } elseif (!empty($r['email']) && is_email($r['email']) && function_exists('wc_create_order')) {
                $co = wp_nonce_url(admin_url('admin.php?page=moovika-rdv&createorder=' . $r['id']), 'mvk_createorder_' . $r['id']);
                $order_link = '  |  <a href="' . esc_url($co) . '" onclick="return confirm(\'Créer la commande et envoyer la demande de paiement au client ?\')" style="font-weight:600;">Créer la commande</a>';
            }
            $complete_link = '';
            if (!empty($r['email']) && is_email($r['email'])) {
                $comp = wp_nonce_url(admin_url('admin.php?page=moovika-rdv&complete=' . $r['id']), 'mvk_complete_' . $r['id']);
                $complete_link = '  |  <a href="' . esc_url($comp) . '" onclick="return confirm(\'Marquer l\\\'installation terminée : mettre la commande au montant actuel et envoyer l\\\'e-mail de fin (paiement + avis Google) ?\')" style="color:#207227;font-weight:600;">Installation terminée</a>';
            }
            echo '<td><a href="' . esc_url($edt) . '">Modifier</a>' . $devis_link . $send_link . $order_link . $complete_link . '  |  <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="11">Aucun rendez-vous pour le moment.</td></tr>';
    }
    echo '</tbody></table></div>';
}

 

Comparaison Avant / Après

[bafg id= »8241″]

Spécifications techniques

Caractéristiques du modèle ÉLITE 9-2CH
Nom du modèle ÉLITE 9-2CH
Canal 2CH
Points forts 4K UHD HDR + 2K QHD HDR, Wi-Fi 2,4-5 GHz, Cloud (données non incluses), Mode parking à économie d’énergie
Dimensions / Poids du produit Avant : Longueur 129,7 mm (5,12 po) × Largeur 40 mm (1,58 po) × Hauteur 55,3 mm (2,18 po), 203,2 g (0,45 lb)
Arrière : Longueur 67 mm (2,64 po) × Largeur 29 mm (1,14 po) × Hauteur 39,1 mm (1,54 po), 38,8 g (0,09 lb)
Connexion de la caméra arrière Câble coaxial
Prise en charge de la mémoire carte microSD jusqu’à 1 To
Enregistrement intelligent des événements Impact de conduite
Impact de stationnement
Survitesse
Accélération brutale
Freinage brutal
Virage brutal
(y compris 10 secondes de tampon avant l’événement)
Mode stationnement Économie d’énergie (< 1 mA)
Détection de mouvement
en accéléré
Notifications vocales d’événements en mode stationnement OUI
Systèmes de protection Arrêt de la minuterie : 1 à 48 h
Arrêt basse tension : véhicule de tourisme 11,8 à 12,5 V ou véhicule lourd 22,8 à 24 V
Arrêt haute température (70 °C)
Capteur d’imagerie Avant et arrière : Capteur CMOS STARVIS 2 (IMX675)
Angle de vision Avant et arrière : Diagonale 142°, Horizontale 118°, Verticale 62°
Résolution / Fréquence d’images Avant : 4K UHD HDR à 30 images/s
Arrière : 2K QHD HDR à 30 images/s
* La fréquence d’images peut varier lors de la diffusion en continu via Wi-Fi
Codec vidéo H.264 (AVC)
Qualité d’image et débit binaire avant/arrière Le plus élevé (extrême) : 25 Mbps
Le plus élevé (par défaut) : 20 Mbps
Élevé : 15 Mbps
Normal : 10 Mbps
Format gratuit OUI (adaptatif)
Protection contre l’écrasement des fichiers d’événements OUI (jusqu’à 50)
Alerte de panne de carte SD OUI
Redémarrage programmé OUI
Extension de fichier vidéo MP4
Wi-Fi Intégré (802.11ac – 2,4-5 GHz)
Compatible avec le Cloud OUI
GPS Intégré (double bande : GPS, GLONASS)
Microphone Intégré
Haut-parleur Intégré
Capteur d’impact Capteur d’accélération à 3 axes
Indicateurs LED État d’enregistrement, connectivité GPS, connectivité A/C (application/cloud)
Bouton Bouton d’alimentation : Appuyez brièvement pour allumer / Appuyez longuement pour éteindre.
Capteur tactile : Appuyez sur le capteur tactile pour déclencher l’enregistrement manuel selon les paramètres du micrologiciel.
Bouton de télécommande Bluetooth (en option) : Enregistrement manuel.
Température de fonctionnement -20 °C − 65 °C (-4 °F − 149 °F)
Température de stockage -20 °C − 80 °C (-4 °F − 176 °F)
Coupure haute température Environ 75 °C (167 °F)
Batterie de secours Supercondensateur intégré
Puissance d’entrée DC 12V-24V (prise DC (Ø3,5 x Ø1,1)) vers fils (noir : GND / jaune : B+ / rouge : ACC)
Consommation d’énergie Mode normal (GPS activé/2 canaux) : 5,52 W (moyenne : 460 mA/12 V)
Mode parking (GPS désactivé/2 canaux) : 4,32 W (moyenne : 360 mA/12 V)
Mode économie d’énergie : moins de 12 mW (0,012 W) (moyenne : < 1 mA/12 V)
* La consommation électrique réelle peut varier en fonction des conditions d’utilisation et de l’environnement.
Certifications Avant : Telec, IC(ISED), CE, FCC, RCM, RoHS, WEEE
Arrière : FCC, IC(ISED), CE, RoHS, WEEE
Logiciel BlackVue Viewer
* Windows 7 ou supérieur, Mac Sierra OS X (10.12) ou supérieur
FLEETA Web Viewer
* Chrome 71 ou supérieur, Safari 13.0 ou supérieur
Application Application BlackVue
* Android 10.0 ou supérieur, iOS 15.0 ou supérieur
Autres Système de gestion de fichiers adaptatif sans format
Bluetooth Intégré (V2.1+EDR/5.3)
LTE Externe (avec module LTE en option)
Garantie 2 ans

Informations complémentaires

Poids 0,9 kg
Dimensions 20 × 15 × 10 cm
Carte mémoire Go

64, 128, 256, 512

Avis

Il n’y a pas encore d’avis.

Soyez le premier à laisser votre avis sur “ELITE 9 2CH”

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *