Spaces:
Running
Running
| window.onerror = function(msg, url, line) { | |
| console.error('ERREUR JS :', msg, line); | |
| }; | |
| const state = { | |
| isReturnTrip: false, | |
| mode: 'course', | |
| disposalDuration: 120, | |
| customWelcome: false, | |
| profile: { firstName: 'Marie', lastName: 'Dupont', phone: '06 12 34 56 78', email: 'marie@email.com', address: '' }, | |
| customerType: 'particulier', | |
| billingAddress: '', | |
| company: { | |
| name: '', | |
| siret: '', | |
| vat: '', | |
| billingEmail: '' | |
| }, | |
| drivers: [ | |
| { id: 'karim', name: 'Karim', photo: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face', status: 'Disponible', vehicle: 'Mercedes Classe E', rate: 2, rides: 18, favorite: true, available: true, phone: '+33612345678' }, | |
| { id: 'sofiane', name: 'Sofiane', photo: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=100&h=100&fit=crop&crop=face', status: 'Disponible', vehicle: 'Tesla Model 3', rate: 2.2, rides: 11, favorite: false, available: true, phone: '+33623456789' }, | |
| { id: 'nadia', name: 'Nadia', photo: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop&crop=face', status: 'Occupé', vehicle: 'Peugeot 508', rate: 1.9, rides: 7, favorite: false, available: false, phone: '+33634567890' }, | |
| { id: 'rachid', name: 'Rachid', photo: 'https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=100&h=100&fit=crop&crop=face', status: 'Hors ligne', vehicle: 'Toyota Corolla', rate: 2, rides: 5, favorite: false, available: false, phone: '+33645678901' } | |
| ], | |
| favoriteAddresses: [ | |
| { id:'home', label:'Maison', tag:'maison', icon:'home', address:'12 Rue de la Paix, Paris' }, | |
| { id:'work', label:'Travail', tag:'travail', icon:'briefcase', address:'Bureau, Paris' }, | |
| { id:'airport', label:'Aéroport', tag:'aéroport', icon:'plane', address:'Aéroport CDG, Terminal 2' }, | |
| { id:'station', label:'Gare', tag:'gare', icon:'train', address:'Gare de Lyon, Paris' } | |
| ], | |
| rides: [ | |
| { id: 1, date: "Aujourd'hui à 14:30", from: '12 Rue de la Paix', to: 'Aéroport CDG', driver: 'Karim', status: 'Confirmée', amount: '52 €', filter: 'today', vehicle: 'Mercedes Classe E', distance: '24 km', rate: 2, passengers: 1, luggage: 1, animal: false }, | |
| { id: 2, date: 'Envoyée il y a 5 min', from: 'Domicile', to: 'Gare de Lyon', driver: 'Demande groupée', status: 'En attente', amount: null, filter: 'today', vehicle: '-', distance: '-', rate: null, passengers: 1, luggage: 1, animal: false }, | |
| { id: 3, date: 'Demain à 09:00', from: 'Orly', to: 'La Défense', driver: 'Sofiane', status: 'Confirmée', amount: '45 €', filter: 'upcoming', vehicle: 'Tesla Model 3', distance: '22 km', rate: 2.2, passengers: 2, luggage: 2, animal: false }, | |
| { id: 4, date: 'Hier à 18:15', from: 'Bureau', to: 'Domicile', driver: 'Karim', status: 'Terminée', amount: '28 €', filter: 'past', vehicle: 'Mercedes Classe E', distance: '14 km', rate: 2, passengers: 1, luggage: 0, animal: false }, | |
| { id: 5, date: '12/01/2026', from: 'Domicile', to: 'Gare du Nord', driver: 'Nadia', status: 'Annulée', amount: '-', filter: 'cancelled', vehicle: 'Peugeot 508', distance: '-', rate: 1.9, passengers: 1, luggage: 1, animal: false } | |
| ], | |
| relatives: [ | |
| { firstName: 'Lucas', lastName: 'Dupont', phone: '06 23 45 67 89' } | |
| ], | |
| segment: 'fav', | |
| when: 'now', | |
| passengers: 1, | |
| luggage: 1, | |
| animal: false, | |
| forOther: false, | |
| currentRide: null | |
| }; | |
| const views = ['home', 'drivers', 'rides', 'profile']; | |
| function showView(name) { | |
| views.forEach(v => { | |
| const el = document.getElementById('view-' + v); | |
| if (el) { | |
| el.classList.add('hidden'); | |
| el.classList.remove('flex'); | |
| el.classList.remove('block'); | |
| } | |
| }); | |
| const target = document.getElementById('view-' + name); | |
| if (target) { | |
| target.classList.remove('hidden'); | |
| if (target.tagName.toLowerCase() === 'main') { | |
| target.classList.add('block'); | |
| } else { | |
| target.classList.add('block'); | |
| } | |
| target.scrollTop = 0; | |
| } | |
| document.querySelectorAll('.nav-btn').forEach(btn => { | |
| if (btn.dataset.view === name) { | |
| btn.classList.remove('text-white/40'); | |
| btn.classList.add('text-emerald-400'); | |
| } else { | |
| btn.classList.remove('text-emerald-400'); | |
| btn.classList.add('text-white/40'); | |
| } | |
| }); | |
| } | |
| function openModal(id) { | |
| const el = document.getElementById(id); | |
| if (!el) return; | |
| el.classList.remove('hidden'); | |
| void el.offsetWidth; | |
| el.classList.add('active'); | |
| lucide.createIcons(); | |
| } | |
| function closeModal(id) { | |
| const el = document.getElementById(id); | |
| if (!el) return; | |
| el.classList.remove('active'); | |
| setTimeout(() => el.classList.add('hidden'), 300); | |
| } | |
| function renderDrivers() { | |
| const container = document.getElementById('drivers-list'); | |
| if (!container) return; | |
| if (state.drivers.length === 0) { | |
| container.innerHTML = '<p class="text-sm text-white/45 text-center py-8">Aucun chauffeur dans votre réseau.</p>'; | |
| lucide.createIcons(); | |
| return; | |
| } | |
| container.innerHTML = state.drivers.map(d => { | |
| const statusColor = d.status === 'Disponible' ? 'text-emerald-400' : d.status === 'Occupé' ? 'text-amber-400' : 'text-white/40'; | |
| return ` | |
| <div class="bg-[#151821] border border-white/10 rounded-2xl p-4 shadow-sm flex items-center gap-4"> | |
| <div class="relative shrink-0"> | |
| <img src="${d.photo}" class="w-12 h-12 rounded-full object-cover opacity-90 border border-white/10"> | |
| <span class="absolute bottom-0 right-0 w-3 h-3 ${d.status === 'Disponible' ? 'bg-emerald-400' : d.status === 'Occupé' ? 'bg-amber-400' : 'bg-gray-500'} rounded-full border-[2.5px] border-[#151821]"></span> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="flex items-center gap-2"> | |
| <p class="text-sm font-semibold text-white">${d.name}</p> | |
| <button onclick="setFavoriteDriver('${d.id}')" class="press"> | |
| <i data-lucide="star" class="w-4 h-4 ${d.favorite ? 'text-yellow-400 fill-yellow-400' : 'text-white/30'}"></i> | |
| </button> | |
| </div> | |
| <p class="text-xs ${statusColor}">${d.status}</p> | |
| <p class="text-xs text-white/45 mt-0.5">${d.vehicle} • ${d.rate.toFixed(1).replace('.0','')} €/km • ${d.rides} courses</p> | |
| </div> | |
| <div class="flex gap-2 items-center"> | |
| ${d.phone ? `<a href="tel:${d.phone}" class="press h-8 w-8 rounded-full bg-[#1a1f2b] border border-white/10 flex items-center justify-center text-emerald-400"><i data-lucide="phone" class="w-4 h-4"></i></a>` : ''} | |
| <button onclick="selectDriver('${d.id}')" class="press h-8 px-3 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-xs font-semibold text-emerald-400 hover:bg-emerald-500/20 transition-colors">Choisir</button> | |
| <button onclick="removeDriver('${d.id}')" class="press h-8 w-8 rounded-full bg-[#1a1f2b] border border-white/10 flex items-center justify-center text-red-400"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| lucide.createIcons(); | |
| } | |
| function setFavoriteDriver(id) { | |
| state.drivers.forEach(d => d.favorite = (d.id === id)); | |
| renderDrivers(); | |
| renderSelectedDriver(); | |
| renderEstimate(); | |
| } | |
| function removeDriver(id) { | |
| const wasFav = state.drivers.find(d => d.id === id)?.favorite; | |
| state.drivers = state.drivers.filter(d => d.id !== id); | |
| if (wasFav && state.drivers.length > 0) { | |
| state.drivers[0].favorite = true; | |
| } | |
| renderDrivers(); | |
| renderSelectedDriver(); | |
| renderEstimate(); | |
| } | |
| function selectDriver(id) { | |
| setFavoriteDriver(id); | |
| setSegment('fav'); | |
| showView('home'); | |
| } | |
| function selectDriverFromModal(id) { | |
| setFavoriteDriver(id); | |
| setSegment('fav'); | |
| renderSelectedDriver(); | |
| renderEstimate(); | |
| closeModal('modal-select-driver'); | |
| closeModal('modal-available-drivers'); | |
| } | |
| function renderSelectedDriver() { | |
| const block = document.getElementById('driver-block'); | |
| if (!block) return; | |
| if (state.segment === 'group') { | |
| block.innerHTML = ` | |
| <div class="flex items-center gap-3 pointer-events-none"> | |
| <div class="w-8 h-8 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400"> | |
| <i data-lucide="users" class="w-4 h-4"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-white">Tous mes chauffeurs</p> | |
| <p class="text-[10px] text-emerald-400">Demande groupée</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2 pointer-events-none"> | |
| <i data-lucide="chevron-right" class="w-4 h-4 text-white/30"></i> | |
| </div> | |
| `; | |
| } else { | |
| const fav = state.drivers.find(d => d.favorite) || state.drivers[0]; | |
| if (!fav) { | |
| block.innerHTML = '<p class="text-sm text-white/45">Aucun chauffeur favori</p>'; | |
| lucide.createIcons(); | |
| return; | |
| } | |
| block.innerHTML = ` | |
| <div class="flex items-center gap-3 pointer-events-none"> | |
| <div class="relative"> | |
| <img src="${fav.photo}" class="w-8 h-8 rounded-full object-cover border border-white/10"> | |
| <span class="absolute bottom-0 right-0 w-2.5 h-2.5 ${fav.status === 'Disponible' ? 'bg-emerald-400' : fav.status === 'Occupé' ? 'bg-amber-400' : 'bg-gray-500'} rounded-full border-2 border-[#0b0b0f]"></span> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-white">${fav.name}</p> | |
| <p class="text-[10px] ${fav.status === 'Disponible' ? 'text-emerald-400' : fav.status === 'Occupé' ? 'text-amber-400' : 'text-white/40'}">${fav.status}</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2 pointer-events-auto"> | |
| ${fav.phone ? `<a href="tel:${fav.phone}" onclick="event.stopPropagation()" class="press h-7 w-7 rounded-full bg-[#1a1f2b] border border-white/10 flex items-center justify-center text-emerald-400"><i data-lucide="phone" class="w-3.5 h-3.5"></i></a>` : ''} | |
| <i data-lucide="chevron-right" class="w-4 h-4 text-white/30 pointer-events-none"></i> | |
| </div> | |
| `; | |
| } | |
| block.classList.add('cursor-pointer'); | |
| block.onclick = openSelectDriverSheet; | |
| lucide.createIcons(); | |
| } | |
| function renderEstimate() { | |
| const dep = document.getElementById('input-depart')?.value?.trim(); | |
| const arr = document.getElementById('input-arrivee')?.value?.trim(); | |
| const block = document.getElementById('estimate-block'); | |
| const priceEl = document.getElementById('estimate-price'); | |
| const preview = document.getElementById('route-preview'); | |
| if (!dep || !arr) { | |
| block?.classList.add('hidden'); | |
| preview?.classList.add('hidden'); | |
| return; | |
| } | |
| block?.classList.remove('hidden'); | |
| preview?.classList.remove('hidden'); | |
| const distance = 12; | |
| let price = 0; | |
| if (state.segment === 'fav') { | |
| const fav = state.drivers.find(d => d.favorite) || state.drivers[0]; | |
| if (fav) price = distance * fav.rate; | |
| } else { | |
| const availableRates = state.drivers.filter(d => d.available).map(d => d.rate); | |
| const minRate = availableRates.length > 0 ? Math.min(...availableRates) : 0; | |
| price = distance * minRate; | |
| } | |
| if (priceEl) priceEl.textContent = price.toFixed(2).replace('.00','') + ' €'; | |
| } | |
| function setSegment(type) { | |
| state.segment = type; | |
| const favBtn = document.getElementById('seg-fav'); | |
| const groupBtn = document.getElementById('seg-group'); | |
| if (type === 'fav') { | |
| favBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'; | |
| groupBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all text-white/45 hover:text-white/70'; | |
| } else { | |
| groupBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'; | |
| favBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all text-white/45 hover:text-white/70'; | |
| } | |
| renderSelectedDriver(); | |
| renderEstimate(); | |
| } | |
| function addStop() { | |
| const container = document.getElementById('extra-stops'); | |
| const div = document.createElement('div'); | |
| div.className = 'flex items-center gap-3 bg-[#0b0b0f] border border-white/10 rounded-xl px-3 py-3 animate-fade-in'; | |
| div.innerHTML = ` | |
| <div class="w-2 h-2 rounded-full bg-amber-400 shrink-0"></div> | |
| <input type="text" placeholder="Adresse de l'arrêt" class="w-full bg-transparent text-sm text-white placeholder-white/30 focus:outline-none" /> | |
| <button onclick="this.parentElement.remove()" class="text-white/30 hover:text-red-400 transition-colors press"><i data-lucide="x" class="w-4 h-4"></i></button> | |
| `; | |
| container.appendChild(div); | |
| lucide.createIcons(); | |
| } | |
| function setWhen(type) { | |
| state.when = type; | |
| const btnNow = document.getElementById('btn-now'); | |
| const btnLater = document.getElementById('btn-later'); | |
| if (type === 'now') { | |
| btnNow.className = 'press flex-1 py-2.5 rounded-xl border text-sm font-medium transition-all bg-emerald-500/10 border-emerald-500/30 text-emerald-400'; | |
| btnLater.className = 'press flex-1 py-2.5 rounded-xl border text-sm font-medium transition-all bg-transparent border-white/10 text-white/45'; | |
| } else { | |
| btnNow.className = 'press flex-1 py-2.5 rounded-xl border text-sm font-medium transition-all bg-transparent border-white/10 text-white/45'; | |
| btnLater.className = 'press flex-1 py-2.5 rounded-xl border text-sm font-medium transition-all bg-emerald-500/10 border-emerald-500/30 text-emerald-400'; | |
| openModal('modal-datetime'); | |
| } | |
| } | |
| function confirmDateTime() { | |
| closeModal('modal-datetime'); | |
| } | |
| function adjust(key, delta) { | |
| state[key] = Math.max(0, state[key] + delta); | |
| if (key === 'passengers' && state[key] < 1) state[key] = 1; | |
| const el = document.getElementById('val-' + key); | |
| if (el) el.textContent = state[key]; | |
| } | |
| function toggleAnimal() { | |
| state.animal = !state.animal; | |
| const btn = document.getElementById('btn-animal'); | |
| if (state.animal) { | |
| btn.textContent = 'Oui'; | |
| btn.classList.remove('border-white/10','text-white/45'); | |
| btn.classList.add('border-emerald-500/30','bg-emerald-500/10','text-emerald-400'); | |
| } else { | |
| btn.textContent = 'Non'; | |
| btn.classList.add('border-white/10','text-white/45'); | |
| btn.classList.remove('border-emerald-500/30','bg-emerald-500/10','text-emerald-400'); | |
| } | |
| } | |
| function toggleForOther() { | |
| state.forOther = !state.forOther; | |
| const btn = document.getElementById('btn-for-other'); | |
| const fields = document.getElementById('fields-for-other'); | |
| if (state.forOther) { | |
| btn.textContent = 'Oui'; | |
| btn.classList.remove('border-white/10','text-white/45'); | |
| btn.classList.add('border-emerald-500/30','bg-emerald-500/10','text-emerald-400'); | |
| fields.classList.remove('hidden'); | |
| } else { | |
| btn.textContent = 'Non'; | |
| btn.classList.add('border-white/10','text-white/45'); | |
| btn.classList.remove('border-emerald-500/30','bg-emerald-500/10','text-emerald-400'); | |
| fields.classList.add('hidden'); | |
| } | |
| } | |
| function setMode(mode) { | |
| state.mode = mode; | |
| const segCourse = document.getElementById('seg-mode-course'); | |
| const segDisposal = document.getElementById('seg-mode-disposal'); | |
| const courseInputs = document.getElementById('course-inputs'); | |
| const disposalFields = document.getElementById('disposal-fields'); | |
| const btnDetails = document.getElementById('btn-details'); | |
| const btnSend = document.getElementById('btn-send'); | |
| if (mode === 'course') { | |
| segCourse.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'; | |
| segDisposal.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all text-white/45 hover:text-white/70'; | |
| courseInputs?.classList.remove('hidden'); | |
| disposalFields?.classList.add('hidden'); | |
| if (btnDetails) btnDetails.classList.remove('hidden'); | |
| if (btnSend) btnSend.textContent = 'Envoyer la demande'; | |
| } else { | |
| segDisposal.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'; | |
| segCourse.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all text-white/45 hover:text-white/70'; | |
| courseInputs?.classList.add('hidden'); | |
| disposalFields?.classList.remove('hidden'); | |
| if (btnDetails) btnDetails.classList.add('hidden'); | |
| if (btnSend) btnSend.textContent = 'Demander une mise à disposition'; | |
| renderDisposalPrice(); | |
| } | |
| } | |
| function renderDisposalPrice() { | |
| const priceEl = document.getElementById('disposal-price'); | |
| if (!priceEl) return; | |
| const hours = state.disposalDuration / 60; | |
| const price = hours * 40; | |
| priceEl.textContent = price.toFixed(0) + ' €'; | |
| } | |
| function adjustDuration(delta) { | |
| const newDuration = state.disposalDuration + delta; | |
| if (newDuration < 120) return; | |
| state.disposalDuration = newDuration; | |
| const hours = Math.floor(state.disposalDuration / 60); | |
| const mins = state.disposalDuration % 60; | |
| const label = document.getElementById('disposal-duration'); | |
| if (label) label.textContent = hours + 'h' + (mins === 0 ? '00' : mins.toString().padStart(2, '0')); | |
| renderDisposalPrice(); | |
| } | |
| function toggleChip(btn) { | |
| const isActive = btn.classList.contains('bg-emerald-500/10'); | |
| if (isActive) { | |
| btn.classList.remove('bg-emerald-500/10', 'border-emerald-500/20', 'text-emerald-400'); | |
| btn.classList.add('border-white/10', 'text-white/55'); | |
| } else { | |
| btn.classList.remove('border-white/10', 'text-white/55'); | |
| btn.classList.add('bg-emerald-500/10', 'border-emerald-500/20', 'text-emerald-400'); | |
| } | |
| } | |
| function toggleCustomWelcome() { | |
| state.customWelcome = !state.customWelcome; | |
| const btn = document.getElementById('btn-custom-welcome'); | |
| const fields = document.getElementById('custom-welcome-fields'); | |
| if (state.customWelcome) { | |
| btn.textContent = 'Oui'; | |
| btn.classList.remove('border-white/10','text-white/45'); | |
| btn.classList.add('border-emerald-500/30','bg-emerald-500/10','text-emerald-400'); | |
| fields?.classList.remove('hidden'); | |
| } else { | |
| btn.textContent = 'Non'; | |
| btn.classList.add('border-white/10','text-white/45'); | |
| btn.classList.remove('border-emerald-500/30','bg-emerald-500/10','text-emerald-400'); | |
| fields?.classList.add('hidden'); | |
| } | |
| } | |
| function setNeed(btn, value) { | |
| document.querySelectorAll('.need-chip').forEach(c => { | |
| c.classList.remove('bg-emerald-500/10','border-emerald-500/20','text-emerald-400'); | |
| c.classList.add('border-white/10','text-white/55'); | |
| }); | |
| btn.classList.remove('border-white/10','text-white/55'); | |
| btn.classList.add('bg-emerald-500/10','border-emerald-500/20','text-emerald-400'); | |
| const otherField = document.getElementById('need-other-field'); | |
| if (value === 'other') { | |
| otherField?.classList.remove('hidden'); | |
| } else { | |
| otherField?.classList.add('hidden'); | |
| } | |
| } | |
| function sendRequest() { | |
| if (state.mode === 'disposal') { | |
| openModal('modal-loading'); | |
| const title = document.getElementById('loading-title'); | |
| const sub = document.getElementById('loading-sub'); | |
| title.textContent = 'Demande envoyée'; | |
| const fav = state.drivers.find(d => d.favorite) || { name: 'Karim' }; | |
| sub.textContent = fav.name + ' consulte votre demande…'; | |
| setTimeout(() => { sub.textContent = 'Votre chauffeur vérifie sa disponibilité'; }, 1800); | |
| setTimeout(() => { sub.textContent = fav.name + ' confirme votre mise à disposition'; title.textContent = 'Prestation confirmée'; }, 3200); | |
| setTimeout(() => { closeModal('modal-loading'); }, 4200); | |
| return; | |
| } | |
| state.isReturnTrip = false; | |
| renderReturnBadge(); | |
| openModal('modal-loading'); | |
| const title = document.getElementById('loading-title'); | |
| const sub = document.getElementById('loading-sub'); | |
| title.textContent = 'Demande envoyée'; | |
| if (state.segment === 'group') { | |
| sub.textContent = 'Vos chauffeurs de confiance reçoivent la demande…'; | |
| setTimeout(() => { sub.textContent = 'Karim consulte…'; }, 1800); | |
| setTimeout(() => { sub.textContent = 'Sofiane consulte…'; }, 2800); | |
| setTimeout(() => { sub.textContent = 'Sofiane confirme votre trajet'; title.textContent = 'Course confirmée'; }, 3800); | |
| setTimeout(() => { closeModal('modal-loading'); openModal('modal-return'); }, 4800); | |
| } else { | |
| const fav = state.drivers.find(d => d.favorite) || { name: 'Karim' }; | |
| sub.textContent = fav.name + ' consulte votre demande…'; | |
| setTimeout(() => { sub.textContent = 'Votre chauffeur vérifie sa disponibilité'; }, 1800); | |
| setTimeout(() => { sub.textContent = fav.name + ' confirme votre trajet'; title.textContent = 'Course confirmée'; }, 3200); | |
| setTimeout(() => { closeModal('modal-loading'); openModal('modal-return'); }, 4200); | |
| } | |
| } | |
| function renderReturnBadge() { | |
| const badge = document.getElementById('return-badge'); | |
| if (!badge) return; | |
| if (state.isReturnTrip) { | |
| badge.classList.remove('hidden'); | |
| } else { | |
| badge.classList.add('hidden'); | |
| } | |
| } | |
| function handleReturn(yes) { | |
| closeModal('modal-return'); | |
| if (yes) { | |
| const dep = document.getElementById('input-depart').value; | |
| const arr = document.getElementById('input-arrivee').value; | |
| document.getElementById('input-depart').value = arr; | |
| document.getElementById('input-arrivee').value = dep; | |
| document.getElementById('extra-stops').innerHTML = ''; | |
| state.isReturnTrip = true; | |
| renderReturnBadge(); | |
| renderEstimate(); | |
| window.scrollTo({top:0, behavior:'smooth'}); | |
| } else { | |
| document.getElementById('input-depart').value = ''; | |
| document.getElementById('input-arrivee').value = ''; | |
| document.getElementById('extra-stops').innerHTML = ''; | |
| state.isReturnTrip = false; | |
| renderReturnBadge(); | |
| renderEstimate(); | |
| } | |
| } | |
| function showManualCode() { | |
| document.getElementById('manual-code-area').classList.remove('hidden'); | |
| } | |
| function addManualCode() { | |
| const val = document.getElementById('manual-code').value; | |
| if (val.trim()) { | |
| closeModal('modal-qr'); | |
| } | |
| } | |
| function renderRides(filter) { | |
| const filters = ['today','upcoming','cancelled','past']; | |
| filters.forEach(f => { | |
| const btn = document.getElementById('filter-' + f); | |
| if (!btn) return; | |
| if (f === filter) { | |
| btn.className = 'press px-3 py-1.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'; | |
| } else { | |
| btn.className = 'press px-3 py-1.5 rounded-full text-xs font-medium bg-[#151821] text-white/60 border border-white/10'; | |
| } | |
| }); | |
| const list = document.getElementById('rides-list'); | |
| const filtered = state.rides.filter(r => r.filter === filter); | |
| if (filtered.length === 0) { | |
| list.innerHTML = '<p class="text-sm text-white/45 text-center py-8">Aucune course dans cette catégorie.</p>'; | |
| return; | |
| } | |
| list.innerHTML = filtered.map(r => { | |
| const statusColor = r.status === 'Confirmée' ? 'bg-emerald-500/10 text-emerald-400' : r.status === 'En attente' ? 'bg-amber-500/10 text-amber-400' : r.status === 'Annulée' ? 'bg-red-500/10 text-red-400' : 'bg-blue-500/10 text-blue-400'; | |
| return ` | |
| <div class="bg-[#151821] border border-white/10 rounded-2xl p-4"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="text-xs text-white/45">${r.date}</span> | |
| <span class="px-2 py-0.5 rounded-full ${statusColor} text-[10px] font-medium">${r.status}</span> | |
| </div> | |
| <p class="text-sm text-white">${r.from} → ${r.to}</p> | |
| <div class="flex items-center justify-between mt-3"> | |
| <p class="text-xs text-white/45">Chauffeur : ${r.driver}</p> | |
| ${r.amount ? `<p class="text-xs font-medium text-white">${r.amount}</p>` : ''} | |
| </div> | |
| <button onclick="openRideDetail(${r.id})" class="press mt-3 w-full py-2 rounded-xl bg-[#1a1f2b] border border-white/10 text-xs font-medium text-white/80">Détail</button> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function openRideDetail(id) { | |
| const ride = state.rides.find(r => r.id === id); | |
| if (!ride) return; | |
| state.currentRide = ride; | |
| const content = document.getElementById('ride-detail-content'); | |
| const statusColor = ride.status === 'Confirmée' ? 'text-emerald-400' : ride.status === 'En attente' ? 'text-amber-400' : ride.status === 'Annulée' ? 'text-red-400' : 'text-blue-400'; | |
| let docButton = ''; | |
| if (ride.status === 'Annulée') { | |
| docButton = '<p class="text-xs text-white/40 mt-2">Aucun document disponible pour une course annulée.</p>'; | |
| } else if (ride.filter === 'today' || ride.filter === 'upcoming') { | |
| docButton = `<button onclick="openDocument('bdc')" class="press mt-3 w-full py-3 rounded-xl bg-emerald-500 text-[#0b0b0f] text-sm font-semibold">Voir le bon de commande</button>`; | |
| } else if (ride.filter === 'past') { | |
| docButton = `<button onclick="openDocument('invoice')" class="press mt-3 w-full py-3 rounded-xl bg-emerald-500 text-[#0b0b0f] text-sm font-semibold">Voir la facture</button>`; | |
| } | |
| content.innerHTML = ` | |
| <div class="space-y-3"> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Date / Heure</span> | |
| <span class="text-sm text-white">${ride.date}</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Trajet</span> | |
| <span class="text-sm text-white text-right">${ride.from} → ${ride.to}</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Chauffeur</span> | |
| <span class="text-sm text-white">${ride.driver}</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Véhicule</span> | |
| <span class="text-sm text-white">${ride.vehicle}</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Distance</span> | |
| <span class="text-sm text-white">${ride.distance}</span> | |
| </div> | |
| ${ride.rate ? `<div class="flex justify-between items-center"><span class="text-xs text-white/45">Tarif/km</span><span class="text-sm text-white">${ride.rate} €/km</span></div>` : ''} | |
| ${ride.amount && ride.amount !== '-' ? `<div class="flex justify-between items-center pt-2 border-t border-white/10"><span class="text-xs text-white/45">Montant</span><span class="text-sm font-semibold text-white">${ride.amount}</span></div>` : ''} | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Statut</span> | |
| <span class="text-sm font-medium ${statusColor}">${ride.status}</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Passagers</span> | |
| <span class="text-sm text-white">${ride.passengers}</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-xs text-white/45">Bagages</span> | |
| <span class="text-sm text-white">${ride.luggage}</span> | |
| </div> | |
| ${docButton} | |
| </div> | |
| `; | |
| openModal('modal-ride-detail'); | |
| } | |
| function openDocument(type) { | |
| const ride = state.currentRide; | |
| if (!ride) return; | |
| if (type === 'bdc') { | |
| document.getElementById('bdc-date').textContent = new Date().toLocaleDateString('fr-FR'); | |
| document.getElementById('bdc-datetime').textContent = ride.date; | |
| document.getElementById('bdc-trajet').textContent = ride.from + ' → ' + ride.to; | |
| document.getElementById('bdc-distance').textContent = 'Distance estimée : ' + (ride.distance || '12 km'); | |
| document.getElementById('bdc-rate').textContent = 'Tarif kilométrique : ' + (ride.rate ? ride.rate + ' €/km' : 'sur devis'); | |
| document.getElementById('bdc-price').textContent = 'Prix indicatif TTC : ' + (ride.amount || '24 €'); | |
| openModal('modal-bdc'); | |
| } else if (type === 'invoice') { | |
| document.getElementById('invoice-date').textContent = new Date().toLocaleDateString('fr-FR'); | |
| document.getElementById('invoice-prestation').textContent = ride.date; | |
| document.getElementById('invoice-time').textContent = ride.date.split('à')[1]?.trim() || '14:30'; | |
| openModal('modal-invoice'); | |
| } | |
| } | |
| function renderProfile() { | |
| const container = document.getElementById('relatives-list'); | |
| if (!container) return; | |
| container.innerHTML = state.relatives.map(r => ` | |
| <div class="bg-[#1a1f2b] border border-white/10 rounded-xl p-3 flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-white">${r.firstName} ${r.lastName}</p> | |
| <p class="text-xs text-white/45">${r.phone}</p> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function addRelative() { | |
| const first = document.getElementById('rel-firstname').value.trim(); | |
| const last = document.getElementById('rel-lastname').value.trim(); | |
| const phone = document.getElementById('rel-phone').value.trim(); | |
| if (!first || !last) return; | |
| state.relatives.push({ firstName: first, lastName: last, phone: phone || '—' }); | |
| document.getElementById('rel-firstname').value = ''; | |
| document.getElementById('rel-lastname').value = ''; | |
| document.getElementById('rel-phone').value = ''; | |
| renderProfile(); | |
| closeModal('modal-add-relative'); | |
| } | |
| function toggleFaq(btn) { | |
| const answer = btn.nextElementSibling; | |
| const icon = btn.querySelector('.faq-icon'); | |
| if (answer.classList.contains('hidden')) { | |
| answer.classList.remove('hidden'); | |
| icon.style.transform = 'rotate(180deg)'; | |
| } else { | |
| answer.classList.add('hidden'); | |
| icon.style.transform = 'rotate(0deg)'; | |
| } | |
| } | |
| function openSelectDriverSheet() { | |
| const container = document.getElementById('select-driver-list'); | |
| if (!container) return; | |
| if (state.drivers.length === 0) { | |
| container.innerHTML = '<p class="text-sm text-white/45 text-center py-4">Aucun chauffeur.</p>'; | |
| } else { | |
| container.innerHTML = state.drivers.map(d => { | |
| const statusColor = d.status === 'Disponible' ? 'text-emerald-400' : d.status === 'Occupé' ? 'text-amber-400' : 'text-white/40'; | |
| return ` | |
| <div class="bg-[#1a1f2b] border border-white/10 rounded-xl p-3 flex items-center gap-3"> | |
| <div class="relative shrink-0"> | |
| <img src="${d.photo}" class="w-10 h-10 rounded-full object-cover border border-white/10"> | |
| <span class="absolute bottom-0 right-0 w-2.5 h-2.5 ${d.status === 'Disponible' ? 'bg-emerald-400' : d.status === 'Occupé' ? 'bg-amber-400' : 'bg-gray-500'} rounded-full border-2 border-[#1a1f2b]"></span> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <p class="text-sm font-medium text-white">${d.name}</p> | |
| <p class="text-xs ${statusColor}">${d.status}</p> | |
| <p class="text-xs text-white/45">${d.vehicle} • ${d.rate.toFixed(1).replace('.0','')} €/km</p> | |
| </div> | |
| <div class="flex gap-2 items-center"> | |
| ${d.phone ? `<a href="tel:${d.phone}" class="press h-8 w-8 rounded-full bg-[#151821] border border-white/10 flex items-center justify-center text-emerald-400"><i data-lucide="phone" class="w-4 h-4"></i></a>` : ''} | |
| <button onclick="selectDriverFromModal('${d.id}')" class="press h-8 px-3 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-xs font-semibold text-emerald-400">Choisir</button> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| openModal('modal-select-driver'); | |
| } | |
| function openAvailableDriversSheet() { | |
| const container = document.getElementById('available-drivers-list'); | |
| if (!container) return; | |
| const available = state.drivers.filter(d => d.available); | |
| if (available.length === 0) { | |
| container.innerHTML = '<p class="text-sm text-white/45 text-center py-4">Aucun chauffeur disponible.</p>'; | |
| } else { | |
| container.innerHTML = available.map(d => ` | |
| <div class="bg-[#1a1f2b] border border-white/10 rounded-xl p-3 flex items-center gap-3"> | |
| <div class="relative shrink-0"> | |
| <img src="${d.photo}" class="w-10 h-10 rounded-full object-cover border border-white/10"> | |
| <span class="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-400 rounded-full border-2 border-[#1a1f2b]"></span> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <p class="text-sm font-medium text-white">${d.name}</p> | |
| <p class="text-xs text-emerald-400">Disponible</p> | |
| <p class="text-xs text-white/45">${d.vehicle}</p> | |
| </div> | |
| <div class="flex gap-2 items-center"> | |
| ${d.phone ? `<a href="tel:${d.phone}" class="press h-8 w-8 rounded-full bg-[#151821] border border-white/10 flex items-center justify-center text-emerald-400"><i data-lucide="phone" class="w-4 h-4"></i></a>` : ''} | |
| <button onclick="selectDriverFromModal('${d.id}')" class="press h-8 px-3 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-xs font-semibold text-emerald-400">Choisir</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| openModal('modal-available-drivers'); | |
| } | |
| function renderFavoriteAddressPills() { | |
| const departContainer = document.getElementById('favorite-depart-pills'); | |
| const arriveeContainer = document.getElementById('favorite-arrivee-pills'); | |
| if (!departContainer || !arriveeContainer) return; | |
| const pills = (target) => state.favoriteAddresses.map(a => ` | |
| <button onclick="setFavoriteAddress('${target}', '${a.id}')" class="press shrink-0 bg-[#151821] border border-white/10 rounded-full px-3 py-1.5 text-xs text-white/55 flex items-center gap-1.5 hover:border-emerald-500/30 hover:text-emerald-400 transition-colors"> | |
| <i data-lucide="${a.icon || 'map-pin'}" class="w-3 h-3"></i> | |
| ${a.label} | |
| </button> | |
| `).join(''); | |
| departContainer.innerHTML = pills('depart'); | |
| arriveeContainer.innerHTML = pills('arrivee'); | |
| lucide.createIcons(); | |
| } | |
| function setFavoriteAddress(target, id) { | |
| const addr = state.favoriteAddresses.find(a => a.id === id); | |
| if (!addr) return; | |
| if (target === 'depart') { | |
| const input = document.getElementById('input-depart'); | |
| if (input) input.value = addr.address; | |
| } else if (target === 'arrivee') { | |
| const input = document.getElementById('input-arrivee'); | |
| if (input) input.value = addr.address; | |
| } | |
| renderEstimate(); | |
| } | |
| function renderFavoriteAddressesProfile() { | |
| const container = document.getElementById('profile-favorite-addresses'); | |
| if (!container) return; | |
| if (state.favoriteAddresses.length === 0) { | |
| container.innerHTML = '<p class="text-xs text-white/45">Aucune adresse favorite.</p>'; | |
| return; | |
| } | |
| container.innerHTML = state.favoriteAddresses.map(a => ` | |
| <div onclick="openEditAddress('${a.id}')" class="cursor-pointer bg-[#1a1f2b] border border-white/10 rounded-xl p-3 flex items-center justify-between press"> | |
| <div class="min-w-0"> | |
| <div class="flex items-center gap-2"> | |
| <i data-lucide="${a.icon || 'map-pin'}" class="w-4 h-4 text-white/45"></i> | |
| <p class="text-sm font-medium text-white">${a.label}</p> | |
| </div> | |
| <p class="text-xs text-white/45 mt-0.5 truncate">${a.address}</p> | |
| </div> | |
| <span class="text-[10px] text-white/30 uppercase tracking-wider border border-white/10 px-2 py-0.5 rounded-full shrink-0">${a.tag}</span> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| function addFavoriteAddress() { | |
| const label = document.getElementById('addr-label').value.trim(); | |
| const address = document.getElementById('addr-address').value.trim(); | |
| const tag = document.getElementById('addr-tag').value.trim() || label.toLowerCase(); | |
| const icon = document.getElementById('addr-icon')?.value || 'map-pin'; | |
| if (!label || !address) return; | |
| const id = 'addr_' + Date.now(); | |
| state.favoriteAddresses.push({ id, label, tag, icon, address }); | |
| document.getElementById('addr-label').value = ''; | |
| document.getElementById('addr-address').value = ''; | |
| document.getElementById('addr-tag').value = ''; | |
| if (document.getElementById('addr-icon')) document.getElementById('addr-icon').value = 'map-pin'; | |
| closeModal('modal-add-address'); | |
| renderFavoriteAddressesProfile(); | |
| renderFavoriteAddressPills(); | |
| } | |
| let editingAddressId = null; | |
| function openEditAddress(id) { | |
| const addr = state.favoriteAddresses.find(a => a.id === id); | |
| if (!addr) return; | |
| editingAddressId = id; | |
| document.getElementById('edit-addr-id').value = id; | |
| document.getElementById('edit-addr-label').value = addr.label; | |
| document.getElementById('edit-addr-address').value = addr.address; | |
| document.getElementById('edit-addr-tag').value = addr.tag; | |
| const iconSelect = document.getElementById('edit-addr-icon'); | |
| if (iconSelect) iconSelect.value = addr.icon || 'map-pin'; | |
| openModal('modal-edit-address'); | |
| } | |
| function saveEditedAddress() { | |
| if (!editingAddressId) return; | |
| const addr = state.favoriteAddresses.find(a => a.id === editingAddressId); | |
| if (!addr) return; | |
| addr.label = document.getElementById('edit-addr-label').value.trim(); | |
| addr.address = document.getElementById('edit-addr-address').value.trim(); | |
| addr.tag = document.getElementById('edit-addr-tag').value.trim(); | |
| const iconSelect = document.getElementById('edit-addr-icon'); | |
| if (iconSelect) addr.icon = iconSelect.value; | |
| editingAddressId = null; | |
| closeModal('modal-edit-address'); | |
| renderFavoriteAddressesProfile(); | |
| renderFavoriteAddressPills(); | |
| } | |
| function deleteFavoriteAddress(id) { | |
| if (!id && editingAddressId) id = editingAddressId; | |
| if (!id) return; | |
| state.favoriteAddresses = state.favoriteAddresses.filter(a => a.id !== id); | |
| editingAddressId = null; | |
| closeModal('modal-edit-address'); | |
| renderFavoriteAddressesProfile(); | |
| renderFavoriteAddressPills(); | |
| } | |
| function setCustomerType(type) { | |
| state.customerType = type; | |
| const partBtn = document.getElementById('seg-type-particulier'); | |
| const proBtn = document.getElementById('seg-type-professionnel'); | |
| const companyFields = document.getElementById('company-fields'); | |
| if (type === 'particulier') { | |
| partBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'; | |
| proBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all text-white/45 hover:text-white/70'; | |
| if (companyFields) companyFields.classList.add('hidden'); | |
| } else { | |
| proBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'; | |
| partBtn.className = 'press flex-1 py-2 rounded-lg text-xs font-medium transition-all text-white/45 hover:text-white/70'; | |
| if (companyFields) companyFields.classList.remove('hidden'); | |
| } | |
| } | |
| function saveProfile() { | |
| const first = document.getElementById('profile-firstname').value.trim(); | |
| const last = document.getElementById('profile-lastname').value.trim(); | |
| const phone = document.getElementById('profile-phone').value.trim(); | |
| const email = document.getElementById('profile-email').value.trim(); | |
| const address = document.getElementById('profile-address').value.trim(); | |
| if (!first || !last) return; | |
| state.profile = { firstName: first, lastName: last, phone, email, address }; | |
| state.billingAddress = address; | |
| const nameDisplay = document.getElementById('profile-name-display'); | |
| const contactDisplay = document.getElementById('profile-contact-display'); | |
| const typeDisplay = document.getElementById('profile-type-display'); | |
| if (nameDisplay) nameDisplay.textContent = first + ' ' + last; | |
| if (contactDisplay) contactDisplay.textContent = (phone ? phone + ' • ' : '') + (email || ''); | |
| if (typeDisplay) { | |
| if (state.customerType === 'professionnel' && state.company.name) { | |
| typeDisplay.textContent = 'Professionnel • ' + state.company.name; | |
| } else { | |
| typeDisplay.textContent = state.customerType === 'particulier' ? 'Particulier' : 'Professionnel'; | |
| } | |
| } | |
| if (state.customerType === 'professionnel') { | |
| state.company.name = document.getElementById('company-name').value.trim(); | |
| state.company.siret = document.getElementById('company-siret').value.trim(); | |
| state.company.vat = document.getElementById('company-vat').value.trim(); | |
| state.company.billingEmail = document.getElementById('company-billing-email').value.trim(); | |
| } | |
| closeModal('modal-edit-profile'); | |
| } | |
| window.addEventListener('DOMContentLoaded', () => { | |
| lucide.createIcons(); | |
| showView('home'); | |
| renderDrivers(); | |
| renderSelectedDriver(); | |
| renderRides('today'); | |
| renderProfile(); | |
| renderFavoriteAddressPills(); | |
| renderFavoriteAddressesProfile(); | |
| const today = new Date().toISOString().split('T')[0]; | |
| const dp = document.getElementById('date-picker'); | |
| if (dp) dp.value = today; | |
| const tp = document.getElementById('time-picker'); | |
| if (tp) tp.value = '14:30'; | |
| const dd = document.getElementById('disposal-date'); | |
| if (dd) dd.value = today; | |
| const dt = document.getElementById('disposal-time'); | |
| if (dt) dt.value = '09:00'; | |
| const depInput = document.getElementById('input-depart'); | |
| const arrInput = document.getElementById('input-arrivee'); | |
| if (depInput) depInput.addEventListener('input', renderEstimate); | |
| if (arrInput) arrInput.addEventListener('input', renderEstimate); | |
| }); |