mise à jour

This commit is contained in:
2026-03-26 15:28:17 +01:00
parent c6954c1f50
commit 90f10674a3
7 changed files with 644 additions and 1096 deletions

View File

@@ -354,21 +354,10 @@ select.form-control {
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
} }
.stat-card.free { .stat-card.free { border-left: 4px solid var(--spot-free); }
border-left: 4px solid var(--spot-free); .stat-card.occupied { border-left: 4px solid var(--spot-occupied); }
} .stat-card.reserved { border-left: 4px solid var(--spot-reserved); }
.stat-card.total { border-left: 4px solid var(--primary); }
.stat-card.occupied {
border-left: 4px solid var(--spot-occupied);
}
.stat-card.reserved {
border-left: 4px solid var(--spot-reserved);
}
.stat-card.total {
border-left: 4px solid var(--primary);
}
.stat-icon { .stat-icon {
font-size: 2rem; font-size: 2rem;
@@ -448,32 +437,14 @@ select.form-control {
background: var(--bg-dark); background: var(--bg-dark);
} }
.parking-spot:hover { .parking-spot:hover { transform: scale(1.05); }
transform: scale(1.05);
}
.parking-spot.free { .parking-spot.free { border-color: var(--spot-free); color: var(--spot-free); }
border-color: var(--spot-free); .parking-spot.occupied { border-color: var(--spot-occupied); color: var(--spot-occupied); }
color: var(--spot-free); .parking-spot.reserved { border-color: var(--spot-reserved); color: var(--spot-reserved); }
}
.parking-spot.occupied { .parking-spot .spot-number { font-size: 1.1rem; }
border-color: var(--spot-occupied); .parking-spot .spot-icon { font-size: 1.3rem; }
color: var(--spot-occupied);
}
.parking-spot.reserved {
border-color: var(--spot-reserved);
color: var(--spot-reserved);
}
.parking-spot .spot-number {
font-size: 1.1rem;
}
.parking-spot .spot-icon {
font-size: 1.3rem;
}
/* Legend */ /* Legend */
.legend { .legend {
@@ -501,9 +472,7 @@ select.form-control {
.legend-color.reserved { background: var(--spot-reserved); } .legend-color.reserved { background: var(--spot-reserved); }
/* Spot details */ /* Spot details */
.spot-details { .spot-details { min-height: 200px; }
min-height: 200px;
}
.no-selection { .no-selection {
color: var(--text-muted); color: var(--text-muted);
@@ -524,14 +493,8 @@ select.form-control {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.spot-info-label { .spot-info-label { color: var(--text-secondary); font-size: 0.9rem; }
color: var(--text-secondary); .spot-info-value { font-weight: 600; }
font-size: 0.9rem;
}
.spot-info-value {
font-weight: 600;
}
.spot-status-free { color: var(--spot-free); } .spot-status-free { color: var(--spot-free); }
.spot-status-occupied { color: var(--spot-occupied); } .spot-status-occupied { color: var(--spot-occupied); }
@@ -605,7 +568,7 @@ select.form-control {
background: var(--bg-card); background: var(--bg-card);
border-radius: var(--border-radius); border-radius: var(--border-radius);
width: 100%; width: 100%;
max-width: 500px; max-width: 480px;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -619,9 +582,7 @@ select.form-control {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.modal-header h3 { .modal-header h3 { margin: 0; }
margin: 0;
}
.modal-close { .modal-close {
background: none; background: none;
@@ -632,15 +593,11 @@ select.form-control {
transition: var(--transition); transition: var(--transition);
} }
.modal-close:hover { .modal-close:hover { color: var(--text-primary); }
color: var(--text-primary);
}
.modal-body { .modal-body { padding: 24px; }
padding: 24px;
}
/* Payment modal */ /* Récapitulatif réservation */
.payment-summary { .payment-summary {
background: var(--bg-dark); background: var(--bg-dark);
border-radius: var(--border-radius-sm); border-radius: var(--border-radius-sm);
@@ -668,34 +625,37 @@ select.form-control {
font-size: 1.1rem; font-size: 1.1rem;
} }
.qr-section { /* ============================================
MESSAGE DE CONFIRMATION (remplace QR code)
============================================ */
.confirmation-message {
text-align: center; text-align: center;
margin: 20px 0; padding: 24px 16px;
} background: rgba(16, 185, 129, 0.08);
border: 1px solid rgba(16, 185, 129, 0.3);
.qr-section p {
margin-bottom: 12px;
color: var(--text-secondary);
}
#qrcode {
display: flex;
justify-content: center;
margin: 16px 0;
}
#qrcode img {
border-radius: var(--border-radius-sm); border-radius: var(--border-radius-sm);
margin-bottom: 20px;
} }
.qr-info { .confirmation-icon {
font-size: 0.85rem; font-size: 3rem;
margin-bottom: 12px;
} }
.payment-actions { .confirmation-message h4 {
display: flex; font-size: 1.1rem;
flex-direction: column; color: var(--success);
gap: 12px; margin-bottom: 8px;
}
.confirmation-message p {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.6;
}
.confirmation-message strong {
color: var(--text-primary);
} }
/* ============================================ /* ============================================
@@ -735,6 +695,13 @@ select.form-control {
gap: 4px; gap: 4px;
} }
.reservation-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.reservation-price { .reservation-price {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
@@ -749,20 +716,9 @@ select.form-control {
text-transform: uppercase; text-transform: uppercase;
} }
.status-active { .status-active { background: rgba(16, 185, 129, 0.2); color: var(--success); }
background: rgba(16, 185, 129, 0.2); .status-completed { background: rgba(148, 163, 184, 0.2); color: var(--text-muted); }
color: var(--success); .status-cancelled { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
}
.status-completed {
background: rgba(148, 163, 184, 0.2);
color: var(--text-muted);
}
.status-cancelled {
background: rgba(239, 68, 68, 0.2);
color: var(--danger);
}
.empty-state { .empty-state {
text-align: center; text-align: center;
@@ -819,9 +775,7 @@ select.form-control {
font-size: 2.5rem; font-size: 2.5rem;
} }
.profile-header h3 { .profile-header h3 { margin-bottom: 8px; }
margin-bottom: 8px;
}
.role-badge { .role-badge {
display: inline-block; display: inline-block;
@@ -929,9 +883,7 @@ select.form-control {
.admin-place-item.reserved { background: rgba(59, 130, 246, 0.2); color: var(--spot-reserved); } .admin-place-item.reserved { background: rgba(59, 130, 246, 0.2); color: var(--spot-reserved); }
/* Tables */ /* Tables */
.table-container { .table-container { overflow-x: auto; }
overflow-x: auto;
}
.data-table { .data-table {
width: 100%; width: 100%;
@@ -954,9 +906,7 @@ select.form-control {
text-transform: uppercase; text-transform: uppercase;
} }
.data-table tr:hover td { .data-table tr:hover td { background: var(--bg-hover); }
background: var(--bg-hover);
}
/* Log container */ /* Log container */
.log-container { .log-container {
@@ -975,9 +925,7 @@ select.form-control {
font-size: 0.85rem; font-size: 0.85rem;
} }
.log-item:last-child { .log-item:last-child { border-bottom: none; }
border-bottom: none;
}
.log-time { .log-time {
color: var(--text-muted); color: var(--text-muted);
@@ -1020,105 +968,44 @@ select.form-control {
/* ============================================ /* ============================================
UTILITAIRES UTILITAIRES
============================================ */ ============================================ */
.hidden { .hidden { display: none !important; }
display: none !important;
}
.admin-only { .admin-only { display: none; }
display: none; .admin-only.visible { display: flex; }
}
.admin-only.visible {
display: flex;
}
/* ============================================ /* ============================================
RESPONSIVE RESPONSIVE
============================================ */ ============================================ */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.stats-grid { .stats-grid { grid-template-columns: repeat(2, 1fr); }
grid-template-columns: repeat(2, 1fr); .parking-section { grid-template-columns: 1fr; }
} .pricing-cards { grid-template-columns: repeat(3, 1fr); }
.profile-container { grid-template-columns: 1fr; }
.parking-section { .admin-page .admin-stats-grid { grid-template-columns: repeat(2, 1fr); }
grid-template-columns: 1fr;
}
.pricing-cards {
grid-template-columns: repeat(3, 1fr);
}
.profile-container {
grid-template-columns: 1fr;
}
.admin-page .admin-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.header .container { .header .container { flex-wrap: wrap; }
flex-wrap: wrap;
}
.nav { .nav {
order: 3; order: 3;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
margin-top: 12px; margin-top: 12px;
} }
.nav-link { padding: 8px 12px; font-size: 0.8rem; }
.nav-link { .stats-grid { grid-template-columns: 1fr; }
padding: 8px 12px; .form-row { grid-template-columns: 1fr; }
font-size: 0.8rem; .pricing-cards { grid-template-columns: repeat(2, 1fr); }
} .parking-map { grid-template-columns: repeat(3, 1fr); }
.stats-cards { grid-template-columns: 1fr; }
.stats-grid { .admin-page .admin-stats-grid { grid-template-columns: 1fr; }
grid-template-columns: 1fr; .admin-places-list { grid-template-columns: repeat(5, 1fr); }
} .reservation-card { flex-direction: column; align-items: flex-start; gap: 12px; }
.reservation-actions { align-items: flex-start; }
.form-row {
grid-template-columns: 1fr;
}
.pricing-cards {
grid-template-columns: repeat(2, 1fr);
}
.parking-map {
grid-template-columns: repeat(3, 1fr);
}
.stats-cards {
grid-template-columns: 1fr;
}
.admin-page .admin-stats-grid {
grid-template-columns: 1fr;
}
.admin-places-list {
grid-template-columns: repeat(5, 1fr);
}
} }
/* Scrollbar */ /* Scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar { width: 8px; height: 8px; }
width: 8px; ::-webkit-scrollbar-track { background: var(--bg-dark); border-radius: 4px; }
height: 8px; ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
} ::-webkit-scrollbar-thumb:hover { background: var(--bg-hover); }
::-webkit-scrollbar-track {
background: var(--bg-dark);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--secondary);
}

View File

@@ -2,70 +2,48 @@
* ============================================ * ============================================
* DASHBOARD.JS - Gestion du dashboard * DASHBOARD.JS - Gestion du dashboard
* Smart Parking - BTS CIEL IR * Smart Parking - BTS CIEL IR
* CORRIGÉ : cancelReservation utilisait spotNumber
* au lieu de spotId pour libérer la place
* ============================================ * ============================================
*/ */
// Configuration
const API_URL = 'http://localhost:3000/api'; const API_URL = 'http://localhost:3000/api';
// État global
let dashboardState = { let dashboardState = {
user: null, user: null,
currentPage: 'map' currentPage: 'map'
}; };
// Initialisation
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('🚗 Initialisation du dashboard...'); console.log('🚗 Initialisation du dashboard...');
// Vérifier l'authentification
checkAuthentication(); checkAuthentication();
// Initialiser la navigation
initNavigation(); initNavigation();
// Initialiser la déconnexion
initLogout(); initLogout();
// Charger les données utilisateur
loadUserData(); loadUserData();
}); });
/**
* Vérifie que l'utilisateur est authentifié
*/
function checkAuthentication() { function checkAuthentication() {
const token = localStorage.getItem('smart_parking_token'); const token = localStorage.getItem('smart_parking_token');
const user = localStorage.getItem('smart_parking_user'); const user = localStorage.getItem('smart_parking_user');
if (!token || !user) { if (!token || !user) {
// Rediriger vers la page de connexion
window.location.href = '../index.html'; window.location.href = '../index.html';
return; return;
} }
dashboardState.user = JSON.parse(user); dashboardState.user = JSON.parse(user);
} }
/**
* Initialise la navigation
*/
function initNavigation() { function initNavigation() {
const navLinks = document.querySelectorAll('.nav-link'); const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => { navLinks.forEach(link => {
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
const page = link.getAttribute('data-page'); const page = link.getAttribute('data-page');
navigateToPage(page); navigateToPage(page);
// Mettre à jour la classe active
navLinks.forEach(l => l.classList.remove('active')); navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active'); link.classList.add('active');
}); });
}); });
// Afficher le lien admin si l'utilisateur est admin
if (dashboardState.user && dashboardState.user.role === 'admin') { if (dashboardState.user && dashboardState.user.role === 'admin') {
const adminLink = document.querySelector('.admin-only'); const adminLink = document.querySelector('.admin-only');
if (adminLink) { if (adminLink) {
@@ -75,25 +53,18 @@ function initNavigation() {
} }
} }
/**
* Navigation entre les pages
*/
function navigateToPage(page) { function navigateToPage(page) {
// Cacher toutes les pages document.querySelectorAll('.page').forEach(p => {
const pages = document.querySelectorAll('.page');
pages.forEach(p => {
p.classList.add('hidden'); p.classList.add('hidden');
p.classList.remove('active'); p.classList.remove('active');
}); });
// Afficher la page demandée
const targetPage = document.getElementById(page); const targetPage = document.getElementById(page);
if (targetPage) { if (targetPage) {
targetPage.classList.remove('hidden'); targetPage.classList.remove('hidden');
targetPage.classList.add('active'); targetPage.classList.add('active');
dashboardState.currentPage = page; dashboardState.currentPage = page;
// Rafraîchir les données selon la page
if (page === 'map' && window.ParkingMap) { if (page === 'map' && window.ParkingMap) {
window.ParkingMap.refresh(); window.ParkingMap.refresh();
} else if (page === 'my-reservations') { } else if (page === 'my-reservations') {
@@ -104,9 +75,6 @@ function navigateToPage(page) {
} }
} }
/**
* Initialise la déconnexion
*/
function initLogout() { function initLogout() {
const logoutBtn = document.getElementById('logoutBtn'); const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) { if (logoutBtn) {
@@ -118,57 +86,39 @@ function initLogout() {
} }
} }
/**
* Charge les données utilisateur
*/
function loadUserData() { function loadUserData() {
const user = dashboardState.user; const user = dashboardState.user;
if (!user) return; if (!user) return;
// Mettre à jour l'affichage
document.getElementById('userName').textContent = user.name; document.getElementById('userName').textContent = user.name;
document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client'; document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
// Page profil
document.getElementById('profileName').textContent = user.name; document.getElementById('profileName').textContent = user.name;
document.getElementById('profileNameInput').value = user.name; document.getElementById('profileNameInput').value = user.name;
document.getElementById('profileEmailInput').value = user.email; document.getElementById('profileEmailInput').value = user.email;
document.getElementById('profilePhoneInput').value = user.phone || ''; document.getElementById('profilePhoneInput').value = user.phone || '';
// Badge rôle
const roleBadge = document.getElementById('profileRole'); const roleBadge = document.getElementById('profileRole');
if (roleBadge) { if (roleBadge) {
roleBadge.textContent = user.role === 'admin' ? 'Administrateur' : 'Client'; roleBadge.textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
roleBadge.className = 'role-badge ' + (user.role === 'admin' ? 'badge-admin' : 'badge-client'); roleBadge.className = 'role-badge ' + (user.role === 'admin' ? 'badge-admin' : 'badge-client');
} }
// Charger les statistiques
loadUserStats(); loadUserStats();
} }
/**
* Charge les statistiques utilisateur
*/
function loadUserStats() { function loadUserStats() {
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
const userReservations = reservations.filter(r => r.userId === dashboardState.user.id); const userReservations = reservations.filter(r => r.userId === dashboardState.user.id);
const totalReservations = userReservations.length; document.getElementById('totalReservations').textContent = userReservations.length;
const activeReservations = userReservations.filter(r => r.status === 'active').length; document.getElementById('activeReservations').textContent = userReservations.filter(r => r.status === 'active').length;
const totalSpent = userReservations.reduce((sum, r) => sum + (r.price || 0), 0); document.getElementById('totalSpent').textContent = userReservations.reduce((s, r) => s + (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() { function loadMyReservations() {
const container = document.getElementById('myReservationsList'); const container = document.getElementById('myReservationsList');
const emptyState = document.getElementById('noReservations'); const emptyState = document.getElementById('noReservations');
if (!container || !emptyState) return; if (!container || !emptyState) return;
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
@@ -181,15 +131,14 @@ function loadMyReservations() {
} }
emptyState.classList.add('hidden'); emptyState.classList.add('hidden');
container.innerHTML = userReservations.slice().reverse().map(res => `
container.innerHTML = userReservations.map(res => `
<div class="reservation-card"> <div class="reservation-card">
<div class="reservation-info"> <div class="reservation-info">
<h4>Place ${res.spotNumber}</h4> <h4>Place ${res.spotNumber}</h4>
<div class="reservation-details"> <div class="reservation-details">
<span>📅 ${res.date}</span> <span>📅 ${res.date}</span>
<span>🕐 ${res.startTime}</span> <span>🕐 ${res.startTime}</span>
<span>⏱️ ${res.duration} min</span> <span>⏱️ ${formatDurationLabel(res.duration)}</span>
<span>🚗 ${res.vehicle || 'N/A'}</span> <span>🚗 ${res.vehicle || 'N/A'}</span>
</div> </div>
</div> </div>
@@ -207,7 +156,9 @@ function loadMyReservations() {
} }
/** /**
* Annule une réservation * 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) { function cancelReservation(reservationId) {
if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return; if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return;
@@ -216,13 +167,12 @@ function cancelReservation(reservationId) {
const reservation = reservations.find(r => r.id === reservationId); const reservation = reservations.find(r => r.id === reservationId);
if (reservation) { if (reservation) {
// Mettre à jour le statut
reservation.status = 'cancelled'; reservation.status = 'cancelled';
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
// Libérer la place // CORRIGÉ : spotId au lieu de spotNumber
if (window.ParkingMap) { if (window.ParkingMap) {
window.ParkingMap.setSpotStatus(reservation.spotNumber, 'free'); window.ParkingMap.setSpotStatus(reservation.spotId, 'free');
} }
showToast('Réservation annulée', 'success'); showToast('Réservation annulée', 'success');
@@ -231,67 +181,47 @@ function cancelReservation(reservationId) {
} }
} }
/** // Mise à jour du profil
* Met à jour le profil
*/
document.getElementById('profileForm')?.addEventListener('submit', (e) => { document.getElementById('profileForm')?.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const phone = document.getElementById('profilePhoneInput').value; const phone = document.getElementById('profilePhoneInput').value;
const newPassword = document.getElementById('profileNewPassword').value; const newPassword = document.getElementById('profileNewPassword').value;
// Mettre à jour l'utilisateur
let user = dashboardState.user; let user = dashboardState.user;
user.phone = phone; user.phone = phone;
if (newPassword) user.password = newPassword;
if (newPassword) {
user.password = newPassword;
}
// Sauvegarder
localStorage.setItem('smart_parking_user', JSON.stringify(user)); 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') || '[]'); let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
const userIndex = users.findIndex(u => u.id === user.id); const idx = users.findIndex(u => u.id === user.id);
if (userIndex !== -1) { if (idx !== -1) {
users[userIndex] = user; users[idx] = user;
localStorage.setItem('smart_parking_users', JSON.stringify(users)); localStorage.setItem('smart_parking_users', JSON.stringify(users));
} }
showToast('Profil mis à jour', 'success'); showToast('Profil mis à jour', 'success');
}); });
/**
* Retourne le label du statut
*/
function getStatusLabel(status) { function getStatusLabel(status) {
const labels = { return { active: 'Active', completed: 'Terminée', cancelled: 'Annulée' }[status] || status;
'active': 'Active', }
'completed': 'Terminée',
'cancelled': 'Annulée' function formatDurationLabel(minutes) {
}; if (minutes >= 480) return 'Journée';
return labels[status] || status; if (minutes >= 60) return Math.floor(minutes / 60) + 'h' + (minutes % 60 ? (minutes % 60) + 'min' : '');
return minutes + ' min';
} }
/**
* Affiche une notification toast
*/
function showToast(message, type = 'info') { function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer'); const container = document.getElementById('toastContainer');
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = `toast ${type}`; toast.className = `toast ${type}`;
toast.textContent = message; toast.textContent = message;
container.appendChild(toast); container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
setTimeout(() => {
toast.remove();
}, 3000);
} }
// Exporter les fonctions
window.Dashboard = { window.Dashboard = {
navigateToPage, navigateToPage,
showToast, showToast,

View File

@@ -2,6 +2,8 @@
* ============================================ * ============================================
* RESERVATION.JS - Système de réservation * RESERVATION.JS - Système de réservation
* Smart Parking - BTS CIEL IR * Smart Parking - BTS CIEL IR
* MODIFIÉ : Suppression du QR code, remplacement
* par une confirmation simple avec badge
* ============================================ * ============================================
*/ */
@@ -33,7 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
initDatePicker(); initDatePicker();
initTimeSlots(); initTimeSlots();
initPricePreview(); initPricePreview();
initPaymentModal(); initConfirmationModal(); // MODIFIÉ : était initPaymentModal()
}); });
/** /**
@@ -42,7 +44,6 @@ document.addEventListener('DOMContentLoaded', () => {
function initReservationForm() { function initReservationForm() {
const form = document.getElementById('reservationForm'); const form = document.getElementById('reservationForm');
if (!form) return; if (!form) return;
form.addEventListener('submit', handleReservationSubmit); form.addEventListener('submit', handleReservationSubmit);
} }
@@ -52,8 +53,6 @@ function initReservationForm() {
function initDatePicker() { function initDatePicker() {
const dateInput = document.getElementById('resDate'); const dateInput = document.getElementById('resDate');
if (!dateInput) return; if (!dateInput) return;
// Date minimum = aujourd'hui
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
dateInput.min = today; dateInput.min = today;
dateInput.value = today; dateInput.value = today;
@@ -73,7 +72,7 @@ function initTimeSlots() {
select.appendChild(option); select.appendChild(option);
}); });
// Sélectionner l'heure actuelle + 1h // Sélectionner le prochain créneau disponible
const now = new Date(); const now = new Date();
const currentHour = now.getHours(); const currentHour = now.getHours();
const currentMinutes = now.getMinutes(); const currentMinutes = now.getMinutes();
@@ -81,10 +80,7 @@ function initTimeSlots() {
const [h, m] = t.split(':').map(Number); const [h, m] = t.split(':').map(Number);
return h > currentHour || (h === currentHour && m > currentMinutes); return h > currentHour || (h === currentHour && m > currentMinutes);
}); });
if (nextSlot) select.value = nextSlot;
if (nextSlot) {
select.value = nextSlot;
}
} }
/** /**
@@ -93,10 +89,7 @@ function initTimeSlots() {
function initPricePreview() { function initPricePreview() {
const durationSelect = document.getElementById('resDuration'); const durationSelect = document.getElementById('resDuration');
if (!durationSelect) return; if (!durationSelect) return;
durationSelect.addEventListener('change', updatePricePreview); durationSelect.addEventListener('change', updatePricePreview);
// Prix initial
updatePricePreview(); updatePricePreview();
} }
@@ -106,12 +99,13 @@ function initPricePreview() {
function updatePricePreview() { function updatePricePreview() {
const duration = parseInt(document.getElementById('resDuration').value); const duration = parseInt(document.getElementById('resDuration').value);
const price = PRICING[duration] || 0; const price = PRICING[duration] || 0;
document.getElementById('previewPrice').textContent = price + '€'; document.getElementById('previewPrice').textContent = price + '€';
} }
/** /**
* Gère la soumission du formulaire * Gère la soumission du formulaire
* MODIFIÉ : la réservation est enregistrée ici directement,
* puis le modal de confirmation s'affiche.
*/ */
function handleReservationSubmit(e) { function handleReservationSubmit(e) {
e.preventDefault(); e.preventDefault();
@@ -139,20 +133,16 @@ function handleReservationSubmit(e) {
if (!spot || spot.status !== 'free') { if (!spot || spot.status !== 'free') {
Dashboard.showToast('Cette place n\'est plus disponible', 'error'); Dashboard.showToast('Cette place n\'est plus disponible', 'error');
// Rafraîchir la carte if (window.ParkingMap) window.ParkingMap.refresh();
if (window.ParkingMap) {
window.ParkingMap.refresh();
}
return; return;
} }
// Calculer l'heure de fin // Calculer l'heure de fin
const [hours, minutes] = startTime.split(':').map(Number);
const endDate = new Date(date + 'T' + startTime); const endDate = new Date(date + 'T' + startTime);
endDate.setMinutes(endDate.getMinutes() + duration); endDate.setMinutes(endDate.getMinutes() + duration);
const endTime = endDate.toTimeString().slice(0, 5); const endTime = endDate.toTimeString().slice(0, 5);
// Créer la réservation // Construire l'objet réservation
currentReservation = { currentReservation = {
id: Date.now(), id: Date.now(),
userId: user.id, userId: user.id,
@@ -165,33 +155,58 @@ function handleReservationSubmit(e) {
duration: duration, duration: duration,
vehicle: vehicle.toUpperCase(), vehicle: vehicle.toUpperCase(),
price: PRICING[duration], price: PRICING[duration],
status: 'pending', status: 'active',
createdAt: new Date().toISOString() createdAt: new Date().toISOString()
}; };
// Afficher le modal de paiement // ---- Enregistrement immédiat (plus de bouton "J'ai payé") ----
showPaymentModal();
// 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 la place en "réservée" sur la carte
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}`
);
// Réinitialiser le formulaire
document.getElementById('reservationForm').reset();
initDatePicker();
updatePricePreview();
// Afficher le modal de confirmation
showConfirmationModal();
}
// ============================================
// MODAL DE CONFIRMATION (remplace le QR code)
// ============================================
/**
* Initialise les événements du modal de confirmation
* REMPLACE : initPaymentModal()
*/
function initConfirmationModal() {
document.getElementById('closeConfirmationModal')?.addEventListener('click', hideConfirmationModal);
document.getElementById('closeConfirmationBtn')?.addEventListener('click', hideConfirmationModal);
} }
/** /**
* Initialise le modal de paiement * Affiche le modal de confirmation
* REMPLACE : showPaymentModal() + generateQRCode()
*/ */
function initPaymentModal() { function showConfirmationModal() {
// 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; if (!currentReservation) return;
const modal = document.getElementById('paymentModal'); const modal = document.getElementById('confirmationModal');
// Remplir le récapitulatif // Remplir le récapitulatif
document.getElementById('paySpot').textContent = 'Place ' + currentReservation.spotNumber; document.getElementById('paySpot').textContent = 'Place ' + currentReservation.spotNumber;
@@ -200,103 +215,34 @@ function showPaymentModal() {
document.getElementById('payDuration').textContent = formatDuration(currentReservation.duration); document.getElementById('payDuration').textContent = formatDuration(currentReservation.duration);
document.getElementById('payTotal').textContent = currentReservation.price + '€'; document.getElementById('payTotal').textContent = currentReservation.price + '€';
// Générer le QR code
generateQRCode();
// Afficher le modal // Afficher le modal
modal.classList.remove('hidden'); modal.classList.remove('hidden');
} }
/** /**
* Cache le modal de paiement * Cache le modal de confirmation
* REMPLACE : hidePaymentModal()
*/ */
function hidePaymentModal() { function hideConfirmationModal() {
document.getElementById('paymentModal').classList.add('hidden'); document.getElementById('confirmationModal').classList.add('hidden');
}
/** // Rediriger vers "Mes réservations" après fermeture
* Génère le QR code if (window.Dashboard) {
*/
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'); Dashboard.navigateToPage('my-reservations');
document.querySelector('[data-page="my-reservations"]').classList.add('active'); document.querySelector('[data-page="my-reservations"]')?.classList.add('active');
document.querySelector('[data-page="reservation"]').classList.remove('active'); document.querySelector('[data-page="reservation"]')?.classList.remove('active');
}, 1500);
} }
// Rafraîchir les stats du profil
if (window.Dashboard) Dashboard.refreshStats();
}
// ============================================
// FONCTIONS UTILITAIRES
// ============================================
/** /**
* Ajoute à l'historique * Ajoute une entrée à l'historique
*/ */
function addToHistory(action, details) { function addToHistory(action, details) {
let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]'); let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]');
@@ -306,17 +252,12 @@ function addToHistory(action, details) {
details: details, details: details,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
if (history.length > 100) history = history.slice(0, 100);
// 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)); localStorage.setItem('smart_parking_history', JSON.stringify(history));
} }
/** /**
* Formate une date * Formate une date DD/MM/YYYY
*/ */
function formatDate(dateString) { function formatDate(dateString) {
const date = new Date(dateString); const date = new Date(dateString);
@@ -328,21 +269,19 @@ function formatDate(dateString) {
} }
/** /**
* Formate une durée * Formate une durée en minutes vers texte lisible
*/ */
function formatDuration(minutes) { function formatDuration(minutes) {
if (minutes >= 480) { if (minutes >= 480) return 'Journée (8h)';
return 'Journée (8h)'; if (minutes >= 60) {
} else if (minutes >= 60) { const h = Math.floor(minutes / 60);
const hours = Math.floor(minutes / 60); const m = minutes % 60;
const mins = minutes % 60; return m > 0 ? `${h}h ${m}min` : `${h}h`;
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`; }
} else {
return `${minutes} min`; return `${minutes} min`;
} }
}
// Exporter les fonctions // Exporter les fonctions publiques
window.Reservation = { window.Reservation = {
PRICING, PRICING,
TIME_SLOTS, TIME_SLOTS,

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "Parking",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -6,8 +6,8 @@
<title>Smart Parking - Dashboard</title> <title>Smart Parking - Dashboard</title>
<link rel="stylesheet" href="../css/style.css"> <link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="../css/dashboard.css"> <link rel="stylesheet" href="../css/dashboard.css">
<!-- SUPPRIMÉ : qrcode.min.js (plus nécessaire) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
</head> </head>
<body> <body>
<!-- Header --> <!-- Header -->
@@ -210,21 +210,23 @@
<span id="previewPrice" class="price-amount">5€</span> <span id="previewPrice" class="price-amount">5€</span>
</div> </div>
<!-- MODIFIÉ : bouton "Valider la réservation" au lieu de "Procéder au paiement" -->
<button type="submit" class="btn btn-primary btn-block"> <button type="submit" class="btn btn-primary btn-block">
<span class="btn-icon">💳</span> <span class="btn-icon"></span>
Procéder au paiement Valider la réservation
</button> </button>
</form> </form>
</div> </div>
<!-- Modal de paiement QR Code --> <!-- REMPLACÉ : Modal de confirmation (sans QR code) -->
<div id="paymentModal" class="modal hidden"> <div id="confirmationModal" class="modal hidden">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3>💳 Paiement</h3> <h3>🎉 Réservation confirmée</h3>
<button class="modal-close" id="closePaymentModal">&times;</button> <button class="modal-close" id="closeConfirmationModal">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<!-- Récapitulatif -->
<div class="payment-summary"> <div class="payment-summary">
<h4>Récapitulatif</h4> <h4>Récapitulatif</h4>
<div class="summary-row"> <div class="summary-row">
@@ -249,21 +251,19 @@
</div> </div>
</div> </div>
<div class="qr-section"> <!-- Message de confirmation -->
<p>Scannez ce QR code pour payer</p> <div class="confirmation-message">
<div id="qrcode"></div> <div class="confirmation-icon">🏷️</div>
<p class="qr-info">Ou utilisez le code: <strong id="paymentCode">-</strong></p> <h4>Merci pour votre réservation !</h4>
<p>Votre place est bien réservée.<br>
Vous recevrez votre <strong>badge d'accès dans les 24 heures</strong>.</p>
</div> </div>
<div class="payment-actions"> <!-- Bouton fermer -->
<button id="confirmPaymentBtn" class="btn btn-success btn-block"> <button id="closeConfirmationBtn" class="btn btn-primary btn-block">
<span class="btn-icon"></span> <span class="btn-icon"></span>
J'ai payé Fermer
</button> </button>
<button id="cancelPaymentBtn" class="btn btn-secondary btn-block">
Annuler
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,11 @@
/**
* ============================================
* DATABASE.JS - Gestion MariaDB
* Smart Parking - BTS CIEL IR
* MODIFIÉ : ajout de getReservationById
* ============================================
*/
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
require('dotenv').config(); require('dotenv').config();
@@ -91,17 +99,19 @@ async function initDatabase() {
console.log('✅ Tables vérifiées/créées avec succès'); console.log('✅ Tables vérifiées/créées avec succès');
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', ['admin@smartparking.fr']); // Compte admin par défaut
const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']);
if (rows.length === 0) { if (rows.length === 0) {
const hashedPassword = await bcrypt.hash('admin123', 10); const hashed = await bcrypt.hash('admin123', 10);
await pool.query( await pool.query(
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashedPassword, 'admin'] ['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashed, 'admin']
); );
console.log('✅ Administrateur par défaut créé'); console.log('✅ Administrateur par défaut créé');
} }
const [spots] = await pool.query('SELECT COUNT(*) as count FROM spots'); // Places par défaut
const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots');
if (spots[0].count === 0) { if (spots[0].count === 0) {
for (let i = 1; i <= 10; i++) { for (let i = 1; i <= 10; i++) {
let status = 'free'; let status = 'free';
@@ -117,7 +127,7 @@ async function initDatabase() {
} }
} catch (err) { } catch (err) {
console.error('❌ Erreur lors de l\'initialisation de la base :', err.message); console.error("❌ Erreur lors de l'initialisation de la base :", err.message);
throw err; throw err;
} }
} }
@@ -215,9 +225,24 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat
return { id: result.insertId }; return { id: result.insertId };
} }
/**
* Récupère une réservation par son ID
* AJOUTÉ : nécessaire pour l'annulation/complétion (libération de la place)
*/
async function getReservationById(id) {
const [rows] = await pool.query(
`SELECT r.*, s.number AS spot_number
FROM reservations r
JOIN spots s ON r.spot_id = s.id
WHERE r.id = ?`,
[id]
);
return rows[0];
}
async function getReservationsByUser(userId) { async function getReservationsByUser(userId) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT r.*, s.number as spot_number `SELECT r.*, s.number AS spot_number
FROM reservations r FROM reservations r
JOIN spots s ON r.spot_id = s.id JOIN spots s ON r.spot_id = s.id
WHERE r.user_id = ? WHERE r.user_id = ?
@@ -229,7 +254,7 @@ async function getReservationsByUser(userId) {
async function getAllReservations() { async function getAllReservations() {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT r.*, s.number as spot_number, u.name as user_name `SELECT r.*, s.number AS spot_number, u.name AS user_name
FROM reservations r FROM reservations r
JOIN spots s ON r.spot_id = s.id JOIN spots s ON r.spot_id = s.id
JOIN users u ON r.user_id = u.id JOIN users u ON r.user_id = u.id
@@ -260,7 +285,7 @@ async function addHistory(action, details, userId = null) {
async function getHistory(limit = 50) { async function getHistory(limit = 50) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT h.*, u.name as user_name `SELECT h.*, u.name AS user_name
FROM history h FROM history h
LEFT JOIN users u ON h.user_id = u.id LEFT JOIN users u ON h.user_id = u.id
ORDER BY h.timestamp DESC ORDER BY h.timestamp DESC
@@ -306,7 +331,7 @@ async function recordMqttEvent(topic, message) {
} }
// ============================================ // ============================================
// FERMETURE DU POOL // FERMETURE
// ============================================ // ============================================
async function closeDatabase() { async function closeDatabase() {
@@ -315,26 +340,20 @@ async function closeDatabase() {
} }
module.exports = { module.exports = {
pool, // exposé pour les requêtes directes si besoin
initDatabase, initDatabase,
closeDatabase, closeDatabase,
createUser, // Utilisateurs
getUserByEmail, createUser, getUserByEmail, getUserById, getAllUsers, updateUser, deleteUser,
getUserById, // Places
getAllUsers, createSpot, getAllSpots, getSpotById, updateSpotStatus, deleteAllSpots,
updateUser, // Réservations
deleteUser, createReservation, getReservationById, getReservationsByUser,
createSpot, getAllReservations, updateReservationStatus,
getAllSpots, // Historique
getSpotById, addHistory, getHistory,
updateSpotStatus, // Stats
deleteAllSpots, recordStats, getStats,
createReservation, // MQTT
getReservationsByUser,
getAllReservations,
updateReservationStatus,
addHistory,
getHistory,
recordStats,
getStats,
recordMqttEvent recordMqttEvent
}; };

View File

@@ -2,13 +2,14 @@
* ============================================ * ============================================
* API ROUTES - Routes de l'API REST * API ROUTES - Routes de l'API REST
* Smart Parking - BTS CIEL IR * Smart Parking - BTS CIEL IR
* CORRIGÉ : annulation libère bien la place
* ajout route /complete pour l'admin
* ============================================ * ============================================
*/ */
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const { v4: uuidv4 } = require('uuid');
const db = require('../db/database'); const db = require('../db/database');
const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth'); const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth');
@@ -16,115 +17,43 @@ const { generateToken, authenticateToken, requireAdmin } = require('../middlewar
// AUTHENTIFICATION // AUTHENTIFICATION
// ============================================ // ============================================
/**
* POST /api/register
* Inscription d'un nouvel utilisateur
*/
router.post('/register', async (req, res) => { router.post('/register', async (req, res) => {
try { try {
const { name, email, phone, password } = req.body; const { name, email, phone, password } = req.body;
if (!name || !email || !password)
if (!name || !email || !password) { return res.status(400).json({ success: false, message: 'Tous les champs sont requis' });
return res.status(400).json({ if (await db.getUserByEmail(email))
success: false, return res.status(409).json({ success: false, message: 'Cet email est déjà utilisé' });
message: 'Tous les champs sont requis'
});
}
// Vérifier si l'email existe déjà
const existingUser = await db.getUserByEmail(email);
if (existingUser) {
return res.status(409).json({
success: false,
message: 'Cet email est déjà utilisé'
});
}
// Hasher le mot de passe
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);
// Créer l'utilisateur
const result = await db.createUser(name, email, phone, hashedPassword, 'client'); const result = await db.createUser(name, email, phone, hashedPassword, 'client');
// Générer le token
const user = await db.getUserById(result.id); const user = await db.getUserById(result.id);
const token = generateToken(user); const token = generateToken(user);
res.status(201).json({ res.status(201).json({
success: true, success: true, message: 'Compte créé avec succès', token,
message: 'Compte créé avec succès', user: { id: user.id, name: user.name, email: user.email, phone: user.phone, role: user.role }
token,
user: {
id: user.id,
name: user.name,
email: user.email,
phone: user.phone,
role: user.role
}
}); });
} catch (err) { } catch (err) {
console.error('❌ Erreur register:', err.message); console.error('❌ Erreur register:', err.message);
res.status(500).json({ res.status(500).json({ success: false, message: 'Erreur serveur' });
success: false,
message: 'Erreur serveur'
});
} }
}); });
/**
* POST /api/login
* Connexion
*/
router.post('/login', async (req, res) => { router.post('/login', async (req, res) => {
try { try {
const { email, password } = req.body; const { email, password } = req.body;
if (!email || !password)
if (!email || !password) { return res.status(400).json({ success: false, message: 'Email et mot de passe requis' });
return res.status(400).json({
success: false,
message: 'Email et mot de passe requis'
});
}
// Récupérer l'utilisateur
const user = await db.getUserByEmail(email); const user = await db.getUserByEmail(email);
if (!user) { if (!user || !(await bcrypt.compare(password, user.password)))
return res.status(401).json({ return res.status(401).json({ success: false, message: 'Email ou mot de passe incorrect' });
success: false,
message: 'Email ou mot de passe incorrect'
});
}
// Vérifier le mot de passe
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({
success: false,
message: 'Email ou mot de passe incorrect'
});
}
// Générer le token
const token = generateToken(user); const token = generateToken(user);
res.json({ res.json({
success: true, success: true, message: 'Connexion réussie', token,
message: 'Connexion réussie', user: { id: user.id, name: user.name, email: user.email, phone: user.phone, role: user.role }
token,
user: {
id: user.id,
name: user.name,
email: user.email,
phone: user.phone,
role: user.role
}
}); });
} catch (err) { } catch (err) {
console.error('❌ Erreur login:', err.message); console.error('❌ Erreur login:', err.message);
res.status(500).json({ res.status(500).json({ success: false, message: 'Erreur serveur' });
success: false,
message: 'Erreur serveur'
});
} }
}); });
@@ -132,44 +61,21 @@ router.post('/login', async (req, res) => {
// UTILISATEURS // UTILISATEURS
// ============================================ // ============================================
/**
* GET /api/users
* Liste tous les utilisateurs (admin uniquement)
*/
router.get('/users', authenticateToken, requireAdmin, async (req, res) => { router.get('/users', authenticateToken, requireAdmin, async (req, res) => {
try { try {
const users = await db.getAllUsers(); const users = await db.getAllUsers();
res.json({ res.json({ success: true, count: users.length, data: users });
success: true,
count: users.length,
data: users
});
} catch (err) { } catch (err) {
console.error('❌ Erreur get users:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
/**
* DELETE /api/users/:id
* Supprime un utilisateur (admin uniquement)
*/
router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) => { router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) => {
try { try {
await db.deleteUser(req.params.id); await db.deleteUser(req.params.id);
res.json({ res.json({ success: true, message: 'Utilisateur supprimé' });
success: true,
message: 'Utilisateur supprimé'
});
} catch (err) { } catch (err) {
console.error('❌ Erreur delete user:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
@@ -177,95 +83,43 @@ router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) =>
// PLACES // PLACES
// ============================================ // ============================================
/**
* GET /api/spots
* Liste toutes les places
*/
router.get('/spots', authenticateToken, async (req, res) => { router.get('/spots', authenticateToken, async (req, res) => {
try { try {
const spots = await db.getAllSpots(); const spots = await db.getAllSpots();
res.json({ res.json({ success: true, count: spots.length, data: spots });
success: true,
count: spots.length,
data: spots
});
} catch (err) { } catch (err) {
console.error('❌ Erreur get spots:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
/**
* PUT /api/spots/:id/status
* Met à jour le statut d'une place
*/
router.put('/spots/:id/status', authenticateToken, async (req, res) => { router.put('/spots/:id/status', authenticateToken, async (req, res) => {
try { try {
const { status } = req.body; const { status } = req.body;
const validStatuses = ['free', 'occupied', 'reserved']; if (!['free', 'occupied', 'reserved'].includes(status))
return res.status(400).json({ success: false, message: 'Statut invalide' });
if (!status || !validStatuses.includes(status)) {
return res.status(400).json({
success: false,
message: 'Statut invalide'
});
}
await db.updateSpotStatus(req.params.id, status); await db.updateSpotStatus(req.params.id, status);
await db.addHistory('Mise à jour place', `Place ${req.params.id} -> ${status}`, req.user.id);
// Ajouter à l'historique res.json({ success: true, message: 'Statut mis à jour' });
await db.addHistory('Mise à jour place', `Place ${req.params.id} - ${status}`, req.user.id);
res.json({
success: true,
message: 'Statut mis à jour'
});
} catch (err) { } catch (err) {
console.error('❌ Erreur update spot:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
/**
* POST /api/spots/init
* Réinitialise les places (admin uniquement)
*/
router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => { router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => {
try { try {
const { count } = req.body; const spotCount = Math.min(Math.max(parseInt(req.body.count) || 10, 5), 50);
const spotCount = count || 10;
// Supprimer les places existantes
await db.deleteAllSpots(); await db.deleteAllSpots();
// Créer les nouvelles places
for (let i = 1; i <= spotCount; i++) { for (let i = 1; i <= spotCount; i++) {
let status = 'free'; let status = 'free';
const rand = Math.random(); const rand = Math.random();
if (rand > 0.85) status = 'reserved'; if (rand > 0.85) status = 'reserved';
else if (rand > 0.60) status = 'occupied'; 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')}`, status);
} }
await db.addHistory('Réinitialisation places', `${spotCount} places créées`, req.user.id); await db.addHistory('Réinitialisation places', `${spotCount} places créées`, req.user.id);
res.json({ success: true, message: `${spotCount} places créées` });
res.json({
success: true,
message: `${spotCount} places créées`
});
} catch (err) { } catch (err) {
console.error('❌ Erreur init spots:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
@@ -273,131 +127,91 @@ router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) =>
// RÉSERVATIONS // RÉSERVATIONS
// ============================================ // ============================================
/**
* GET /api/reservations
* Liste les réservations de l'utilisateur connecté
*/
router.get('/reservations', authenticateToken, async (req, res) => { router.get('/reservations', authenticateToken, async (req, res) => {
try { try {
const reservations = await db.getReservationsByUser(req.user.id); const reservations = await db.getReservationsByUser(req.user.id);
res.json({ res.json({ success: true, count: reservations.length, data: reservations });
success: true,
count: reservations.length,
data: reservations
});
} catch (err) { } catch (err) {
console.error('❌ Erreur get reservations:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
/**
* GET /api/reservations/all
* Liste toutes les réservations (admin uniquement)
*/
router.get('/reservations/all', authenticateToken, requireAdmin, async (req, res) => { router.get('/reservations/all', authenticateToken, requireAdmin, async (req, res) => {
try { try {
const reservations = await db.getAllReservations(); const reservations = await db.getAllReservations();
res.json({ res.json({ success: true, count: reservations.length, data: reservations });
success: true,
count: reservations.length,
data: reservations
});
} catch (err) { } catch (err) {
console.error('❌ Erreur get all reservations:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
/**
* POST /api/reservations
* Crée une nouvelle réservation
*/
router.post('/reservations', authenticateToken, async (req, res) => { router.post('/reservations', authenticateToken, async (req, res) => {
try { try {
const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body; const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body;
if (!spotId || !date || !startTime || !endTime || !duration || !price)
if (!spotId || !date || !startTime || !endTime || !duration || !price) { return res.status(400).json({ success: false, message: 'Tous les champs sont requis' });
return res.status(400).json({
success: false,
message: 'Tous les champs sont requis'
});
}
// Vérifier que la place est libre
const spot = await db.getSpotById(spotId); const spot = await db.getSpotById(spotId);
if (!spot || spot.status !== 'free') { if (!spot || spot.status !== 'free')
return res.status(409).json({ return res.status(409).json({ success: false, message: "Cette place n'est plus disponible" });
success: false,
message: 'Cette place n\'est plus disponible'
});
}
// Générer un code de paiement
const paymentCode = 'PARK' + Date.now().toString().slice(-8); const paymentCode = 'PARK' + Date.now().toString().slice(-8);
// Créer la réservation
const result = await db.createReservation( const result = await db.createReservation(
req.user.id, req.user.id, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode
spotId,
date,
startTime,
endTime,
duration,
vehicle,
price,
paymentCode
); );
// Mettre à jour le statut de la place
await db.updateSpotStatus(spotId, 'reserved'); await db.updateSpotStatus(spotId, 'reserved');
await db.addHistory('Nouvelle réservation', `Place ${spot.number} - ${price}EUR`, req.user.id);
// Ajouter à l'historique res.status(201).json({ success: true, message: 'Réservation créée', data: { id: result.id, paymentCode } });
await db.addHistory('Nouvelle réservation', `Place ${spot.number} - ${price}`, req.user.id);
res.status(201).json({
success: true,
message: 'Réservation créée',
data: {
id: result.id,
paymentCode
}
});
} catch (err) { } catch (err) {
console.error('❌ Erreur create reservation:', err.message); console.error('❌ Erreur create reservation:', err.message);
res.status(500).json({ res.status(500).json({ success: false, message: 'Erreur serveur' });
success: false,
message: 'Erreur serveur'
});
} }
}); });
/** /**
* PUT /api/reservations/:id/cancel * PUT /api/reservations/:id/cancel
* Annule une réservation * CORRIGÉ : libère désormais la place associée
*/ */
router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => { router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => {
try { try {
const reservation = await db.getReservationById(req.params.id);
if (!reservation)
return res.status(404).json({ success: false, message: 'Réservation introuvable' });
if (reservation.user_id !== req.user.id && req.user.role !== 'admin')
return res.status(403).json({ success: false, message: 'Accès refusé' });
await db.updateReservationStatus(req.params.id, 'cancelled'); await db.updateReservationStatus(req.params.id, 'cancelled');
await db.updateSpotStatus(reservation.spot_id, 'free'); // ← BUG CORRIGÉ ICI
// TODO: Libérer la place associée await db.addHistory(
'Annulation réservation',
res.json({ `Reservation #${req.params.id} annulee - place ${reservation.spot_id} liberee`,
success: true, req.user.id
message: 'Réservation annulée' );
}); res.json({ success: true, message: 'Réservation annulée' });
} catch (err) { } catch (err) {
console.error('❌ Erreur cancel reservation:', err.message); console.error('❌ Erreur cancel reservation:', err.message);
res.status(500).json({ res.status(500).json({ success: false, message: 'Erreur serveur' });
success: false, }
message: 'Erreur serveur'
}); });
/**
* PUT /api/reservations/:id/complete (admin uniquement)
*/
router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async (req, res) => {
try {
const reservation = await db.getReservationById(req.params.id);
if (!reservation)
return res.status(404).json({ success: false, message: 'Réservation introuvable' });
await db.updateReservationStatus(req.params.id, 'completed');
await db.updateSpotStatus(reservation.spot_id, 'free');
await db.addHistory(
'Réservation terminée',
`Reservation #${req.params.id} terminee - place ${reservation.spot_id} liberee`,
req.user.id
);
res.json({ success: true, message: 'Réservation terminée' });
} catch (err) {
console.error('❌ Erreur complete reservation:', err.message);
res.status(500).json({ success: false, message: 'Erreur serveur' });
} }
}); });
@@ -405,10 +219,6 @@ router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => {
// STATISTIQUES // STATISTIQUES
// ============================================ // ============================================
/**
* GET /api/stats
* Récupère les statistiques
*/
router.get('/stats', authenticateToken, async (req, res) => { router.get('/stats', authenticateToken, async (req, res) => {
try { try {
const spots = await db.getAllSpots(); const spots = await db.getAllSpots();
@@ -417,67 +227,24 @@ router.get('/stats', authenticateToken, async (req, res) => {
const occupied = spots.filter(s => s.status === 'occupied').length; const occupied = spots.filter(s => s.status === 'occupied').length;
const reserved = spots.filter(s => s.status === 'reserved').length; const reserved = spots.filter(s => s.status === 'reserved').length;
const occupancyRate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0; const occupancyRate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
res.json({ success: true, data: { total, free, occupied, reserved, occupancyRate } });
res.json({
success: true,
data: {
total,
free,
occupied,
reserved,
occupancyRate
}
});
} catch (err) { } catch (err) {
console.error('❌ Erreur get stats:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
// ============================================
// HISTORIQUE
// ============================================
/**
* GET /api/history
* Récupère l'historique
*/
router.get('/history', authenticateToken, requireAdmin, async (req, res) => { router.get('/history', authenticateToken, requireAdmin, async (req, res) => {
try { try {
const limit = parseInt(req.query.limit) || 50; const limit = parseInt(req.query.limit) || 50;
const history = await db.getHistory(limit); const history = await db.getHistory(limit);
res.json({ res.json({ success: true, count: history.length, data: history });
success: true,
count: history.length,
data: history
});
} catch (err) { } catch (err) {
console.error('❌ Erreur get history:', err.message); res.status(500).json({ success: false, message: 'Erreur serveur' });
res.status(500).json({
success: false,
message: 'Erreur serveur'
});
} }
}); });
// ============================================ router.get('/status', (_req, res) => {
// STATUS res.json({ success: true, message: 'Smart Parking API operationnelle', version: '1.0.0', timestamp: new Date().toISOString() });
// ============================================
/**
* GET /api/status
* Vérifie le statut du serveur
*/
router.get('/status', (req, res) => {
res.json({
success: true,
message: 'Smart Parking API opérationnelle',
version: '1.0.0',
timestamp: new Date().toISOString()
});
}); });
module.exports = router; module.exports = router;