Mise à jour

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

19
.env
View File

@@ -1,25 +1,10 @@
# ============================================================
# Smart Parking v2.0 — Variables d'environnement
# Copiez ce fichier en .env à la racine du projet
# ============================================================
# ── Base de données ──────────────────────────────────────────
DB_HOST=db
DB_PORT=3306
DB_USER=smartparking_user
DB_PASSWORD=smartparking_pass
DB_NAME=smartparking
# ── JWT ──────────────────────────────────────────────────────
# ⚠️ Changez cette valeur ! Mettez une chaîne longue et aléatoire.
JWT_SECRET=une_chaine_tres_longue_et_secrete_changez_moi_absolument
# ── Serveur ──────────────────────────────────────────────────
JWT_SECRET=une_chaine_tres_longue_et_secrete
PORT=3000
NODE_ENV=production
# ── MQTT (Mosquitto) ─────────────────────────────────────────
# Si Mosquitto tourne dans Docker (service "mqtt") → mqtt
# Si Mosquitto est installé directement sur le Pi → localhost
MQTT_HOST=mqtt
MQTT_HOST=172.18.0.1
MQTT_PORT=1883

375
README.md
View File

@@ -1,208 +1,239 @@
# 🅿️ Smart Parking
# Smart Parking
> Système complet de gestion de parking intelligent avec authentification, réservation et paiement QR code
Systeme de gestion et de surveillance de parking automatise avec detection en temps reel par capteurs infrarouges, reservation en ligne et administration complete.
## 📋 Fonctionnalités
Projet BTS CIEL IR — APON BARUA — Groupe scolaire La Salle Saint-Denis — 2025/2026
### 🔐 Authentification
- Inscription avec nom, email, téléphone et mot de passe
- Connexion sécurisée
- Gestion de profil
- Deux rôles : Client et Administrateur
## Fonctionnalites
### 🗺️ Carte du Parking
- **10 places** visuelles (modifiable par l'admin)
- 3 états : Libre ✅, Occupée 🚗, Réservée 📅
- Mise à jour en temps réel
- Détails de chaque place au clic
### Authentification
- Inscription et connexion securisees
- Mots de passe haches avec BCrypt
- Authentification par token JWT (expire apres 24h)
- Deux roles : Client et Administrateur
### 📅 Système de Réservation
- Sélection de la place
- Choix de la date et heure
- Durée : 30min, 1h, 2h, 4h, Journée
### Carte du Parking
- Affichage en temps reel de l'etat des places
- 3 etats : Libre, Occupee, Reservee
- Mise a jour automatique toutes les 3 secondes
- Details de chaque place au clic
### Detection automatique
- Capteurs infrarouges IR LM393 sur chaque place
- ESP32 connecte en WiFi au Raspberry Pi
- Communication MQTT vers le serveur
- Changement d'etat instantane sur le site
### Systeme de Reservation
- Selection de la place, date, heure et duree
- Verification des conflits d'horaire
- Saisie de la plaque d'immatriculation
- Expiration automatique des reservations
### 💳 Paiement QR Code
- Génération de QR code unique
- Code de paiement affiché
- Confirmation du paiement
### Panel Administrateur
- Statistiques globales du parking
- Gestion de l'etat de chaque place (1 a 20 places)
- Liste des utilisateurs avec suppression
- Gestion des reservations (terminer, annuler)
- Historique complet des actions
### 👤 Espace Client
- Consulter la carte des places
- Voir les tarifs
- Faire une réservation
- Voir l'historique des réservations
- Gérer son profil
## Tarifs
### ⚙️ Panel Admin
- Voir toutes les statistiques
- Modifier le nombre de places
- Gérer l'état de chaque place
- Voir tous les utilisateurs
- Voir toutes les réservations
- Annuler/terminer des réservations
- Voir l'historique complet
## 💰 Tarifs
| Durée | Prix |
| Duree | Prix |
|-------|------|
| 30 minutes | 2€ |
| 1 heure | 3€ |
| 2 heures | 5€ |
| 4 heures | 8€ |
| Journée (8h) | 15€ |
| Journee (8h) | 15€ |
## 🚀 Installation
### Prérequis
- Node.js 18+
- npm
### Étape 1 : Installer les dépendances
```bash
cd server
npm install
```
### Étape 2 : Démarrer le serveur
```bash
npm start
```
Pour le développement (avec redémarrage automatique) :
```bash
npm run dev
```
### Étape 3 : Accéder au site
Ouvrir un navigateur et aller sur :
```
http://localhost:3000
```
## 🔑 Compte par défaut
**Administrateur :**
- Email : `admin@smartparking.fr`
- Mot de passe : `admin123`
## 📁 Structure du projet
## Architecture technique
```
smart-parking/
├── index.html # Page de connexion/inscription
├── css/
│ ├── style.css # Styles globaux
│ ├── auth.css # Styles authentification
│ └── dashboard.css # Styles dashboard
├── js/
│ ├── auth.js # Gestion authentification
│ ├── dashboard.js # Gestion dashboard
│ ├── map.js # Carte des places
│ ├── reservation.js # Système de réservation
│ └── admin.js # Panel admin
├── pages/
│ └── dashboard.html # Dashboard principal
├── server/
│ ├── package.json # Dépendances Node.js
│ ├── server.js # Serveur principal
│ ├── db/
│ │ └── database.js # Gestion SQLite
│ ├── middleware/
│ │ └── auth.js # Middleware JWT
│ └── routes/
│ └── api.js # Routes API
└── README.md # Ce fichier
[Voiture] → [Capteur IR LM393] → [ESP32 WiFi] → [MQTT] → [Mosquitto]
→ [Node.js] → [MariaDB] → [API REST] → [Site web]
```
## 🔌 API REST
### Materiel
| Composant | Reference | Quantite |
|-----------|-----------|----------|
| Microcontroleur WiFi | ESP32 Freenove WROVER | 1 |
| Capteur infrarouge | Module IR LM393 | 3 |
| Serveur central | Raspberry Pi 4 | 1 |
| Cables | Dupont F-F et F-M | Plusieurs |
### Authentification
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| POST | `/api/register` | Inscription |
| POST | `/api/login` | Connexion |
### Configuration reseau
| Interface | Adresse IP | Utilisation |
|-----------|-----------|-------------|
| Ethernet | 172.16.60.40 | Reseau ecole |
| WiFi (hotspot) | 172.20.10.2 | Communication ESP32 |
| Docker bridge | 172.18.0.1 | Reseau interne Docker |
### Utilisateurs
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/api/users` | Liste des utilisateurs (admin) |
| DELETE | `/api/users/:id` | Supprimer un utilisateur (admin) |
### Ports
| Service | Port |
|---------|------|
| Node.js (HTTP) | 3000 |
| MariaDB | 3306 |
| Mosquitto (MQTT) | 1883 |
### Places
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/api/spots` | Liste des places |
| PUT | `/api/spots/:id/status` | Modifier le statut |
| POST | `/api/spots/init` | Réinitialiser les places (admin) |
## Technologies utilisees
### Réservations
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/api/reservations` | Mes réservations |
| GET | `/api/reservations/all` | Toutes les réservations (admin) |
| POST | `/api/reservations` | Créer une réservation |
| PUT | `/api/reservations/:id/cancel` | Annuler une réservation |
### Statistiques
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/api/stats` | Statistiques du parking |
| GET | `/api/history` | Historique (admin) |
## 🛠️ Technologies utilisées
### Frontend
- HTML5
- CSS3 (responsive)
- JavaScript vanilla
- Chart.js (graphiques)
- QRCode.js (génération QR)
### Hardware
- ESP32 Freenove WROVER (WiFi + GPIO)
- Capteurs IR LM393 (GPIO 15, 4, 12)
- Raspberry Pi 4 (serveur central)
### Backend
- Node.js
- Express.js
- SQLite3
- Node.js + Express.js
- MariaDB (base de donnees relationnelle)
- Mosquitto (broker MQTT)
- Docker + Docker Compose (conteneurisation)
- JWT (authentification)
- bcryptjs (hashage mots de passe)
- BCrypt (hachage mots de passe)
## 📱 Fonctionnement
### Frontend
- HTML5 / CSS3 / JavaScript (sans framework)
- Design responsive (PC et mobile)
### Pour les clients :
1. Créer un compte ou se connecter
2. Consulter la carte des places disponibles
3. Choisir une place libre
4. Sélectionner date, heure et durée
5. Scanner le QR code pour payer
6. La place est réservée !
### Outils
- Arduino IDE (programmation ESP32)
- Gitea + GitHub (versioning)
- Draw.io (schemas)
### Pour l'administrateur :
1. Se connecter avec le compte admin
2. Accéder au panel Admin
3. Voir toutes les statistiques
4. Gérer les places (cliquer pour changer l'état)
5. Modifier le nombre total de places
6. Gérer les utilisateurs et réservations
## Structure du projet
## 🔒 Sécurité
```
Parking/
├── index.html
├── css/
│ ├── style.css
│ ├── auth.css
│ └── dashboard.css
├── js/
│ ├── auth.js
│ ├── dashboard.js
│ ├── map.js
│ ├── reservation.js
│ └── admin.js
├── pages/
│ └── dashboard.html
├── server/
│ ├── server.js
│ ├── package.json
│ ├── db/
│ │ └── database.js
│ ├── middleware/
│ │ └── auth.js
│ └── routes/
│ └── api.js
├── init.sql
├── Dockerfile
├── docker-compose.yml
├── mosquitto.conf
└── README.md
```
- Mots de passe hashés avec bcrypt
- Authentification JWT
- Protection des routes sensibles
- Validation des données
## API REST
## 📝 Notes
### Authentification
| Methode | Endpoint | Description |
|---------|----------|-------------|
| POST | /api/register | Creer un compte |
| POST | /api/login | Se connecter |
- Les données sont stockées dans SQLite (`server/db/smart-parking.db`)
- Le système fonctionne aussi en mode offline (stockage local)
- La simulation automatique change l'état des places toutes les 5 secondes
### Utilisateurs
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | /api/users | Liste des utilisateurs (admin) |
| PUT | /api/users/profile | Modifier mon profil |
| DELETE | /api/users/:id | Supprimer un utilisateur (admin) |
### Places
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | /api/spots | Liste des places |
| PUT | /api/spots/:id/status | Modifier le statut |
| POST | /api/spots/init | Reinitialiser les places (admin) |
### Reservations
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | /api/reservations | Mes reservations |
| GET | /api/reservations/all | Toutes les reservations (admin) |
| POST | /api/reservations | Creer une reservation |
| PUT | /api/reservations/:id/cancel | Annuler une reservation |
| PUT | /api/reservations/:id/complete | Terminer une reservation (admin) |
### Statistiques
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | /api/stats | Statistiques du parking |
| GET | /api/history | Historique (admin) |
## Base de donnees (6 tables)
| Table | Description |
|-------|-------------|
| users | Comptes utilisateurs (id, name, email, password, role) |
| spots | Places de parking (id, number, status, sensor_id) |
| reservations | Reservations (user_id, spot_id, date, start_time, end_time, vehicle, price) |
| history | Historique des actions (action, details, user_id, timestamp) |
| stats | Statistiques periodiques (occupation, taux) |
| mqtt_events | Messages MQTT recus (topic, message, received_at) |
## Installation et demarrage
### 1. Demarrer le hotspot WiFi sur le telephone
### 2. Sur le Raspberry Pi
```bash
sudo systemctl start mosquitto
sudo iptables -I INPUT -s 172.18.0.0/16 -p tcp --dport 1883 -j ACCEPT
sudo iptables -I INPUT -s 172.17.0.0/16 -p tcp --dport 1883 -j ACCEPT
cd /home/aponlucas/Parking
docker compose up -d
```
### 3. Televerser le code ESP32
Ouvrir Arduino IDE, selectionner ESP32 Wrover Module, televerser le code.
### 4. Acceder au site
```
http://172.20.10.2:3000
```
## Compte administrateur par defaut
- Email : admin@smartparking.fr
- Mot de passe : admin123
## Securite
- Mots de passe haches avec BCrypt (10 rounds)
- Authentification JWT avec expiration 24h
- Middleware de verification sur toutes les routes protegees
- Fichier .env non versionne (secrets exclus de GitHub)
- Regles iptables pour le pare-feu Linux
## Chiffres cles
| Metrique | Valeur |
|----------|--------|
| Lignes de code | ~3 900 |
| Fichiers | 17 |
| Langages | JavaScript, HTML, CSS, C++, SQL |
| Endpoints API | 12 |
| Tables MariaDB | 6 |
| Delai de mise a jour | Moins de 3 secondes |
## Equipe
| Membre | Role |
|--------|------|
| APON BARUA | IoT, Web, Backend, BDD, Docker, Securite |
| Lucas | Electronique, Arduino, Barrieres, Schemas |
| Mahmoud | Camera de surveillance |
---
<p align="center">
🅿️ <strong>Smart Parking - BTS CIEL IR 2025</strong> 🅿️
</p>
Smart Parking — BTS CIEL IR — APON BARUA — 2025/2026

View File

@@ -1,7 +1,3 @@
/* ============================================
AUTHENTIFICATION - STYLES
============================================ */
.auth-page {
min-height: 100vh;
display: flex;
@@ -149,7 +145,6 @@
color: var(--success);
}
/* Responsive */
@media (max-width: 480px) {
.auth-box {
padding: 24px;

View File

@@ -1,8 +1,3 @@
/* ============================================
DASHBOARD - STYLES SPÉCIFIQUES
============================================ */
/* Reservation form */
.reservation-form-container {
max-width: 600px;
margin: 0 auto;
@@ -35,14 +30,14 @@
color: var(--primary-light);
}
/* Chart container */
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
/* Admin table actions */
.table-actions {
display: flex;
gap: 8px;
@@ -53,7 +48,7 @@
font-size: 1rem;
}
/* Status badges */
.badge {
display: inline-block;
padding: 4px 12px;
@@ -73,7 +68,7 @@
color: var(--info);
}
/* Animation pour les mises à jour */
@keyframes pulse-update {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
@@ -83,7 +78,6 @@
animation: pulse-update 0.3s ease;
}
/* Loading state */
.loading {
position: relative;
pointer-events: none;

View File

@@ -1,45 +1,40 @@
/* ============================================
SMART PARKING - STYLES GLOBAUX
============================================ */
:root {
/* Couleurs principales */
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #818cf8;
/* Couleurs états */
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #06b6d4;
/* Couleurs places */
--spot-free: #10b981;
--spot-occupied: #ef4444;
--spot-reserved: #3b82f6;
/* Fonds */
--bg-dark: #0f172a;
--bg-darker: #020617;
--bg-card: #1e293b;
--bg-hover: #334155;
/* Texte */
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
/* Bordures */
--border: #334155;
--border-radius: 12px;
--border-radius-sm: 8px;
/* Ombres */
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
/* Transitions */
--transition: all 0.3s ease;
}
@@ -67,9 +62,7 @@ body {
padding: 0 20px;
}
/* ============================================
HEADER
============================================ */
.header {
background: var(--bg-card);
border-bottom: 1px solid var(--border);
@@ -110,7 +103,7 @@ body {
-webkit-text-fill-color: transparent;
}
/* Navigation */
.nav {
display: flex;
gap: 8px;
@@ -143,7 +136,7 @@ body {
font-size: 1.1rem;
}
/* Header right */
.header-right {
display: flex;
align-items: center;
@@ -166,9 +159,7 @@ body {
color: var(--text-muted);
}
/* ============================================
MAIN & PAGES
============================================ */
.main {
padding: 30px 0;
min-height: calc(100vh - 80px);
@@ -201,9 +192,7 @@ body {
font-size: 1.5rem;
}
/* ============================================
BOUTONS
============================================ */
.btn {
display: inline-flex;
align-items: center;
@@ -262,9 +251,7 @@ body {
cursor: not-allowed;
}
/* ============================================
FORMULAIRES
============================================ */
.form-group {
margin-bottom: 16px;
}
@@ -317,9 +304,6 @@ select.form-control {
flex: 1;
}
/* ============================================
CARTES
============================================ */
.card {
background: var(--bg-card);
border-radius: var(--border-radius);
@@ -328,9 +312,7 @@ select.form-control {
box-shadow: var(--shadow);
}
/* ============================================
STATS GRID
============================================ */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -392,9 +374,7 @@ select.form-control {
margin-top: 4px;
}
/* ============================================
PARKING MAP
============================================ */
.parking-section {
display: grid;
grid-template-columns: 1fr 350px;
@@ -446,7 +426,7 @@ select.form-control {
.parking-spot .spot-number { font-size: 1.1rem; }
.parking-spot .spot-icon { font-size: 1.3rem; }
/* Legend */
.legend {
display: flex;
justify-content: center;
@@ -471,7 +451,7 @@ select.form-control {
.legend-color.occupied { background: var(--spot-occupied); }
.legend-color.reserved { background: var(--spot-reserved); }
/* Spot details */
.spot-details { min-height: 200px; }
.no-selection {
@@ -500,9 +480,7 @@ select.form-control {
.spot-status-occupied { color: var(--spot-occupied); }
.spot-status-reserved { color: var(--spot-reserved); }
/* ============================================
PRICING
============================================ */
.pricing-section {
background: var(--bg-card);
border-radius: var(--border-radius);
@@ -547,9 +525,7 @@ select.form-control {
color: var(--primary-light);
}
/* ============================================
MODAL
============================================ */
.modal {
position: fixed;
top: 0;
@@ -597,7 +573,7 @@ select.form-control {
.modal-body { padding: 24px; }
/* Récapitulatif réservation */
.payment-summary {
background: var(--bg-dark);
border-radius: var(--border-radius-sm);
@@ -625,9 +601,7 @@ select.form-control {
font-size: 1.1rem;
}
/* ============================================
MESSAGE DE CONFIRMATION (remplace QR code)
============================================ */
.confirmation-message {
text-align: center;
padding: 24px 16px;
@@ -658,9 +632,7 @@ select.form-control {
color: var(--text-primary);
}
/* ============================================
RESERVATIONS LIST
============================================ */
.reservations-list {
display: flex;
flex-direction: column;
@@ -739,9 +711,7 @@ select.form-control {
margin-bottom: 20px;
}
/* ============================================
PROFILE
============================================ */
.profile-container {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -813,9 +783,7 @@ select.form-control {
color: var(--text-muted);
}
/* ============================================
ADMIN
============================================ */
.admin-page .admin-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -882,7 +850,7 @@ select.form-control {
.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; }
.data-table {
@@ -908,7 +876,7 @@ select.form-control {
.data-table tr:hover td { background: var(--bg-hover); }
/* Log container */
.log-container {
max-height: 300px;
overflow-y: auto;
@@ -933,9 +901,7 @@ select.form-control {
font-size: 0.75rem;
}
/* ============================================
TOAST NOTIFICATIONS
============================================ */
.toast-container {
position: fixed;
bottom: 20px;
@@ -965,17 +931,13 @@ select.form-control {
to { transform: translateX(0); opacity: 1; }
}
/* ============================================
UTILITAIRES
============================================ */
.hidden { display: none !important; }
.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; }
@@ -1004,7 +966,6 @@ select.form-control {
.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; }

View File

@@ -1,22 +1,13 @@
version: '3.8'
# ============================================================
# Smart Parking v2.0 — Docker Compose
# Services : MariaDB + App Node.js + Mosquitto MQTT
# ============================================================
services:
# ── Base de données MariaDB ────────────────────────────────
db:
image: mariadb:10.11
container_name: smartparking-db
restart: always
environment:
MARIADB_ROOT_PASSWORD: rootpassword # ⚠️ À changer en production
MARIADB_ROOT_PASSWORD: rootpassword
MARIADB_DATABASE: smartparking
MARIADB_USER: smartparking_user
MARIADB_PASSWORD: smartparking_pass # ⚠️ À changer en production
MARIADB_PASSWORD: smartparking_pass
volumes:
- db_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
@@ -27,25 +18,6 @@ services:
timeout: 10s
retries: 5
# ── Broker MQTT Mosquitto ──────────────────────────────────
# Si vous avez déjà Mosquitto installé directement sur le Pi
# (pas dans Docker), commentez ce bloc et mettez
# MQTT_HOST=localhost dans la section "app" ci-dessous.
mqtt:
image: eclipse-mosquitto:2
container_name: smartparking-mqtt
restart: always
ports:
- "1883:1883" # Port MQTT (Arduino se connecte ici)
- "9001:9001" # Port WebSocket (optionnel)
volumes:
- ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf
- mosquitto_data:/mosquitto/data
- mosquitto_log:/mosquitto/log
networks:
- smartparking-network
# ── Application Node.js ────────────────────────────────────
app:
build: .
container_name: smartparking-app
@@ -55,31 +27,23 @@ services:
depends_on:
db:
condition: service_healthy
mqtt:
condition: service_started
environment:
# Base de données
DB_HOST: db
DB_PORT: 3306
DB_USER: smartparking_user
DB_HOST: db
DB_PORT: 3306
DB_USER: smartparking_user
DB_PASSWORD: smartparking_pass
DB_NAME: smartparking
# JWT
JWT_SECRET: ${JWT_SECRET:-une_chaine_tres_longue_et_secrete_changez_moi}
# MQTT — utiliser "mqtt" si Mosquitto est dans Docker
# utiliser "localhost" si Mosquitto est installé directement sur le Pi
MQTT_HOST: mqtt
MQTT_PORT: 1883
# Environnement
DB_NAME: smartparking
JWT_SECRET: une_chaine_tres_longue_et_secrete
NODE_ENV: production
PORT: 3000
MQTT_HOST: 172.18.0.1
MQTT_PORT: 1883
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- smartparking-network
volumes:
db_data:
mosquitto_data:
mosquitto_log:
networks:
smartparking-network:

View File

@@ -16,7 +16,7 @@
<p>Gestion intelligente de parking</p>
</div>
<!-- Formulaire de connexion -->
<form id="loginForm" class="auth-form">
<h2>Connexion</h2>
<div class="form-group">
@@ -37,7 +37,7 @@
</p>
</form>
<!-- Formulaire d'inscription -->
<form id="registerForm" class="auth-form hidden">
<h2>Créer un compte</h2>
<div class="form-group">
@@ -70,7 +70,7 @@
</p>
</form>
<!-- Message d'erreur -->
<div id="authMessage" class="auth-message hidden"></div>
</div>
</div>

View File

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

View File

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

View File

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

117
js/map.js
View File

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

View File

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

View File

@@ -6,10 +6,8 @@
<title>Smart Parking - Dashboard</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="../css/dashboard.css">
<!-- Chart.js retiré car le graphique d'occupation a été supprimé -->
</head>
<body>
<!-- Header -->
<header class="header">
<div class="container">
<div class="header-left">
@@ -49,7 +47,7 @@
<main class="main">
<!-- ═══ PAGE : CARTE DES PLACES ═══ -->
<section id="map" class="page active">
<div class="container">
<h2 class="page-title"><span class="icon">🗺️</span>Carte du Parking</h2>
@@ -69,17 +67,10 @@
<span class="stat-label">Places occupées</span>
</div>
</div>
<div class="stat-card reserved">
<div class="stat-icon">📅</div>
<div class="stat-content">
<span class="stat-value" id="reservedCount">0</span>
<span class="stat-label">Places réservées</span>
</div>
</div>
<div class="stat-card total">
<div class="stat-icon">🅿️</div>
<div class="stat-content">
<span class="stat-value" id="totalCount">10</span>
<span class="stat-value" id="totalCount">3</span>
<span class="stat-label">Total places</span>
</div>
</div>
@@ -92,7 +83,6 @@
<div class="legend">
<div class="legend-item"><span class="legend-color free"></span><span>Libre</span></div>
<div class="legend-item"><span class="legend-color occupied"></span><span>Occupée</span></div>
<div class="legend-item"><span class="legend-color reserved"></span><span>Réservée</span></div>
</div>
</div>
<div class="spot-details-container">
@@ -116,7 +106,7 @@
</div>
</section>
<!-- ═══ PAGE : RÉSERVATION ═══ -->
<section id="reservation" class="page hidden">
<div class="container">
<h2 class="page-title"><span class="icon">📅</span>Réserver une place</h2>
@@ -167,7 +157,7 @@
</form>
</div>
<!-- Modal de confirmation -->
<div id="confirmationModal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
@@ -198,7 +188,7 @@
</div>
</section>
<!-- ═══ PAGE : MES RÉSERVATIONS ═══ -->
<section id="my-reservations" class="page hidden">
<div class="container">
<h2 class="page-title"><span class="icon">🎫</span>Mes réservations</h2>
@@ -211,7 +201,7 @@
</div>
</section>
<!-- ═══ PAGE : PROFIL ═══ -->
<section id="profile" class="page hidden">
<div class="container">
<h2 class="page-title"><span class="icon">👤</span>Mon profil</h2>
@@ -266,12 +256,12 @@
</div>
</section>
<!-- ═══ PAGE : ADMIN ═══ -->
<section id="admin" class="page hidden admin-page">
<div class="container">
<h2 class="page-title"><span class="icon">⚙️</span>Administration</h2>
<!-- Stats admin -->
<div class="admin-stats-grid">
<div class="admin-stat">
<span class="admin-stat-value" id="adminTotalUsers">0</span>
@@ -291,7 +281,7 @@
</div>
</div>
<!-- Gestion des places -->
<div class="admin-section">
<h3>🅿️ Gestion des places</h3>
<div class="admin-places-control">
@@ -299,7 +289,7 @@
<label>Nombre total de places</label>
<div class="input-group">
<input type="number" id="adminTotalSpots" class="form-control"
min="5" max="50" value="10">
min="1" max="20" value="10">
<button id="updateSpotsBtn" class="btn btn-primary">Mettre à jour</button>
</div>
</div>
@@ -307,7 +297,7 @@
<div class="admin-places-list" id="adminPlacesList"></div>
</div>
<!-- Utilisateurs -->
<div class="admin-section">
<h3>👥 Utilisateurs</h3>
<div class="table-container">
@@ -323,7 +313,7 @@
</div>
</div>
<!-- Réservations -->
<div class="admin-section">
<h3>📅 Toutes les réservations</h3>
<div class="table-container">
@@ -339,9 +329,8 @@
</div>
</div>
<!-- ═══ GRAPHIQUE D'OCCUPATION SUPPRIMÉ ici ═══ -->
<!-- Historique -->
<div class="admin-section">
<h3>📜 Historique</h3>
<div class="log-container" id="adminLogContainer"></div>
@@ -358,6 +347,5 @@
<script src="../js/map.js"></script>
<script src="../js/reservation.js"></script>
<script src="../js/admin.js"></script>
<!-- Chart.js et admin.js n'utilisent plus le graphique -->
</body>
</html>

View File

@@ -1,13 +1,3 @@
/**
* ============================================
* DATABASE.JS - Gestion MariaDB
* Smart Parking v3.0
* AJOUTÉ : checkReservationConflict()
* → vérifie les conflits d'horaire
* au lieu de bloquer toute la place
* ============================================
*/
const mysql = require('mysql2/promise');
const bcrypt = require('bcryptjs');
require('dotenv').config();
@@ -23,9 +13,6 @@ const pool = mysql.createPool({
queueLimit: 0
});
// ============================================
// INITIALISATION
// ============================================
async function initDatabase() {
try {
@@ -105,7 +92,6 @@ async function initDatabase() {
console.log('✅ Tables vérifiées/créées');
// 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);
@@ -116,7 +102,7 @@ async function initDatabase() {
console.log('✅ Admin par défaut créé');
}
// 10 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++) {
@@ -134,9 +120,6 @@ async function initDatabase() {
}
}
// ============================================
// UTILISATEURS
// ============================================
async function createUser(name, email, phone, hashedPassword, role = 'client') {
const [result] = await pool.query(
@@ -180,9 +163,6 @@ async function deleteUser(id) {
return { deleted: result.affectedRows };
}
// ============================================
// PLACES
// ============================================
async function createSpot(number, sensorId, status = 'free') {
const [result] = await pool.query(
@@ -215,9 +195,6 @@ async function deleteAllSpots() {
return { deleted: result.affectedRows };
}
// ============================================
// RÉSERVATIONS
// ============================================
async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) {
const [result] = await pool.query(
@@ -229,21 +206,7 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat
return { id: result.insertId };
}
/**
* ⭐ NOUVELLE FONCTION — Vérification des conflits d'horaire
*
* Problème corrigé : avant, quand une place était réservée,
* elle restait bloquée pour TOUS les jours et TOUTES les heures.
*
* Maintenant on vérifie uniquement s'il y a une réservation
* qui se chevauche sur la MÊME date et le MÊME créneau horaire.
*
* Exemple :
* Place 2 réservée aujourd'hui 10h-11h ✅
* Place 2 réservée aujourd'hui 14h-15h ✅ (pas de conflit)
* Place 2 réservée demain 10h-11h ✅ (pas de conflit)
* Place 2 réservée aujourd'hui 10h30-11h30 ❌ (conflit !)
*/
async function checkReservationConflict(spotId, date, startTime, endTime) {
const [rows] = await pool.query(`
SELECT id FROM reservations
@@ -254,7 +217,7 @@ async function checkReservationConflict(spotId, date, startTime, endTime) {
AND end_time > ?
`, [spotId, date, endTime, startTime]);
return rows.length > 0; // true = conflit, false = créneau libre
return rows.length > 0;
}
async function getReservationById(id) {
@@ -299,10 +262,7 @@ async function updateReservationStatus(id, status) {
return { changed: result.affectedRows };
}
/**
* Expiration automatique des réservations
* Appelée toutes les 60 secondes par server.js
*/
async function expireReservations() {
const [expiredRows] = await pool.query(`
SELECT r.id, r.spot_id, r.user_id, s.number AS spot_number
@@ -334,9 +294,36 @@ async function expireReservations() {
return expiredRows.length;
}
// ============================================
// HISTORIQUE
// ============================================
async function cleanupStaleReservedSpots() {
try {
const [staleSpots] = await pool.query(`
SELECT s.id, s.number FROM spots s
WHERE s.status = 'reserved'
AND s.id NOT IN (
SELECT r.spot_id FROM reservations r
WHERE r.status IN ('active', 'pending')
AND r.date = CURDATE()
AND r.start_time <= CURTIME()
AND r.end_time > CURTIME()
)
`);
for (const spot of staleSpots) {
await pool.query(
"UPDATE spots SET status = 'free', last_update = NOW() WHERE id = ?",
[spot.id]
);
console.log(`🧹 Place ${spot.number} nettoyée : reserved → free (pas de réservation en cours)`);
}
return staleSpots.length;
} catch (err) {
console.error('❌ Erreur cleanup stale spots:', err.message);
return 0;
}
}
async function addHistory(action, details, userId = null) {
const [result] = await pool.query(
@@ -358,9 +345,6 @@ async function getHistory(limit = 50) {
return rows;
}
// ============================================
// STATISTIQUES
// ============================================
async function recordStats(total, free, occupied, reserved) {
const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
@@ -381,9 +365,7 @@ async function getStats(days = 7) {
return rows;
}
// ============================================
// MQTT
// ============================================
async function recordMqttEvent(topic, message) {
const [result] = await pool.query(
@@ -393,9 +375,6 @@ async function recordMqttEvent(topic, message) {
return { id: result.insertId };
}
// ============================================
// FERMETURE
// ============================================
async function closeDatabase() {
await pool.end();
@@ -411,7 +390,7 @@ module.exports = {
createReservation, checkReservationConflict,
getReservationById, getReservationsByUser,
getAllReservations, updateReservationStatus,
expireReservations,
expireReservations, cleanupStaleReservedSpots,
addHistory, getHistory,
recordStats, getStats,
recordMqttEvent

View File

@@ -1,22 +1,10 @@
/**
* ============================================
* API ROUTES - Routes de l'API REST
* Smart Parking v3.0
* CORRIGÉ : réservation vérifie les conflits
* d'horaire au lieu de bloquer la place
* définitivement
* ============================================
*/
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const db = require('../db/database');
const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth');
// ============================================
// AUTHENTIFICATION
// ============================================
router.post('/register', async (req, res) => {
try {
@@ -58,9 +46,7 @@ router.post('/login', async (req, res) => {
}
});
// ============================================
// UTILISATEURS
// ============================================
router.get('/users', authenticateToken, requireAdmin, async (req, res) => {
try {
@@ -80,12 +66,48 @@ router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) =>
}
});
// ============================================
// PLACES
// ============================================
router.put('/users/profile', authenticateToken, async (req, res) => {
try {
const { phone, password } = req.body;
const userId = req.user.id;
const updates = {};
if (phone !== undefined) updates.phone = phone;
if (password && password.length >= 8) {
updates.password = await bcrypt.hash(password, 10);
} else if (password && password.length < 8) {
return res.status(400).json({
success: false,
message: 'Le mot de passe doit faire au moins 8 caractères'
});
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ success: false, message: 'Aucune modification fournie' });
}
await db.updateUser(userId, updates);
const updatedUser = await db.getUserById(userId);
await db.addHistory('Modification profil', `Utilisateur #${userId} a modifié son profil`, userId);
res.json({
success: true, message: 'Profil mis à jour avec succès',
user: { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email, phone: updatedUser.phone, role: updatedUser.role }
});
} catch (err) {
console.error('❌ Erreur update profile:', err.message);
res.status(500).json({ success: false, message: 'Erreur serveur' });
}
});
router.get('/spots', authenticateToken, async (req, res) => {
try {
await db.cleanupStaleReservedSpots();
const spots = await db.getAllSpots();
res.json({ success: true, count: spots.length, data: spots });
} catch (err) {
@@ -99,16 +121,17 @@ router.put('/spots/:id/status', authenticateToken, async (req, res) => {
if (!['free', 'occupied', 'reserved'].includes(status))
return res.status(400).json({ success: false, message: 'Statut invalide' });
await db.updateSpotStatus(req.params.id, status);
await db.addHistory('Mise à jour place', `Place ${req.params.id} -> ${status}`, req.user.id);
await db.addHistory('Mise à jour place', `Place ${req.params.id} ${status}`, req.user.id);
res.json({ success: true, message: 'Statut mis à jour' });
} catch (err) {
res.status(500).json({ success: false, message: 'Erreur serveur' });
}
});
router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => {
try {
const spotCount = Math.min(Math.max(parseInt(req.body.count) || 10, 5), 50);
const spotCount = Math.min(Math.max(parseInt(req.body.count) || 10, 1), 20);
await db.deleteAllSpots();
for (let i = 1; i <= spotCount; i++) {
await db.createSpot(i, `SENSOR_${String(i).padStart(3, '0')}`, 'free');
@@ -120,9 +143,6 @@ router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) =>
}
});
// ============================================
// RÉSERVATIONS
// ============================================
router.get('/reservations', authenticateToken, async (req, res) => {
try {
@@ -142,19 +162,7 @@ router.get('/reservations/all', authenticateToken, requireAdmin, async (req, res
}
});
/**
* POST /api/reservations
*
* CORRIGÉ : on ne bloque plus la place entière définitivement.
* On vérifie uniquement s'il y a un CONFLIT d'horaire sur
* la même date et le même créneau.
*
* Exemple de ce qui est maintenant possible :
* Place 2 — 10h-11h aujourd'hui ✅
* Place 2 — 14h-15h aujourd'hui ✅ (pas de conflit)
* Place 2 — 10h-11h demain ✅ (pas de conflit)
* Place 2 — 10h30-11h30 aujourd'hui ❌ (conflit !)
*/
router.post('/reservations', authenticateToken, async (req, res) => {
try {
const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body;
@@ -166,16 +174,15 @@ router.post('/reservations', authenticateToken, async (req, res) => {
if (!spot)
return res.status(404).json({ success: false, message: 'Place introuvable' });
// CORRIGÉ : bloquer uniquement si une voiture est physiquement là
if (spot.status === 'occupied')
return res.status(409).json({ success: false, message: "Une voiture est déjà sur cette place" });
return res.status(409).json({ success: false, message: "Une voiture est physiquement sur cette place" });
// CORRIGÉ : vérifier les conflits d'horaire au lieu du statut global
const conflict = await db.checkReservationConflict(spotId, date, startTime, endTime);
if (conflict)
return res.status(409).json({
success: false,
message: `Cette place est déjà réservée sur ce créneau. Choisissez un autre horaire ou une autre date.`
message: 'Cette place est déjà réservée sur ce créneau horaire. Choisissez un autre horaire ou une autre date.'
});
const paymentCode = 'PARK' + Date.now().toString().slice(-8);
@@ -183,18 +190,14 @@ router.post('/reservations', authenticateToken, async (req, res) => {
req.user.id, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode
);
// On ne change le statut de la place QUE si la réservation est pour aujourd'hui
// et que l'heure de début est maintenant ou dans moins de 30 minutes
const today = new Date().toISOString().split('T')[0];
const now = new Date();
const today = now.toISOString().split('T')[0];
const resStart = new Date(`${date}T${startTime}`);
const diffMin = (resStart - now) / 60000;
if (date === today && diffMin <= 30) {
await db.updateSpotStatus(spotId, 'reserved');
}
// Pour une réservation future, le statut de la place reste inchangé
// Le timer d'expiration (server.js) le mettra à jour au bon moment
await db.addHistory(
'Nouvelle réservation',
@@ -213,9 +216,6 @@ router.post('/reservations', authenticateToken, async (req, res) => {
}
});
/**
* PUT /api/reservations/:id/cancel
*/
router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => {
try {
const reservation = await db.getReservationById(req.params.id);
@@ -238,9 +238,6 @@ router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => {
}
});
/**
* PUT /api/reservations/:id/complete (admin)
*/
router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async (req, res) => {
try {
const reservation = await db.getReservationById(req.params.id);
@@ -261,10 +258,6 @@ router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async
}
});
// ============================================
// STATISTIQUES
// ============================================
router.get('/stats', authenticateToken, async (req, res) => {
try {
const spots = await db.getAllSpots();
@@ -290,7 +283,7 @@ router.get('/history', authenticateToken, requireAdmin, async (req, res) => {
});
router.get('/status', (_req, res) => {
res.json({ success: true, message: 'Smart Parking API opérationnelle', version: '3.0.0', timestamp: new Date().toISOString() });
res.json({ success: true, message: 'Smart Parking API opérationnelle', version: '4.1.0', timestamp: new Date().toISOString() });
});
module.exports = router;

View File

@@ -1,10 +1,3 @@
/**
* ============================================
* SERVER.JS - Serveur principal Smart Parking
* VERSION 2.0 - MQTT Arduino + Expiration réservations
* ============================================
*/
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
@@ -27,27 +20,10 @@ app.use('/api', apiRoutes);
app.get('/', (_req, res) => res.sendFile(path.join(__dirname, '..', 'index.html')));
app.get('/dashboard', (_req, res) => res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html')));
// ============================================
// CONNEXION MQTT (Mosquitto sur le Raspberry Pi)
// ============================================
/**
* Topics attendus depuis l'Arduino :
*
* smartparking/sensor/1 → "1" = voiture détectée (occupée)
* smartparking/sensor/1 → "0" = place libre
*
* L'Arduino publie sur ce topic via le shield Ethernet/WiFi.
* Le numéro à la fin correspond au numéro de place (1 à N).
*
* Pour tester sans Arduino (ligne de commande sur le Pi) :
* mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "1"
* mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "0"
*/
const MQTT_HOST = process.env.MQTT_HOST || 'localhost';
const MQTT_PORT = process.env.MQTT_PORT || 1883;
const MQTT_TOPIC = 'smartparking/sensor/#'; // # = wildcard, reçoit tous les capteurs
const MQTT_TOPIC = 'smartparking/sensor/#';
let mqttClient = null;
@@ -58,7 +34,7 @@ function connectMQTT() {
mqttClient = mqtt.connect(brokerUrl, {
clientId: 'smartparking-server-' + Math.random().toString(16).slice(3),
keepalive: 60,
reconnectPeriod: 5000, // Reconnexion automatique toutes les 5s si coupure
reconnectPeriod: 5000,
connectTimeout: 10000
});
@@ -73,11 +49,11 @@ function connectMQTT() {
});
});
// Message reçu depuis un capteur Arduino
mqttClient.on('message', async (topic, messageBuffer) => {
const message = messageBuffer.toString().trim();
const topicParts = topic.split('/'); // ['smartparking', 'sensor', '1']
const spotNumber = parseInt(topicParts[2]); // numéro de place
const topicParts = topic.split('/');
const spotNumber = parseInt(topicParts[2]);
if (isNaN(spotNumber)) {
console.warn(`⚠️ Topic MQTT invalide : ${topic}`);
@@ -86,11 +62,11 @@ function connectMQTT() {
console.log(`📩 MQTT reçu → topic: ${topic} | valeur: ${message}`);
// "1" = voiture présente → occupée | "0" = libre
const newStatus = message === '1' ? 'occupied' : 'free';
try {
// Récupérer la place par son numéro
const spots = await db.getAllSpots();
const spot = spots.find(s => s.number === spotNumber);
@@ -99,15 +75,14 @@ function connectMQTT() {
return;
}
// Ne pas écraser une place RÉSERVÉE avec un simple signal capteur
// (la réservation prime sur le capteur physique)
if (spot.status === 'reserved' && newStatus === 'occupied') {
console.log(` Place ${spotNumber} déjà réservée — capteur ignoré`);
await db.recordMqttEvent(topic, message);
return;
}
// Mettre à jour en base
await db.updateSpotStatus(spot.id, newStatus);
await db.recordMqttEvent(topic, message);
@@ -130,9 +105,7 @@ function connectMQTT() {
});
}
// ============================================
// DÉMARRAGE DU SERVEUR
// ============================================
async function startServer() {
try {
@@ -153,10 +126,10 @@ async function startServer() {
`);
});
// 3. Connecter le client MQTT (broker Mosquitto)
connectMQTT();
// 4. Enregistrer les statistiques toutes les 5 minutes
setInterval(async () => {
try {
const spots = await db.getAllSpots();
@@ -170,8 +143,7 @@ async function startServer() {
}
}, 5 * 60 * 1000);
// 5. ⭐ EXPIRATION AUTOMATIQUE DES RÉSERVATIONS toutes les minutes
// Libère les places dont l'heure de fin est dépassée
setInterval(async () => {
try {
const count = await db.expireReservations();
@@ -181,7 +153,7 @@ async function startServer() {
} catch (err) {
console.error('❌ Erreur expiration réservations :', err.message);
}
}, 60 * 1000); // toutes les 60 secondes
}, 60 * 1000);
} catch (err) {
console.error('❌ Erreur au démarrage :', err);
@@ -189,7 +161,7 @@ async function startServer() {
}
}
// Arrêt propre
process.on('SIGINT', async () => {
console.log('\n🛑 Arrêt du serveur...');
if (mqttClient) mqttClient.end();