Mise à jour

This commit is contained in:
2026-06-03 14:47:35 +02:00
parent 085cf33114
commit ad0d86e734
16 changed files with 669 additions and 889 deletions

View File

@@ -1,14 +1,3 @@
/**
* ============================================
* ADMIN.JS - Panel d'administration
* Smart Parking v3.0
* CORRIGÉ :
* - Suppression du graphique d'occupation
* - Historique affiche la date complète
* (jour + mois + année + heure + minute)
* ============================================
*/
document.addEventListener('DOMContentLoaded', () => {
console.log('⚙️ Initialisation du panel admin...');
if (!isAdmin()) return;
@@ -27,7 +16,6 @@ function initAdminPanel() {
loadReservationsTable();
loadHistoryLog();
// Rafraîchissement périodique toutes les 10 secondes
setInterval(() => {
loadAdminStats();
loadReservationsTable();
@@ -35,9 +23,6 @@ function initAdminPanel() {
}, 10000);
}
// ============================================
// STATISTIQUES ADMIN
// ============================================
function loadAdminStats() {
const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
@@ -62,21 +47,44 @@ function loadAdminStats() {
document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%';
}
// ============================================
// GESTION DES PLACES
// ============================================
function initPlacesControl() {
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
const spotsInput = document.getElementById('adminTotalSpots');
if (spotsInput) spotsInput.value = spots.length || 10;
document.getElementById('updateSpotsBtn')?.addEventListener('click', () => {
document.getElementById('updateSpotsBtn')?.addEventListener('click', async () => {
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');
if (newCount < 1 || newCount > 20) {
Dashboard.showToast('Le nombre de places doit être entre 1 et 20', 'error');
return;
}
const token = localStorage.getItem('smart_parking_token');
if (token) {
try {
const response = await fetch('/api/spots/init', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ count: newCount })
});
const data = await response.json();
if (data.success) {
if (window.ParkingMap) await window.ParkingMap.refresh();
renderAdminPlacesList();
Dashboard.showToast('Nombre de places mis à jour', 'success');
return;
}
} catch (_err) { /* fallback local */ }
}
if (window.ParkingMap) {
window.ParkingMap.setTotalSpots(newCount);
renderAdminPlacesList();
@@ -104,27 +112,48 @@ function renderAdminPlacesList() {
`).join('');
}
function toggleSpotStatus(spotId) {
async function toggleSpotStatus(spotId) {
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
const spot = spots.find(s => s.id === spotId);
if (!spot) return;
const cycle = ['free', 'occupied', 'reserved'];
const nextStatus = cycle[(cycle.indexOf(spot.status) + 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];
const token = localStorage.getItem('smart_parking_token');
if (token) {
try {
const response = await fetch(`/api/spots/${spot.id}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ status: nextStatus })
});
const data = await response.json();
if (!data.success) {
Dashboard.showToast('Erreur : ' + data.message, 'error');
return;
}
} catch (_err) {
console.warn('⚠️ API indisponible, modification locale uniquement');
}
}
spot.status = nextStatus;
spot.lastUpdate = new Date().toISOString();
localStorage.setItem('smart_parking_spots', JSON.stringify(spots));
renderAdminPlacesList();
if (window.ParkingMap) window.ParkingMap.refresh();
loadAdminStats();
Dashboard.showToast(`Place ${spot.number} ${getStatusLabel(nextStatus)}`, 'success');
Dashboard.showToast(`Place ${spot.number} ${getStatusLabel(nextStatus)}`, 'success');
}
// ============================================
// TABLEAU UTILISATEURS
// ============================================
function loadUsersTable() {
const tbody = document.getElementById('adminUsersTable');
@@ -176,9 +205,7 @@ function deleteUser(userId) {
Dashboard.showToast('Utilisateur supprimé', 'success');
}
// ============================================
// TABLEAU RÉSERVATIONS
// ============================================
function loadReservationsTable() {
const tbody = document.getElementById('adminReservationsTable');
@@ -218,11 +245,22 @@ function loadReservationsTable() {
`).join('');
}
function completeReservation(reservationId) {
async function completeReservation(reservationId) {
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
const reservation = reservations.find(r => r.id === reservationId);
if (!reservation) return;
const token = localStorage.getItem('smart_parking_token');
if (token) {
try {
await fetch(`/api/reservations/${reservationId}/complete`, {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + token }
});
} catch (_err) { /* fallback local */ }
}
reservation.status = 'completed';
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
@@ -233,13 +271,24 @@ function completeReservation(reservationId) {
Dashboard.showToast('Réservation terminée', 'success');
}
function adminCancelReservation(reservationId) {
async 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') || '[]');
const reservation = reservations.find(r => r.id === reservationId);
if (!reservation) return;
// Appeler l'API
const token = localStorage.getItem('smart_parking_token');
if (token) {
try {
await fetch(`/api/reservations/${reservationId}/cancel`, {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + token }
});
} catch (_err) { /* fallback local */ }
}
reservation.status = 'cancelled';
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
@@ -250,9 +299,6 @@ function adminCancelReservation(reservationId) {
Dashboard.showToast('Réservation annulée', 'success');
}
// ============================================
// HISTORIQUE — CORRIGÉ : date complète
// ============================================
function loadHistoryLog() {
const container = document.getElementById('adminLogContainer');
@@ -273,15 +319,8 @@ function loadHistoryLog() {
`).join('');
}
// ============================================
// FONCTIONS DE FORMAT DATE
// ============================================
/**
* CORRIGÉ — Affiche la date complète dans l'historique
* Avant : seulement "14:32"
* Après : "12/06/2025 à 14:32"
*/
function formatDateComplete(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
@@ -312,9 +351,6 @@ function getStatusLabel(status) {
}[status] || status;
}
// ============================================
// EXPORT
// ============================================
window.AdminModule = {
refresh: () => {

View File

@@ -1,57 +1,33 @@
/**
* ============================================
* AUTH.JS - Système d'authentification
* Smart Parking - BTS CIEL IR
* ============================================
*/
// Configuration
const AUTH_CONFIG = {
apiUrl: 'http://localhost:3000/api',
apiUrl: '/api',
tokenKey: 'smart_parking_token',
userKey: 'smart_parking_user'
};
// État de l'authentification
let authState = {
isLoggedIn: false,
user: null,
token: null
};
// Initialisation
document.addEventListener('DOMContentLoaded', () => {
console.log('🔐 Initialisation du système d\'authentification...');
// Vérifier si déjà connecté
checkAuthStatus();
// Initialiser les écouteurs d'événements
initEventListeners();
});
/**
* Vérifie le statut d'authentification
*/
function checkAuthStatus() {
const token = localStorage.getItem(AUTH_CONFIG.tokenKey);
const user = localStorage.getItem(AUTH_CONFIG.userKey);
if (token && user) {
authState.token = token;
authState.user = JSON.parse(user);
authState.isLoggedIn = true;
// Rediriger vers le dashboard
window.location.href = 'pages/dashboard.html';
}
}
/**
* Initialise les écouteurs d'événements
*/
function initEventListeners() {
// Basculer vers l'inscription
const showRegister = document.getElementById('showRegister');
if (showRegister) {
showRegister.addEventListener('click', (e) => {
@@ -59,8 +35,7 @@ function initEventListeners() {
showRegisterForm();
});
}
// Basculer vers la connexion
const showLogin = document.getElementById('showLogin');
if (showLogin) {
showLogin.addEventListener('click', (e) => {
@@ -68,172 +43,121 @@ function initEventListeners() {
showLoginForm();
});
}
// Formulaire de connexion
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', handleLogin);
}
// Formulaire d'inscription
if (loginForm) loginForm.addEventListener('submit', handleLogin);
const registerForm = document.getElementById('registerForm');
if (registerForm) {
registerForm.addEventListener('submit', handleRegister);
}
if (registerForm) registerForm.addEventListener('submit', handleRegister);
}
/**
* Affiche le formulaire d'inscription
*/
function showRegisterForm() {
document.getElementById('loginForm').classList.add('hidden');
document.getElementById('registerForm').classList.remove('hidden');
hideMessage();
}
/**
* Affiche le formulaire de connexion
*/
function showLoginForm() {
document.getElementById('registerForm').classList.add('hidden');
document.getElementById('loginForm').classList.remove('hidden');
hideMessage();
}
/**
* Gère la connexion
*/
async function handleLogin(e) {
e.preventDefault();
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
if (!email || !password) {
showMessage('Veuillez remplir tous les champs', 'error');
return;
}
try {
// Appel API de connexion
const response = await fetch(`${AUTH_CONFIG.apiUrl}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
// Stocker les informations
localStorage.setItem(AUTH_CONFIG.tokenKey, data.token);
localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(data.user));
showMessage('Connexion réussie ! Redirection...', 'success');
// Rediriger vers le dashboard
setTimeout(() => {
window.location.href = 'pages/dashboard.html';
}, 1000);
setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000);
} else {
showMessage(data.message || 'Email ou mot de passe incorrect', 'error');
}
} catch (error) {
console.error('Erreur connexion:', error);
// Mode offline - simulation
handleOfflineLogin(email, password);
}
}
/**
* Gère la connexion en mode offline (simulation)
*/
function handleOfflineLogin(email, password) {
// Vérifier dans les utilisateurs stockés localement
const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
const user = users.find(u => u.email === email && u.password === password);
if (user) {
const token = generateToken();
localStorage.setItem(AUTH_CONFIG.tokenKey, token);
localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(user));
showMessage('Connexion réussie ! Redirection...', 'success');
setTimeout(() => {
window.location.href = 'pages/dashboard.html';
}, 1000);
setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000);
} else {
// Compte admin par défaut
if (email === 'admin@smartparking.fr' && password === 'admin123') {
const adminUser = {
id: 1,
name: 'Administrateur',
id: 1, name: 'Administrateur',
email: 'admin@smartparking.fr',
phone: '01 23 45 67 89',
role: 'admin'
phone: '01 23 45 67 89', role: 'admin'
};
const token = generateToken();
localStorage.setItem(AUTH_CONFIG.tokenKey, token);
localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(adminUser));
showMessage('Connexion admin réussie ! Redirection...', 'success');
setTimeout(() => {
window.location.href = 'pages/dashboard.html';
}, 1000);
setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000);
return;
}
showMessage('Email ou mot de passe incorrect', 'error');
}
}
/**
* Gère l'inscription
*/
async function handleRegister(e) {
e.preventDefault();
const name = document.getElementById('registerName').value;
const email = document.getElementById('registerEmail').value;
const phone = document.getElementById('registerPhone').value;
const password = document.getElementById('registerPassword').value;
const passwordConfirm = document.getElementById('registerPasswordConfirm').value;
// Validation
if (!name || !email || !phone || !password) {
showMessage('Veuillez remplir tous les champs', 'error');
return;
}
if (password !== passwordConfirm) {
showMessage('Les mots de passe ne correspondent pas', 'error');
return;
}
if (password.length < 8) {
showMessage('Le mot de passe doit faire au moins 8 caractères', 'error');
return;
}
try {
// Appel API d'inscription
const response = await fetch(`${AUTH_CONFIG.apiUrl}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, phone, password })
});
const data = await response.json();
if (data.success) {
showMessage('Compte créé avec succès ! Vous pouvez vous connecter.', 'success');
setTimeout(() => {
showLoginForm();
// Pré-remplir l'email
document.getElementById('loginEmail').value = email;
}, 1500);
} else {
@@ -241,39 +165,22 @@ async function handleRegister(e) {
}
} catch (error) {
console.error('Erreur inscription:', error);
// Mode offline - stockage local
handleOfflineRegister(name, email, phone, password);
}
}
/**
* Gère l'inscription en mode offline (simulation)
*/
function handleOfflineRegister(name, email, phone, password) {
// Récupérer les utilisateurs existants
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
// Vérifier si l'email existe déjà
if (users.find(u => u.email === email)) {
showMessage('Cet email est déjà utilisé', 'error');
return;
}
// Créer le nouvel utilisateur
const newUser = {
id: Date.now(),
name,
email,
phone,
password, // En production: hasher le mot de passe !
role: 'client',
createdAt: new Date().toISOString()
id: Date.now(), name, email, phone, password,
role: 'client', createdAt: new Date().toISOString()
};
// Ajouter à la liste
users.push(newUser);
localStorage.setItem('smart_parking_users', JSON.stringify(users));
showMessage('Compte créé avec succès ! Vous pouvez vous connecter.', 'success');
setTimeout(() => {
showLoginForm();
@@ -281,16 +188,10 @@ function handleOfflineRegister(name, email, phone, password) {
}, 1500);
}
/**
* Génère un token simple
*/
function generateToken() {
return 'token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
/**
* Affiche un message
*/
function showMessage(message, type) {
const messageEl = document.getElementById('authMessage');
messageEl.textContent = message;
@@ -298,24 +199,17 @@ function showMessage(message, type) {
messageEl.classList.remove('hidden');
}
/**
* Cache le message
*/
function hideMessage() {
const messageEl = document.getElementById('authMessage');
messageEl.classList.add('hidden');
}
/**
* Déconnexion
*/
function logout() {
localStorage.removeItem(AUTH_CONFIG.tokenKey);
localStorage.removeItem(AUTH_CONFIG.userKey);
window.location.href = '../index.html';
}
// Exporter les fonctions
window.Auth = {
logout,
getToken: () => localStorage.getItem(AUTH_CONFIG.tokenKey),
@@ -324,4 +218,4 @@ window.Auth = {
const user = JSON.parse(localStorage.getItem(AUTH_CONFIG.userKey) || 'null');
return user && user.role === 'admin';
}
};
};

View File

@@ -1,13 +1,4 @@
/**
* ============================================
* DASHBOARD.JS - Gestion du dashboard
* Smart Parking - BTS CIEL IR
* CORRIGÉ : cancelReservation utilisait spotNumber
* au lieu de spotId pour libérer la place
* ============================================
*/
const API_URL = 'http://localhost:3000/api';
const API_URL = '/api';
let dashboardState = {
user: null,
@@ -93,10 +84,10 @@ function loadUserData() {
document.getElementById('userName').textContent = user.name;
document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
document.getElementById('profileName').textContent = user.name;
document.getElementById('profileNameInput').value = user.name;
document.getElementById('profileEmailInput').value = user.email;
document.getElementById('profilePhoneInput').value = user.phone || '';
document.getElementById('profileName').textContent = user.name;
document.getElementById('profileNameInput').value = user.name;
document.getElementById('profileEmailInput').value = user.email;
document.getElementById('profilePhoneInput').value = user.phone || '';
const roleBadge = document.getElementById('profileRole');
if (roleBadge) {
@@ -137,7 +128,7 @@ function loadMyReservations() {
<h4>Place ${res.spotNumber}</h4>
<div class="reservation-details">
<span>📅 ${res.date}</span>
<span>🕐 ${res.startTime}</span>
<span>🕐 ${res.startTime} - ${res.endTime}</span>
<span>⏱️ ${formatDurationLabel(res.duration)}</span>
<span>🚗 ${res.vehicle || 'N/A'}</span>
</div>
@@ -155,22 +146,16 @@ function loadMyReservations() {
`).join('');
}
/**
* CORRIGÉ : utilisait reservation.spotNumber (le numéro visible)
* au lieu de reservation.spotId (l'id interne), ce qui empêchait
* la place d'être libérée sur la carte.
*/
function cancelReservation(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));
// CORRIGÉ : spotId au lieu de spotNumber
if (window.ParkingMap) {
window.ParkingMap.setSpotStatus(reservation.spotId, 'free');
}
@@ -181,26 +166,82 @@ function cancelReservation(reservationId) {
}
}
// Mise à jour du profil
document.getElementById('profileForm')?.addEventListener('submit', (e) => {
document.getElementById('profileForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const phone = document.getElementById('profilePhoneInput').value;
const newPassword = document.getElementById('profileNewPassword').value;
const confirmPass = document.getElementById('profileNewPassword').value;
let user = dashboardState.user;
user.phone = phone;
if (newPassword) user.password = newPassword;
localStorage.setItem('smart_parking_user', JSON.stringify(user));
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
const idx = users.findIndex(u => u.id === user.id);
if (idx !== -1) {
users[idx] = user;
localStorage.setItem('smart_parking_users', JSON.stringify(users));
if (newPassword && newPassword.length < 8) {
showToast('Le mot de passe doit faire au moins 8 caractères', 'error');
return;
}
showToast('Profil mis à jour', 'success');
const token = localStorage.getItem('smart_parking_token');
if (token) {
try {
const updateData = { phone };
if (newPassword) {
updateData.password = newPassword;
}
const response = await fetch(`${API_URL}/users/profile`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(updateData)
});
const data = await response.json();
if (data.success) {
let user = dashboardState.user;
user.phone = phone;
dashboardState.user = user;
localStorage.setItem('smart_parking_user', JSON.stringify(user));
document.getElementById('profileNewPassword').value = '';
showToast('Profil mis à jour avec succès !', 'success');
} else {
showToast(data.message || 'Erreur lors de la mise à jour', 'error');
}
} catch (_err) {
let user = dashboardState.user;
user.phone = phone;
if (newPassword) user.password = newPassword;
dashboardState.user = user;
localStorage.setItem('smart_parking_user', JSON.stringify(user));
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
const idx = users.findIndex(u => u.id === user.id);
if (idx !== -1) {
users[idx] = user;
localStorage.setItem('smart_parking_users', JSON.stringify(users));
}
document.getElementById('profileNewPassword').value = '';
showToast('Profil mis à jour', 'success');
}
} else {
let user = dashboardState.user;
user.phone = phone;
if (newPassword) user.password = newPassword;
dashboardState.user = user;
localStorage.setItem('smart_parking_user', JSON.stringify(user));
showToast('Profil mis à jour', 'success');
}
});
function getStatusLabel(status) {
@@ -208,7 +249,7 @@ function getStatusLabel(status) {
}
function formatDurationLabel(minutes) {
if (minutes >= 480) return 'Journée';
if (minutes >= 480) return 'Journée (8h)';
if (minutes >= 60) return Math.floor(minutes / 60) + 'h' + (minutes % 60 ? (minutes % 60) + 'min' : '');
return minutes + ' min';
}

117
js/map.js
View File

@@ -1,15 +1,6 @@
/**
* ============================================
* MAP.JS - Carte des places de parking
* Smart Parking v2.0
* MODIFIÉ : polling API toutes les 3s pour recevoir
* les mises à jour en temps réel depuis Arduino
* ============================================
*/
const MAP_CONFIG = {
totalSpots: 10,
updateInterval: 3000 // Refresh depuis l'API toutes les 3 secondes
updateInterval: 3000
};
let spotsState = {
@@ -19,9 +10,7 @@ let spotsState = {
const SPOT_STATUS = { FREE: 'free', OCCUPIED: 'occupied', RESERVED: 'reserved' };
// ============================================
// INITIALISATION
// ============================================
document.addEventListener('DOMContentLoaded', () => {
console.log('🗺️ Initialisation de la carte...');
@@ -29,30 +18,17 @@ document.addEventListener('DOMContentLoaded', () => {
});
async function initParkingMap() {
// Essayer d'abord de charger depuis l'API (données MariaDB + Arduino)
const loaded = await loadSpotsFromAPI();
// Si pas d'API disponible, utiliser le localStorage (mode offline)
if (!loaded) {
loadSpotsFromStorage();
}
if (!loaded) loadSpotsFromStorage();
renderMap();
updateStats();
updateReservationForm();
// ⭐ Polling toutes les 3 secondes pour recevoir les updates Arduino
startAPIPolling();
}
// ============================================
// CHARGEMENT DES PLACES
// ============================================
/**
* Charge les places depuis l'API (MariaDB)
* Retourne true si succès, false si hors-ligne
*/
async function loadSpotsFromAPI() {
try {
const token = localStorage.getItem('smart_parking_token');
@@ -67,7 +43,6 @@ async function loadSpotsFromAPI() {
const data = await response.json();
if (!data.success || !data.data.length) return false;
// Convertir le format API → format interne
spotsState.spots = data.data.map(s => ({
id: s.id,
number: s.number,
@@ -76,19 +51,14 @@ async function loadSpotsFromAPI() {
sensorId: s.sensor_id
}));
// Synchroniser avec localStorage pour le mode offline
saveSpots();
return true;
} catch (_err) {
// Serveur non joignable → mode offline
return false;
}
}
/**
* Charge les places depuis le localStorage (mode offline)
*/
function loadSpotsFromStorage() {
const stored = localStorage.getItem('smart_parking_spots');
if (stored) {
@@ -116,23 +86,22 @@ function saveSpots() {
localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots));
}
// ============================================
// ⭐ POLLING TEMPS RÉEL (mises à jour Arduino)
// ============================================
/**
* Interroge l'API toutes les 3 secondes.
* Si l'Arduino a changé l'état d'une place via MQTT,
* la carte se met à jour automatiquement.
*/
function startAPIPolling() {
setInterval(async () => {
const loaded = await loadSpotsFromAPI();
if (loaded) {
renderMap();
updateStats();
updateReservationForm();
// Rafraîchir les détails si une place est sélectionnée
const resSpot = document.getElementById('resSpot');
const hasSelection = resSpot && resSpot.value !== '';
if (!hasSelection) {
updateReservationForm();
}
if (spotsState.selectedSpot) {
const updated = spotsState.spots.find(s => s.id === spotsState.selectedSpot.id);
if (updated) {
@@ -144,9 +113,6 @@ function startAPIPolling() {
}, MAP_CONFIG.updateInterval);
}
// ============================================
// RENDU DE LA CARTE
// ============================================
function renderMap() {
const mapContainer = document.getElementById('parkingMap');
@@ -202,21 +168,21 @@ function showSpotDetails(spot) {
</div>
${reservation ? `
<div class="spot-info-row">
<span class="spot-info-label">Réservé par</span>
<span class="spot-info-value">${reservation.userName}</span>
</div>
<div class="spot-info-row">
<span class="spot-info-label">Jusqu'à</span>
<span class="spot-info-label">Réservé jusqu'à</span>
<span class="spot-info-value">${reservation.endTime}</span>
</div>
` : ''}
${spot.status === SPOT_STATUS.FREE ? `
${spot.status !== SPOT_STATUS.OCCUPIED ? `
<button class="btn btn-primary btn-block"
onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});">
<span class="btn-icon">📅</span>
Réserver cette place
</button>
` : ''}
` : `
<p style="color: var(--danger); text-align: center; margin-top: 12px;">
🚗 Une voiture est physiquement sur cette place
</p>
`}
</div>
`;
}
@@ -226,9 +192,6 @@ function findReservationForSpot(spotId) {
return reservations.find(r => r.spotId === spotId && r.status === 'active');
}
// ============================================
// STATISTIQUES & FORMULAIRE
// ============================================
function updateStats() {
const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length;
@@ -241,22 +204,35 @@ function updateStats() {
document.getElementById('totalCount').textContent = spotsState.spots.length;
}
function updateReservationForm() {
const select = document.getElementById('resSpot');
if (!select) return;
const currentValue = select.value;
const firstOption = select.options[0];
select.innerHTML = '';
select.appendChild(firstOption);
spotsState.spots
.filter(s => s.status === SPOT_STATUS.FREE)
.forEach(spot => {
const option = document.createElement('option');
option.value = spot.id;
option.textContent = `Place ${spot.number}`;
select.appendChild(option);
});
const availableSpots = spotsState.spots.filter(s => s.status !== SPOT_STATUS.OCCUPIED);
availableSpots.forEach(spot => {
const option = document.createElement('option');
option.value = spot.id;
option.textContent = spot.status === SPOT_STATUS.RESERVED
? `Place ${spot.number} (réservée en ce moment)`
: `Place ${spot.number}`;
select.appendChild(option);
});
if (currentValue) {
select.value = currentValue;
}
}
function selectSpotForReservation(spotId) {
@@ -264,9 +240,7 @@ function selectSpotForReservation(spotId) {
if (select) select.value = spotId;
}
// ============================================
// ACTIONS ADMIN & SIMULATION
// ============================================
async function setSpotStatus(spotId, status) {
const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId);
@@ -279,7 +253,6 @@ async function setSpotStatus(spotId, status) {
updateStats();
updateReservationForm();
// Synchroniser avec l'API si connecté
try {
const token = localStorage.getItem('smart_parking_token');
if (token) {
@@ -315,9 +288,6 @@ function setTotalSpots(count) {
updateReservationForm();
}
// ============================================
// UTILITAIRES
// ============================================
function isAdmin() {
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
@@ -340,9 +310,6 @@ function formatDate(dateString) {
});
}
// ============================================
// EXPORT
// ============================================
window.ParkingMap = {
refresh: async () => {

View File

@@ -1,13 +1,3 @@
/**
* ============================================
* RESERVATION.JS - Système de réservation
* Smart Parking v3.0
* CORRIGÉ : ne bloque plus une place pour
* toujours — vérifie uniquement si
* une voiture est physiquement là
* ============================================
*/
const PRICING = {
30: 2,
60: 3,
@@ -27,14 +17,46 @@ const TIME_SLOTS = [
let currentReservation = null;
document.addEventListener('DOMContentLoaded', () => {
console.log('📅 Initialisation du système de réservation...');
cleanupExpiredReservations();
initReservationForm();
initDatePicker();
initTimeSlots();
initPricePreview();
initConfirmationModal();
setInterval(cleanupExpiredReservations, 60000);
});
function cleanupExpiredReservations() {
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
const now = new Date();
let cleaned = 0;
reservations.forEach(r => {
if (r.status !== 'active' && r.status !== 'pending') return;
const endDateTime = new Date(r.date + 'T' + r.endTime);
if (endDateTime < now) {
r.status = 'completed';
cleaned++;
}
});
if (cleaned > 0) {
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
}
}
function checkLocalConflict(spotId, date, startTime, endTime) {
cleanupExpiredReservations();
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
return reservations.some(r => {
if (r.spotId !== spotId) return false;
if (r.date !== date) return false;
if (r.status !== 'active' && r.status !== 'pending') return false;
return r.startTime < endTime && r.endTime > startTime;
});
}
function initReservationForm() {
const form = document.getElementById('reservationForm');
if (!form) return;
@@ -52,18 +74,16 @@ function initDatePicker() {
function initTimeSlots() {
const select = document.getElementById('resStartTime');
if (!select) return;
TIME_SLOTS.forEach(time => {
const option = document.createElement('option');
option.value = time;
option.textContent = time;
select.appendChild(option);
});
const now = new Date();
const currentHour = now.getHours();
const currentMins = 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 > currentMins);
});
@@ -83,14 +103,6 @@ function updatePricePreview() {
document.getElementById('previewPrice').textContent = price + '€';
}
/**
* Gère la soumission du formulaire
*
* 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").
*/
async function handleReservationSubmit(e) {
e.preventDefault();
@@ -111,25 +123,29 @@ async function handleReservationSubmit(e) {
return;
}
// 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 === 'occupied') {
Dashboard.showToast('Une voiture est déjà sur cette place', 'error');
Dashboard.showToast('Une voiture est deja sur cette place', 'error');
if (window.ParkingMap) window.ParkingMap.refresh();
return;
}
// Calculer l'heure de fin
const endDate = new Date(date + 'T' + startTime);
endDate.setMinutes(endDate.getMinutes() + duration);
const endTime = endDate.toTimeString().slice(0, 5);
// Essayer de créer la réservation via l'API
// Le serveur vérifiera les conflits d'horaire
if (checkLocalConflict(spotId, date, startTime, endTime)) {
Dashboard.showToast(
'Cette place est deja reservee a cet horaire. Essayez un autre creneau ou une autre date.',
'error'
);
return;
}
const token = localStorage.getItem('smart_parking_token');
let apiSuccess = false;
if (token) {
try {
@@ -149,12 +165,10 @@ async function handleReservationSubmit(e) {
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,
@@ -167,28 +181,34 @@ async function handleReservationSubmit(e) {
status: 'active',
createdAt: new Date().toISOString()
};
apiSuccess = true;
} catch (_err) {
// Mode offline : enregistrement local
currentReservation = creerReservationLocale(
user, spotId, spot, date, startTime, endTime, duration, vehicle
);
apiSuccess = false;
}
} else {
// Pas de token : mode offline
currentReservation = creerReservationLocale(
user, spotId, spot, date, startTime, endTime, duration, vehicle
);
}
// Sauvegarder dans le localStorage
if (!apiSuccess) {
currentReservation = {
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()
};
}
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
reservations.push(currentReservation);
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
// 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 today = new Date().toISOString().split('T')[0];
const now = new Date();
const resStart = new Date(date + 'T' + startTime);
const diffMin = (resStart - now) / 60000;
@@ -197,40 +217,16 @@ async function handleReservationSubmit(e) {
}
addToHistory(
'Réservation',
`Place ${currentReservation.spotNumber} réservée le ${date} de ${startTime} à ${endTime}${PRICING[duration]}`
'Reservation',
'Place ' + currentReservation.spotNumber + ' reservee le ' + date + ' de ' + startTime + ' a ' + endTime + ' — ' + PRICING[duration] + '€'
);
// Réinitialiser le formulaire
document.getElementById('reservationForm').reset();
initDatePicker();
updatePricePreview();
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
// ============================================
function initConfirmationModal() {
document.getElementById('closeConfirmationModal')?.addEventListener('click', hideConfirmationModal);
document.getElementById('closeConfirmationBtn')?.addEventListener('click', hideConfirmationModal);
@@ -238,60 +234,44 @@ function initConfirmationModal() {
function showConfirmationModal() {
if (!currentReservation) return;
const modal = document.getElementById('confirmationModal');
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 + '€';
modal.classList.remove('hidden');
}
function hideConfirmationModal() {
document.getElementById('confirmationModal').classList.add('hidden');
if (window.Dashboard) {
Dashboard.navigateToPage('my-reservations');
document.querySelector('[data-page="my-reservations"]')?.classList.add('active');
document.querySelector('[data-page="reservation"]')?.classList.remove('active');
}
if (window.Dashboard) Dashboard.refreshStats();
}
// ============================================
// UTILITAIRES
// ============================================
function addToHistory(action, details) {
let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]');
history.unshift({
id: Date.now(),
action,
details,
timestamp: new Date().toISOString()
});
history.unshift({ id: Date.now(), action, details, timestamp: new Date().toISOString() });
if (history.length > 100) history = history.slice(0, 100);
localStorage.setItem('smart_parking_history', JSON.stringify(history));
}
function formatDate(dateString) {
return new Date(dateString).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' });
}
function formatDuration(minutes) {
if (minutes >= 480) return 'Journée (8h)';
if (minutes >= 480) return 'Journee (8h)';
if (minutes >= 60) {
const h = Math.floor(minutes / 60);
const m = minutes % 60;
return m > 0 ? `${h}h ${m}min` : `${h}h`;
return m > 0 ? h + 'h ' + m + 'min' : h + 'h';
}
return `${minutes} min`;
return minutes + ' min';
}
window.Reservation = { PRICING, TIME_SLOTS, formatDuration, addToHistory };