diff --git a/.env b/.env index 5472096..627e939 100644 --- a/.env +++ b/.env @@ -1,15 +1,25 @@ -# Configuration de la base de données +# ============================================================ +# Smart Parking v2.0 — Variables d'environnement +# Copiez ce fichier en .env à la racine du projet +# ============================================================ + +# ── Base de données ────────────────────────────────────────── DB_HOST=db DB_PORT=3306 DB_USER=smartparking_user DB_PASSWORD=smartparking_pass DB_NAME=smartparking -# Configuration JWT -JWT_SECRET=une_chaine_tres_longue_et_secrete_à_changer +# ── JWT ────────────────────────────────────────────────────── +# ⚠️ Changez cette valeur ! Mettez une chaîne longue et aléatoire. +JWT_SECRET=une_chaine_tres_longue_et_secrete_changez_moi_absolument -# Port du serveur +# ── Serveur ────────────────────────────────────────────────── PORT=3000 +NODE_ENV=production -# Mode environnement -NODE_ENV=production \ No newline at end of file +# ── MQTT (Mosquitto) ───────────────────────────────────────── +# Si Mosquitto tourne dans Docker (service "mqtt") → mqtt +# Si Mosquitto est installé directement sur le Pi → localhost +MQTT_HOST=mqtt +MQTT_PORT=1883 \ No newline at end of file diff --git a/js/admin.js b/js/admin.js index 30744ac..8e1f93d 100644 --- a/js/admin.js +++ b/js/admin.js @@ -1,175 +1,141 @@ /** * ============================================ * ADMIN.JS - Panel d'administration - * Smart Parking - BTS CIEL IR + * Smart Parking v3.0 + * CORRIGÉ : + * - Suppression du graphique d'occupation + * - Historique affiche la date complète + * (jour + mois + année + heure + minute) * ============================================ */ -// Initialisation document.addEventListener('DOMContentLoaded', () => { console.log('⚙️ Initialisation du panel admin...'); - - // Vérifier si l'utilisateur est admin if (!isAdmin()) return; - initAdminPanel(); }); -/** - * Vérifie si l'utilisateur est admin - */ function isAdmin() { const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null'); return user && user.role === 'admin'; } -/** - * Initialise le panel admin - */ function initAdminPanel() { loadAdminStats(); initPlacesControl(); loadUsersTable(); loadReservationsTable(); - initOccupancyChart(); loadHistoryLog(); - - // Rafraîchissement périodique + + // Rafraîchissement périodique toutes les 10 secondes setInterval(() => { loadAdminStats(); loadReservationsTable(); + loadHistoryLog(); }, 10000); } -/** - * Charge les statistiques admin - */ +// ============================================ +// STATISTIQUES ADMIN +// ============================================ + function loadAdminStats() { - const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); + const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); - const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); - - // Calculer les stats - const totalUsers = users.length + 1; // +1 pour l'admin par défaut + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); + + const totalUsers = users.length + 1; const totalReservations = reservations.length; - const totalRevenue = reservations + const totalRevenue = reservations .filter(r => r.status === 'active' || r.status === 'completed') .reduce((sum, r) => sum + (r.price || 0), 0); - - const occupied = spots.filter(s => s.status === 'occupied').length; - const reserved = spots.filter(s => s.status === 'reserved').length; - const occupancyRate = spots.length > 0 - ? Math.round(((occupied + reserved) / spots.length) * 100) + + const occupied = spots.filter(s => s.status === 'occupied').length; + const reserved = spots.filter(s => s.status === 'reserved').length; + const occupancyRate = spots.length > 0 + ? Math.round(((occupied + reserved) / spots.length) * 100) : 0; - - // Mettre à jour l'affichage - document.getElementById('adminTotalUsers').textContent = totalUsers; + + document.getElementById('adminTotalUsers').textContent = totalUsers; document.getElementById('adminTotalReservations').textContent = totalReservations; - document.getElementById('adminTotalRevenue').textContent = totalRevenue + '€'; - document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%'; + document.getElementById('adminTotalRevenue').textContent = totalRevenue + '€'; + document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%'; } -/** - * Initialise le contrôle des places - */ +// ============================================ +// GESTION DES PLACES +// ============================================ + function initPlacesControl() { - const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); - - // Mettre à jour le champ du nombre de places + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); const spotsInput = document.getElementById('adminTotalSpots'); - if (spotsInput) { - spotsInput.value = spots.length || 10; - } - - // Bouton de mise à jour + if (spotsInput) spotsInput.value = spots.length || 10; + document.getElementById('updateSpotsBtn')?.addEventListener('click', () => { const newCount = parseInt(document.getElementById('adminTotalSpots').value); if (newCount < 5 || newCount > 50) { Dashboard.showToast('Le nombre de places doit être entre 5 et 50', 'error'); return; } - if (window.ParkingMap) { window.ParkingMap.setTotalSpots(newCount); renderAdminPlacesList(); Dashboard.showToast('Nombre de places mis à jour', 'success'); } }); - - // Rendre la liste des places + renderAdminPlacesList(); } -/** - * Rend la liste des places dans l'admin - */ function renderAdminPlacesList() { const container = document.getElementById('adminPlacesList'); if (!container) return; - + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); - + container.innerHTML = spots.map(spot => ` -
${spot.number}
`).join(''); } -/** - * Change le statut d'une place (admin) - */ function toggleSpotStatus(spotId) { const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); - const spot = spots.find(s => s.id === spotId); - + const spot = spots.find(s => s.id === spotId); if (!spot) return; - - // Cycle: free -> occupied -> reserved -> free - const cycle = ['free', 'occupied', 'reserved']; - const currentIndex = cycle.indexOf(spot.status); - const nextStatus = cycle[(currentIndex + 1) % cycle.length]; - - spot.status = nextStatus; - spot.lastUpdate = new Date().toISOString(); - + + const cycle = ['free', 'occupied', 'reserved']; + const nextStatus = cycle[(cycle.indexOf(spot.status) + 1) % cycle.length]; + spot.status = nextStatus; + spot.lastUpdate = new Date().toISOString(); + localStorage.setItem('smart_parking_spots', JSON.stringify(spots)); - - // Rafraîchir renderAdminPlacesList(); - if (window.ParkingMap) { - window.ParkingMap.refresh(); - } + if (window.ParkingMap) window.ParkingMap.refresh(); loadAdminStats(); - - Dashboard.showToast(`Place ${spot.number} - ${getStatusLabel(nextStatus)}`, 'success'); + + Dashboard.showToast(`Place ${spot.number} — ${getStatusLabel(nextStatus)}`, 'success'); } -/** - * Charge le tableau des utilisateurs - */ +// ============================================ +// TABLEAU UTILISATEURS +// ============================================ + function loadUsersTable() { const tbody = document.getElementById('adminUsersTable'); if (!tbody) return; - - const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); - - // Ajouter l'admin par défaut + + const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); const allUsers = [ - { - id: 0, - name: 'Administrateur', - email: 'admin@smartparking.fr', - phone: '01 23 45 67 89', - role: 'admin' - }, + { id: 0, name: 'Administrateur', email: 'admin@smartparking.fr', phone: '01 23 45 67 89', role: 'admin' }, ...users ]; - + tbody.innerHTML = allUsers.map(user => ` #${user.id} @@ -184,9 +150,8 @@ function loadUsersTable() {
${user.role !== 'admin' ? ` - + ` : '-'}
@@ -194,48 +159,44 @@ function loadUsersTable() { `).join(''); } -/** - * Supprime un utilisateur - */ function deleteUser(userId) { if (!confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?')) return; - + let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); - users = users.filter(u => u.id !== userId); + users = users.filter(u => u.id !== userId); localStorage.setItem('smart_parking_users', JSON.stringify(users)); - - // Supprimer aussi ses réservations + let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); - reservations = reservations.filter(r => r.userId !== userId); + reservations = reservations.filter(r => r.userId !== userId); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); - + loadUsersTable(); loadReservationsTable(); loadAdminStats(); - Dashboard.showToast('Utilisateur supprimé', 'success'); } -/** - * Charge le tableau des réservations - */ +// ============================================ +// TABLEAU RÉSERVATIONS +// ============================================ + function loadReservationsTable() { const tbody = document.getElementById('adminReservationsTable'); if (!tbody) return; - + const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); - + if (reservations.length === 0) { - tbody.innerHTML = 'Aucune réservation'; + tbody.innerHTML = 'Aucune réservation'; return; } - + tbody.innerHTML = reservations.slice().reverse().map(res => ` #${res.id} ${res.userName} Place ${res.spotNumber} - ${formatDate(res.date)} + ${formatDateShort(res.date)} ${res.startTime} - ${res.endTime} ${res.price}€ @@ -246,12 +207,10 @@ function loadReservationsTable() {
${res.status === 'active' ? ` - - + + ` : '-'}
@@ -259,168 +218,104 @@ function loadReservationsTable() { `).join(''); } -/** - * Termine une réservation - */ function completeReservation(reservationId) { - let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservation = reservations.find(r => r.id === reservationId); - - if (reservation) { - reservation.status = 'completed'; - localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); - - // Libérer la place - if (window.ParkingMap) { - window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); - } - - loadReservationsTable(); - loadAdminStats(); - Dashboard.showToast('Réservation terminée', 'success'); - } + if (!reservation) return; + + reservation.status = 'completed'; + localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); + + if (window.ParkingMap) window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); + + loadReservationsTable(); + loadAdminStats(); + Dashboard.showToast('Réservation terminée', 'success'); } -/** - * Annule une réservation (admin) - */ function adminCancelReservation(reservationId) { if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return; - - let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + + let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservation = reservations.find(r => r.id === reservationId); - - if (reservation) { - reservation.status = 'cancelled'; - localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); - - // Libérer la place - if (window.ParkingMap) { - window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); - } - - loadReservationsTable(); - loadAdminStats(); - Dashboard.showToast('Réservation annulée', 'success'); - } + if (!reservation) return; + + reservation.status = 'cancelled'; + localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); + + if (window.ParkingMap) window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); + + loadReservationsTable(); + loadAdminStats(); + Dashboard.showToast('Réservation annulée', 'success'); } -/** - * Initialise le graphique d'occupation - */ -function initOccupancyChart() { - const ctx = document.getElementById('adminOccupancyChart'); - if (!ctx) return; - - // Générer des données d'exemple - const labels = []; - const data = []; - - for (let i = 6; i >= 0; i--) { - const date = new Date(); - date.setDate(date.getDate() - i); - labels.push(date.toLocaleDateString('fr-FR', { weekday: 'short' })); - data.push(Math.floor(Math.random() * 40) + 30); // 30-70% d'occupation - } - - new Chart(ctx, { - type: 'bar', - data: { - labels: labels, - datasets: [{ - label: 'Taux d\'occupation (%)', - data: data, - backgroundColor: 'rgba(99, 102, 241, 0.5)', - borderColor: 'rgba(99, 102, 241, 1)', - borderWidth: 1, - borderRadius: 4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - labels: { color: '#f1f5f9' } - } - }, - scales: { - x: { - grid: { color: '#334155' }, - ticks: { color: '#94a3b8' } - }, - y: { - min: 0, - max: 100, - grid: { color: '#334155' }, - ticks: { - color: '#94a3b8', - callback: value => value + '%' - } - } - } - } - }); -} +// ============================================ +// HISTORIQUE — CORRIGÉ : date complète +// ============================================ -/** - * Charge l'historique - */ function loadHistoryLog() { const container = document.getElementById('adminLogContainer'); if (!container) return; - + const history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]'); - + if (history.length === 0) { - container.innerHTML = '

Aucun historique

'; + container.innerHTML = '

Aucun historique

'; return; } - - container.innerHTML = history.slice(0, 20).map(item => ` + + container.innerHTML = history.slice(0, 50).map(item => `
- ${formatTime(item.timestamp)} - ${item.action}: ${item.details} + ${formatDateComplete(item.timestamp)} + ${item.action} : ${item.details}
`).join(''); } +// ============================================ +// FONCTIONS DE FORMAT DATE +// ============================================ + /** - * Formate une date + * CORRIGÉ — Affiche la date complète dans l'historique + * Avant : seulement "14:32" + * Après : "12/06/2025 à 14:32" */ -function formatDate(dateString) { +function formatDateComplete(dateString) { + if (!dateString) return 'N/A'; const date = new Date(dateString); - return date.toLocaleDateString('fr-FR', { - day: '2-digit', - month: '2-digit' + const jour = String(date.getDate()).padStart(2, '0'); + const mois = String(date.getMonth() + 1).padStart(2, '0'); + const annee = date.getFullYear(); + const heure = String(date.getHours()).padStart(2, '0'); + const min = String(date.getMinutes()).padStart(2, '0'); + return `${jour}/${mois}/${annee} à ${heure}:${min}`; +} + +function formatDateShort(dateString) { + if (!dateString) return 'N/A'; + return new Date(dateString).toLocaleDateString('fr-FR', { + day: '2-digit', month: '2-digit' }); } -/** - * Formate une heure - */ -function formatTime(dateString) { - const date = new Date(dateString); - return date.toLocaleTimeString('fr-FR', { - hour: '2-digit', - minute: '2-digit' - }); -} - -/** - * Retourne le label du statut - */ function getStatusLabel(status) { - const labels = { - 'pending': 'En attente', - 'active': 'Active', - 'completed': 'Terminée', - 'cancelled': 'Annulée' - }; - return labels[status] || status; + return { + pending: 'En attente', + active: 'Active', + completed: 'Terminée', + cancelled: 'Annulée', + free: 'Libre', + occupied: 'Occupée', + reserved: 'Réservée' + }[status] || status; } -// Exporter les fonctions +// ============================================ +// EXPORT +// ============================================ + window.AdminModule = { refresh: () => { loadAdminStats(); @@ -433,4 +328,4 @@ window.AdminModule = { deleteUser, completeReservation, adminCancelReservation -}; +}; \ No newline at end of file diff --git a/js/reservation.js b/js/reservation.js index cba13e1..49f8b5d 100644 --- a/js/reservation.js +++ b/js/reservation.js @@ -1,22 +1,21 @@ /** * ============================================ * RESERVATION.JS - Système de réservation - * Smart Parking - BTS CIEL IR - * MODIFIÉ : Suppression du QR code, remplacement - * par une confirmation simple avec badge + * Smart Parking v3.0 + * CORRIGÉ : ne bloque plus une place pour + * toujours — vérifie uniquement si + * une voiture est physiquement là * ============================================ */ -// Tarifs const PRICING = { - 30: 2, // 30 min = 2€ - 60: 3, // 1h = 3€ - 120: 5, // 2h = 5€ - 240: 8, // 4h = 8€ - 480: 15 // 8h (journée) = 15€ + 30: 2, + 60: 3, + 120: 5, + 240: 8, + 480: 15 }; -// Horaires disponibles const TIME_SLOTS = [ '06:00', '06:30', '07:00', '07:30', '08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00', '13:30', @@ -25,67 +24,52 @@ const TIME_SLOTS = [ '22:00' ]; -// État de la réservation en cours let currentReservation = null; -// Initialisation document.addEventListener('DOMContentLoaded', () => { console.log('📅 Initialisation du système de réservation...'); initReservationForm(); initDatePicker(); initTimeSlots(); initPricePreview(); - initConfirmationModal(); // MODIFIÉ : était initPaymentModal() + initConfirmationModal(); }); -/** - * Initialise le formulaire de réservation - */ function initReservationForm() { const form = document.getElementById('reservationForm'); if (!form) return; form.addEventListener('submit', handleReservationSubmit); } -/** - * Initialise le sélecteur de date - */ function initDatePicker() { const dateInput = document.getElementById('resDate'); if (!dateInput) return; const today = new Date().toISOString().split('T')[0]; - dateInput.min = today; + dateInput.min = today; dateInput.value = today; } -/** - * Initialise les créneaux horaires - */ function initTimeSlots() { const select = document.getElementById('resStartTime'); if (!select) return; TIME_SLOTS.forEach(time => { - const option = document.createElement('option'); - option.value = time; + const option = document.createElement('option'); + option.value = time; option.textContent = time; select.appendChild(option); }); - // Sélectionner le prochain créneau disponible - const now = new Date(); - const currentHour = now.getHours(); - const currentMinutes = now.getMinutes(); - const nextSlot = TIME_SLOTS.find(t => { + const now = new Date(); + const currentHour = now.getHours(); + const currentMins = now.getMinutes(); + const nextSlot = TIME_SLOTS.find(t => { const [h, m] = t.split(':').map(Number); - return h > currentHour || (h === currentHour && m > currentMinutes); + return h > currentHour || (h === currentHour && m > currentMins); }); if (nextSlot) select.value = nextSlot; } -/** - * Initialise la prévisualisation du prix - */ function initPricePreview() { const durationSelect = document.getElementById('resDuration'); if (!durationSelect) return; @@ -93,21 +77,21 @@ function initPricePreview() { updatePricePreview(); } -/** - * Met à jour la prévisualisation du prix - */ function updatePricePreview() { const duration = parseInt(document.getElementById('resDuration').value); - const price = PRICING[duration] || 0; + const price = PRICING[duration] || 0; document.getElementById('previewPrice').textContent = price + '€'; } /** * Gère la soumission du formulaire - * MODIFIÉ : la réservation est enregistrée ici directement, - * puis le modal de confirmation s'affiche. + * + * CORRIGÉ : on n'empêche plus la réservation si la place + * est "reserved" dans le localStorage. On laisse le serveur + * vérifier les conflits d'horaire. On bloque uniquement si + * une voiture est physiquement détectée (status "occupied"). */ -function handleReservationSubmit(e) { +async function handleReservationSubmit(e) { e.preventDefault(); const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null'); @@ -127,12 +111,13 @@ function handleReservationSubmit(e) { return; } - // Vérifier que la place est toujours libre + // CORRIGÉ : on bloque uniquement si une voiture est physiquement là + // Une place "reserved" peut quand même être réservée à un autre horaire const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); const spot = spots.find(s => s.id === spotId); - if (!spot || spot.status !== 'free') { - Dashboard.showToast('Cette place n\'est plus disponible', 'error'); + if (spot && spot.status === 'occupied') { + Dashboard.showToast('Une voiture est déjà sur cette place', 'error'); if (window.ParkingMap) window.ParkingMap.refresh(); return; } @@ -142,39 +127,78 @@ function handleReservationSubmit(e) { endDate.setMinutes(endDate.getMinutes() + duration); const endTime = endDate.toTimeString().slice(0, 5); - // Construire l'objet réservation - currentReservation = { - id: Date.now(), - userId: user.id, - userName: user.name, - spotId: spotId, - spotNumber: spot.number, - date: date, - startTime: startTime, - endTime: endTime, - duration: duration, - vehicle: vehicle.toUpperCase(), - price: PRICING[duration], - status: 'active', - createdAt: new Date().toISOString() - }; + // Essayer de créer la réservation via l'API + // Le serveur vérifiera les conflits d'horaire + const token = localStorage.getItem('smart_parking_token'); - // ---- Enregistrement immédiat (plus de bouton "J'ai payé") ---- + if (token) { + try { + const response = await fetch('/api/reservations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + spotId, date, startTime, endTime, + duration, vehicle: vehicle.toUpperCase(), + price: PRICING[duration] + }) + }); - // Sauvegarder la réservation + const data = await response.json(); + + if (!data.success) { + // Le serveur a détecté un conflit ou une erreur + Dashboard.showToast(data.message, 'error'); + return; + } + + // Succès via API + currentReservation = { + id: data.data.id, + userId: user.id, + userName: user.name, + spotId: spotId, + spotNumber: spot ? spot.number : spotId, + date, startTime, endTime, duration, + vehicle: vehicle.toUpperCase(), + price: PRICING[duration], + status: 'active', + createdAt: new Date().toISOString() + }; + + } catch (_err) { + // Mode offline : enregistrement local + currentReservation = creerReservationLocale( + user, spotId, spot, date, startTime, endTime, duration, vehicle + ); + } + } else { + // Pas de token : mode offline + currentReservation = creerReservationLocale( + user, spotId, spot, date, startTime, endTime, duration, vehicle + ); + } + + // Sauvegarder dans le localStorage let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); reservations.push(currentReservation); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); - // Mettre la place en "réservée" sur la carte - if (window.ParkingMap) { - window.ParkingMap.setSpotStatus(currentReservation.spotId, 'reserved'); + // Mettre à jour la carte seulement si la réservation est pour aujourd'hui + const today = new Date().toISOString().split('T')[0]; + const now = new Date(); + const resStart = new Date(date + 'T' + startTime); + const diffMin = (resStart - now) / 60000; + + if (date === today && diffMin <= 30 && window.ParkingMap) { + window.ParkingMap.setSpotStatus(spotId, 'reserved'); } - // Ajouter à l'historique admin addToHistory( 'Réservation', - `Place ${currentReservation.spotNumber} réservée par ${currentReservation.userName} - ${currentReservation.price}€` + `Place ${currentReservation.spotNumber} réservée le ${date} de ${startTime} à ${endTime} — ${PRICING[duration]}€` ); // Réinitialiser le formulaire @@ -182,95 +206,84 @@ function handleReservationSubmit(e) { initDatePicker(); updatePricePreview(); - // Afficher le modal de confirmation showConfirmationModal(); } +/** + * Crée une réservation en mode offline (localStorage) + */ +function creerReservationLocale(user, spotId, spot, date, startTime, endTime, duration, vehicle) { + return { + id: Date.now(), + userId: user.id, + userName: user.name, + spotId: spotId, + spotNumber: spot ? spot.number : spotId, + date, startTime, endTime, duration, + vehicle: vehicle.toUpperCase(), + price: PRICING[duration], + status: 'active', + createdAt: new Date().toISOString() + }; +} + // ============================================ -// MODAL DE CONFIRMATION (remplace le QR code) +// MODAL DE CONFIRMATION // ============================================ -/** - * Initialise les événements du modal de confirmation - * REMPLACE : initPaymentModal() - */ function initConfirmationModal() { document.getElementById('closeConfirmationModal')?.addEventListener('click', hideConfirmationModal); document.getElementById('closeConfirmationBtn')?.addEventListener('click', hideConfirmationModal); } -/** - * Affiche le modal de confirmation - * REMPLACE : showPaymentModal() + generateQRCode() - */ function showConfirmationModal() { if (!currentReservation) return; const modal = document.getElementById('confirmationModal'); - // Remplir le récapitulatif document.getElementById('paySpot').textContent = 'Place ' + currentReservation.spotNumber; document.getElementById('payDate').textContent = formatDate(currentReservation.date); document.getElementById('payTime').textContent = currentReservation.startTime + ' - ' + currentReservation.endTime; document.getElementById('payDuration').textContent = formatDuration(currentReservation.duration); document.getElementById('payTotal').textContent = currentReservation.price + '€'; - // Afficher le modal modal.classList.remove('hidden'); } -/** - * Cache le modal de confirmation - * REMPLACE : hidePaymentModal() - */ function hideConfirmationModal() { document.getElementById('confirmationModal').classList.add('hidden'); - // Rediriger vers "Mes réservations" après fermeture if (window.Dashboard) { Dashboard.navigateToPage('my-reservations'); document.querySelector('[data-page="my-reservations"]')?.classList.add('active'); document.querySelector('[data-page="reservation"]')?.classList.remove('active'); } - // Rafraîchir les stats du profil if (window.Dashboard) Dashboard.refreshStats(); } // ============================================ -// FONCTIONS UTILITAIRES +// UTILITAIRES // ============================================ -/** - * Ajoute une entrée à l'historique - */ function addToHistory(action, details) { let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]'); history.unshift({ id: Date.now(), - action: action, - details: details, + action, + details, timestamp: new Date().toISOString() }); if (history.length > 100) history = history.slice(0, 100); localStorage.setItem('smart_parking_history', JSON.stringify(history)); } -/** - * Formate une date DD/MM/YYYY - */ function formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleDateString('fr-FR', { - day: '2-digit', - month: '2-digit', - year: 'numeric' + return new Date(dateString).toLocaleDateString('fr-FR', { + day: '2-digit', month: '2-digit', year: 'numeric' }); } -/** - * Formate une durée en minutes vers texte lisible - */ function formatDuration(minutes) { if (minutes >= 480) return 'Journée (8h)'; if (minutes >= 60) { @@ -281,10 +294,4 @@ function formatDuration(minutes) { return `${minutes} min`; } -// Exporter les fonctions publiques -window.Reservation = { - PRICING, - TIME_SLOTS, - formatDuration, - addToHistory -}; \ No newline at end of file +window.Reservation = { PRICING, TIME_SLOTS, formatDuration, addToHistory }; \ No newline at end of file diff --git a/pages/dashboard.html b/pages/dashboard.html index 264432f..41dccb2 100644 --- a/pages/dashboard.html +++ b/pages/dashboard.html @@ -6,8 +6,7 @@ Smart Parking - Dashboard - - + @@ -21,25 +20,19 @@
@@ -48,24 +41,19 @@ Client
-
- + +
-

- 🗺️ - Carte du Parking -

+

🗺️Carte du Parking

-
@@ -97,30 +85,16 @@
-

Vue du parking

-
- -
+
-
- - Libre -
-
- - Occupée -
-
- - Réservée -
+
Libre
+
Occupée
+
Réservée
- -

Détails de la place

@@ -129,42 +103,23 @@
-

💰 Nos tarifs

-
-

30 minutes

- 2€ -
-
-

1 heure

- 3€ -
-
-

2 heures

- 5€ -
-
-

4 heures

- 8€ -
-
-

Journée

- 15€ -
+

30 minutes

2€
+

1 heure

3€
+

2 heures

5€
+

4 heures

8€
+

Journée

15€
- + - + - + - + +
-
+ diff --git a/server/db/database.js b/server/db/database.js index f877c0e..aeb9448 100644 --- a/server/db/database.js +++ b/server/db/database.js @@ -1,8 +1,10 @@ /** * ============================================ * DATABASE.JS - Gestion MariaDB - * Smart Parking v2.0 - * AJOUTÉ : expireReservations() — libère auto les places + * Smart Parking v3.0 + * AJOUTÉ : checkReservationConflict() + * → vérifie les conflits d'horaire + * au lieu de bloquer toute la place * ============================================ */ @@ -123,7 +125,7 @@ async function initDatabase() { [i, `SENSOR_${String(i).padStart(3, '0')}`, 'free'] ); } - console.log('✅ 10 places créées (toutes libres)'); + console.log('✅ 10 places créées'); } } catch (err) { @@ -227,6 +229,34 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat return { id: result.insertId }; } +/** + * ⭐ NOUVELLE FONCTION — Vérification des conflits d'horaire + * + * Problème corrigé : avant, quand une place était réservée, + * elle restait bloquée pour TOUS les jours et TOUTES les heures. + * + * Maintenant on vérifie uniquement s'il y a une réservation + * qui se chevauche sur la MÊME date et le MÊME créneau horaire. + * + * Exemple : + * Place 2 réservée aujourd'hui 10h-11h ✅ + * Place 2 réservée aujourd'hui 14h-15h ✅ (pas de conflit) + * Place 2 réservée demain 10h-11h ✅ (pas de conflit) + * Place 2 réservée aujourd'hui 10h30-11h30 ❌ (conflit !) + */ +async function checkReservationConflict(spotId, date, startTime, endTime) { + const [rows] = await pool.query(` + SELECT id FROM reservations + WHERE spot_id = ? + AND date = ? + AND status IN ('active', 'pending') + AND start_time < ? + AND end_time > ? + `, [spotId, date, endTime, startTime]); + + return rows.length > 0; // true = conflit, false = créneau libre +} + async function getReservationById(id) { const [rows] = await pool.query( `SELECT r.*, s.number AS spot_number @@ -270,16 +300,10 @@ async function updateReservationStatus(id, status) { } /** - * ⭐ NOUVELLE FONCTION — Expiration automatique des réservations - * - * Cherche toutes les réservations actives dont la date+heure de fin - * est déjà dépassée, les passe en "completed" et libère les places. - * - * Appelée toutes les 60 secondes par server.js. - * Cela résout le problème des places qui restent "réservées" indéfiniment. + * Expiration automatique des réservations + * Appelée toutes les 60 secondes par server.js */ async function expireReservations() { - // Trouver les réservations actives dont l'heure de fin est passée const [expiredRows] = await pool.query(` SELECT r.id, r.spot_id, r.user_id, s.number AS spot_number FROM reservations r @@ -289,20 +313,14 @@ async function expireReservations() { `); for (const res of expiredRows) { - // Passer la réservation en "completed" await pool.query( "UPDATE reservations SET status = 'completed' WHERE id = ?", [res.id] ); - - // Libérer la place (la passer en "free") - // (le capteur Arduino prendra le relais ensuite si une voiture est encore là) await pool.query( "UPDATE spots SET status = 'free', last_update = NOW() WHERE id = ?", [res.spot_id] ); - - // Ajouter à l'historique await pool.query( 'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)', [ @@ -364,7 +382,7 @@ async function getStats(days = 7) { } // ============================================ -// MQTT EVENTS +// MQTT // ============================================ async function recordMqttEvent(topic, message) { @@ -388,18 +406,13 @@ module.exports = { pool, initDatabase, closeDatabase, - // Utilisateurs createUser, getUserByEmail, getUserById, getAllUsers, updateUser, deleteUser, - // Places createSpot, getAllSpots, getSpotById, updateSpotStatus, deleteAllSpots, - // Réservations - createReservation, getReservationById, getReservationsByUser, + createReservation, checkReservationConflict, + getReservationById, getReservationsByUser, getAllReservations, updateReservationStatus, - expireReservations, // ← NOUVEAU - // Historique + expireReservations, addHistory, getHistory, - // Stats recordStats, getStats, - // MQTT recordMqttEvent }; \ No newline at end of file diff --git a/server/routes/api.js b/server/routes/api.js index d0ed2e5..19fe33a 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -1,9 +1,10 @@ /** * ============================================ * API ROUTES - Routes de l'API REST - * Smart Parking - BTS CIEL IR - * CORRIGÉ : annulation libère bien la place - * ajout route /complete pour l'admin + * Smart Parking v3.0 + * CORRIGÉ : réservation vérifie les conflits + * d'horaire au lieu de bloquer la place + * définitivement * ============================================ */ @@ -110,11 +111,7 @@ router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => const spotCount = Math.min(Math.max(parseInt(req.body.count) || 10, 5), 50); await db.deleteAllSpots(); for (let i = 1; i <= spotCount; i++) { - let status = 'free'; - const rand = Math.random(); - if (rand > 0.85) status = 'reserved'; - else if (rand > 0.60) status = 'occupied'; - await db.createSpot(i, `SENSOR_${String(i).padStart(3, '0')}`, status); + await db.createSpot(i, `SENSOR_${String(i).padStart(3, '0')}`, 'free'); } await db.addHistory('Réinitialisation places', `${spotCount} places créées`, req.user.id); res.json({ success: true, message: `${spotCount} places créées` }); @@ -145,21 +142,71 @@ router.get('/reservations/all', authenticateToken, requireAdmin, async (req, res } }); +/** + * POST /api/reservations + * + * CORRIGÉ : on ne bloque plus la place entière définitivement. + * On vérifie uniquement s'il y a un CONFLIT d'horaire sur + * la même date et le même créneau. + * + * Exemple de ce qui est maintenant possible : + * Place 2 — 10h-11h aujourd'hui ✅ + * Place 2 — 14h-15h aujourd'hui ✅ (pas de conflit) + * Place 2 — 10h-11h demain ✅ (pas de conflit) + * Place 2 — 10h30-11h30 aujourd'hui ❌ (conflit !) + */ router.post('/reservations', authenticateToken, async (req, res) => { try { const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body; + if (!spotId || !date || !startTime || !endTime || !duration || !price) return res.status(400).json({ success: false, message: 'Tous les champs sont requis' }); + const spot = await db.getSpotById(spotId); - if (!spot || spot.status !== 'free') - return res.status(409).json({ success: false, message: "Cette place n'est plus disponible" }); + if (!spot) + return res.status(404).json({ success: false, message: 'Place introuvable' }); + + // CORRIGÉ : bloquer uniquement si une voiture est physiquement là + if (spot.status === 'occupied') + return res.status(409).json({ success: false, message: "Une voiture est déjà sur cette place" }); + + // CORRIGÉ : vérifier les conflits d'horaire au lieu du statut global + const conflict = await db.checkReservationConflict(spotId, date, startTime, endTime); + if (conflict) + return res.status(409).json({ + success: false, + message: `Cette place est déjà réservée sur ce créneau. Choisissez un autre horaire ou une autre date.` + }); + const paymentCode = 'PARK' + Date.now().toString().slice(-8); const result = await db.createReservation( req.user.id, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode ); - await db.updateSpotStatus(spotId, 'reserved'); - await db.addHistory('Nouvelle réservation', `Place ${spot.number} - ${price}EUR`, req.user.id); - res.status(201).json({ success: true, message: 'Réservation créée', data: { id: result.id, paymentCode } }); + + // On ne change le statut de la place QUE si la réservation est pour aujourd'hui + // et que l'heure de début est maintenant ou dans moins de 30 minutes + const now = new Date(); + const today = now.toISOString().split('T')[0]; + const resStart = new Date(`${date}T${startTime}`); + const diffMin = (resStart - now) / 60000; + + if (date === today && diffMin <= 30) { + await db.updateSpotStatus(spotId, 'reserved'); + } + // Pour une réservation future, le statut de la place reste inchangé + // Le timer d'expiration (server.js) le mettra à jour au bon moment + + await db.addHistory( + 'Nouvelle réservation', + `Place ${spot.number} réservée le ${date} de ${startTime} à ${endTime} — ${price}EUR`, + req.user.id + ); + + res.status(201).json({ + success: true, + message: 'Réservation créée', + data: { id: result.id, paymentCode } + }); } catch (err) { console.error('❌ Erreur create reservation:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' }); @@ -168,7 +215,6 @@ router.post('/reservations', authenticateToken, async (req, res) => { /** * PUT /api/reservations/:id/cancel - * CORRIGÉ : libère désormais la place associée */ router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => { try { @@ -179,10 +225,10 @@ router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => { return res.status(403).json({ success: false, message: 'Accès refusé' }); await db.updateReservationStatus(req.params.id, 'cancelled'); - await db.updateSpotStatus(reservation.spot_id, 'free'); // ← BUG CORRIGÉ ICI + await db.updateSpotStatus(reservation.spot_id, 'free'); await db.addHistory( 'Annulation réservation', - `Reservation #${req.params.id} annulee - place ${reservation.spot_id} liberee`, + `Réservation #${req.params.id} annulée — place ${reservation.spot_number} libérée`, req.user.id ); res.json({ success: true, message: 'Réservation annulée' }); @@ -193,7 +239,7 @@ router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => { }); /** - * PUT /api/reservations/:id/complete (admin uniquement) + * PUT /api/reservations/:id/complete (admin) */ router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async (req, res) => { try { @@ -205,7 +251,7 @@ router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async await db.updateSpotStatus(reservation.spot_id, 'free'); await db.addHistory( 'Réservation terminée', - `Reservation #${req.params.id} terminee - place ${reservation.spot_id} liberee`, + `Réservation #${req.params.id} terminée — place ${reservation.spot_number} libérée`, req.user.id ); res.json({ success: true, message: 'Réservation terminée' }); @@ -221,11 +267,11 @@ router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async router.get('/stats', authenticateToken, async (req, res) => { try { - const spots = await db.getAllSpots(); - const total = spots.length; - const free = spots.filter(s => s.status === 'free').length; - const occupied = spots.filter(s => s.status === 'occupied').length; - const reserved = spots.filter(s => s.status === 'reserved').length; + const spots = await db.getAllSpots(); + const total = spots.length; + const free = spots.filter(s => s.status === 'free').length; + const occupied = spots.filter(s => s.status === 'occupied').length; + const reserved = spots.filter(s => s.status === 'reserved').length; const occupancyRate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0; res.json({ success: true, data: { total, free, occupied, reserved, occupancyRate } }); } catch (err) { @@ -244,7 +290,7 @@ router.get('/history', authenticateToken, requireAdmin, async (req, res) => { }); router.get('/status', (_req, res) => { - res.json({ success: true, message: 'Smart Parking API operationnelle', version: '1.0.0', timestamp: new Date().toISOString() }); + res.json({ success: true, message: 'Smart Parking API opérationnelle', version: '3.0.0', timestamp: new Date().toISOString() }); }); module.exports = router; \ No newline at end of file