import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { Activity, AlertTriangle, Building2, CheckCircle, Clock, FastForward, MapPin, MessageCircle, Package, Pause, Play, ShieldAlert, Smartphone, TrendingDown, TrendingUp, Truck, Users, Volume2, VolumeX } from 'lucide-react'; /** * GEDIMAT LUNEL NEGOCE - SIMULATEUR STRATÉGIQUE V3.55 (CINEMA MODE) * Vise à rendre tangible le rapport "Fidélisation par l'Excellence Logistique" * - Horloge LED + compte à rebours accéléré * - Flashcards plein écran puis retour en colonne * - Carte logistique animée (camions/navette, états) * - WhatsApp Chantier Direct en plein écran (auteur EXCEL) * - Journal MAJ EXCEL, objectifs pédagogiques */ // ------------------------- CONSTANTS & STYLES ------------------------- const COLORS = { bg: '#0f172a', card: '#1e293b', success: '#10b981', danger: '#ef4444', warning: '#f59e0b', primary: '#3b82f6', text: '#f8fafc', textMuted: '#94a3b8', border: 'rgba(255,255,255,0.1)' }; // Inject keyframes once const styleSheet = document.createElement('style'); styleSheet.innerText = ` @keyframes popIn { 0% { opacity: 0; transform: scale(0.9); } 100% { opacity: 1; transform: scale(1); } } @keyframes slideIn { 0% { transform: translateY(20px); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } @keyframes pulseRed { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } @keyframes pulseGreen { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } } @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } } `; document.head.appendChild(styleSheet); const STYLES: Record = { container: { fontFamily: '"Inter", "Segoe UI", sans-serif', backgroundColor: COLORS.bg, color: COLORS.text, height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }, header: { height: '72px', backgroundColor: 'rgba(15, 23, 42, 0.95)', borderBottom: `1px solid ${COLORS.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 18px', zIndex: 30 }, grid: { display: 'grid', gridTemplateColumns: '320px 1fr 320px', height: 'calc(100vh - 72px)', overflow: 'hidden' }, colLeft: { borderRight: `1px solid ${COLORS.border}`, padding: '18px', display: 'flex', flexDirection: 'column', gap: '14px', background: 'linear-gradient(180deg, rgba(30,41,59,0.5), rgba(15,23,42,0.6))', position: 'relative' }, colCenter: { position: 'relative', background: 'radial-gradient(circle at 30% 20%, #1e293b 0%, #0f172a 65%)', overflow: 'hidden' }, colRight: { borderLeft: `1px solid ${COLORS.border}`, display: 'flex', flexDirection: 'column', background: 'linear-gradient(180deg, rgba(30,41,59,0.5), rgba(15,23,42,0.6))' }, flashCard: { backgroundColor: COLORS.card, border: `1px solid ${COLORS.primary}`, borderRadius: '14px', padding: '18px', boxShadow: '0 14px 30px -12px rgba(0,0,0,0.5)', animation: 'popIn 0.45s cubic-bezier(0.16, 1, 0.3, 1)' }, logContainer: { flex: 1, padding: '14px', overflowY: 'hidden', display: 'flex', flexDirection: 'column', gap: '8px', justifyContent: 'flex-end' } }; // --------------------------- AUDIO ENGINE --------------------------- class SoundEngine { ctx: AudioContext | null = null; muted = false; init() { if (!this.ctx) { this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); } } toggleMute() { this.muted = !this.muted; return this.muted; } playTone(freq: number, type: OscillatorType, duration: number, vol = 0.1) { if (this.muted || !this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, this.ctx.currentTime); gain.gain.setValueAtTime(vol, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + duration); } click() { this.playTone(850, 'sine', 0.08, 0.05); } whoosh() { this.playTone(280, 'sine', 0.25, 0.08); } alert() { this.playTone(440, 'square', 0.12, 0.08); setTimeout(() => this.playTone(440, 'square', 0.12, 0.08), 140); } type() { this.playTone(620 + Math.random() * 120, 'triangle', 0.05, 0.02); } success() { this.playTone(440, 'sine', 0.18, 0.08); setTimeout(() => this.playTone(554, 'sine', 0.18, 0.08), 90); setTimeout(() => this.playTone(659, 'sine', 0.25, 0.08), 180); } } const sfx = new SoundEngine(); // ------------------------------ UTILS ------------------------------ const fmtTime = (m: number) => { const h = Math.floor(m / 60); const mm = m % 60; return `${String(h).padStart(2, '0')}:${String(mm).padStart(2, '0')}`; }; // ---------------------------- COMPONENTS ---------------------------- const StatCounter = ({ label, value, type }: { label: string; value: number; type: 'cost' | 'profit' }) => (
{label}
{type === 'cost' ? : } {value.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 })}
); const LEDClock = ({ time, speed, countdownLabel, countdownMinutes }: { time: string; speed: number; countdownLabel: string; countdownMinutes: number; }) => { const pct = Math.max(0, Math.min(100, 100 - (countdownMinutes / 60) * 100)); return (
HEURE RAPIDE
{time}
{countdownLabel} ({Math.max(countdownMinutes, 0)} min)
{speed > 1 &&
x{speed} SPEED
}
); }; type OrderState = { id: string; pos: { x: number; y: number }; duration: number; color: string; label?: string; type?: 'truck' | 'box'; }; const LogisticsMap = ({ orders }: { orders: OrderState[] }) => { const NODES = { supplier: { x: 16, y: 22, label: 'Fournisseur', icon: }, meru: { x: 48, y: 30, label: 'Dépôt Méru', icon: }, gisors: { x: 48, y: 72, label: 'Hub Gisors', icon: }, client: { x: 82, y: 50, label: 'Chantier VIP', icon: } }; return (
{Object.entries(NODES).map(([key, node]) => (
{node.icon}
{node.label}
))} {orders.map(order => (
{order.type === 'box' ? : }
{order.label && (
{order.label}
)}
))}
); }; const DecisionLog = ({ logs }: { logs: any[] }) => { const endRef = useRef(null); useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [logs]); return (
DÉCISIONS EN TEMPS RÉEL
{logs.map((log, i) => (
{log.time} {log.impact}
{log.text} {log.excel && MAJ EXCEL}
))}
); }; const ObjectiveList = ({ objectives }: { objectives: Record }) => (
OBJECTIFS À MAÎTRISER
{[ ['sync', 'Synchroniser J-1 (14:00)'], ['planB', 'Plan B à 15:30 (Taxi-Colis)'], ['whatsapp', 'Informer en direct sur WhatsApp'], ['stock', 'Éviter stock mort (double peine)'], ['stress', 'Gérer incident chauffeur'], ['relation', 'Relationnel/NPS client'] ].map(([key, label]) => (
{label}
))}
); const FlashcardOverlay = ({ flashcard, focus }: { flashcard: any; focus: boolean }) => { if (!flashcard || !focus) return null; return (

{flashcard.title}

{flashcard.content}

Pourquoi
{flashcard.why}
Impact {flashcard.impact}
); }; const FullscreenWhatsApp = ({ show, messages, isTyping, groupName }: any) => { if (!show) return null; return (
{groupName}
EXCEL, Client, Commercial, Logistique
{messages.map((m: any, i: number) => (
{m.author || 'EXCEL'}
{m.text}
{m.time}
))} {isTyping && (
EXCEL écrit...
)}
Message...
); }; const VirtualCursor = ({ x, y, clicking }: { x: number; y: number; clicking: boolean }) => (
{clicking && (
)}
); // ------------------------------ MAIN APP ------------------------------ export default function GedimatSimulator() { const [metrics, setMetrics] = useState({ waste: 0, savings: 0 }); const [logs, setLogs] = useState([]); const [cursor, setCursor] = useState({ x: window.innerWidth / 2, y: window.innerHeight / 2, clicking: false }); const [module, setModule] = useState(0); const [flashcard, setFlashcard] = useState(null); const [flashFocus, setFlashFocus] = useState(false); const [mapOrders, setMapOrders] = useState([]); const [timeMinutes, setTimeMinutes] = useState(14 * 60); const [targetMinutes, setTargetMinutes] = useState(14 * 60 + 5); const [countdownLabel, setCountdownLabel] = useState('Vers 15:30'); const [phone, setPhone] = useState({ show: false, messages: [] as any[], isTyping: false, group: '' }); const [objectives, setObjectives] = useState>({ sync: false, planB: false, whatsapp: false, stock: false, stress: false, relation: false }); const [speed, setSpeed] = useState(1); const [audioEnabled, setAudioEnabled] = useState(false); const startBtnRef = useRef(null); const btnActionRef = useRef(null); const clockStr = useMemo(() => fmtTime(timeMinutes), [timeMinutes]); const countdownMinutes = Math.max(targetMinutes - timeMinutes, 0); // Helpers const addLog = (time: string, text: string, type: 'good' | 'bad', impact: string, excel = false) => { setLogs(prev => [...prev, { time, text, type, impact, excel }]); if (type === 'good') sfx.success(); else sfx.alert(); }; const moveCursor = (ref: React.RefObject, delay: number, callback?: () => void) => { setTimeout(() => { if (ref.current) { const rect = ref.current.getBoundingClientRect(); setCursor({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, clicking: false }); setTimeout(() => { setCursor(prev => ({ ...prev, clicking: true })); sfx.click(); setTimeout(() => { setCursor(prev => ({ ...prev, clicking: false })); if (callback) callback(); }, 260 / speed); }, 700 / speed); } }, delay / speed); }; const showFlashcard = (fc: any) => { setFlashcard(fc); setFlashFocus(true); sfx.whoosh(); setTimeout(() => setFlashFocus(false), 1800 / speed); }; const typeMessage = (text: string, group: string, author = 'EXCEL', timeLabel?: string, callback?: () => void) => { setPhone({ show: true, messages: [], isTyping: true, group }); const typingInterval = setInterval(() => { if (Math.random() > 0.4) sfx.type(); }, 110); setTimeout(() => { clearInterval(typingInterval); setPhone(prev => ({ ...prev, isTyping: false, messages: [...prev.messages, { text, isMe: true, author, time: timeLabel || clockStr }] })); sfx.success(); setTimeout(() => { setPhone(prev => ({ ...prev, show: false })); callback && callback(); }, 2200 / speed); }, 2600 / speed); }; const updateMapOrder = (id: string, pos: { x: number; y: number }, duration: number, color: string, label?: string, type: 'truck' | 'box' = 'truck') => { setMapOrders(prev => { const existing = prev.find(o => o.id === id); if (existing) { return prev.map(o => o.id === id ? { ...o, pos, duration: duration / speed, color, label, type } : o); } return [...prev, { id, pos, duration: duration / speed, color, label, type }]; }); }; // Timer ticks forward continually toward target useEffect(() => { const id = setInterval(() => { setTimeMinutes(m => Math.min(m + 1 * speed, targetMinutes)); }, 600); return () => clearInterval(id); }, [speed, targetMinutes]); // Scenario engine useEffect(() => { let timeout: any; const run = () => { // INTRO if (module === 0) { setTimeMinutes(14 * 60); setTargetMinutes(14 * 60 + 1); setCountdownLabel('Vers 14:00'); setMapOrders([]); setFlashcard(null); timeout = setTimeout(() => { if (startBtnRef.current) moveCursor(startBtnRef, 600, () => setModule(1)); }, 900); } // MODULE 1: CHECK J-1 if (module === 1) { setTimeMinutes(14 * 60); setTargetMinutes(14 * 60 + 5); setCountdownLabel('Vers 14:05 (Check J-1)'); setMapOrders([]); updateMapOrder('g1', { x: 16, y: 22 }, 0, COLORS.warning, 'Tuiles (Gisors)'); updateMapOrder('g2', { x: 18, y: 24 }, 0, COLORS.warning, 'Ciment (Méru)'); showFlashcard({ title: 'CHECK J-1 (14:00)', content: 'Deux commandes Fournisseur Beauvais détectées pour Gisors et Méru.', why: 'Sans consolidation : 2 camions facturés, 0 synergie.', impact: 'Risque -360€ (double affrètement)' }); moveCursor(btnActionRef, 3000, () => { addLog('14:05', 'Consolidation Gisors+Méru validée', 'good', '+180€', true); setObjectives(prev => ({ ...prev, sync: true, relation: true })); setMetrics(prev => ({ ...prev, savings: prev.savings + 180 })); setFlashcard(null); updateMapOrder('g1', { x: 48, y: 30 }, 1800, COLORS.success, 'Groupage'); updateMapOrder('g2', { x: 48, y: 30 }, 1800, COLORS.success, ''); setTimeout(() => { updateMapOrder('g1', { x: 48, y: 72 }, 2000, COLORS.primary, 'Navette Interne'); setTimeout(() => setModule(2), 2400 / speed); }, 2000 / speed); }); } // MODULE 2: PROTOCOLE 15:30 if (module === 2) { setTimeMinutes(15 * 60 + 30); setTargetMinutes(15 * 60 + 45); setCountdownLabel('Décision Plan B avant 15:45'); setMapOrders([]); updateMapOrder('p1', { x: 16, y: 22 }, 0, COLORS.danger, 'Médiafret silencieux'); showFlashcard({ title: 'PROTOCOLE 15:30', content: 'Silence radio du transporteur. Le chantier risque la rupture demain.', why: 'Attendre 17:00 = client en colère et chantier à l’arrêt.', impact: 'Perte client, surcoût chantier' }); moveCursor(btnActionRef, 3400, () => { addLog('15:45', 'Plan B Taxi-Colis activé', 'good', 'Client sauvé', true); addLog('15:45', 'Surcoût Taxi accepté', 'bad', '-50€', true); setObjectives(prev => ({ ...prev, planB: true })); setMetrics(prev => ({ ...prev, waste: prev.waste + 50, savings: prev.savings + 2000 })); setFlashcard(null); updateMapOrder('p1', { x: 82, y: 50 }, 1400, COLORS.warning, 'Taxi-Colis Express'); setTimeout(() => setModule(3), 1800 / speed); }); } // MODULE 3: WHATSAPP DIRECT (EXCEL écrit) if (module === 3) { setTimeMinutes(16 * 60); setTargetMinutes(16 * 60 + 3); setCountdownLabel('Informer le client à 16:00'); setMapOrders([]); showFlashcard({ title: 'WHATSAPP CHANTIER DIRECT', content: 'EXCEL doit prévenir le client AVANT qu’il ne s’inquiète.', why: 'Transparence immédiate = confiance.', impact: 'Fidélisation et temps gagné' }); moveCursor(btnActionRef, 2800, () => { setFlashcard(null); typeMessage( "⚠️ EXCEL : Taxi-colis confirmé. Arrivée chantier 08h00. Vous pouvez maintenir la pose à 10h. On reste connectés.", 'Chantier DURAND (VIP)', 'EXCEL', '16:00', () => { addLog('16:02', 'Client rassuré (WhatsApp)', 'good', 'Confiance++', true); setObjectives(prev => ({ ...prev, whatsapp: true, relation: true })); setMetrics(prev => ({ ...prev, savings: prev.savings + 500 })); setModule(4); } ); }); } // MODULE 4: STOCK MORT if (module === 4) { setTimeMinutes(14 * 60 + 15); setTargetMinutes(14 * 60 + 30); setCountdownLabel('Reroutage avant annulation'); setMapOrders([]); updateMapOrder('s1', { x: 48, y: 72 }, 0, COLORS.danger, 'Commande spéciale en péril', 'box'); showFlashcard({ title: 'RISQUE STOCK MORT', content: 'Commande spéciale : le client menace d’annuler.', why: 'Double peine : marge perdue + stock invendable.', impact: '-6 000€ évitables' }); moveCursor(btnActionRef, 3000, () => { addLog('14:30', 'Reroutage immédiat validé', 'good', '6 000€ sauvés', true); setObjectives(prev => ({ ...prev, stock: true })); setMetrics(prev => ({ ...prev, savings: prev.savings + 6000 })); setFlashcard(null); updateMapOrder('s1', { x: 82, y: 50 }, 1800, COLORS.success, 'Reroutage client', 'truck'); setTimeout(() => setModule(5), 2200 / speed); }); } // MODULE 5: STRESS TEST CHAUFFEUR if (module === 5) { setTimeMinutes(11 * 60); setTargetMinutes(11 * 60 + 6); setCountdownLabel('Prévenir avant 11:06'); setMapOrders([]); updateMapOrder('c1', { x: 48, y: 30 }, 0, COLORS.danger, 'Chauffeur bloqué'); showFlashcard({ title: 'STRESS TEST INCIDENT', content: 'J-0, 11:00. Chauffeur bloqué. VIP attend.', why: 'Silence = perte client. Transparence = récupération.', impact: 'Satisfaction préservée' }); moveCursor(btnActionRef, 3000, () => { setFlashcard(null); typeMessage( "🚨 EXCEL : Chauffeur bloqué. Nouvelle ETA 13h00. On suit en direct, le chef de dépôt est mobilisé.", 'Chantier VIP (Incident chauffeur)', 'EXCEL', '11:03', () => { addLog('11:05', 'Transparence immédiate', 'good', 'Fidélité', true); setObjectives(prev => ({ ...prev, stress: true, relation: true })); setMetrics(prev => ({ ...prev, savings: prev.savings + 1000 })); setModule(6); } ); }); } }; run(); return () => clearTimeout(timeout); }, [module, speed, clockStr, targetMinutes]); // ------------------------------ RENDER ------------------------------ return (
{/* HEADER */}
GEDIMAT XCEL
CINEMA MODE v3.55
{/* GRID */}
{/* LEFT */}

Contexte & Flashcards

{flashcard && (

{flashcard.title}

{flashcard.content}

Pourquoi
{flashcard.why}
Impact {flashcard.impact}
)}
Tutoriel : suivez le curseur, lisez la flashcard au centre puis observez la carte et le journal des décisions.
{/* CENTER */}
{module === 0 && (

Excellence Logistique

Démonstration immersive v3.55

🔊 Audio recommandé
)} {module > 0 && module < 6 && } {module > 0 && module < 6 && (
)} {module === 6 && (

Séquence terminée

Pertes Évitées
{metrics.waste.toLocaleString()}€
Gains Stratégiques
{metrics.savings.toLocaleString()}€
)}
{/* RIGHT */}
); } const root = createRoot(document.getElementById('root')!); root.render();