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

@@ -185,7 +185,7 @@ body {
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); } from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.page-title { .page-title {
@@ -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;
@@ -392,10 +381,10 @@ select.form-control {
line-height: 1; line-height: 1;
} }
.stat-card.free .stat-value { color: var(--spot-free); } .stat-card.free .stat-value { color: var(--spot-free); }
.stat-card.occupied .stat-value { color: var(--spot-occupied); } .stat-card.occupied .stat-value { color: var(--spot-occupied); }
.stat-card.reserved .stat-value { color: var(--spot-reserved); } .stat-card.reserved .stat-value { color: var(--spot-reserved); }
.stat-card.total .stat-value { color: var(--primary); } .stat-card.total .stat-value { color: var(--primary); }
.stat-label { .stat-label {
font-size: 0.85rem; font-size: 0.85rem;
@@ -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 {
@@ -496,14 +467,12 @@ select.form-control {
border-radius: 4px; border-radius: 4px;
} }
.legend-color.free { background: var(--spot-free); } .legend-color.free { background: var(--spot-free); }
.legend-color.occupied { background: var(--spot-occupied); } .legend-color.occupied { background: var(--spot-occupied); }
.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,16 +493,10 @@ 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 { .spot-status-free { color: var(--spot-free); }
font-weight: 600;
}
.spot-status-free { color: var(--spot-free); }
.spot-status-occupied { color: var(--spot-occupied); } .spot-status-occupied { color: var(--spot-occupied); }
.spot-status-reserved { color: var(--spot-reserved); } .spot-status-reserved { color: var(--spot-reserved); }
@@ -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;
@@ -924,14 +878,12 @@ select.form-control {
cursor: pointer; cursor: pointer;
} }
.admin-place-item.free { background: rgba(16, 185, 129, 0.2); color: var(--spot-free); } .admin-place-item.free { background: rgba(16, 185, 129, 0.2); color: var(--spot-free); }
.admin-place-item.occupied { background: rgba(239, 68, 68, 0.2); color: var(--spot-occupied); } .admin-place-item.occupied { background: rgba(239, 68, 68, 0.2); color: var(--spot-occupied); }
.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);
@@ -1009,116 +957,55 @@ select.form-control {
} }
.toast.success { border-left-color: var(--success); } .toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--danger); } .toast.error { border-left-color: var(--danger); }
.toast.warning { border-left-color: var(--warning); } .toast.warning { border-left-color: var(--warning); }
@keyframes slideIn { @keyframes slideIn {
from { transform: translateX(100%); opacity: 0; } from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; } to { transform: translateX(0); opacity: 1; }
} }
/* ============================================ /* ============================================
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,60 +86,42 @@ 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') || '[]');
const userReservations = reservations.filter(r => r.userId === dashboardState.user.id); const userReservations = reservations.filter(r => r.userId === dashboardState.user.id);
if (userReservations.length === 0) { if (userReservations.length === 0) {
@@ -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,70 +181,50 @@ 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,
getUser: () => dashboardState.user, getUser: () => dashboardState.user,
refreshStats: loadUserStats refreshStats: loadUserStats
}; };

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();
@@ -122,11 +116,11 @@ function handleReservationSubmit(e) {
return; return;
} }
const spotId = parseInt(document.getElementById('resSpot').value); const spotId = parseInt(document.getElementById('resSpot').value);
const date = document.getElementById('resDate').value; const date = document.getElementById('resDate').value;
const startTime = document.getElementById('resStartTime').value; const startTime = document.getElementById('resStartTime').value;
const duration = parseInt(document.getElementById('resDuration').value); const duration = parseInt(document.getElementById('resDuration').value);
const vehicle = document.getElementById('resVehicle').value; const vehicle = document.getElementById('resVehicle').value;
if (!spotId || !date || !startTime || !vehicle) { if (!spotId || !date || !startTime || !vehicle) {
Dashboard.showToast('Veuillez remplir tous les champs', 'error'); Dashboard.showToast('Veuillez remplir tous les champs', 'error');
@@ -135,214 +129,159 @@ function handleReservationSubmit(e) {
// Vérifier que la place est toujours libre // Vérifier que la place est toujours libre
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
const spot = spots.find(s => s.id === spotId); const spot = spots.find(s => s.id === spotId);
if (!spot || 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,
userName: user.name, userName: user.name,
spotId: spotId, spotId: spotId,
spotNumber: spot.number, spotNumber: spot.number,
date: date, date: date,
startTime: startTime, startTime: startTime,
endTime: endTime, endTime: endTime,
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();
}
/**
* 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 // Sauvegarder la réservation
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
reservations.push(currentReservation); reservations.push(currentReservation);
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
// Mettre à jour le statut de la place // Mettre la place en "réservée" sur la carte
if (window.ParkingMap) { if (window.ParkingMap) {
window.ParkingMap.setSpotStatus(currentReservation.spotId, 'reserved'); window.ParkingMap.setSpotStatus(currentReservation.spotId, 'reserved');
} }
// Ajouter à l'historique admin // Ajouter à l'historique admin
addToHistory('Réservation', `Place ${currentReservation.spotNumber} réservée par ${currentReservation.userName} - ${currentReservation.price}`); addToHistory(
'Réservation',
// Fermer le modal `Place ${currentReservation.spotNumber} réservée par ${currentReservation.userName} - ${currentReservation.price}`
hidePaymentModal(); );
// Réinitialiser le formulaire // Réinitialiser le formulaire
document.getElementById('reservationForm').reset(); document.getElementById('reservationForm').reset();
initDatePicker(); initDatePicker();
updatePricePreview(); updatePricePreview();
// Afficher confirmation // Afficher le modal de confirmation
Dashboard.showToast('Réservation confirmée !', 'success'); showConfirmationModal();
}
// Rediriger vers mes réservations // ============================================
setTimeout(() => { // MODAL DE CONFIRMATION (remplace le QR code)
Dashboard.navigateToPage('my-reservations'); // ============================================
document.querySelector('[data-page="my-reservations"]').classList.add('active');
document.querySelector('[data-page="reservation"]').classList.remove('active'); /**
}, 1500); * Initialise les événements du modal de confirmation
* REMPLACE : initPaymentModal()
*/
function initConfirmationModal() {
document.getElementById('closeConfirmationModal')?.addEventListener('click', hideConfirmationModal);
document.getElementById('closeConfirmationBtn')?.addEventListener('click', hideConfirmationModal);
} }
/** /**
* Ajoute à l'historique * Affiche le modal de confirmation
* REMPLACE : showPaymentModal() + generateQRCode()
*/
function showConfirmationModal() {
if (!currentReservation) return;
const modal = document.getElementById('confirmationModal');
// Remplir le récapitulatif
document.getElementById('paySpot').textContent = 'Place ' + currentReservation.spotNumber;
document.getElementById('payDate').textContent = formatDate(currentReservation.date);
document.getElementById('payTime').textContent = currentReservation.startTime + ' - ' + currentReservation.endTime;
document.getElementById('payDuration').textContent = formatDuration(currentReservation.duration);
document.getElementById('payTotal').textContent = currentReservation.price + '€';
// Afficher le modal
modal.classList.remove('hidden');
}
/**
* Cache le modal de confirmation
* REMPLACE : hidePaymentModal()
*/
function hideConfirmationModal() {
document.getElementById('confirmationModal').classList.add('hidden');
// Rediriger vers "Mes réservations" après fermeture
if (window.Dashboard) {
Dashboard.navigateToPage('my-reservations');
document.querySelector('[data-page="my-reservations"]')?.classList.add('active');
document.querySelector('[data-page="reservation"]')?.classList.remove('active');
}
// Rafraîchir les stats du profil
if (window.Dashboard) Dashboard.refreshStats();
}
// ============================================
// FONCTIONS UTILITAIRES
// ============================================
/**
* 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') || '[]');
history.unshift({ history.unshift({
id: Date.now(), id: Date.now(),
action: action, action: action,
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);
return date.toLocaleDateString('fr-FR', { return date.toLocaleDateString('fr-FR', {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric' year: 'numeric'
}); });
} }
/** /**
* 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 -->
@@ -206,64 +206,64 @@
</div> </div>
<div class="price-preview"> <div class="price-preview">
<span>Prix total:</span> <span>Prix total :</span>
<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">
<span>Place:</span> <span>Place :</span>
<span id="paySpot">-</span> <span id="paySpot">-</span>
</div> </div>
<div class="summary-row"> <div class="summary-row">
<span>Date:</span> <span>Date :</span>
<span id="payDate">-</span> <span id="payDate">-</span>
</div> </div>
<div class="summary-row"> <div class="summary-row">
<span>Heure:</span> <span>Heure :</span>
<span id="payTime">-</span> <span id="payTime">-</span>
</div> </div>
<div class="summary-row"> <div class="summary-row">
<span>Durée:</span> <span>Durée :</span>
<span id="payDuration">-</span> <span id="payDuration">-</span>
</div> </div>
<div class="summary-row total"> <div class="summary-row total">
<span>Total:</span> <span>Total :</span>
<span id="payTotal">-</span> <span id="payTotal">-</span>
</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,125 +1,135 @@
const mysql = require('mysql2/promise'); /**
* ============================================
* DATABASE.JS - Gestion MariaDB
* Smart Parking - BTS CIEL IR
* MODIFIÉ : ajout de getReservationById
* ============================================
*/
const mysql = require('mysql2/promise');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
require('dotenv').config(); require('dotenv').config();
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306, port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'smartparking_user', user: process.env.DB_USER || 'smartparking_user',
password: process.env.DB_PASSWORD || 'smartparking_pass', password: process.env.DB_PASSWORD || 'smartparking_pass',
database: process.env.DB_NAME || 'smartparking', database: process.env.DB_NAME || 'smartparking',
waitForConnections: true, waitForConnections: true,
connectionLimit: 10, connectionLimit: 10,
queueLimit: 0 queueLimit: 0
}); });
async function initDatabase() { async function initDatabase() {
try { try {
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(50), phone VARCHAR(50),
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
role ENUM('admin','client') DEFAULT 'client', role ENUM('admin','client') DEFAULT 'client',
status ENUM('active','inactive') DEFAULT 'active', status ENUM('active','inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
`); `);
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS spots ( CREATE TABLE IF NOT EXISTS spots (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
number INT UNIQUE NOT NULL, number INT UNIQUE NOT NULL,
status ENUM('free','occupied','reserved') DEFAULT 'free', status ENUM('free','occupied','reserved') DEFAULT 'free',
sensor_id VARCHAR(100), sensor_id VARCHAR(100),
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) )
`); `);
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS reservations ( CREATE TABLE IF NOT EXISTS reservations (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL, user_id INT NOT NULL,
spot_id INT NOT NULL, spot_id INT NOT NULL,
date DATE NOT NULL, date DATE NOT NULL,
start_time TIME NOT NULL, start_time TIME NOT NULL,
end_time TIME NOT NULL, end_time TIME NOT NULL,
duration INT NOT NULL, duration INT NOT NULL,
vehicle VARCHAR(20), vehicle VARCHAR(20),
price DECIMAL(10,2) NOT NULL, price DECIMAL(10,2) NOT NULL,
status ENUM('pending','active','completed','cancelled') DEFAULT 'pending', status ENUM('pending','active','completed','cancelled') DEFAULT 'pending',
payment_code VARCHAR(50), payment_code VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (spot_id) REFERENCES spots(id) ON DELETE CASCADE FOREIGN KEY (spot_id) REFERENCES spots(id) ON DELETE CASCADE
) )
`); `);
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS history ( CREATE TABLE IF NOT EXISTS history (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
action VARCHAR(255) NOT NULL, action VARCHAR(255) NOT NULL,
details TEXT, details TEXT,
user_id INT, user_id INT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
) )
`); `);
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS stats ( CREATE TABLE IF NOT EXISTS stats (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
total_spots INT, total_spots INT,
free_spots INT, free_spots INT,
occupied_spots INT, occupied_spots INT,
reserved_spots INT, reserved_spots INT,
occupancy_rate DECIMAL(5,2), occupancy_rate DECIMAL(5,2),
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
`); `);
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS mqtt_events ( CREATE TABLE IF NOT EXISTS mqtt_events (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
topic VARCHAR(255) NOT NULL, topic VARCHAR(255) NOT NULL,
message TEXT, message TEXT,
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
`); `);
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
if (rows.length === 0) { const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']);
const hashedPassword = await bcrypt.hash('admin123', 10); if (rows.length === 0) {
await pool.query( const hashed = await bcrypt.hash('admin123', 10);
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)', await pool.query(
['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashedPassword, 'admin'] 'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
); ['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éé');
}
// Places par défaut
const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots');
if (spots[0].count === 0) {
for (let i = 1; i <= 10; i++) {
let status = 'free';
const rand = Math.random();
if (rand > 0.85) status = 'reserved';
else if (rand > 0.60) status = 'occupied';
await pool.query(
'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)',
[i, `SENSOR_${String(i).padStart(3, '0')}`, status]
);
}
console.log('✅ 10 places par défaut créées');
}
} catch (err) {
console.error("❌ Erreur lors de l'initialisation de la base :", err.message);
throw err;
} }
const [spots] = await pool.query('SELECT COUNT(*) as count FROM spots');
if (spots[0].count === 0) {
for (let i = 1; i <= 10; i++) {
let status = 'free';
const rand = Math.random();
if (rand > 0.85) status = 'reserved';
else if (rand > 0.60) status = 'occupied';
await pool.query(
'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)',
[i, `SENSOR_${String(i).padStart(3, '0')}`, status]
);
}
console.log('✅ 10 places par défaut créées');
}
} catch (err) {
console.error('❌ Erreur lors de l\'initialisation de la base :', err.message);
throw err;
}
} }
// ============================================ // ============================================
@@ -127,43 +137,43 @@ async function initDatabase() {
// ============================================ // ============================================
async function createUser(name, email, phone, hashedPassword, role = 'client') { async function createUser(name, email, phone, hashedPassword, role = 'client') {
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
[name, email, phone, hashedPassword, role] [name, email, phone, hashedPassword, role]
); );
return { id: result.insertId }; return { id: result.insertId };
} }
async function getUserByEmail(email) { async function getUserByEmail(email) {
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]); const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
return rows[0]; return rows[0];
} }
async function getUserById(id) { async function getUserById(id) {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT id, name, email, phone, role, status, created_at FROM users WHERE id = ?', 'SELECT id, name, email, phone, role, status, created_at FROM users WHERE id = ?',
[id] [id]
); );
return rows[0]; return rows[0];
} }
async function getAllUsers() { async function getAllUsers() {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT id, name, email, phone, role, status, created_at FROM users ORDER BY name' 'SELECT id, name, email, phone, role, status, created_at FROM users ORDER BY name'
); );
return rows; return rows;
} }
async function updateUser(id, updates) { async function updateUser(id, updates) {
const fields = Object.keys(updates).map(k => `${k} = ?`).join(', '); const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
const values = Object.values(updates); const values = Object.values(updates);
const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]); const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]);
return { changed: result.affectedRows }; return { changed: result.affectedRows };
} }
async function deleteUser(id) { async function deleteUser(id) {
const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]); const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);
return { deleted: result.affectedRows }; return { deleted: result.affectedRows };
} }
// ============================================ // ============================================
@@ -171,34 +181,34 @@ async function deleteUser(id) {
// ============================================ // ============================================
async function createSpot(number, sensorId, status = 'free') { async function createSpot(number, sensorId, status = 'free') {
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)', 'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)',
[number, sensorId, status] [number, sensorId, status]
); );
return { id: result.insertId }; return { id: result.insertId };
} }
async function getAllSpots() { async function getAllSpots() {
const [rows] = await pool.query('SELECT * FROM spots ORDER BY number'); const [rows] = await pool.query('SELECT * FROM spots ORDER BY number');
return rows; return rows;
} }
async function getSpotById(id) { async function getSpotById(id) {
const [rows] = await pool.query('SELECT * FROM spots WHERE id = ?', [id]); const [rows] = await pool.query('SELECT * FROM spots WHERE id = ?', [id]);
return rows[0]; return rows[0];
} }
async function updateSpotStatus(id, status) { async function updateSpotStatus(id, status) {
const [result] = await pool.query( const [result] = await pool.query(
'UPDATE spots SET status = ?, last_update = CURRENT_TIMESTAMP WHERE id = ?', 'UPDATE spots SET status = ?, last_update = CURRENT_TIMESTAMP WHERE id = ?',
[status, id] [status, id]
); );
return { changed: result.affectedRows }; return { changed: result.affectedRows };
} }
async function deleteAllSpots() { async function deleteAllSpots() {
const [result] = await pool.query('DELETE FROM spots'); const [result] = await pool.query('DELETE FROM spots');
return { deleted: result.affectedRows }; return { deleted: result.affectedRows };
} }
// ============================================ // ============================================
@@ -206,44 +216,59 @@ async function deleteAllSpots() {
// ============================================ // ============================================
async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) { async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) {
const [result] = await pool.query( const [result] = await pool.query(
`INSERT INTO reservations `INSERT INTO reservations
(user_id, spot_id, date, start_time, end_time, duration, vehicle, price, payment_code, status) (user_id, spot_id, date, start_time, end_time, duration, vehicle, price, payment_code, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`,
[userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode] [userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode]
); );
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 = ?
ORDER BY r.created_at DESC`, ORDER BY r.created_at DESC`,
[userId] [userId]
); );
return rows; return rows;
} }
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
ORDER BY r.created_at DESC` ORDER BY r.created_at DESC`
); );
return rows; return rows;
} }
async function updateReservationStatus(id, status) { async function updateReservationStatus(id, status) {
const [result] = await pool.query( const [result] = await pool.query(
'UPDATE reservations SET status = ? WHERE id = ?', 'UPDATE reservations SET status = ? WHERE id = ?',
[status, id] [status, id]
); );
return { changed: result.affectedRows }; return { changed: result.affectedRows };
} }
// ============================================ // ============================================
@@ -251,23 +276,23 @@ async function updateReservationStatus(id, status) {
// ============================================ // ============================================
async function addHistory(action, details, userId = null) { async function addHistory(action, details, userId = null) {
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)', 'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)',
[action, details, userId] [action, details, userId]
); );
return { id: result.insertId }; return { id: result.insertId };
} }
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
LIMIT ?`, LIMIT ?`,
[limit] [limit]
); );
return rows; return rows;
} }
// ============================================ // ============================================
@@ -275,22 +300,22 @@ async function getHistory(limit = 50) {
// ============================================ // ============================================
async function recordStats(total, free, occupied, reserved) { async function recordStats(total, free, occupied, reserved) {
const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0; const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO stats (total_spots, free_spots, occupied_spots, reserved_spots, occupancy_rate) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO stats (total_spots, free_spots, occupied_spots, reserved_spots, occupancy_rate) VALUES (?, ?, ?, ?, ?)',
[total, free, occupied, reserved, rate] [total, free, occupied, reserved, rate]
); );
return { id: result.insertId }; return { id: result.insertId };
} }
async function getStats(days = 7) { async function getStats(days = 7) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT * FROM stats `SELECT * FROM stats
WHERE recorded_at > DATE_SUB(NOW(), INTERVAL ? DAY) WHERE recorded_at > DATE_SUB(NOW(), INTERVAL ? DAY)
ORDER BY recorded_at DESC`, ORDER BY recorded_at DESC`,
[days] [days]
); );
return rows; return rows;
} }
// ============================================ // ============================================
@@ -298,43 +323,37 @@ async function getStats(days = 7) {
// ============================================ // ============================================
async function recordMqttEvent(topic, message) { async function recordMqttEvent(topic, message) {
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO mqtt_events (topic, message) VALUES (?, ?)', 'INSERT INTO mqtt_events (topic, message) VALUES (?, ?)',
[topic, message] [topic, message]
); );
return { id: result.insertId }; return { id: result.insertId };
} }
// ============================================ // ============================================
// FERMETURE DU POOL // FERMETURE
// ============================================ // ============================================
async function closeDatabase() { async function closeDatabase() {
await pool.end(); await pool.end();
console.log('🔌 Connexions à la base fermées'); console.log('🔌 Connexions à la base fermées');
} }
module.exports = { module.exports = {
initDatabase, pool, // exposé pour les requêtes directes si besoin
closeDatabase, initDatabase,
createUser, closeDatabase,
getUserByEmail, // Utilisateurs
getUserById, createUser, getUserByEmail, getUserById, getAllUsers, updateUser, deleteUser,
getAllUsers, // Places
updateUser, createSpot, getAllSpots, getSpotById, updateSpotStatus, deleteAllSpots,
deleteUser, // Réservations
createSpot, createReservation, getReservationById, getReservationsByUser,
getAllSpots, getAllReservations, updateReservationStatus,
getSpotById, // Historique
updateSpotStatus, addHistory, getHistory,
deleteAllSpots, // Stats
createReservation, recordStats, getStats,
getReservationsByUser, // MQTT
getAllReservations, recordMqttEvent
updateReservationStatus,
addHistory,
getHistory,
recordStats,
getStats,
recordMqttEvent
}; };

View File

@@ -2,129 +2,58 @@
* ============================================ * ============================================
* 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');
// ============================================ // ============================================
// 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');
const user = await db.getUserById(result.id);
// Générer le token const token = generateToken(user);
const user = await db.getUserById(result.id);
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,79 +219,32 @@ 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();
const total = spots.length; const total = spots.length;
const free = spots.filter(s => s.status === 'free').length; const free = spots.filter(s => s.status === 'free').length;
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;