first commit

This commit is contained in:
2026-03-11 17:18:34 +01:00
commit c6954c1f50
20 changed files with 5049 additions and 0 deletions

436
js/admin.js Normal file
View File

@@ -0,0 +1,436 @@
/**
* ============================================
* ADMIN.JS - Panel d'administration
* Smart Parking - BTS CIEL IR
* ============================================
*/
// 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
setInterval(() => {
loadAdminStats();
loadReservationsTable();
}, 10000);
}
/**
* Charge les statistiques admin
*/
function loadAdminStats() {
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 totalReservations = reservations.length;
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)
: 0;
// Mettre à jour l'affichage
document.getElementById('adminTotalUsers').textContent = totalUsers;
document.getElementById('adminTotalReservations').textContent = totalReservations;
document.getElementById('adminTotalRevenue').textContent = totalRevenue + '€';
document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%';
}
/**
* Initialise le contrôle des places
*/
function initPlacesControl() {
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
// Mettre à jour le champ du nombre de places
const spotsInput = document.getElementById('adminTotalSpots');
if (spotsInput) {
spotsInput.value = spots.length || 10;
}
// Bouton de mise à jour
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 => `
<div
class="admin-place-item ${spot.status}"
onclick="toggleSpotStatus(${spot.id})"
title="Place ${spot.number} - Cliquez pour changer"
>
${spot.number}
</div>
`).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);
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();
localStorage.setItem('smart_parking_spots', JSON.stringify(spots));
// Rafraîchir
renderAdminPlacesList();
if (window.ParkingMap) {
window.ParkingMap.refresh();
}
loadAdminStats();
Dashboard.showToast(`Place ${spot.number} - ${getStatusLabel(nextStatus)}`, 'success');
}
/**
* Charge le tableau des 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 allUsers = [
{
id: 0,
name: 'Administrateur',
email: 'admin@smartparking.fr',
phone: '01 23 45 67 89',
role: 'admin'
},
...users
];
tbody.innerHTML = allUsers.map(user => `
<tr>
<td>#${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
<td>${user.phone || '-'}</td>
<td>
<span class="badge ${user.role === 'admin' ? 'badge-admin' : 'badge-client'}">
${user.role === 'admin' ? 'Admin' : 'Client'}
</span>
</td>
<td>
<div class="table-actions">
${user.role !== 'admin' ? `
<button class="btn btn-danger btn-small btn-icon-only" onclick="deleteUser(${user.id})" title="Supprimer">
🗑️
</button>
` : '-'}
</div>
</td>
</tr>
`).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);
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);
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
loadUsersTable();
loadReservationsTable();
loadAdminStats();
Dashboard.showToast('Utilisateur supprimé', 'success');
}
/**
* Charge le tableau des 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 = '<tr><td colspan="8" style="text-align: center; color: var(--text-muted);">Aucune réservation</td></tr>';
return;
}
tbody.innerHTML = reservations.slice().reverse().map(res => `
<tr>
<td>#${res.id}</td>
<td>${res.userName}</td>
<td>Place ${res.spotNumber}</td>
<td>${formatDate(res.date)}</td>
<td>${res.startTime} - ${res.endTime}</td>
<td>${res.price}€</td>
<td>
<span class="reservation-status status-${res.status}">
${getStatusLabel(res.status)}
</span>
</td>
<td>
<div class="table-actions">
${res.status === 'active' ? `
<button class="btn btn-success btn-small btn-icon-only" onclick="completeReservation(${res.id})" title="Terminer">
</button>
<button class="btn btn-danger btn-small btn-icon-only" onclick="adminCancelReservation(${res.id})" title="Annuler">
</button>
` : '-'}
</div>
</td>
</tr>
`).join('');
}
/**
* Termine une réservation
*/
function completeReservation(reservationId) {
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');
}
}
/**
* 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') || '[]');
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');
}
}
/**
* 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 + '%'
}
}
}
}
});
}
/**
* 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 = '<p style="color: var(--text-muted); text-align: center;">Aucun historique</p>';
return;
}
container.innerHTML = history.slice(0, 20).map(item => `
<div class="log-item">
<span class="log-time">${formatTime(item.timestamp)}</span>
<span><strong>${item.action}:</strong> ${item.details}</span>
</div>
`).join('');
}
/**
* Formate une date
*/
function formatDate(dateString) {
const date = new Date(dateString);
return date.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;
}
// Exporter les fonctions
window.AdminModule = {
refresh: () => {
loadAdminStats();
renderAdminPlacesList();
loadUsersTable();
loadReservationsTable();
loadHistoryLog();
},
toggleSpotStatus,
deleteUser,
completeReservation,
adminCancelReservation
};

327
js/auth.js Normal file
View File

@@ -0,0 +1,327 @@
/**
* ============================================
* AUTH.JS - Système d'authentification
* Smart Parking - BTS CIEL IR
* ============================================
*/
// Configuration
const AUTH_CONFIG = {
apiUrl: 'http://localhost:3000/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) => {
e.preventDefault();
showRegisterForm();
});
}
// Basculer vers la connexion
const showLogin = document.getElementById('showLogin');
if (showLogin) {
showLogin.addEventListener('click', (e) => {
e.preventDefault();
showLoginForm();
});
}
// Formulaire de connexion
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', handleLogin);
}
// Formulaire d'inscription
const registerForm = document.getElementById('registerForm');
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'
},
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);
} 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);
} else {
// Compte admin par défaut
if (email === 'admin@smartparking.fr' && password === 'admin123') {
const adminUser = {
id: 1,
name: 'Administrateur',
email: 'admin@smartparking.fr',
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);
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'
},
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 {
showMessage(data.message || 'Erreur lors de la création du compte', 'error');
}
} 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()
};
// 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();
document.getElementById('loginEmail').value = email;
}, 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;
messageEl.className = `auth-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),
getUser: () => JSON.parse(localStorage.getItem(AUTH_CONFIG.userKey) || 'null'),
isAdmin: () => {
const user = JSON.parse(localStorage.getItem(AUTH_CONFIG.userKey) || 'null');
return user && user.role === 'admin';
}
};

300
js/dashboard.js Normal file
View File

@@ -0,0 +1,300 @@
/**
* ============================================
* DASHBOARD.JS - Gestion du dashboard
* Smart Parking - BTS CIEL IR
* ============================================
*/
// Configuration
const API_URL = 'http://localhost:3000/api';
// État global
let dashboardState = {
user: null,
currentPage: 'map'
};
// Initialisation
document.addEventListener('DOMContentLoaded', () => {
console.log('🚗 Initialisation du dashboard...');
// Vérifier l'authentification
checkAuthentication();
// Initialiser la navigation
initNavigation();
// Initialiser la déconnexion
initLogout();
// Charger les données utilisateur
loadUserData();
});
/**
* Vérifie que l'utilisateur est authentifié
*/
function checkAuthentication() {
const token = localStorage.getItem('smart_parking_token');
const user = localStorage.getItem('smart_parking_user');
if (!token || !user) {
// Rediriger vers la page de connexion
window.location.href = '../index.html';
return;
}
dashboardState.user = JSON.parse(user);
}
/**
* Initialise la navigation
*/
function initNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const page = link.getAttribute('data-page');
navigateToPage(page);
// Mettre à jour la classe active
navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
});
});
// Afficher le lien admin si l'utilisateur est admin
if (dashboardState.user && dashboardState.user.role === 'admin') {
const adminLink = document.querySelector('.admin-only');
if (adminLink) {
adminLink.classList.remove('hidden');
adminLink.classList.add('visible');
}
}
}
/**
* Navigation entre les pages
*/
function navigateToPage(page) {
// Cacher toutes les pages
const pages = document.querySelectorAll('.page');
pages.forEach(p => {
p.classList.add('hidden');
p.classList.remove('active');
});
// Afficher la page demandée
const targetPage = document.getElementById(page);
if (targetPage) {
targetPage.classList.remove('hidden');
targetPage.classList.add('active');
dashboardState.currentPage = page;
// Rafraîchir les données selon la page
if (page === 'map' && window.ParkingMap) {
window.ParkingMap.refresh();
} else if (page === 'my-reservations') {
loadMyReservations();
} else if (page === 'admin' && window.AdminModule) {
window.AdminModule.refresh();
}
}
}
/**
* Initialise la déconnexion
*/
function initLogout() {
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
localStorage.removeItem('smart_parking_token');
localStorage.removeItem('smart_parking_user');
window.location.href = '../index.html';
});
}
}
/**
* Charge les données utilisateur
*/
function loadUserData() {
const user = dashboardState.user;
if (!user) return;
// Mettre à jour l'affichage
document.getElementById('userName').textContent = user.name;
document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
// Page profil
document.getElementById('profileName').textContent = user.name;
document.getElementById('profileNameInput').value = user.name;
document.getElementById('profileEmailInput').value = user.email;
document.getElementById('profilePhoneInput').value = user.phone || '';
// Badge rôle
const roleBadge = document.getElementById('profileRole');
if (roleBadge) {
roleBadge.textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
roleBadge.className = 'role-badge ' + (user.role === 'admin' ? 'badge-admin' : 'badge-client');
}
// Charger les statistiques
loadUserStats();
}
/**
* Charge les statistiques utilisateur
*/
function loadUserStats() {
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
const userReservations = reservations.filter(r => r.userId === dashboardState.user.id);
const totalReservations = userReservations.length;
const activeReservations = userReservations.filter(r => r.status === 'active').length;
const totalSpent = userReservations.reduce((sum, r) => sum + (r.price || 0), 0);
document.getElementById('totalReservations').textContent = totalReservations;
document.getElementById('activeReservations').textContent = activeReservations;
document.getElementById('totalSpent').textContent = totalSpent + '€';
}
/**
* Charge les réservations de l'utilisateur
*/
function loadMyReservations() {
const container = document.getElementById('myReservationsList');
const emptyState = document.getElementById('noReservations');
if (!container || !emptyState) return;
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
const userReservations = reservations.filter(r => r.userId === dashboardState.user.id);
if (userReservations.length === 0) {
container.innerHTML = '';
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
container.innerHTML = userReservations.map(res => `
<div class="reservation-card">
<div class="reservation-info">
<h4>Place ${res.spotNumber}</h4>
<div class="reservation-details">
<span>📅 ${res.date}</span>
<span>🕐 ${res.startTime}</span>
<span>⏱️ ${res.duration} min</span>
<span>🚗 ${res.vehicle || 'N/A'}</span>
</div>
</div>
<div class="reservation-actions">
<span class="reservation-price">${res.price}€</span>
<span class="reservation-status status-${res.status}">${getStatusLabel(res.status)}</span>
${res.status === 'active' ? `
<button class="btn btn-danger btn-small" onclick="cancelReservation(${res.id})">
Annuler
</button>
` : ''}
</div>
</div>
`).join('');
}
/**
* Annule une réservation
*/
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') || '[]');
const reservation = reservations.find(r => r.id === reservationId);
if (reservation) {
// Mettre à jour le statut
reservation.status = 'cancelled';
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
// Libérer la place
if (window.ParkingMap) {
window.ParkingMap.setSpotStatus(reservation.spotNumber, 'free');
}
showToast('Réservation annulée', 'success');
loadMyReservations();
loadUserStats();
}
}
/**
* Met à jour le profil
*/
document.getElementById('profileForm')?.addEventListener('submit', (e) => {
e.preventDefault();
const phone = document.getElementById('profilePhoneInput').value;
const newPassword = document.getElementById('profileNewPassword').value;
// Mettre à jour l'utilisateur
let user = dashboardState.user;
user.phone = phone;
if (newPassword) {
user.password = newPassword;
}
// Sauvegarder
localStorage.setItem('smart_parking_user', JSON.stringify(user));
// Mettre à jour aussi dans la liste des utilisateurs
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
const userIndex = users.findIndex(u => u.id === user.id);
if (userIndex !== -1) {
users[userIndex] = user;
localStorage.setItem('smart_parking_users', JSON.stringify(users));
}
showToast('Profil mis à jour', 'success');
});
/**
* Retourne le label du statut
*/
function getStatusLabel(status) {
const labels = {
'active': 'Active',
'completed': 'Terminée',
'cancelled': 'Annulée'
};
return labels[status] || status;
}
/**
* Affiche une notification toast
*/
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// Exporter les fonctions
window.Dashboard = {
navigateToPage,
showToast,
getUser: () => dashboardState.user,
refreshStats: loadUserStats
};

367
js/map.js Normal file
View File

@@ -0,0 +1,367 @@
/**
* ============================================
* MAP.JS - Carte des places de parking
* Smart Parking - BTS CIEL IR
* ============================================
*/
// Configuration
const MAP_CONFIG = {
totalSpots: 10, // Nombre total de places
updateInterval: 5000 // Intervalle de mise à jour
};
// État des places
let spotsState = {
spots: [],
selectedSpot: null
};
// Types de places
const SPOT_STATUS = {
FREE: 'free',
OCCUPIED: 'occupied',
RESERVED: 'reserved'
};
// Initialisation
document.addEventListener('DOMContentLoaded', () => {
console.log('🗺️ Initialisation de la carte...');
initParkingMap();
});
/**
* Initialise la carte du parking
*/
function initParkingMap() {
// Charger les places depuis le stockage local ou créer les places par défaut
loadSpots();
// Rendre la carte
renderMap();
// Mettre à jour les statistiques
updateStats();
// Mettre à jour le formulaire de réservation
updateReservationForm();
// Démarrer la simulation (si pas admin)
if (!isAdmin()) {
startSimulation();
}
}
/**
* Charge les places
*/
function loadSpots() {
const stored = localStorage.getItem('smart_parking_spots');
if (stored) {
spotsState.spots = JSON.parse(stored);
} else {
// Créer les places par défaut
createDefaultSpots();
}
}
/**
* Crée les places par défaut
*/
function createDefaultSpots() {
spotsState.spots = [];
for (let i = 1; i <= MAP_CONFIG.totalSpots; i++) {
// Distribution: 60% libre, 25% occupé, 15% réservé
const rand = Math.random();
let status = SPOT_STATUS.FREE;
if (rand > 0.85) {
status = SPOT_STATUS.RESERVED;
} else if (rand > 0.60) {
status = SPOT_STATUS.OCCUPIED;
}
spotsState.spots.push({
id: i,
number: i,
status: status,
lastUpdate: new Date().toISOString(),
sensorId: `SENSOR_${String(i).padStart(3, '0')}`
});
}
saveSpots();
}
/**
* Sauvegarde les places
*/
function saveSpots() {
localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots));
}
/**
* Rend la carte
*/
function renderMap() {
const mapContainer = document.getElementById('parkingMap');
if (!mapContainer) return;
mapContainer.innerHTML = spotsState.spots.map(spot => `
<div
class="parking-spot ${spot.status}"
data-id="${spot.id}"
onclick="handleSpotClick(${spot.id})"
title="Place ${spot.number} - ${getStatusLabel(spot.status)}"
>
<span class="spot-number">${spot.number}</span>
<span class="spot-icon">${getStatusIcon(spot.status)}</span>
</div>
`).join('');
}
/**
* Gère le clic sur une place
*/
function handleSpotClick(spotId) {
const spot = spotsState.spots.find(s => s.id === spotId);
if (!spot) return;
spotsState.selectedSpot = spot;
showSpotDetails(spot);
}
/**
* Affiche les détails d'une place
*/
function showSpotDetails(spot) {
const container = document.getElementById('spotDetails');
if (!container) return;
const isReserved = spot.status === SPOT_STATUS.RESERVED;
const reservation = isReserved ? findReservationForSpot(spot.id) : null;
container.innerHTML = `
<div class="spot-info-detail">
<div class="spot-info-row">
<span class="spot-info-label">Numéro</span>
<span class="spot-info-value">Place ${spot.number}</span>
</div>
<div class="spot-info-row">
<span class="spot-info-label">État</span>
<span class="spot-info-value spot-status-${spot.status}">
${getStatusLabel(spot.status)}
</span>
</div>
<div class="spot-info-row">
<span class="spot-info-label">Capteur</span>
<span class="spot-info-value">${spot.sensorId}</span>
</div>
<div class="spot-info-row">
<span class="spot-info-label">Dernière mise à jour</span>
<span class="spot-info-value">${formatDate(spot.lastUpdate)}</span>
</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-value">${reservation.endTime}</span>
</div>
` : ''}
${spot.status === SPOT_STATUS.FREE ? `
<button class="btn btn-primary btn-block" onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});">
<span class="btn-icon">📅</span>
Réserver cette place
</button>
` : ''}
</div>
`;
}
/**
* Trouve la réservation pour une place
*/
function findReservationForSpot(spotId) {
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
return reservations.find(r => r.spotId === spotId && r.status === 'active');
}
/**
* Met à jour les statistiques
*/
function updateStats() {
const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length;
const occupied = spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length;
const reserved = spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length;
document.getElementById('freeCount').textContent = free;
document.getElementById('occupiedCount').textContent = occupied;
document.getElementById('reservedCount').textContent = reserved;
document.getElementById('totalCount').textContent = spotsState.spots.length;
}
/**
* Met à jour le formulaire de réservation
*/
function updateReservationForm() {
const select = document.getElementById('resSpot');
if (!select) return;
// Garder la première option
const firstOption = select.options[0];
select.innerHTML = '';
select.appendChild(firstOption);
// Ajouter uniquement les places libres
const freeSpots = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE);
freeSpots.forEach(spot => {
const option = document.createElement('option');
option.value = spot.id;
option.textContent = `Place ${spot.number}`;
select.appendChild(option);
});
}
/**
* Sélectionne une place pour la réservation
*/
function selectSpotForReservation(spotId) {
const select = document.getElementById('resSpot');
if (select) {
select.value = spotId;
}
}
/**
* Définit le statut d'une place
*/
function setSpotStatus(spotId, status) {
const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId);
if (spot) {
spot.status = status;
spot.lastUpdate = new Date().toISOString();
saveSpots();
renderMap();
updateStats();
updateReservationForm();
}
}
/**
* Change le nombre total de places
*/
function setTotalSpots(count) {
MAP_CONFIG.totalSpots = count;
// Ajuster le tableau des places
if (count > spotsState.spots.length) {
// Ajouter des places
for (let i = spotsState.spots.length + 1; i <= count; i++) {
spotsState.spots.push({
id: i,
number: i,
status: SPOT_STATUS.FREE,
lastUpdate: new Date().toISOString(),
sensorId: `SENSOR_${String(i).padStart(3, '0')}`
});
}
} else if (count < spotsState.spots.length) {
// Supprimer des places
spotsState.spots = spotsState.spots.slice(0, count);
}
saveSpots();
renderMap();
updateStats();
updateReservationForm();
}
/**
* Simulation automatique
*/
function startSimulation() {
setInterval(() => {
// 20% de chance de changer une place
if (Math.random() > 0.8) {
const randomSpot = spotsState.spots[Math.floor(Math.random() * spotsState.spots.length)];
if (randomSpot.status === SPOT_STATUS.FREE && Math.random() > 0.5) {
setSpotStatus(randomSpot.id, SPOT_STATUS.OCCUPIED);
} else if (randomSpot.status === SPOT_STATUS.OCCUPIED && Math.random() > 0.3) {
setSpotStatus(randomSpot.id, SPOT_STATUS.FREE);
}
}
}, MAP_CONFIG.updateInterval);
}
/**
* Vérifie si l'utilisateur est admin
*/
function isAdmin() {
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
return user && user.role === 'admin';
}
/**
* Retourne le label du statut
*/
function getStatusLabel(status) {
const labels = {
[SPOT_STATUS.FREE]: 'Libre',
[SPOT_STATUS.OCCUPIED]: 'Occupée',
[SPOT_STATUS.RESERVED]: 'Réservée'
};
return labels[status] || 'Inconnu';
}
/**
* Retourne l'icône du statut
*/
function getStatusIcon(status) {
const icons = {
[SPOT_STATUS.FREE]: '✓',
[SPOT_STATUS.OCCUPIED]: '🚗',
[SPOT_STATUS.RESERVED]: '📅'
};
return icons[status] || '?';
}
/**
* Formate une date
*/
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Exporter les fonctions
window.ParkingMap = {
refresh: () => {
loadSpots();
renderMap();
updateStats();
updateReservationForm();
},
setSpotStatus,
setTotalSpots,
getSpots: () => spotsState.spots,
getFreeSpots: () => spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE),
getStats: () => ({
total: spotsState.spots.length,
free: spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length,
occupied: spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length,
reserved: spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length
})
};

351
js/reservation.js Normal file
View File

@@ -0,0 +1,351 @@
/**
* ============================================
* RESERVATION.JS - Système de réservation
* Smart Parking - BTS CIEL IR
* ============================================
*/
// 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€
};
// 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',
'14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30',
'18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30',
'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();
initPaymentModal();
});
/**
* 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;
// Date minimum = aujourd'hui
const today = new Date().toISOString().split('T')[0];
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;
option.textContent = time;
select.appendChild(option);
});
// Sélectionner l'heure actuelle + 1h
const now = new Date();
const currentHour = now.getHours();
const currentMinutes = now.getMinutes();
const nextSlot = TIME_SLOTS.find(t => {
const [h, m] = t.split(':').map(Number);
return h > currentHour || (h === currentHour && m > currentMinutes);
});
if (nextSlot) {
select.value = nextSlot;
}
}
/**
* Initialise la prévisualisation du prix
*/
function initPricePreview() {
const durationSelect = document.getElementById('resDuration');
if (!durationSelect) return;
durationSelect.addEventListener('change', updatePricePreview);
// Prix initial
updatePricePreview();
}
/**
* Met à jour la prévisualisation du prix
*/
function updatePricePreview() {
const duration = parseInt(document.getElementById('resDuration').value);
const price = PRICING[duration] || 0;
document.getElementById('previewPrice').textContent = price + '€';
}
/**
* Gère la soumission du formulaire
*/
function handleReservationSubmit(e) {
e.preventDefault();
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
if (!user) {
Dashboard.showToast('Veuillez vous connecter', 'error');
return;
}
const spotId = parseInt(document.getElementById('resSpot').value);
const date = document.getElementById('resDate').value;
const startTime = document.getElementById('resStartTime').value;
const duration = parseInt(document.getElementById('resDuration').value);
const vehicle = document.getElementById('resVehicle').value;
if (!spotId || !date || !startTime || !vehicle) {
Dashboard.showToast('Veuillez remplir tous les champs', 'error');
return;
}
// Vérifier que la place est toujours libre
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');
// Rafraîchir la carte
if (window.ParkingMap) {
window.ParkingMap.refresh();
}
return;
}
// Calculer l'heure de fin
const [hours, minutes] = startTime.split(':').map(Number);
const endDate = new Date(date + 'T' + startTime);
endDate.setMinutes(endDate.getMinutes() + duration);
const endTime = endDate.toTimeString().slice(0, 5);
// Créer la 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: 'pending',
createdAt: new Date().toISOString()
};
// Afficher le modal de paiement
showPaymentModal();
}
/**
* Initialise le modal de paiement
*/
function initPaymentModal() {
// Fermer le modal
document.getElementById('closePaymentModal')?.addEventListener('click', hidePaymentModal);
document.getElementById('cancelPaymentBtn')?.addEventListener('click', hidePaymentModal);
// Confirmer le paiement
document.getElementById('confirmPaymentBtn')?.addEventListener('click', confirmPayment);
}
/**
* Affiche le modal de paiement
*/
function showPaymentModal() {
if (!currentReservation) return;
const modal = document.getElementById('paymentModal');
// 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 + '€';
// Générer le QR code
generateQRCode();
// Afficher le modal
modal.classList.remove('hidden');
}
/**
* Cache le modal de paiement
*/
function hidePaymentModal() {
document.getElementById('paymentModal').classList.add('hidden');
}
/**
* Génère le QR code
*/
function generateQRCode() {
const container = document.getElementById('qrcode');
container.innerHTML = '';
// Générer un code de paiement unique
const paymentCode = 'PARK' + Date.now().toString().slice(-8);
document.getElementById('paymentCode').textContent = paymentCode;
// Créer le QR code
const qrData = JSON.stringify({
type: 'parking_payment',
reservationId: currentReservation.id,
amount: currentReservation.price,
code: paymentCode
});
// Utiliser QRCode.js si disponible, sinon afficher un faux QR
if (typeof QRCode !== 'undefined') {
new QRCode(container, {
text: qrData,
width: 200,
height: 200,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
} else {
// Fallback - afficher un QR code simulé
container.innerHTML = `
<div style="width: 200px; height: 200px; background: white; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
<div style="font-size: 80px;">📱</div>
<div style="color: #333; font-size: 12px; margin-top: 10px;">QR Code de paiement</div>
</div>
`;
}
}
/**
* Confirme le paiement
*/
function confirmPayment() {
if (!currentReservation) return;
// Mettre à jour le statut
currentReservation.status = 'active';
// Sauvegarder la réservation
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
reservations.push(currentReservation);
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
// Mettre à jour le statut de la place
if (window.ParkingMap) {
window.ParkingMap.setSpotStatus(currentReservation.spotId, 'reserved');
}
// Ajouter à l'historique admin
addToHistory('Réservation', `Place ${currentReservation.spotNumber} réservée par ${currentReservation.userName} - ${currentReservation.price}`);
// Fermer le modal
hidePaymentModal();
// Réinitialiser le formulaire
document.getElementById('reservationForm').reset();
initDatePicker();
updatePricePreview();
// Afficher confirmation
Dashboard.showToast('Réservation confirmée !', 'success');
// Rediriger vers mes réservations
setTimeout(() => {
Dashboard.navigateToPage('my-reservations');
document.querySelector('[data-page="my-reservations"]').classList.add('active');
document.querySelector('[data-page="reservation"]').classList.remove('active');
}, 1500);
}
/**
* Ajoute à l'historique
*/
function addToHistory(action, details) {
let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]');
history.unshift({
id: Date.now(),
action: action,
details: details,
timestamp: new Date().toISOString()
});
// Garder seulement les 100 dernières entrées
if (history.length > 100) {
history = history.slice(0, 100);
}
localStorage.setItem('smart_parking_history', JSON.stringify(history));
}
/**
* Formate une date
*/
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
/**
* Formate une durée
*/
function formatDuration(minutes) {
if (minutes >= 480) {
return 'Journée (8h)';
} else if (minutes >= 60) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`;
} else {
return `${minutes} min`;
}
}
// Exporter les fonctions
window.Reservation = {
PRICING,
TIME_SLOTS,
formatDuration,
addToHistory
};