From 0a06cff0f26c028afb69fae7fdb280983247549b Mon Sep 17 00:00:00 2001 From: Danny Stocker Date: Fri, 21 Nov 2025 12:33:20 +0100 Subject: [PATCH] Add immersive cinema mode with live logistics and WhatsApp overlays --- index.tsx | 1196 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 766 insertions(+), 430 deletions(-) diff --git a/index.tsx b/index.tsx index 17d6a61..4237bdb 100644 --- a/index.tsx +++ b/index.tsx @@ -1,26 +1,63 @@ - -import React, { useState, useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; -import { AlertTriangle, CheckCircle, TrendingUp, TrendingDown, Truck, MessageCircle, Clock, Smartphone } from 'lucide-react'; +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 V2.0 (CINEMA MODE) - * Version Pure JS pour compatibilité maximale + * 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 */ -// --- CONSTANTES & STYLES --- +// ------------------------- CONSTANTS & STYLES ------------------------- const COLORS = { - bg: "#0f172a", // Slate 900 - card: "#1e293b", // Slate 800 - success: "#10b981", // Emerald 500 - danger: "#ef4444", // Red 500 - warning: "#f59e0b", // Amber 500 - primary: "#3b82f6", // Blue 500 - text: "#f8fafc", - textMuted: "#94a3b8" + bg: '#0f172a', + card: '#1e293b', + success: '#10b981', + danger: '#ef4444', + warning: '#f59e0b', + primary: '#3b82f6', + text: '#f8fafc', + textMuted: '#94a3b8', + border: 'rgba(255,255,255,0.1)' }; -const STYLES = { +// 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, @@ -28,494 +65,793 @@ const STYLES = { height: '100vh', display: 'flex', flexDirection: 'column', - overflow: 'hidden', - position: 'relative', - } as React.CSSProperties, + overflow: 'hidden' + }, header: { - padding: '20px', - backgroundColor: 'rgba(15, 23, 42, 0.9)', - borderBottom: `1px solid ${COLORS.card}`, + height: '72px', + backgroundColor: 'rgba(15, 23, 42, 0.95)', + borderBottom: `1px solid ${COLORS.border}`, display: 'flex', - justifyContent: 'space-between', alignItems: 'center', - zIndex: 10, - height: '80px' - } as React.CSSProperties, - metricBox: { - padding: '8px 16px', - borderRadius: '8px', + 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', - alignItems: 'center', - minWidth: '120px', - border: '1px solid rgba(255,255,255,0.05)' - } as React.CSSProperties, - mainStage: { - flex: 1, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', + gap: '14px', + background: 'linear-gradient(180deg, rgba(30,41,59,0.5), rgba(15,23,42,0.6))', + position: 'relative' + }, + colCenter: { position: 'relative', - padding: '20px' - } as React.CSSProperties, - card: { + 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, - borderRadius: '16px', - padding: '40px', - width: '650px', - maxWidth: '100%', - boxShadow: '0 20px 50px rgba(0,0,0,0.5)', - position: 'relative', - border: `1px solid ${COLORS.textMuted}20` - } as React.CSSProperties, - button: (variant: string, isActive = false): React.CSSProperties => ({ - padding: '16px 24px', - borderRadius: '12px', - fontWeight: 600, - cursor: 'pointer', + 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', - alignItems: 'center', - gap: '12px', - width: '100%', - transition: 'all 0.2s', - backgroundColor: isActive ? (variant === 'success' ? `${COLORS.success}30` : `${COLORS.danger}30`) : 'rgba(255,255,255,0.03)', - color: isActive ? (variant === 'success' ? COLORS.success : COLORS.danger) : COLORS.textMuted, - border: `1px solid ${isActive ? (variant === 'success' ? COLORS.success : COLORS.danger) : 'rgba(255,255,255,0.1)'}`, - transform: isActive ? 'scale(1.02)' : 'scale(1)' - }) + flexDirection: 'column', + gap: '8px', + justifyContent: 'flex-end' + } }; -// --- COMPOSANTS UI --- +// --------------------------- AUDIO ENGINE --------------------------- +class SoundEngine { + ctx: AudioContext | null = null; + muted = false; -const StatCounter = ({ label, value, type }: { label: string; value: number; type: string }) => ( -
- {label} -
- {type === 'cost' ? : } - + 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 && (
)}
); -const FlashCard = ({ title, message, type }: { title: string; message: string; type: string }) => ( -
- {type === 'bad' ? : } -

{title}

-

{message}

-
-); - -const PhoneMockup = ({ show, type }: { show: boolean; type: string }) => { - if (!show) return null; - return ( -
- {/* Notch */} -
- - {/* Header */} -
-
16:02
-
- {type === 'whatsapp' &&
} - {type === 'whatsapp' ? 'Chantier DUPONT' : 'Messagerie'} -
-
- - {/* Body */} -
- - {type === 'whatsapp' ? ( - <> -
Aujourd'hui
- - {/* Message Commercial */} -
-
Sophie (Logistique)
- ✅ Commande #402 chargée.
Arrivée demain 10h00. -
16:00
-
- - {/* Photo */} -
-
- - [PHOTO PALETTES] -
-
16:00
-
- - ) : ( - <> -
Hier 17:12
-
- GEDIMAT: Votre livraison est prévue pour demain dans la journée. -
- - )} -
- - {/* Footer Input */} -
-
- ); -}; - -function GedimatSimulator() { +// ------------------------------ MAIN APP ------------------------------ +export default function GedimatSimulator() { const [metrics, setMetrics] = useState({ waste: 0, savings: 0 }); - const [cursor, setCursor] = useState({ x: -100, y: -100, clicking: false }); - const [activeModule, setActiveModule] = useState(0); // 0: Intro, 1: Transport, 2: 15:30, 3: WhatsApp, 4: Summary - const [flashcard, setFlashcard] = useState<{ title: string; msg: string; type: string } | null>(null); - const [simStep, setSimStep] = useState(0); - const [phoneType, setPhoneType] = useState(null); + 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 btnBadRef = useRef(null); - const btnGoodRef = useRef(null); const startBtnRef = useRef(null); + const btnActionRef = useRef(null); - // Initialiser cursor au centre - useEffect(() => { - setCursor({ x: window.innerWidth / 2, y: window.innerHeight / 2, clicking: false }); - }, []); + const clockStr = useMemo(() => fmtTime(timeMinutes), [timeMinutes]); + const countdownMinutes = Math.max(targetMinutes - timeMinutes, 0); - const moveCursorTo = (ref: React.RefObject, callback?: () => void) => { - if (ref.current) { - const rect = ref.current.getBoundingClientRect(); - const targetX = rect.left + rect.width / 2; - const targetY = rect.top + rect.height / 2; - - setCursor(prev => ({ ...prev, x: targetX, y: targetY })); - - setTimeout(() => { - setCursor(prev => ({ ...prev, clicking: true })); // Click down - setTimeout(() => { - setCursor(prev => ({ ...prev, clicking: false })); // Click up - if (callback) callback(); - }, 300); - }, 800); // Temps de trajet - } + // 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(); }; - // --- SCÉNARIO AUTOMATIQUE --- + 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(() => { - let timeout: ReturnType; + const id = setInterval(() => { + setTimeMinutes(m => Math.min(m + 1 * speed, targetMinutes)); + }, 600); + return () => clearInterval(id); + }, [speed, targetMinutes]); - const runScenario = async () => { - // AUTO START - if (activeModule === 0) { - timeout = setTimeout(() => { - if (startBtnRef.current) { - moveCursorTo(startBtnRef, () => setActiveModule(1)); - } - }, 2000); + // 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: TRANSPORT ---------------- - if (activeModule === 1) { - if (simStep === 0) { - // Expliquer le problème - setFlashcard({ title: "LE PROBLÈME", msg: "Gisors et Méru commandent séparément au même fournisseur.", type: 'bad' }); - timeout = setTimeout(() => { - setFlashcard(null); - // Action "Mauvaise" - moveCursorTo(btnBadRef, () => { - setMetrics(prev => ({ ...prev, waste: prev.waste + 180 })); - setFlashcard({ title: "DOUBLE FACTURATION", msg: "2 camions payés pour rien. Gaspillage : 180€.", type: 'bad' }); - setTimeout(() => { setFlashcard(null); setSimStep(1); }, 3000); - }); - }, 3000); - } else if (simStep === 1) { - // Expliquer la solution - timeout = setTimeout(() => { - // Action "Bonne" - moveCursorTo(btnGoodRef, () => { - setMetrics(prev => ({ ...prev, savings: prev.savings + 90 })); - setFlashcard({ title: "SOLUTION XCEL", msg: "Consolidation J-1. Livraison unique à Méru + Navette interne.", type: 'good' }); - setTimeout(() => { setFlashcard(null); setActiveModule(2); setSimStep(0); }, 3500); - }); - }, 1000); - } + // 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 (activeModule === 2) { - if (simStep === 0) { - setFlashcard({ title: "LE PROBLÈME", msg: "Le camion n'est pas là à 15h30. On ne sait rien.", type: 'bad' }); - timeout = setTimeout(() => { - setFlashcard(null); - moveCursorTo(btnBadRef, () => { - setMetrics(prev => ({ ...prev, waste: prev.waste + 2000 })); - setFlashcard({ title: "CLIENT PERDU", msg: "Le client découvre le retard demain matin. Chantier arrêté.", type: 'bad' }); - setTimeout(() => { setFlashcard(null); setSimStep(1); }, 3000); - }); - }, 3000); - } else if (simStep === 1) { - timeout = setTimeout(() => { - moveCursorTo(btnGoodRef, () => { + + // 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 })); - setFlashcard({ title: "PROTOCOLE 15:30", msg: "Alerte immédiate. Taxi-colis activé. Client prévenu à 16h.", type: 'good' }); - setTimeout(() => { setFlashcard(null); setActiveModule(3); setSimStep(0); }, 3500); - }); - }, 1000); - } + setModule(4); + } + ); + }); } - // ---------------- MODULE 3: WHATSAPP CONCIERGE ---------------- - if (activeModule === 3) { - if (simStep === 0) { - setFlashcard({ title: "LE PROBLÈME", msg: "SMS impersonnel envoyé à 17h. Aucune réponse.", type: 'bad' }); - timeout = setTimeout(() => { - setFlashcard(null); - moveCursorTo(btnBadRef, () => { - setPhoneType('sms'); // Show SMS phone - setFlashcard({ title: "COMMUNICATION FROIDE", msg: "Le client n'est pas rassuré. Il rappelle le commercial.", type: 'bad' }); - setTimeout(() => { setPhoneType(null); setFlashcard(null); setSimStep(1); }, 4000); - }); - }, 3000); - } else if (simStep === 1) { - timeout = setTimeout(() => { - moveCursorTo(btnGoodRef, () => { - setMetrics(prev => ({ ...prev, savings: prev.savings + 1200 })); - setPhoneType('whatsapp'); // Show WhatsApp phone - setFlashcard({ title: "EFFET 'CONCIERGE'", msg: "Groupe WhatsApp Chantier. Photo envoyée. Confiance totale.", type: 'good' }); - setTimeout(() => { setPhoneType(null); setFlashcard(null); setActiveModule(4); }, 5000); - }); - }, 1000); - } + // 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); + } + ); + }); } }; - runScenario(); + run(); return () => clearTimeout(timeout); - }, [activeModule, simStep]); - - // --- RENDU DES MODULES --- + }, [module, speed, clockStr, targetMinutes]); + // ------------------------------ RENDER ------------------------------ return (
- {/* HEADER P&L */} + {/* HEADER */}
-
GEDIMAT XCEL
-
- - +
+
+ +
+
+
GEDIMAT XCEL
+
CINEMA MODE v3.55
+
+
+ +
+ +
+ + +
+ +
+ +
- {/* MAIN STAGE */} -
-
- {activeModule === 0 && ( -
-

Gedimat XCEL v3.55

-

Simulation d'Impact Stratégique & Financier

- +
🔊 Audio recommandé
+
+ )} + + {module > 0 && module < 6 && } + + {module > 0 && module < 6 && ( +
+
)} - {activeModule === 1 && ( -
-
-

- Consolidation Gisors-Méru -

- MODULE 1/3 -
- -
-

- Gisors commande des tuiles à Beauvais. Méru commande du ciment à Beauvais.
- Que fait-on ? -

-
- -
- - -
-
- )} - - {activeModule === 2 && ( -
-
-

- Protocole 15:30 -

- MODULE 2/3 -
- -
-
15:30
-

- Pas de nouvelles de Médiafret pour la livraison de demain. -

-
- -
- - -
-
- )} - - {activeModule === 3 && ( -
-
-

- Interface Client -

- MODULE 3/3 -
- -
-

- Le client s'inquiète pour sa livraison de demain.
Comment communiquer ? -

-
- -
- - -
- - -
- )} - - {activeModule === 4 && ( -
-

DÉMONSTRATION TERMINÉE

-
-
-
- {metrics.waste.toLocaleString()} € -
-
Pertes Identifiées
+ {module === 6 && ( +
+

Séquence terminée

+
+
+
Pertes Évitées
+
{metrics.waste.toLocaleString()}€
-
-
- {metrics.savings.toLocaleString()} € -
-
Gains Stratégiques
+
+
Gains Stratégiques
+
{metrics.savings.toLocaleString()}€
-
)} - {/* Overlay Flashcard */} - {flashcard && } + + +
+ + {/* RIGHT */} +
+
- {/* CURSOR LAYER */} - - {/* FOOTER LABEL */} -
- Démonstration Automatique — Algorithme v3.55 -
); }