mise à jour
This commit is contained in:
291
css/style.css
291
css/style.css
@@ -185,7 +185,7 @@ body {
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.page-title {
|
||||
@@ -354,21 +354,10 @@ select.form-control {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-card.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.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-icon {
|
||||
font-size: 2rem;
|
||||
@@ -392,10 +381,10 @@ select.form-control {
|
||||
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.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 {
|
||||
font-size: 0.85rem;
|
||||
@@ -448,32 +437,14 @@ select.form-control {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.parking-spot:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.parking-spot:hover { transform: scale(1.05); }
|
||||
|
||||
.parking-spot.free {
|
||||
border-color: var(--spot-free);
|
||||
color: var(--spot-free);
|
||||
}
|
||||
.parking-spot.free { border-color: var(--spot-free); color: var(--spot-free); }
|
||||
.parking-spot.occupied { border-color: var(--spot-occupied); color: var(--spot-occupied); }
|
||||
.parking-spot.reserved { border-color: var(--spot-reserved); color: var(--spot-reserved); }
|
||||
|
||||
.parking-spot.occupied {
|
||||
border-color: var(--spot-occupied);
|
||||
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;
|
||||
}
|
||||
.parking-spot .spot-number { font-size: 1.1rem; }
|
||||
.parking-spot .spot-icon { font-size: 1.3rem; }
|
||||
|
||||
/* Legend */
|
||||
.legend {
|
||||
@@ -496,14 +467,12 @@ select.form-control {
|
||||
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.reserved { background: var(--spot-reserved); }
|
||||
|
||||
/* Spot details */
|
||||
.spot-details {
|
||||
min-height: 200px;
|
||||
}
|
||||
.spot-details { min-height: 200px; }
|
||||
|
||||
.no-selection {
|
||||
color: var(--text-muted);
|
||||
@@ -524,16 +493,10 @@ select.form-control {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.spot-info-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.spot-info-label { color: var(--text-secondary); font-size: 0.9rem; }
|
||||
.spot-info-value { font-weight: 600; }
|
||||
|
||||
.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-reserved { color: var(--spot-reserved); }
|
||||
|
||||
@@ -605,7 +568,7 @@ select.form-control {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
@@ -619,9 +582,7 @@ select.form-control {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.modal-header h3 { margin: 0; }
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
@@ -632,15 +593,11 @@ select.form-control {
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.modal-close:hover { color: var(--text-primary); }
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
.modal-body { padding: 24px; }
|
||||
|
||||
/* Payment modal */
|
||||
/* Récapitulatif réservation */
|
||||
.payment-summary {
|
||||
background: var(--bg-dark);
|
||||
border-radius: var(--border-radius-sm);
|
||||
@@ -668,34 +625,37 @@ select.form-control {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
/* ============================================
|
||||
MESSAGE DE CONFIRMATION (remplace QR code)
|
||||
============================================ */
|
||||
.confirmation-message {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.qr-section p {
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#qrcode {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
#qrcode img {
|
||||
padding: 24px 16px;
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: var(--border-radius-sm);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qr-info {
|
||||
font-size: 0.85rem;
|
||||
.confirmation-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.payment-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.confirmation-message h4 {
|
||||
font-size: 1.1rem;
|
||||
color: var(--success);
|
||||
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;
|
||||
}
|
||||
|
||||
.reservation-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reservation-price {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
@@ -749,20 +716,9 @@ select.form-control {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
.status-active { background: rgba(16, 185, 129, 0.2); color: var(--success); }
|
||||
.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 {
|
||||
text-align: center;
|
||||
@@ -819,9 +775,7 @@ select.form-control {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.profile-header h3 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.profile-header h3 { margin-bottom: 8px; }
|
||||
|
||||
.role-badge {
|
||||
display: inline-block;
|
||||
@@ -924,14 +878,12 @@ select.form-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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.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.reserved { background: rgba(59, 130, 246, 0.2); color: var(--spot-reserved); }
|
||||
|
||||
/* Tables */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.table-container { overflow-x: auto; }
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
@@ -954,9 +906,7 @@ select.form-control {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.data-table tr:hover td {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.data-table tr:hover td { background: var(--bg-hover); }
|
||||
|
||||
/* Log container */
|
||||
.log-container {
|
||||
@@ -975,9 +925,7 @@ select.form-control {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.log-item:last-child { border-bottom: none; }
|
||||
|
||||
.log-time {
|
||||
color: var(--text-muted);
|
||||
@@ -1009,116 +957,55 @@ select.form-control {
|
||||
}
|
||||
|
||||
.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); }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITAIRES
|
||||
============================================ */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.admin-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-only.visible {
|
||||
display: flex;
|
||||
}
|
||||
.admin-only { display: none; }
|
||||
.admin-only.visible { display: flex; }
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
@media (max-width: 1024px) {
|
||||
.stats-grid {
|
||||
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;
|
||||
}
|
||||
|
||||
.admin-page .admin-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.stats-grid { 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; }
|
||||
.admin-page .admin-stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header .container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header .container { flex-wrap: wrap; }
|
||||
.nav {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
.nav-link { padding: 8px 12px; font-size: 0.8rem; }
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.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); }
|
||||
.reservation-card { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||
.reservation-actions { align-items: flex-start; }
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-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);
|
||||
}
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-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(--bg-hover); }
|
||||
190
js/dashboard.js
190
js/dashboard.js
@@ -2,70 +2,48 @@
|
||||
* ============================================
|
||||
* DASHBOARD.JS - Gestion du dashboard
|
||||
* 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';
|
||||
|
||||
// État global
|
||||
let dashboardState = {
|
||||
user: null,
|
||||
user: null,
|
||||
currentPage: 'map'
|
||||
};
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('🚗 Initialisation du dashboard...');
|
||||
|
||||
// Vérifier l'authentification
|
||||
checkAuthentication();
|
||||
|
||||
// Initialiser la navigation
|
||||
initNavigation();
|
||||
|
||||
// Initialiser la déconnexion
|
||||
initLogout();
|
||||
|
||||
// Charger les données utilisateur
|
||||
loadUserData();
|
||||
});
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur est authentifié
|
||||
*/
|
||||
function checkAuthentication() {
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
const user = localStorage.getItem('smart_parking_user');
|
||||
|
||||
const user = localStorage.getItem('smart_parking_user');
|
||||
if (!token || !user) {
|
||||
// Rediriger vers la page de connexion
|
||||
window.location.href = '../index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardState.user = JSON.parse(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise la navigation
|
||||
*/
|
||||
function initNavigation() {
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const page = link.getAttribute('data-page');
|
||||
navigateToPage(page);
|
||||
|
||||
// Mettre à jour la classe active
|
||||
navLinks.forEach(l => l.classList.remove('active'));
|
||||
link.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Afficher le lien admin si l'utilisateur est admin
|
||||
|
||||
if (dashboardState.user && dashboardState.user.role === 'admin') {
|
||||
const adminLink = document.querySelector('.admin-only');
|
||||
if (adminLink) {
|
||||
@@ -75,25 +53,18 @@ function initNavigation() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation entre les pages
|
||||
*/
|
||||
function navigateToPage(page) {
|
||||
// Cacher toutes les pages
|
||||
const pages = document.querySelectorAll('.page');
|
||||
pages.forEach(p => {
|
||||
document.querySelectorAll('.page').forEach(p => {
|
||||
p.classList.add('hidden');
|
||||
p.classList.remove('active');
|
||||
});
|
||||
|
||||
// Afficher la page demandée
|
||||
|
||||
const targetPage = document.getElementById(page);
|
||||
if (targetPage) {
|
||||
targetPage.classList.remove('hidden');
|
||||
targetPage.classList.add('active');
|
||||
dashboardState.currentPage = page;
|
||||
|
||||
// Rafraîchir les données selon la page
|
||||
|
||||
if (page === 'map' && window.ParkingMap) {
|
||||
window.ParkingMap.refresh();
|
||||
} else if (page === 'my-reservations') {
|
||||
@@ -104,9 +75,6 @@ function navigateToPage(page) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise la déconnexion
|
||||
*/
|
||||
function initLogout() {
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
@@ -118,78 +86,59 @@ function initLogout() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les données utilisateur
|
||||
*/
|
||||
function loadUserData() {
|
||||
const user = dashboardState.user;
|
||||
if (!user) return;
|
||||
|
||||
// Mettre à jour l'affichage
|
||||
|
||||
document.getElementById('userName').textContent = user.name;
|
||||
document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
|
||||
|
||||
// Page profil
|
||||
document.getElementById('profileName').textContent = user.name;
|
||||
document.getElementById('profileNameInput').value = user.name;
|
||||
document.getElementById('profileEmailInput').value = user.email;
|
||||
document.getElementById('profilePhoneInput').value = user.phone || '';
|
||||
|
||||
// Badge rôle
|
||||
|
||||
document.getElementById('profileName').textContent = user.name;
|
||||
document.getElementById('profileNameInput').value = user.name;
|
||||
document.getElementById('profileEmailInput').value = user.email;
|
||||
document.getElementById('profilePhoneInput').value = user.phone || '';
|
||||
|
||||
const roleBadge = document.getElementById('profileRole');
|
||||
if (roleBadge) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les statistiques utilisateur
|
||||
*/
|
||||
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 totalReservations = userReservations.length;
|
||||
const activeReservations = userReservations.filter(r => r.status === 'active').length;
|
||||
const totalSpent = userReservations.reduce((sum, r) => sum + (r.price || 0), 0);
|
||||
|
||||
document.getElementById('totalReservations').textContent = totalReservations;
|
||||
document.getElementById('activeReservations').textContent = activeReservations;
|
||||
document.getElementById('totalSpent').textContent = totalSpent + '€';
|
||||
|
||||
document.getElementById('totalReservations').textContent = userReservations.length;
|
||||
document.getElementById('activeReservations').textContent = userReservations.filter(r => r.status === 'active').length;
|
||||
document.getElementById('totalSpent').textContent = userReservations.reduce((s, r) => s + (r.price || 0), 0) + '€';
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les réservations de l'utilisateur
|
||||
*/
|
||||
function loadMyReservations() {
|
||||
const container = document.getElementById('myReservationsList');
|
||||
const container = document.getElementById('myReservationsList');
|
||||
const emptyState = document.getElementById('noReservations');
|
||||
|
||||
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);
|
||||
|
||||
|
||||
if (userReservations.length === 0) {
|
||||
container.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
container.innerHTML = userReservations.map(res => `
|
||||
container.innerHTML = userReservations.slice().reverse().map(res => `
|
||||
<div class="reservation-card">
|
||||
<div class="reservation-info">
|
||||
<h4>Place ${res.spotNumber}</h4>
|
||||
<div class="reservation-details">
|
||||
<span>📅 ${res.date}</span>
|
||||
<span>🕐 ${res.startTime}</span>
|
||||
<span>⏱️ ${res.duration} min</span>
|
||||
<span>⏱️ ${formatDurationLabel(res.duration)}</span>
|
||||
<span>🚗 ${res.vehicle || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,94 +156,75 @@ 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) {
|
||||
if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return;
|
||||
|
||||
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
const reservation = reservations.find(r => r.id === reservationId);
|
||||
|
||||
|
||||
if (reservation) {
|
||||
// Mettre à jour le statut
|
||||
reservation.status = 'cancelled';
|
||||
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
|
||||
|
||||
// Libérer la place
|
||||
|
||||
// CORRIGÉ : spotId au lieu de spotNumber
|
||||
if (window.ParkingMap) {
|
||||
window.ParkingMap.setSpotStatus(reservation.spotNumber, 'free');
|
||||
window.ParkingMap.setSpotStatus(reservation.spotId, 'free');
|
||||
}
|
||||
|
||||
|
||||
showToast('Réservation annulée', 'success');
|
||||
loadMyReservations();
|
||||
loadUserStats();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le profil
|
||||
*/
|
||||
// Mise à jour du profil
|
||||
document.getElementById('profileForm')?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const phone = document.getElementById('profilePhoneInput').value;
|
||||
const phone = document.getElementById('profilePhoneInput').value;
|
||||
const newPassword = document.getElementById('profileNewPassword').value;
|
||||
|
||||
// Mettre à jour l'utilisateur
|
||||
|
||||
let user = dashboardState.user;
|
||||
user.phone = phone;
|
||||
|
||||
if (newPassword) {
|
||||
user.password = newPassword;
|
||||
}
|
||||
|
||||
// Sauvegarder
|
||||
if (newPassword) user.password = newPassword;
|
||||
|
||||
localStorage.setItem('smart_parking_user', JSON.stringify(user));
|
||||
|
||||
// Mettre à jour aussi dans la liste des utilisateurs
|
||||
|
||||
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
|
||||
const userIndex = users.findIndex(u => u.id === user.id);
|
||||
if (userIndex !== -1) {
|
||||
users[userIndex] = user;
|
||||
const idx = users.findIndex(u => u.id === user.id);
|
||||
if (idx !== -1) {
|
||||
users[idx] = user;
|
||||
localStorage.setItem('smart_parking_users', JSON.stringify(users));
|
||||
}
|
||||
|
||||
|
||||
showToast('Profil mis à jour', 'success');
|
||||
});
|
||||
|
||||
/**
|
||||
* Retourne le label du statut
|
||||
*/
|
||||
function getStatusLabel(status) {
|
||||
const labels = {
|
||||
'active': 'Active',
|
||||
'completed': 'Terminée',
|
||||
'cancelled': 'Annulée'
|
||||
};
|
||||
return labels[status] || status;
|
||||
return { active: 'Active', completed: 'Terminée', cancelled: 'Annulée' }[status] || status;
|
||||
}
|
||||
|
||||
function formatDurationLabel(minutes) {
|
||||
if (minutes >= 480) return 'Journée';
|
||||
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') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
}
|
||||
|
||||
// Exporter les fonctions
|
||||
window.Dashboard = {
|
||||
navigateToPage,
|
||||
showToast,
|
||||
getUser: () => dashboardState.user,
|
||||
getUser: () => dashboardState.user,
|
||||
refreshStats: loadUserStats
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,8 @@
|
||||
* ============================================
|
||||
* RESERVATION.JS - Système de réservation
|
||||
* 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();
|
||||
initTimeSlots();
|
||||
initPricePreview();
|
||||
initPaymentModal();
|
||||
initConfirmationModal(); // MODIFIÉ : était initPaymentModal()
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -42,7 +44,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function initReservationForm() {
|
||||
const form = document.getElementById('reservationForm');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', handleReservationSubmit);
|
||||
}
|
||||
|
||||
@@ -52,8 +53,6 @@ function initReservationForm() {
|
||||
function initDatePicker() {
|
||||
const dateInput = document.getElementById('resDate');
|
||||
if (!dateInput) return;
|
||||
|
||||
// Date minimum = aujourd'hui
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
dateInput.min = today;
|
||||
dateInput.value = today;
|
||||
@@ -65,15 +64,15 @@ function initDatePicker() {
|
||||
function initTimeSlots() {
|
||||
const select = document.getElementById('resStartTime');
|
||||
if (!select) return;
|
||||
|
||||
|
||||
TIME_SLOTS.forEach(time => {
|
||||
const option = document.createElement('option');
|
||||
option.value = time;
|
||||
option.textContent = time;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// Sélectionner l'heure actuelle + 1h
|
||||
|
||||
// Sélectionner le prochain créneau disponible
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMinutes = now.getMinutes();
|
||||
@@ -81,10 +80,7 @@ function initTimeSlots() {
|
||||
const [h, m] = t.split(':').map(Number);
|
||||
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() {
|
||||
const durationSelect = document.getElementById('resDuration');
|
||||
if (!durationSelect) return;
|
||||
|
||||
durationSelect.addEventListener('change', updatePricePreview);
|
||||
|
||||
// Prix initial
|
||||
updatePricePreview();
|
||||
}
|
||||
|
||||
@@ -106,246 +99,192 @@ function initPricePreview() {
|
||||
function updatePricePreview() {
|
||||
const duration = parseInt(document.getElementById('resDuration').value);
|
||||
const price = PRICING[duration] || 0;
|
||||
|
||||
document.getElementById('previewPrice').textContent = price + '€';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire
|
||||
* MODIFIÉ : la réservation est enregistrée ici directement,
|
||||
* puis le modal de confirmation s'affiche.
|
||||
*/
|
||||
function handleReservationSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
|
||||
if (!user) {
|
||||
Dashboard.showToast('Veuillez vous connecter', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const spotId = parseInt(document.getElementById('resSpot').value);
|
||||
const date = document.getElementById('resDate').value;
|
||||
|
||||
const spotId = parseInt(document.getElementById('resSpot').value);
|
||||
const date = document.getElementById('resDate').value;
|
||||
const startTime = document.getElementById('resStartTime').value;
|
||||
const duration = parseInt(document.getElementById('resDuration').value);
|
||||
const vehicle = document.getElementById('resVehicle').value;
|
||||
|
||||
const duration = parseInt(document.getElementById('resDuration').value);
|
||||
const vehicle = document.getElementById('resVehicle').value;
|
||||
|
||||
if (!spotId || !date || !startTime || !vehicle) {
|
||||
Dashboard.showToast('Veuillez remplir tous les champs', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Vérifier que la place est toujours libre
|
||||
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
|
||||
const spot = spots.find(s => s.id === spotId);
|
||||
|
||||
const spot = spots.find(s => s.id === spotId);
|
||||
|
||||
if (!spot || spot.status !== 'free') {
|
||||
Dashboard.showToast('Cette place n\'est plus disponible', 'error');
|
||||
// Rafraîchir la carte
|
||||
if (window.ParkingMap) {
|
||||
window.ParkingMap.refresh();
|
||||
}
|
||||
if (window.ParkingMap) window.ParkingMap.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Calculer l'heure de fin
|
||||
const [hours, minutes] = startTime.split(':').map(Number);
|
||||
const endDate = new Date(date + 'T' + startTime);
|
||||
endDate.setMinutes(endDate.getMinutes() + duration);
|
||||
const endTime = endDate.toTimeString().slice(0, 5);
|
||||
|
||||
// Créer la réservation
|
||||
|
||||
// Construire l'objet réservation
|
||||
currentReservation = {
|
||||
id: Date.now(),
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
spotId: spotId,
|
||||
spotNumber: spot.number,
|
||||
date: date,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
duration: duration,
|
||||
vehicle: vehicle.toUpperCase(),
|
||||
price: PRICING[duration],
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
id: Date.now(),
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
spotId: spotId,
|
||||
spotNumber: spot.number,
|
||||
date: date,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
duration: duration,
|
||||
vehicle: vehicle.toUpperCase(),
|
||||
price: PRICING[duration],
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Afficher le modal de paiement
|
||||
showPaymentModal();
|
||||
|
||||
// ---- Enregistrement immédiat (plus de bouton "J'ai payé") ----
|
||||
|
||||
// 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() {
|
||||
// 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() {
|
||||
function showConfirmationModal() {
|
||||
if (!currentReservation) return;
|
||||
|
||||
const modal = document.getElementById('paymentModal');
|
||||
|
||||
|
||||
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('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();
|
||||
|
||||
document.getElementById('payTotal').textContent = currentReservation.price + '€';
|
||||
|
||||
// Afficher le modal
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache le modal de paiement
|
||||
* Cache le modal de confirmation
|
||||
* REMPLACE : hidePaymentModal()
|
||||
*/
|
||||
function hidePaymentModal() {
|
||||
document.getElementById('paymentModal').classList.add('hidden');
|
||||
}
|
||||
function hideConfirmationModal() {
|
||||
document.getElementById('confirmationModal').classList.add('hidden');
|
||||
|
||||
/**
|
||||
* Génère le QR code
|
||||
*/
|
||||
function generateQRCode() {
|
||||
const container = document.getElementById('qrcode');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Générer un code de paiement unique
|
||||
const paymentCode = 'PARK' + Date.now().toString().slice(-8);
|
||||
document.getElementById('paymentCode').textContent = paymentCode;
|
||||
|
||||
// Créer le QR code
|
||||
const qrData = JSON.stringify({
|
||||
type: 'parking_payment',
|
||||
reservationId: currentReservation.id,
|
||||
amount: currentReservation.price,
|
||||
code: paymentCode
|
||||
});
|
||||
|
||||
// Utiliser QRCode.js si disponible, sinon afficher un faux QR
|
||||
if (typeof QRCode !== 'undefined') {
|
||||
new QRCode(container, {
|
||||
text: qrData,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
} else {
|
||||
// Fallback - afficher un QR code simulé
|
||||
container.innerHTML = `
|
||||
<div style="width: 200px; height: 200px; background: white; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
|
||||
<div style="font-size: 80px;">📱</div>
|
||||
<div style="color: #333; font-size: 12px; margin-top: 10px;">QR Code de paiement</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirme le paiement
|
||||
*/
|
||||
function confirmPayment() {
|
||||
if (!currentReservation) return;
|
||||
|
||||
// Mettre à jour le statut
|
||||
currentReservation.status = 'active';
|
||||
|
||||
// Sauvegarder la réservation
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
reservations.push(currentReservation);
|
||||
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
|
||||
|
||||
// Mettre à jour le statut de la place
|
||||
if (window.ParkingMap) {
|
||||
window.ParkingMap.setSpotStatus(currentReservation.spotId, 'reserved');
|
||||
}
|
||||
|
||||
// Ajouter à l'historique admin
|
||||
addToHistory('Réservation', `Place ${currentReservation.spotNumber} réservée par ${currentReservation.userName} - ${currentReservation.price}€`);
|
||||
|
||||
// Fermer le modal
|
||||
hidePaymentModal();
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
document.getElementById('reservationForm').reset();
|
||||
initDatePicker();
|
||||
updatePricePreview();
|
||||
|
||||
// Afficher confirmation
|
||||
Dashboard.showToast('Réservation confirmée !', 'success');
|
||||
|
||||
// Rediriger vers mes réservations
|
||||
setTimeout(() => {
|
||||
// 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');
|
||||
}, 1500);
|
||||
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 à l'historique
|
||||
* Ajoute une entrée à l'historique
|
||||
*/
|
||||
function addToHistory(action, details) {
|
||||
let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]');
|
||||
history.unshift({
|
||||
id: Date.now(),
|
||||
action: action,
|
||||
details: details,
|
||||
id: Date.now(),
|
||||
action: action,
|
||||
details: details,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Garder seulement les 100 dernières entrées
|
||||
if (history.length > 100) {
|
||||
history = history.slice(0, 100);
|
||||
}
|
||||
|
||||
if (history.length > 100) history = history.slice(0, 100);
|
||||
localStorage.setItem('smart_parking_history', JSON.stringify(history));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date
|
||||
* Formate une date DD/MM/YYYY
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
day: '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) {
|
||||
if (minutes >= 480) {
|
||||
return 'Journée (8h)';
|
||||
} else if (minutes >= 60) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`;
|
||||
} else {
|
||||
return `${minutes} min`;
|
||||
if (minutes >= 480) return 'Journée (8h)';
|
||||
if (minutes >= 60) {
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = minutes % 60;
|
||||
return m > 0 ? `${h}h ${m}min` : `${h}h`;
|
||||
}
|
||||
return `${minutes} min`;
|
||||
}
|
||||
|
||||
// Exporter les fonctions
|
||||
// Exporter les fonctions publiques
|
||||
window.Reservation = {
|
||||
PRICING,
|
||||
TIME_SLOTS,
|
||||
formatDuration,
|
||||
addToHistory
|
||||
};
|
||||
};
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Parking",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>Smart Parking - Dashboard</title>
|
||||
<link rel="stylesheet" href="../css/style.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://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
@@ -206,64 +206,64 @@
|
||||
</div>
|
||||
|
||||
<div class="price-preview">
|
||||
<span>Prix total:</span>
|
||||
<span>Prix total :</span>
|
||||
<span id="previewPrice" class="price-amount">5€</span>
|
||||
</div>
|
||||
|
||||
<!-- MODIFIÉ : bouton "Valider la réservation" au lieu de "Procéder au paiement" -->
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<span class="btn-icon">💳</span>
|
||||
Procéder au paiement
|
||||
<span class="btn-icon">✅</span>
|
||||
Valider la réservation
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal de paiement QR Code -->
|
||||
<div id="paymentModal" class="modal hidden">
|
||||
<!-- REMPLACÉ : Modal de confirmation (sans QR code) -->
|
||||
<div id="confirmationModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>💳 Paiement</h3>
|
||||
<button class="modal-close" id="closePaymentModal">×</button>
|
||||
<h3>🎉 Réservation confirmée</h3>
|
||||
<button class="modal-close" id="closeConfirmationModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Récapitulatif -->
|
||||
<div class="payment-summary">
|
||||
<h4>Récapitulatif</h4>
|
||||
<div class="summary-row">
|
||||
<span>Place:</span>
|
||||
<span>Place :</span>
|
||||
<span id="paySpot">-</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>Date:</span>
|
||||
<span>Date :</span>
|
||||
<span id="payDate">-</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>Heure:</span>
|
||||
<span>Heure :</span>
|
||||
<span id="payTime">-</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>Durée:</span>
|
||||
<span>Durée :</span>
|
||||
<span id="payDuration">-</span>
|
||||
</div>
|
||||
<div class="summary-row total">
|
||||
<span>Total:</span>
|
||||
<span>Total :</span>
|
||||
<span id="payTotal">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qr-section">
|
||||
<p>Scannez ce QR code pour payer</p>
|
||||
<div id="qrcode"></div>
|
||||
<p class="qr-info">Ou utilisez le code: <strong id="paymentCode">-</strong></p>
|
||||
<!-- Message de confirmation -->
|
||||
<div class="confirmation-message">
|
||||
<div class="confirmation-icon">🏷️</div>
|
||||
<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 class="payment-actions">
|
||||
<button id="confirmPaymentBtn" class="btn btn-success btn-block">
|
||||
<span class="btn-icon">✅</span>
|
||||
J'ai payé
|
||||
</button>
|
||||
<button id="cancelPaymentBtn" class="btn btn-secondary btn-block">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
<!-- Bouton fermer -->
|
||||
<button id="closeConfirmationBtn" class="btn btn-primary btn-block">
|
||||
<span class="btn-icon">✅</span>
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'smartparking_user',
|
||||
password: process.env.DB_PASSWORD || 'smartparking_pass',
|
||||
database: process.env.DB_NAME || 'smartparking',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'smartparking_user',
|
||||
password: process.env.DB_PASSWORD || 'smartparking_pass',
|
||||
database: process.env.DB_NAME || 'smartparking',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
async function initDatabase() {
|
||||
try {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone VARCHAR(50),
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','client') DEFAULT 'client',
|
||||
status ENUM('active','inactive') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
try {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone VARCHAR(50),
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','client') DEFAULT 'client',
|
||||
status ENUM('active','inactive') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS spots (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
number INT UNIQUE NOT NULL,
|
||||
status ENUM('free','occupied','reserved') DEFAULT 'free',
|
||||
sensor_id VARCHAR(100),
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS spots (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
number INT UNIQUE NOT NULL,
|
||||
status ENUM('free','occupied','reserved') DEFAULT 'free',
|
||||
sensor_id VARCHAR(100),
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS reservations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
spot_id INT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
duration INT NOT NULL,
|
||||
vehicle VARCHAR(20),
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
status ENUM('pending','active','completed','cancelled') DEFAULT 'pending',
|
||||
payment_code VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (spot_id) REFERENCES spots(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS reservations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
spot_id INT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
duration INT NOT NULL,
|
||||
vehicle VARCHAR(20),
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
status ENUM('pending','active','completed','cancelled') DEFAULT 'pending',
|
||||
payment_code VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (spot_id) REFERENCES spots(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
details TEXT,
|
||||
user_id INT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
details TEXT,
|
||||
user_id INT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS stats (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
total_spots INT,
|
||||
free_spots INT,
|
||||
occupied_spots INT,
|
||||
reserved_spots INT,
|
||||
occupancy_rate DECIMAL(5,2),
|
||||
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS stats (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
total_spots INT,
|
||||
free_spots INT,
|
||||
occupied_spots INT,
|
||||
reserved_spots INT,
|
||||
occupancy_rate DECIMAL(5,2),
|
||||
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS mqtt_events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic VARCHAR(255) NOT NULL,
|
||||
message TEXT,
|
||||
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS mqtt_events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic VARCHAR(255) NOT NULL,
|
||||
message TEXT,
|
||||
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']);
|
||||
if (rows.length === 0) {
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||
await pool.query(
|
||||
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
|
||||
['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashedPassword, 'admin']
|
||||
);
|
||||
console.log('✅ Administrateur par défaut créé');
|
||||
// Compte admin par défaut
|
||||
const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']);
|
||||
if (rows.length === 0) {
|
||||
const hashed = await bcrypt.hash('admin123', 10);
|
||||
await pool.query(
|
||||
'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éé');
|
||||
}
|
||||
|
||||
// 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') {
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
|
||||
[name, email, phone, hashedPassword, role]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
|
||||
[name, email, phone, hashedPassword, role]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
}
|
||||
|
||||
async function getUserByEmail(email) {
|
||||
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
|
||||
return rows[0];
|
||||
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async function getUserById(id) {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, name, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
return rows[0];
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, name, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async function getAllUsers() {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, name, email, phone, role, status, created_at FROM users ORDER BY name'
|
||||
);
|
||||
return rows;
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, name, email, phone, role, status, created_at FROM users ORDER BY name'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function updateUser(id, updates) {
|
||||
const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
|
||||
const values = Object.values(updates);
|
||||
const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]);
|
||||
return { changed: result.affectedRows };
|
||||
const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
|
||||
const values = Object.values(updates);
|
||||
const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]);
|
||||
return { changed: result.affectedRows };
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);
|
||||
return { deleted: result.affectedRows };
|
||||
const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);
|
||||
return { deleted: result.affectedRows };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -171,34 +181,34 @@ async function deleteUser(id) {
|
||||
// ============================================
|
||||
|
||||
async function createSpot(number, sensorId, status = 'free') {
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)',
|
||||
[number, sensorId, status]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)',
|
||||
[number, sensorId, status]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
}
|
||||
|
||||
async function getAllSpots() {
|
||||
const [rows] = await pool.query('SELECT * FROM spots ORDER BY number');
|
||||
return rows;
|
||||
const [rows] = await pool.query('SELECT * FROM spots ORDER BY number');
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getSpotById(id) {
|
||||
const [rows] = await pool.query('SELECT * FROM spots WHERE id = ?', [id]);
|
||||
return rows[0];
|
||||
const [rows] = await pool.query('SELECT * FROM spots WHERE id = ?', [id]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async function updateSpotStatus(id, status) {
|
||||
const [result] = await pool.query(
|
||||
'UPDATE spots SET status = ?, last_update = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[status, id]
|
||||
);
|
||||
return { changed: result.affectedRows };
|
||||
const [result] = await pool.query(
|
||||
'UPDATE spots SET status = ?, last_update = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[status, id]
|
||||
);
|
||||
return { changed: result.affectedRows };
|
||||
}
|
||||
|
||||
async function deleteAllSpots() {
|
||||
const [result] = await pool.query('DELETE FROM spots');
|
||||
return { deleted: result.affectedRows };
|
||||
const [result] = await pool.query('DELETE FROM spots');
|
||||
return { deleted: result.affectedRows };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -206,44 +216,59 @@ async function deleteAllSpots() {
|
||||
// ============================================
|
||||
|
||||
async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO reservations
|
||||
(user_id, spot_id, date, start_time, end_time, duration, vehicle, price, payment_code, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`,
|
||||
[userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO reservations
|
||||
(user_id, spot_id, date, start_time, end_time, duration, vehicle, price, payment_code, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`,
|
||||
[userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode]
|
||||
);
|
||||
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) {
|
||||
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.user_id = ?
|
||||
ORDER BY r.created_at DESC`,
|
||||
[userId]
|
||||
);
|
||||
return rows;
|
||||
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.user_id = ?
|
||||
ORDER BY r.created_at DESC`,
|
||||
[userId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getAllReservations() {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT r.*, s.number as spot_number, u.name as user_name
|
||||
FROM reservations r
|
||||
JOIN spots s ON r.spot_id = s.id
|
||||
JOIN users u ON r.user_id = u.id
|
||||
ORDER BY r.created_at DESC`
|
||||
);
|
||||
return rows;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT r.*, s.number AS spot_number, u.name AS user_name
|
||||
FROM reservations r
|
||||
JOIN spots s ON r.spot_id = s.id
|
||||
JOIN users u ON r.user_id = u.id
|
||||
ORDER BY r.created_at DESC`
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function updateReservationStatus(id, status) {
|
||||
const [result] = await pool.query(
|
||||
'UPDATE reservations SET status = ? WHERE id = ?',
|
||||
[status, id]
|
||||
);
|
||||
return { changed: result.affectedRows };
|
||||
const [result] = await pool.query(
|
||||
'UPDATE reservations SET status = ? WHERE id = ?',
|
||||
[status, id]
|
||||
);
|
||||
return { changed: result.affectedRows };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -251,23 +276,23 @@ async function updateReservationStatus(id, status) {
|
||||
// ============================================
|
||||
|
||||
async function addHistory(action, details, userId = null) {
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)',
|
||||
[action, details, userId]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)',
|
||||
[action, details, userId]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
}
|
||||
|
||||
async function getHistory(limit = 50) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT h.*, u.name as user_name
|
||||
FROM history h
|
||||
LEFT JOIN users u ON h.user_id = u.id
|
||||
ORDER BY h.timestamp DESC
|
||||
LIMIT ?`,
|
||||
[limit]
|
||||
);
|
||||
return rows;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT h.*, u.name AS user_name
|
||||
FROM history h
|
||||
LEFT JOIN users u ON h.user_id = u.id
|
||||
ORDER BY h.timestamp DESC
|
||||
LIMIT ?`,
|
||||
[limit]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -275,22 +300,22 @@ async function getHistory(limit = 50) {
|
||||
// ============================================
|
||||
|
||||
async function recordStats(total, free, occupied, reserved) {
|
||||
const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO stats (total_spots, free_spots, occupied_spots, reserved_spots, occupancy_rate) VALUES (?, ?, ?, ?, ?)',
|
||||
[total, free, occupied, reserved, rate]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO stats (total_spots, free_spots, occupied_spots, reserved_spots, occupancy_rate) VALUES (?, ?, ?, ?, ?)',
|
||||
[total, free, occupied, reserved, rate]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
}
|
||||
|
||||
async function getStats(days = 7) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT * FROM stats
|
||||
WHERE recorded_at > DATE_SUB(NOW(), INTERVAL ? DAY)
|
||||
ORDER BY recorded_at DESC`,
|
||||
[days]
|
||||
);
|
||||
return rows;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT * FROM stats
|
||||
WHERE recorded_at > DATE_SUB(NOW(), INTERVAL ? DAY)
|
||||
ORDER BY recorded_at DESC`,
|
||||
[days]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -298,43 +323,37 @@ async function getStats(days = 7) {
|
||||
// ============================================
|
||||
|
||||
async function recordMqttEvent(topic, message) {
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO mqtt_events (topic, message) VALUES (?, ?)',
|
||||
[topic, message]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO mqtt_events (topic, message) VALUES (?, ?)',
|
||||
[topic, message]
|
||||
);
|
||||
return { id: result.insertId };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FERMETURE DU POOL
|
||||
// FERMETURE
|
||||
// ============================================
|
||||
|
||||
async function closeDatabase() {
|
||||
await pool.end();
|
||||
console.log('🔌 Connexions à la base fermées');
|
||||
await pool.end();
|
||||
console.log('🔌 Connexions à la base fermées');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initDatabase,
|
||||
closeDatabase,
|
||||
createUser,
|
||||
getUserByEmail,
|
||||
getUserById,
|
||||
getAllUsers,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
createSpot,
|
||||
getAllSpots,
|
||||
getSpotById,
|
||||
updateSpotStatus,
|
||||
deleteAllSpots,
|
||||
createReservation,
|
||||
getReservationsByUser,
|
||||
getAllReservations,
|
||||
updateReservationStatus,
|
||||
addHistory,
|
||||
getHistory,
|
||||
recordStats,
|
||||
getStats,
|
||||
recordMqttEvent
|
||||
pool, // exposé pour les requêtes directes si besoin
|
||||
initDatabase,
|
||||
closeDatabase,
|
||||
// Utilisateurs
|
||||
createUser, getUserByEmail, getUserById, getAllUsers, updateUser, deleteUser,
|
||||
// Places
|
||||
createSpot, getAllSpots, getSpotById, updateSpotStatus, deleteAllSpots,
|
||||
// Réservations
|
||||
createReservation, getReservationById, getReservationsByUser,
|
||||
getAllReservations, updateReservationStatus,
|
||||
// Historique
|
||||
addHistory, getHistory,
|
||||
// Stats
|
||||
recordStats, getStats,
|
||||
// MQTT
|
||||
recordMqttEvent
|
||||
};
|
||||
@@ -2,129 +2,58 @@
|
||||
* ============================================
|
||||
* API ROUTES - Routes de l'API REST
|
||||
* Smart Parking - BTS CIEL IR
|
||||
* CORRIGÉ : annulation libère bien la place
|
||||
* ajout route /complete pour l'admin
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const db = require('../db/database');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const db = require('../db/database');
|
||||
const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// ============================================
|
||||
// AUTHENTIFICATION
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /api/register
|
||||
* Inscription d'un nouvel utilisateur
|
||||
*/
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { name, email, phone, password } = req.body;
|
||||
|
||||
if (!name || !email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
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
|
||||
if (!name || !email || !password)
|
||||
return res.status(400).json({ success: false, message: 'Tous les champs sont requis' });
|
||||
if (await db.getUserByEmail(email))
|
||||
return res.status(409).json({ success: false, message: 'Cet email est déjà utilisé' });
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Créer l'utilisateur
|
||||
const result = await db.createUser(name, email, phone, hashedPassword, 'client');
|
||||
|
||||
// Générer le token
|
||||
const user = await db.getUserById(result.id);
|
||||
const token = generateToken(user);
|
||||
|
||||
const user = await db.getUserById(result.id);
|
||||
const token = generateToken(user);
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Compte créé avec succès',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
role: user.role
|
||||
}
|
||||
success: true, message: 'Compte créé avec succès', token,
|
||||
user: { id: user.id, name: user.name, email: user.email, phone: user.phone, role: user.role }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur register:', err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Erreur serveur'
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/login
|
||||
* Connexion
|
||||
*/
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Email et mot de passe requis'
|
||||
});
|
||||
}
|
||||
|
||||
// Récupérer l'utilisateur
|
||||
if (!email || !password)
|
||||
return res.status(400).json({ success: false, message: 'Email et mot de passe requis' });
|
||||
const user = await db.getUserByEmail(email);
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
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
|
||||
if (!user || !(await bcrypt.compare(password, user.password)))
|
||||
return res.status(401).json({ success: false, message: 'Email ou mot de passe incorrect' });
|
||||
const token = generateToken(user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Connexion réussie',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
role: user.role
|
||||
}
|
||||
success: true, message: 'Connexion réussie', token,
|
||||
user: { id: user.id, name: user.name, email: user.email, phone: user.phone, role: user.role }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur login:', err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Erreur serveur'
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,44 +61,21 @@ router.post('/login', async (req, res) => {
|
||||
// UTILISATEURS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/users
|
||||
* Liste tous les utilisateurs (admin uniquement)
|
||||
*/
|
||||
router.get('/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const users = await db.getAllUsers();
|
||||
res.json({
|
||||
success: true,
|
||||
count: users.length,
|
||||
data: users
|
||||
});
|
||||
res.json({ success: true, count: users.length, data: users });
|
||||
} 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) => {
|
||||
try {
|
||||
await db.deleteUser(req.params.id);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Utilisateur supprimé'
|
||||
});
|
||||
res.json({ success: true, message: 'Utilisateur supprimé' });
|
||||
} 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
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/spots
|
||||
* Liste toutes les places
|
||||
*/
|
||||
router.get('/spots', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const spots = await db.getAllSpots();
|
||||
res.json({
|
||||
success: true,
|
||||
count: spots.length,
|
||||
data: spots
|
||||
});
|
||||
res.json({ success: true, count: spots.length, data: spots });
|
||||
} 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) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
const validStatuses = ['free', 'occupied', 'reserved'];
|
||||
|
||||
if (!status || !validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Statut invalide'
|
||||
});
|
||||
}
|
||||
|
||||
if (!['free', 'occupied', 'reserved'].includes(status))
|
||||
return res.status(400).json({ success: false, message: 'Statut invalide' });
|
||||
await db.updateSpotStatus(req.params.id, status);
|
||||
|
||||
// Ajouter à l'historique
|
||||
await db.addHistory('Mise à jour place', `Place ${req.params.id} - ${status}`, req.user.id);
|
||||
|
||||
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) {
|
||||
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) => {
|
||||
try {
|
||||
const { count } = req.body;
|
||||
const spotCount = count || 10;
|
||||
|
||||
// Supprimer les places existantes
|
||||
const spotCount = Math.min(Math.max(parseInt(req.body.count) || 10, 5), 50);
|
||||
await db.deleteAllSpots();
|
||||
|
||||
// Créer les nouvelles places
|
||||
for (let i = 1; i <= spotCount; i++) {
|
||||
let status = 'free';
|
||||
const rand = Math.random();
|
||||
if (rand > 0.85) status = 'reserved';
|
||||
else if (rand > 0.60) status = 'occupied';
|
||||
|
||||
await db.createSpot(i, `SENSOR_${String(i).padStart(3, '0')}`, status);
|
||||
}
|
||||
|
||||
await db.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) {
|
||||
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
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/reservations
|
||||
* Liste les réservations de l'utilisateur connecté
|
||||
*/
|
||||
router.get('/reservations', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const reservations = await db.getReservationsByUser(req.user.id);
|
||||
res.json({
|
||||
success: true,
|
||||
count: reservations.length,
|
||||
data: reservations
|
||||
});
|
||||
res.json({ success: true, count: reservations.length, data: reservations });
|
||||
} 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) => {
|
||||
try {
|
||||
const reservations = await db.getAllReservations();
|
||||
res.json({
|
||||
success: true,
|
||||
count: reservations.length,
|
||||
data: reservations
|
||||
});
|
||||
res.json({ success: true, count: reservations.length, data: reservations });
|
||||
} 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) => {
|
||||
try {
|
||||
const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body;
|
||||
|
||||
if (!spotId || !date || !startTime || !endTime || !duration || !price) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tous les champs sont requis'
|
||||
});
|
||||
}
|
||||
|
||||
// Vérifier que la place est libre
|
||||
if (!spotId || !date || !startTime || !endTime || !duration || !price)
|
||||
return res.status(400).json({ success: false, message: 'Tous les champs sont requis' });
|
||||
const spot = await db.getSpotById(spotId);
|
||||
if (!spot || spot.status !== 'free') {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: 'Cette place n\'est plus disponible'
|
||||
});
|
||||
}
|
||||
|
||||
// Générer un code de paiement
|
||||
if (!spot || spot.status !== 'free')
|
||||
return res.status(409).json({ success: false, message: "Cette place n'est plus disponible" });
|
||||
const paymentCode = 'PARK' + Date.now().toString().slice(-8);
|
||||
|
||||
// Créer la réservation
|
||||
const result = await db.createReservation(
|
||||
req.user.id,
|
||||
spotId,
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
duration,
|
||||
vehicle,
|
||||
price,
|
||||
paymentCode
|
||||
req.user.id, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode
|
||||
);
|
||||
|
||||
// Mettre à jour le statut de la place
|
||||
await db.updateSpotStatus(spotId, 'reserved');
|
||||
|
||||
// Ajouter à l'historique
|
||||
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
|
||||
}
|
||||
});
|
||||
await db.addHistory('Nouvelle réservation', `Place ${spot.number} - ${price}EUR`, req.user.id);
|
||||
res.status(201).json({ success: true, message: 'Réservation créée', data: { id: result.id, paymentCode } });
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur create reservation:', err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Erreur serveur'
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
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');
|
||||
|
||||
// TODO: Libérer la place associée
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Réservation annulée'
|
||||
});
|
||||
await db.updateSpotStatus(reservation.spot_id, 'free'); // ← BUG CORRIGÉ ICI
|
||||
await db.addHistory(
|
||||
'Annulation réservation',
|
||||
`Reservation #${req.params.id} annulee - place ${reservation.spot_id} liberee`,
|
||||
req.user.id
|
||||
);
|
||||
res.json({ success: true, message: 'Réservation annulée' });
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur cancel reservation:', err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Erreur serveur'
|
||||
});
|
||||
res.status(500).json({ 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
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/stats
|
||||
* Récupère les statistiques
|
||||
*/
|
||||
router.get('/stats', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const spots = await db.getAllSpots();
|
||||
const total = spots.length;
|
||||
const free = spots.filter(s => s.status === 'free').length;
|
||||
const free = spots.filter(s => s.status === 'free').length;
|
||||
const occupied = spots.filter(s => s.status === 'occupied').length;
|
||||
const reserved = spots.filter(s => s.status === 'reserved').length;
|
||||
const occupancyRate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total,
|
||||
free,
|
||||
occupied,
|
||||
reserved,
|
||||
occupancyRate
|
||||
}
|
||||
});
|
||||
res.json({ success: true, data: { total, free, occupied, reserved, occupancyRate } });
|
||||
} 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) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const history = await db.getHistory(limit);
|
||||
res.json({
|
||||
success: true,
|
||||
count: history.length,
|
||||
data: history
|
||||
});
|
||||
res.json({ success: true, count: history.length, data: history });
|
||||
} 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' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// STATUS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 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()
|
||||
});
|
||||
router.get('/status', (_req, res) => {
|
||||
res.json({ success: true, message: 'Smart Parking API operationnelle', version: '1.0.0', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user