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_HOST=db
DB_PORT=3306 DB_PORT=3306
DB_USER=smartparking_user DB_USER=smartparking_user
DB_PASSWORD=smartparking_pass DB_PASSWORD=smartparking_pass
DB_NAME=smartparking DB_NAME=smartparking
JWT_SECRET=une_chaine_tres_longue_et_secrete
# ── JWT ──────────────────────────────────────────────────────
# ⚠️ Changez cette valeur ! Mettez une chaîne longue et aléatoire.
JWT_SECRET=une_chaine_tres_longue_et_secrete_changez_moi_absolument
# ── Serveur ──────────────────────────────────────────────────
PORT=3000 PORT=3000
NODE_ENV=production NODE_ENV=production
MQTT_HOST=172.18.0.1
# ── MQTT (Mosquitto) ─────────────────────────────────────────
# Si Mosquitto tourne dans Docker (service "mqtt") → mqtt
# Si Mosquitto est installé directement sur le Pi → localhost
MQTT_HOST=mqtt
MQTT_PORT=1883 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 ## Fonctionnalites
- Inscription avec nom, email, téléphone et mot de passe
- Connexion sécurisée
- Gestion de profil
- Deux rôles : Client et Administrateur
### 🗺️ Carte du Parking ### Authentification
- **10 places** visuelles (modifiable par l'admin) - Inscription et connexion securisees
- 3 états : Libre ✅, Occupée 🚗, Réservée 📅 - Mots de passe haches avec BCrypt
- Mise à jour en temps réel - Authentification par token JWT (expire apres 24h)
- Détails de chaque place au clic - Deux roles : Client et Administrateur
### 📅 Système de Réservation ### Carte du Parking
- Sélection de la place - Affichage en temps reel de l'etat des places
- Choix de la date et heure - 3 etats : Libre, Occupee, Reservee
- Durée : 30min, 1h, 2h, 4h, Journée - 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 - Saisie de la plaque d'immatriculation
- Expiration automatique des reservations
### 💳 Paiement QR Code ### Panel Administrateur
- Génération de QR code unique - Statistiques globales du parking
- Code de paiement affiché - Gestion de l'etat de chaque place (1 a 20 places)
- Confirmation du paiement - Liste des utilisateurs avec suppression
- Gestion des reservations (terminer, annuler)
- Historique complet des actions
### 👤 Espace Client ## Tarifs
- Consulter la carte des places
- Voir les tarifs
- Faire une réservation
- Voir l'historique des réservations
- Gérer son profil
### ⚙️ Panel Admin | Duree | Prix |
- 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 |
|-------|------| |-------|------|
| 30 minutes | 2€ | | 30 minutes | 2€ |
| 1 heure | 3€ | | 1 heure | 3€ |
| 2 heures | 5€ | | 2 heures | 5€ |
| 4 heures | 8€ | | 4 heures | 8€ |
| Journée (8h) | 15€ | | Journee (8h) | 15€ |
## 🚀 Installation ## Architecture technique
### 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
``` ```
smart-parking/ [Voiture] → [Capteur IR LM393] → [ESP32 WiFi] → [MQTT] → [Mosquitto]
├── index.html # Page de connexion/inscription → [Node.js] → [MariaDB] → [API REST] → [Site web]
├── 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
``` ```
## 🔌 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 ### Configuration reseau
| Méthode | Endpoint | Description | | Interface | Adresse IP | Utilisation |
|---------|----------|-------------| |-----------|-----------|-------------|
| POST | `/api/register` | Inscription | | Ethernet | 172.16.60.40 | Reseau ecole |
| POST | `/api/login` | Connexion | | WiFi (hotspot) | 172.20.10.2 | Communication ESP32 |
| Docker bridge | 172.18.0.1 | Reseau interne Docker |
### Utilisateurs ### Ports
| Méthode | Endpoint | Description | | Service | Port |
|---------|----------|-------------| |---------|------|
| GET | `/api/users` | Liste des utilisateurs (admin) | | Node.js (HTTP) | 3000 |
| DELETE | `/api/users/:id` | Supprimer un utilisateur (admin) | | MariaDB | 3306 |
| Mosquitto (MQTT) | 1883 |
### Places ## Technologies utilisees
| 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) |
### Réservations ### Hardware
| Méthode | Endpoint | Description | - ESP32 Freenove WROVER (WiFi + GPIO)
|---------|----------|-------------| - Capteurs IR LM393 (GPIO 15, 4, 12)
| GET | `/api/reservations` | Mes réservations | - Raspberry Pi 4 (serveur central)
| 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)
### Backend ### Backend
- Node.js - Node.js + Express.js
- Express.js - MariaDB (base de donnees relationnelle)
- SQLite3 - Mosquitto (broker MQTT)
- Docker + Docker Compose (conteneurisation)
- JWT (authentification) - 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 : ### Outils
1. Créer un compte ou se connecter - Arduino IDE (programmation ESP32)
2. Consulter la carte des places disponibles - Gitea + GitHub (versioning)
3. Choisir une place libre - Draw.io (schemas)
4. Sélectionner date, heure et durée
5. Scanner le QR code pour payer
6. La place est réservée !
### Pour l'administrateur : ## Structure du projet
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
## 🔒 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 ## API REST
- Authentification JWT
- Protection des routes sensibles
- Validation des données
## 📝 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`) ### Utilisateurs
- Le système fonctionne aussi en mode offline (stockage local) | Methode | Endpoint | Description |
- La simulation automatique change l'état des places toutes les 5 secondes |---------|----------|-------------|
| 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"> Smart Parking — BTS CIEL IR — APON BARUA — 2025/2026
🅿️ <strong>Smart Parking - BTS CIEL IR 2025</strong> 🅿️
</p>

View File

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

View File

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

View File

@@ -1,45 +1,40 @@
/* ============================================
SMART PARKING - STYLES GLOBAUX
============================================ */
:root { :root {
/* Couleurs principales */
--primary: #6366f1; --primary: #6366f1;
--primary-dark: #4f46e5; --primary-dark: #4f46e5;
--primary-light: #818cf8; --primary-light: #818cf8;
/* Couleurs états */
--success: #10b981; --success: #10b981;
--danger: #ef4444; --danger: #ef4444;
--warning: #f59e0b; --warning: #f59e0b;
--info: #06b6d4; --info: #06b6d4;
/* Couleurs places */
--spot-free: #10b981; --spot-free: #10b981;
--spot-occupied: #ef4444; --spot-occupied: #ef4444;
--spot-reserved: #3b82f6; --spot-reserved: #3b82f6;
/* Fonds */
--bg-dark: #0f172a; --bg-dark: #0f172a;
--bg-darker: #020617; --bg-darker: #020617;
--bg-card: #1e293b; --bg-card: #1e293b;
--bg-hover: #334155; --bg-hover: #334155;
/* Texte */
--text-primary: #f8fafc; --text-primary: #f8fafc;
--text-secondary: #cbd5e1; --text-secondary: #cbd5e1;
--text-muted: #94a3b8; --text-muted: #94a3b8;
/* Bordures */
--border: #334155; --border: #334155;
--border-radius: 12px; --border-radius: 12px;
--border-radius-sm: 8px; --border-radius-sm: 8px;
/* Ombres */
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.4); --shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
/* Transitions */
--transition: all 0.3s ease; --transition: all 0.3s ease;
} }
@@ -67,9 +62,7 @@ body {
padding: 0 20px; padding: 0 20px;
} }
/* ============================================
HEADER
============================================ */
.header { .header {
background: var(--bg-card); background: var(--bg-card);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
@@ -110,7 +103,7 @@ body {
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
/* Navigation */
.nav { .nav {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -143,7 +136,7 @@ body {
font-size: 1.1rem; font-size: 1.1rem;
} }
/* Header right */
.header-right { .header-right {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -166,9 +159,7 @@ body {
color: var(--text-muted); color: var(--text-muted);
} }
/* ============================================
MAIN & PAGES
============================================ */
.main { .main {
padding: 30px 0; padding: 30px 0;
min-height: calc(100vh - 80px); min-height: calc(100vh - 80px);
@@ -201,9 +192,7 @@ body {
font-size: 1.5rem; font-size: 1.5rem;
} }
/* ============================================
BOUTONS
============================================ */
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -262,9 +251,7 @@ body {
cursor: not-allowed; cursor: not-allowed;
} }
/* ============================================
FORMULAIRES
============================================ */
.form-group { .form-group {
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -317,9 +304,6 @@ select.form-control {
flex: 1; flex: 1;
} }
/* ============================================
CARTES
============================================ */
.card { .card {
background: var(--bg-card); background: var(--bg-card);
border-radius: var(--border-radius); border-radius: var(--border-radius);
@@ -328,9 +312,7 @@ select.form-control {
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
/* ============================================
STATS GRID
============================================ */
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@@ -392,9 +374,7 @@ select.form-control {
margin-top: 4px; margin-top: 4px;
} }
/* ============================================
PARKING MAP
============================================ */
.parking-section { .parking-section {
display: grid; display: grid;
grid-template-columns: 1fr 350px; grid-template-columns: 1fr 350px;
@@ -446,7 +426,7 @@ select.form-control {
.parking-spot .spot-number { font-size: 1.1rem; } .parking-spot .spot-number { font-size: 1.1rem; }
.parking-spot .spot-icon { font-size: 1.3rem; } .parking-spot .spot-icon { font-size: 1.3rem; }
/* Legend */
.legend { .legend {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -471,7 +451,7 @@ select.form-control {
.legend-color.occupied { background: var(--spot-occupied); } .legend-color.occupied { background: var(--spot-occupied); }
.legend-color.reserved { background: var(--spot-reserved); } .legend-color.reserved { background: var(--spot-reserved); }
/* Spot details */
.spot-details { min-height: 200px; } .spot-details { min-height: 200px; }
.no-selection { .no-selection {
@@ -500,9 +480,7 @@ select.form-control {
.spot-status-occupied { color: var(--spot-occupied); } .spot-status-occupied { color: var(--spot-occupied); }
.spot-status-reserved { color: var(--spot-reserved); } .spot-status-reserved { color: var(--spot-reserved); }
/* ============================================
PRICING
============================================ */
.pricing-section { .pricing-section {
background: var(--bg-card); background: var(--bg-card);
border-radius: var(--border-radius); border-radius: var(--border-radius);
@@ -547,9 +525,7 @@ select.form-control {
color: var(--primary-light); color: var(--primary-light);
} }
/* ============================================
MODAL
============================================ */
.modal { .modal {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -597,7 +573,7 @@ select.form-control {
.modal-body { padding: 24px; } .modal-body { padding: 24px; }
/* Récapitulatif réservation */
.payment-summary { .payment-summary {
background: var(--bg-dark); background: var(--bg-dark);
border-radius: var(--border-radius-sm); border-radius: var(--border-radius-sm);
@@ -625,9 +601,7 @@ select.form-control {
font-size: 1.1rem; font-size: 1.1rem;
} }
/* ============================================
MESSAGE DE CONFIRMATION (remplace QR code)
============================================ */
.confirmation-message { .confirmation-message {
text-align: center; text-align: center;
padding: 24px 16px; padding: 24px 16px;
@@ -658,9 +632,7 @@ select.form-control {
color: var(--text-primary); color: var(--text-primary);
} }
/* ============================================
RESERVATIONS LIST
============================================ */
.reservations-list { .reservations-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -739,9 +711,7 @@ select.form-control {
margin-bottom: 20px; margin-bottom: 20px;
} }
/* ============================================
PROFILE
============================================ */
.profile-container { .profile-container {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -813,9 +783,7 @@ select.form-control {
color: var(--text-muted); color: var(--text-muted);
} }
/* ============================================
ADMIN
============================================ */
.admin-page .admin-stats-grid { .admin-page .admin-stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); 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.occupied { background: rgba(239, 68, 68, 0.2); color: var(--spot-occupied); }
.admin-place-item.reserved { background: rgba(59, 130, 246, 0.2); color: var(--spot-reserved); } .admin-place-item.reserved { background: rgba(59, 130, 246, 0.2); color: var(--spot-reserved); }
/* Tables */
.table-container { overflow-x: auto; } .table-container { overflow-x: auto; }
.data-table { .data-table {
@@ -908,7 +876,7 @@ select.form-control {
.data-table tr:hover td { background: var(--bg-hover); } .data-table tr:hover td { background: var(--bg-hover); }
/* Log container */
.log-container { .log-container {
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
@@ -933,9 +901,7 @@ select.form-control {
font-size: 0.75rem; font-size: 0.75rem;
} }
/* ============================================
TOAST NOTIFICATIONS
============================================ */
.toast-container { .toast-container {
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
@@ -965,17 +931,13 @@ select.form-control {
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 { display: none; }
.admin-only.visible { display: flex; } .admin-only.visible { display: flex; }
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); } .stats-grid { grid-template-columns: repeat(2, 1fr); }
.parking-section { grid-template-columns: 1fr; } .parking-section { grid-template-columns: 1fr; }
@@ -1004,7 +966,6 @@ select.form-control {
.reservation-actions { align-items: flex-start; } .reservation-actions { align-items: flex-start; }
} }
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg-dark); border-radius: 4px; } ::-webkit-scrollbar-track { background: var(--bg-dark); border-radius: 4px; }
::-webkit-scrollbar-thumb { background: var(--border); 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: services:
# ── Base de données MariaDB ────────────────────────────────
db: db:
image: mariadb:10.11 image: mariadb:10.11
container_name: smartparking-db container_name: smartparking-db
restart: always restart: always
environment: environment:
MARIADB_ROOT_PASSWORD: rootpassword # ⚠️ À changer en production MARIADB_ROOT_PASSWORD: rootpassword
MARIADB_DATABASE: smartparking MARIADB_DATABASE: smartparking
MARIADB_USER: smartparking_user MARIADB_USER: smartparking_user
MARIADB_PASSWORD: smartparking_pass # ⚠️ À changer en production MARIADB_PASSWORD: smartparking_pass
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql - ./init.sql:/docker-entrypoint-initdb.d/init.sql
@@ -27,25 +18,6 @@ services:
timeout: 10s timeout: 10s
retries: 5 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: app:
build: . build: .
container_name: smartparking-app container_name: smartparking-app
@@ -55,31 +27,23 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
mqtt:
condition: service_started
environment: environment:
# Base de données DB_HOST: db
DB_HOST: db DB_PORT: 3306
DB_PORT: 3306 DB_USER: smartparking_user
DB_USER: smartparking_user
DB_PASSWORD: smartparking_pass DB_PASSWORD: smartparking_pass
DB_NAME: smartparking DB_NAME: smartparking
# JWT JWT_SECRET: une_chaine_tres_longue_et_secrete
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
NODE_ENV: production NODE_ENV: production
PORT: 3000 MQTT_HOST: 172.18.0.1
MQTT_PORT: 1883
extra_hosts:
- "host.docker.internal:host-gateway"
networks: networks:
- smartparking-network - smartparking-network
volumes: volumes:
db_data: db_data:
mosquitto_data:
mosquitto_log:
networks: networks:
smartparking-network: smartparking-network:

View File

@@ -16,7 +16,7 @@
<p>Gestion intelligente de parking</p> <p>Gestion intelligente de parking</p>
</div> </div>
<!-- Formulaire de connexion -->
<form id="loginForm" class="auth-form"> <form id="loginForm" class="auth-form">
<h2>Connexion</h2> <h2>Connexion</h2>
<div class="form-group"> <div class="form-group">
@@ -37,7 +37,7 @@
</p> </p>
</form> </form>
<!-- Formulaire d'inscription -->
<form id="registerForm" class="auth-form hidden"> <form id="registerForm" class="auth-form hidden">
<h2>Créer un compte</h2> <h2>Créer un compte</h2>
<div class="form-group"> <div class="form-group">
@@ -70,7 +70,7 @@
</p> </p>
</form> </form>
<!-- Message d'erreur -->
<div id="authMessage" class="auth-message hidden"></div> <div id="authMessage" class="auth-message hidden"></div>
</div> </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', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('⚙️ Initialisation du panel admin...'); console.log('⚙️ Initialisation du panel admin...');
if (!isAdmin()) return; if (!isAdmin()) return;
@@ -27,7 +16,6 @@ function initAdminPanel() {
loadReservationsTable(); loadReservationsTable();
loadHistoryLog(); loadHistoryLog();
// Rafraîchissement périodique toutes les 10 secondes
setInterval(() => { setInterval(() => {
loadAdminStats(); loadAdminStats();
loadReservationsTable(); loadReservationsTable();
@@ -35,9 +23,6 @@ function initAdminPanel() {
}, 10000); }, 10000);
} }
// ============================================
// STATISTIQUES ADMIN
// ============================================
function loadAdminStats() { function loadAdminStats() {
const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
@@ -62,21 +47,44 @@ function loadAdminStats() {
document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%'; document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%';
} }
// ============================================
// GESTION DES PLACES
// ============================================
function initPlacesControl() { function initPlacesControl() {
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
const spotsInput = document.getElementById('adminTotalSpots'); const spotsInput = document.getElementById('adminTotalSpots');
if (spotsInput) spotsInput.value = spots.length || 10; 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); 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; 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) { if (window.ParkingMap) {
window.ParkingMap.setTotalSpots(newCount); window.ParkingMap.setTotalSpots(newCount);
renderAdminPlacesList(); renderAdminPlacesList();
@@ -104,27 +112,48 @@ function renderAdminPlacesList() {
`).join(''); `).join('');
} }
function toggleSpotStatus(spotId) {
async function toggleSpotStatus(spotId) {
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
const spot = spots.find(s => s.id === spotId); const spot = spots.find(s => s.id === spotId);
if (!spot) return; if (!spot) return;
const cycle = ['free', 'occupied', 'reserved']; const cycle = ['free', 'occupied', 'reserved'];
const nextStatus = cycle[(cycle.indexOf(spot.status) + 1) % cycle.length]; const nextStatus = cycle[(cycle.indexOf(spot.status) + 1) % cycle.length];
spot.status = nextStatus;
spot.lastUpdate = new Date().toISOString();
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)); localStorage.setItem('smart_parking_spots', JSON.stringify(spots));
renderAdminPlacesList(); renderAdminPlacesList();
if (window.ParkingMap) window.ParkingMap.refresh(); if (window.ParkingMap) window.ParkingMap.refresh();
loadAdminStats(); loadAdminStats();
Dashboard.showToast(`Place ${spot.number} ${getStatusLabel(nextStatus)}`, 'success'); Dashboard.showToast(`Place ${spot.number} ${getStatusLabel(nextStatus)}`, 'success');
} }
// ============================================
// TABLEAU UTILISATEURS
// ============================================
function loadUsersTable() { function loadUsersTable() {
const tbody = document.getElementById('adminUsersTable'); const tbody = document.getElementById('adminUsersTable');
@@ -176,9 +205,7 @@ function deleteUser(userId) {
Dashboard.showToast('Utilisateur supprimé', 'success'); Dashboard.showToast('Utilisateur supprimé', 'success');
} }
// ============================================
// TABLEAU RÉSERVATIONS
// ============================================
function loadReservationsTable() { function loadReservationsTable() {
const tbody = document.getElementById('adminReservationsTable'); const tbody = document.getElementById('adminReservationsTable');
@@ -218,11 +245,22 @@ function loadReservationsTable() {
`).join(''); `).join('');
} }
function completeReservation(reservationId) { async function completeReservation(reservationId) {
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); const reservation = reservations.find(r => r.id === reservationId);
if (!reservation) return; 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'; reservation.status = 'completed';
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
@@ -233,13 +271,24 @@ function completeReservation(reservationId) {
Dashboard.showToast('Réservation terminée', 'success'); 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; 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); const reservation = reservations.find(r => r.id === reservationId);
if (!reservation) return; 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'; reservation.status = 'cancelled';
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
@@ -250,9 +299,6 @@ function adminCancelReservation(reservationId) {
Dashboard.showToast('Réservation annulée', 'success'); Dashboard.showToast('Réservation annulée', 'success');
} }
// ============================================
// HISTORIQUE — CORRIGÉ : date complète
// ============================================
function loadHistoryLog() { function loadHistoryLog() {
const container = document.getElementById('adminLogContainer'); const container = document.getElementById('adminLogContainer');
@@ -273,15 +319,8 @@ function loadHistoryLog() {
`).join(''); `).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) { function formatDateComplete(dateString) {
if (!dateString) return 'N/A'; if (!dateString) return 'N/A';
const date = new Date(dateString); const date = new Date(dateString);
@@ -312,9 +351,6 @@ function getStatusLabel(status) {
}[status] || status; }[status] || status;
} }
// ============================================
// EXPORT
// ============================================
window.AdminModule = { window.AdminModule = {
refresh: () => { refresh: () => {

View File

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

View File

@@ -1,13 +1,4 @@
/** const API_URL = '/api';
* ============================================
* 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';
let dashboardState = { let dashboardState = {
user: null, user: null,
@@ -93,10 +84,10 @@ function loadUserData() {
document.getElementById('userName').textContent = user.name; document.getElementById('userName').textContent = user.name;
document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client'; document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
document.getElementById('profileName').textContent = user.name; document.getElementById('profileName').textContent = user.name;
document.getElementById('profileNameInput').value = user.name; document.getElementById('profileNameInput').value = user.name;
document.getElementById('profileEmailInput').value = user.email; document.getElementById('profileEmailInput').value = user.email;
document.getElementById('profilePhoneInput').value = user.phone || ''; document.getElementById('profilePhoneInput').value = user.phone || '';
const roleBadge = document.getElementById('profileRole'); const roleBadge = document.getElementById('profileRole');
if (roleBadge) { if (roleBadge) {
@@ -137,7 +128,7 @@ function loadMyReservations() {
<h4>Place ${res.spotNumber}</h4> <h4>Place ${res.spotNumber}</h4>
<div class="reservation-details"> <div class="reservation-details">
<span>📅 ${res.date}</span> <span>📅 ${res.date}</span>
<span>🕐 ${res.startTime}</span> <span>🕐 ${res.startTime} - ${res.endTime}</span>
<span>⏱️ ${formatDurationLabel(res.duration)}</span> <span>⏱️ ${formatDurationLabel(res.duration)}</span>
<span>🚗 ${res.vehicle || 'N/A'}</span> <span>🚗 ${res.vehicle || 'N/A'}</span>
</div> </div>
@@ -155,22 +146,16 @@ function loadMyReservations() {
`).join(''); `).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) { function cancelReservation(reservationId) {
if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return; if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return;
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); const reservation = reservations.find(r => r.id === reservationId);
if (reservation) { if (reservation) {
reservation.status = 'cancelled'; reservation.status = 'cancelled';
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
// CORRIGÉ : spotId au lieu de spotNumber
if (window.ParkingMap) { if (window.ParkingMap) {
window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); window.ParkingMap.setSpotStatus(reservation.spotId, 'free');
} }
@@ -181,26 +166,82 @@ function cancelReservation(reservationId) {
} }
} }
// Mise à jour du profil document.getElementById('profileForm')?.addEventListener('submit', async (e) => {
document.getElementById('profileForm')?.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const phone = document.getElementById('profilePhoneInput').value; const phone = document.getElementById('profilePhoneInput').value;
const newPassword = document.getElementById('profileNewPassword').value; const newPassword = document.getElementById('profileNewPassword').value;
const confirmPass = document.getElementById('profileNewPassword').value;
let user = dashboardState.user;
user.phone = phone; if (newPassword && newPassword.length < 8) {
if (newPassword) user.password = newPassword; showToast('Le mot de passe doit faire au moins 8 caractères', 'error');
return;
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));
} }
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) { function getStatusLabel(status) {
@@ -208,7 +249,7 @@ function getStatusLabel(status) {
} }
function formatDurationLabel(minutes) { 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' : ''); if (minutes >= 60) return Math.floor(minutes / 60) + 'h' + (minutes % 60 ? (minutes % 60) + 'min' : '');
return minutes + ' 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 = { const MAP_CONFIG = {
totalSpots: 10, totalSpots: 10,
updateInterval: 3000 // Refresh depuis l'API toutes les 3 secondes updateInterval: 3000
}; };
let spotsState = { let spotsState = {
@@ -19,9 +10,7 @@ let spotsState = {
const SPOT_STATUS = { FREE: 'free', OCCUPIED: 'occupied', RESERVED: 'reserved' }; const SPOT_STATUS = { FREE: 'free', OCCUPIED: 'occupied', RESERVED: 'reserved' };
// ============================================
// INITIALISATION
// ============================================
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('🗺️ Initialisation de la carte...'); console.log('🗺️ Initialisation de la carte...');
@@ -29,30 +18,17 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
async function initParkingMap() { async function initParkingMap() {
// Essayer d'abord de charger depuis l'API (données MariaDB + Arduino)
const loaded = await loadSpotsFromAPI(); const loaded = await loadSpotsFromAPI();
if (!loaded) loadSpotsFromStorage();
// Si pas d'API disponible, utiliser le localStorage (mode offline)
if (!loaded) {
loadSpotsFromStorage();
}
renderMap(); renderMap();
updateStats(); updateStats();
updateReservationForm(); updateReservationForm();
// ⭐ Polling toutes les 3 secondes pour recevoir les updates Arduino
startAPIPolling(); startAPIPolling();
} }
// ============================================
// CHARGEMENT DES PLACES
// ============================================
/**
* Charge les places depuis l'API (MariaDB)
* Retourne true si succès, false si hors-ligne
*/
async function loadSpotsFromAPI() { async function loadSpotsFromAPI() {
try { try {
const token = localStorage.getItem('smart_parking_token'); const token = localStorage.getItem('smart_parking_token');
@@ -67,7 +43,6 @@ async function loadSpotsFromAPI() {
const data = await response.json(); const data = await response.json();
if (!data.success || !data.data.length) return false; if (!data.success || !data.data.length) return false;
// Convertir le format API → format interne
spotsState.spots = data.data.map(s => ({ spotsState.spots = data.data.map(s => ({
id: s.id, id: s.id,
number: s.number, number: s.number,
@@ -76,19 +51,14 @@ async function loadSpotsFromAPI() {
sensorId: s.sensor_id sensorId: s.sensor_id
})); }));
// Synchroniser avec localStorage pour le mode offline
saveSpots(); saveSpots();
return true; return true;
} catch (_err) { } catch (_err) {
// Serveur non joignable → mode offline
return false; return false;
} }
} }
/**
* Charge les places depuis le localStorage (mode offline)
*/
function loadSpotsFromStorage() { function loadSpotsFromStorage() {
const stored = localStorage.getItem('smart_parking_spots'); const stored = localStorage.getItem('smart_parking_spots');
if (stored) { if (stored) {
@@ -116,23 +86,22 @@ function saveSpots() {
localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots)); 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() { function startAPIPolling() {
setInterval(async () => { setInterval(async () => {
const loaded = await loadSpotsFromAPI(); const loaded = await loadSpotsFromAPI();
if (loaded) { if (loaded) {
renderMap(); renderMap();
updateStats(); 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) { if (spotsState.selectedSpot) {
const updated = spotsState.spots.find(s => s.id === spotsState.selectedSpot.id); const updated = spotsState.spots.find(s => s.id === spotsState.selectedSpot.id);
if (updated) { if (updated) {
@@ -144,9 +113,6 @@ function startAPIPolling() {
}, MAP_CONFIG.updateInterval); }, MAP_CONFIG.updateInterval);
} }
// ============================================
// RENDU DE LA CARTE
// ============================================
function renderMap() { function renderMap() {
const mapContainer = document.getElementById('parkingMap'); const mapContainer = document.getElementById('parkingMap');
@@ -202,21 +168,21 @@ function showSpotDetails(spot) {
</div> </div>
${reservation ? ` ${reservation ? `
<div class="spot-info-row"> <div class="spot-info-row">
<span class="spot-info-label">Réservé par</span> <span class="spot-info-label">Réservé jusqu'à</span>
<span class="spot-info-value">${reservation.userName}</span>
</div>
<div class="spot-info-row">
<span class="spot-info-label">Jusqu'à</span>
<span class="spot-info-value">${reservation.endTime}</span> <span class="spot-info-value">${reservation.endTime}</span>
</div> </div>
` : ''} ` : ''}
${spot.status === SPOT_STATUS.FREE ? ` ${spot.status !== SPOT_STATUS.OCCUPIED ? `
<button class="btn btn-primary btn-block" <button class="btn btn-primary btn-block"
onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});"> onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});">
<span class="btn-icon">📅</span> <span class="btn-icon">📅</span>
Réserver cette place Réserver cette place
</button> </button>
` : ''} ` : `
<p style="color: var(--danger); text-align: center; margin-top: 12px;">
🚗 Une voiture est physiquement sur cette place
</p>
`}
</div> </div>
`; `;
} }
@@ -226,9 +192,6 @@ function findReservationForSpot(spotId) {
return reservations.find(r => r.spotId === spotId && r.status === 'active'); return reservations.find(r => r.spotId === spotId && r.status === 'active');
} }
// ============================================
// STATISTIQUES & FORMULAIRE
// ============================================
function updateStats() { function updateStats() {
const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length; 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; document.getElementById('totalCount').textContent = spotsState.spots.length;
} }
function updateReservationForm() { function updateReservationForm() {
const select = document.getElementById('resSpot'); const select = document.getElementById('resSpot');
if (!select) return; if (!select) return;
const currentValue = select.value;
const firstOption = select.options[0]; const firstOption = select.options[0];
select.innerHTML = ''; select.innerHTML = '';
select.appendChild(firstOption); select.appendChild(firstOption);
spotsState.spots
.filter(s => s.status === SPOT_STATUS.FREE) const availableSpots = spotsState.spots.filter(s => s.status !== SPOT_STATUS.OCCUPIED);
.forEach(spot => {
const option = document.createElement('option'); availableSpots.forEach(spot => {
option.value = spot.id; const option = document.createElement('option');
option.textContent = `Place ${spot.number}`; option.value = spot.id;
select.appendChild(option);
}); 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) { function selectSpotForReservation(spotId) {
@@ -264,9 +240,7 @@ function selectSpotForReservation(spotId) {
if (select) select.value = spotId; if (select) select.value = spotId;
} }
// ============================================
// ACTIONS ADMIN & SIMULATION
// ============================================
async function setSpotStatus(spotId, status) { async function setSpotStatus(spotId, status) {
const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId); const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId);
@@ -279,7 +253,6 @@ async function setSpotStatus(spotId, status) {
updateStats(); updateStats();
updateReservationForm(); updateReservationForm();
// Synchroniser avec l'API si connecté
try { try {
const token = localStorage.getItem('smart_parking_token'); const token = localStorage.getItem('smart_parking_token');
if (token) { if (token) {
@@ -315,9 +288,6 @@ function setTotalSpots(count) {
updateReservationForm(); updateReservationForm();
} }
// ============================================
// UTILITAIRES
// ============================================
function isAdmin() { function isAdmin() {
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null'); const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
@@ -340,9 +310,6 @@ function formatDate(dateString) {
}); });
} }
// ============================================
// EXPORT
// ============================================
window.ParkingMap = { window.ParkingMap = {
refresh: async () => { 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 = { const PRICING = {
30: 2, 30: 2,
60: 3, 60: 3,
@@ -27,14 +17,46 @@ const TIME_SLOTS = [
let currentReservation = null; let currentReservation = null;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('📅 Initialisation du système de réservation...'); cleanupExpiredReservations();
initReservationForm(); initReservationForm();
initDatePicker(); initDatePicker();
initTimeSlots(); initTimeSlots();
initPricePreview(); initPricePreview();
initConfirmationModal(); 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() { function initReservationForm() {
const form = document.getElementById('reservationForm'); const form = document.getElementById('reservationForm');
if (!form) return; if (!form) return;
@@ -52,18 +74,16 @@ function initDatePicker() {
function initTimeSlots() { function initTimeSlots() {
const select = document.getElementById('resStartTime'); const select = document.getElementById('resStartTime');
if (!select) return; if (!select) return;
TIME_SLOTS.forEach(time => { TIME_SLOTS.forEach(time => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = time; option.value = time;
option.textContent = time; option.textContent = time;
select.appendChild(option); select.appendChild(option);
}); });
const now = new Date();
const now = new Date(); const currentHour = now.getHours();
const currentHour = now.getHours(); const currentMins = now.getMinutes();
const currentMins = now.getMinutes(); const nextSlot = TIME_SLOTS.find(t => {
const nextSlot = TIME_SLOTS.find(t => {
const [h, m] = t.split(':').map(Number); const [h, m] = t.split(':').map(Number);
return h > currentHour || (h === currentHour && m > currentMins); return h > currentHour || (h === currentHour && m > currentMins);
}); });
@@ -83,14 +103,6 @@ function updatePricePreview() {
document.getElementById('previewPrice').textContent = price + '€'; 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) { async function handleReservationSubmit(e) {
e.preventDefault(); e.preventDefault();
@@ -111,25 +123,29 @@ async function handleReservationSubmit(e) {
return; 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 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 === 'occupied') { 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(); if (window.ParkingMap) window.ParkingMap.refresh();
return; return;
} }
// Calculer l'heure de fin
const endDate = new Date(date + 'T' + startTime); const endDate = new Date(date + 'T' + startTime);
endDate.setMinutes(endDate.getMinutes() + duration); endDate.setMinutes(endDate.getMinutes() + duration);
const endTime = endDate.toTimeString().slice(0, 5); const endTime = endDate.toTimeString().slice(0, 5);
// Essayer de créer la réservation via l'API if (checkLocalConflict(spotId, date, startTime, endTime)) {
// Le serveur vérifiera les conflits d'horaire 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'); const token = localStorage.getItem('smart_parking_token');
let apiSuccess = false;
if (token) { if (token) {
try { try {
@@ -149,12 +165,10 @@ async function handleReservationSubmit(e) {
const data = await response.json(); const data = await response.json();
if (!data.success) { if (!data.success) {
// Le serveur a détecté un conflit ou une erreur
Dashboard.showToast(data.message, 'error'); Dashboard.showToast(data.message, 'error');
return; return;
} }
// Succès via API
currentReservation = { currentReservation = {
id: data.data.id, id: data.data.id,
userId: user.id, userId: user.id,
@@ -167,28 +181,34 @@ async function handleReservationSubmit(e) {
status: 'active', status: 'active',
createdAt: new Date().toISOString() createdAt: new Date().toISOString()
}; };
apiSuccess = true;
} catch (_err) { } catch (_err) {
// Mode offline : enregistrement local apiSuccess = false;
currentReservation = creerReservationLocale(
user, spotId, spot, date, startTime, endTime, duration, vehicle
);
} }
} 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') || '[]'); let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
reservations.push(currentReservation); reservations.push(currentReservation);
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
// Mettre à jour la carte seulement si la réservation est pour aujourd'hui const today = new Date().toISOString().split('T')[0];
const today = new Date().toISOString().split('T')[0]; const now = new Date();
const now = new Date();
const resStart = new Date(date + 'T' + startTime); const resStart = new Date(date + 'T' + startTime);
const diffMin = (resStart - now) / 60000; const diffMin = (resStart - now) / 60000;
@@ -197,40 +217,16 @@ async function handleReservationSubmit(e) {
} }
addToHistory( addToHistory(
'Réservation', 'Reservation',
`Place ${currentReservation.spotNumber} réservée le ${date} de ${startTime} à ${endTime}${PRICING[duration]}` 'Place ' + currentReservation.spotNumber + ' reservee le ' + date + ' de ' + startTime + ' a ' + endTime + ' — ' + PRICING[duration] + '€'
); );
// Réinitialiser le formulaire
document.getElementById('reservationForm').reset(); document.getElementById('reservationForm').reset();
initDatePicker(); initDatePicker();
updatePricePreview(); updatePricePreview();
showConfirmationModal(); 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() { function initConfirmationModal() {
document.getElementById('closeConfirmationModal')?.addEventListener('click', hideConfirmationModal); document.getElementById('closeConfirmationModal')?.addEventListener('click', hideConfirmationModal);
document.getElementById('closeConfirmationBtn')?.addEventListener('click', hideConfirmationModal); document.getElementById('closeConfirmationBtn')?.addEventListener('click', hideConfirmationModal);
@@ -238,60 +234,44 @@ function initConfirmationModal() {
function showConfirmationModal() { function showConfirmationModal() {
if (!currentReservation) return; if (!currentReservation) return;
const modal = document.getElementById('confirmationModal'); const modal = document.getElementById('confirmationModal');
document.getElementById('paySpot').textContent = 'Place ' + currentReservation.spotNumber; document.getElementById('paySpot').textContent = 'Place ' + currentReservation.spotNumber;
document.getElementById('payDate').textContent = formatDate(currentReservation.date); document.getElementById('payDate').textContent = formatDate(currentReservation.date);
document.getElementById('payTime').textContent = currentReservation.startTime + ' - ' + currentReservation.endTime; document.getElementById('payTime').textContent = currentReservation.startTime + ' - ' + currentReservation.endTime;
document.getElementById('payDuration').textContent = formatDuration(currentReservation.duration); document.getElementById('payDuration').textContent = formatDuration(currentReservation.duration);
document.getElementById('payTotal').textContent = currentReservation.price + '€'; document.getElementById('payTotal').textContent = currentReservation.price + '€';
modal.classList.remove('hidden'); modal.classList.remove('hidden');
} }
function hideConfirmationModal() { function hideConfirmationModal() {
document.getElementById('confirmationModal').classList.add('hidden'); document.getElementById('confirmationModal').classList.add('hidden');
if (window.Dashboard) { if (window.Dashboard) {
Dashboard.navigateToPage('my-reservations'); Dashboard.navigateToPage('my-reservations');
document.querySelector('[data-page="my-reservations"]')?.classList.add('active'); document.querySelector('[data-page="my-reservations"]')?.classList.add('active');
document.querySelector('[data-page="reservation"]')?.classList.remove('active'); document.querySelector('[data-page="reservation"]')?.classList.remove('active');
} }
if (window.Dashboard) Dashboard.refreshStats(); if (window.Dashboard) Dashboard.refreshStats();
} }
// ============================================
// UTILITAIRES
// ============================================
function addToHistory(action, details) { function addToHistory(action, details) {
let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]'); let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]');
history.unshift({ history.unshift({ id: Date.now(), action, details, timestamp: new Date().toISOString() });
id: Date.now(),
action,
details,
timestamp: new Date().toISOString()
});
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)); localStorage.setItem('smart_parking_history', JSON.stringify(history));
} }
function formatDate(dateString) { function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('fr-FR', { return new Date(dateString).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
day: '2-digit', month: '2-digit', year: 'numeric'
});
} }
function formatDuration(minutes) { function formatDuration(minutes) {
if (minutes >= 480) return 'Journée (8h)'; if (minutes >= 480) return 'Journee (8h)';
if (minutes >= 60) { if (minutes >= 60) {
const h = Math.floor(minutes / 60); const h = Math.floor(minutes / 60);
const m = 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 }; window.Reservation = { PRICING, TIME_SLOTS, formatDuration, addToHistory };

View File

@@ -6,10 +6,8 @@
<title>Smart Parking - Dashboard</title> <title>Smart Parking - Dashboard</title>
<link rel="stylesheet" href="../css/style.css"> <link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="../css/dashboard.css"> <link rel="stylesheet" href="../css/dashboard.css">
<!-- Chart.js retiré car le graphique d'occupation a été supprimé -->
</head> </head>
<body> <body>
<!-- Header -->
<header class="header"> <header class="header">
<div class="container"> <div class="container">
<div class="header-left"> <div class="header-left">
@@ -49,7 +47,7 @@
<main class="main"> <main class="main">
<!-- ═══ PAGE : CARTE DES PLACES ═══ -->
<section id="map" class="page active"> <section id="map" class="page active">
<div class="container"> <div class="container">
<h2 class="page-title"><span class="icon">🗺️</span>Carte du Parking</h2> <h2 class="page-title"><span class="icon">🗺️</span>Carte du Parking</h2>
@@ -69,17 +67,10 @@
<span class="stat-label">Places occupées</span> <span class="stat-label">Places occupées</span>
</div> </div>
</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-card total">
<div class="stat-icon">🅿️</div> <div class="stat-icon">🅿️</div>
<div class="stat-content"> <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> <span class="stat-label">Total places</span>
</div> </div>
</div> </div>
@@ -92,7 +83,6 @@
<div class="legend"> <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 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 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> </div>
<div class="spot-details-container"> <div class="spot-details-container">
@@ -116,7 +106,7 @@
</div> </div>
</section> </section>
<!-- ═══ PAGE : RÉSERVATION ═══ -->
<section id="reservation" class="page hidden"> <section id="reservation" class="page hidden">
<div class="container"> <div class="container">
<h2 class="page-title"><span class="icon">📅</span>Réserver une place</h2> <h2 class="page-title"><span class="icon">📅</span>Réserver une place</h2>
@@ -167,7 +157,7 @@
</form> </form>
</div> </div>
<!-- Modal de confirmation -->
<div id="confirmationModal" class="modal hidden"> <div id="confirmationModal" class="modal hidden">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -198,7 +188,7 @@
</div> </div>
</section> </section>
<!-- ═══ PAGE : MES RÉSERVATIONS ═══ -->
<section id="my-reservations" class="page hidden"> <section id="my-reservations" class="page hidden">
<div class="container"> <div class="container">
<h2 class="page-title"><span class="icon">🎫</span>Mes réservations</h2> <h2 class="page-title"><span class="icon">🎫</span>Mes réservations</h2>
@@ -211,7 +201,7 @@
</div> </div>
</section> </section>
<!-- ═══ PAGE : PROFIL ═══ -->
<section id="profile" class="page hidden"> <section id="profile" class="page hidden">
<div class="container"> <div class="container">
<h2 class="page-title"><span class="icon">👤</span>Mon profil</h2> <h2 class="page-title"><span class="icon">👤</span>Mon profil</h2>
@@ -266,12 +256,12 @@
</div> </div>
</section> </section>
<!-- ═══ PAGE : ADMIN ═══ -->
<section id="admin" class="page hidden admin-page"> <section id="admin" class="page hidden admin-page">
<div class="container"> <div class="container">
<h2 class="page-title"><span class="icon">⚙️</span>Administration</h2> <h2 class="page-title"><span class="icon">⚙️</span>Administration</h2>
<!-- Stats admin -->
<div class="admin-stats-grid"> <div class="admin-stats-grid">
<div class="admin-stat"> <div class="admin-stat">
<span class="admin-stat-value" id="adminTotalUsers">0</span> <span class="admin-stat-value" id="adminTotalUsers">0</span>
@@ -291,7 +281,7 @@
</div> </div>
</div> </div>
<!-- Gestion des places -->
<div class="admin-section"> <div class="admin-section">
<h3>🅿️ Gestion des places</h3> <h3>🅿️ Gestion des places</h3>
<div class="admin-places-control"> <div class="admin-places-control">
@@ -299,7 +289,7 @@
<label>Nombre total de places</label> <label>Nombre total de places</label>
<div class="input-group"> <div class="input-group">
<input type="number" id="adminTotalSpots" class="form-control" <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> <button id="updateSpotsBtn" class="btn btn-primary">Mettre à jour</button>
</div> </div>
</div> </div>
@@ -307,7 +297,7 @@
<div class="admin-places-list" id="adminPlacesList"></div> <div class="admin-places-list" id="adminPlacesList"></div>
</div> </div>
<!-- Utilisateurs -->
<div class="admin-section"> <div class="admin-section">
<h3>👥 Utilisateurs</h3> <h3>👥 Utilisateurs</h3>
<div class="table-container"> <div class="table-container">
@@ -323,7 +313,7 @@
</div> </div>
</div> </div>
<!-- Réservations -->
<div class="admin-section"> <div class="admin-section">
<h3>📅 Toutes les réservations</h3> <h3>📅 Toutes les réservations</h3>
<div class="table-container"> <div class="table-container">
@@ -339,9 +329,8 @@
</div> </div>
</div> </div>
<!-- ═══ GRAPHIQUE D'OCCUPATION SUPPRIMÉ ici ═══ -->
<!-- Historique -->
<div class="admin-section"> <div class="admin-section">
<h3>📜 Historique</h3> <h3>📜 Historique</h3>
<div class="log-container" id="adminLogContainer"></div> <div class="log-container" id="adminLogContainer"></div>
@@ -358,6 +347,5 @@
<script src="../js/map.js"></script> <script src="../js/map.js"></script>
<script src="../js/reservation.js"></script> <script src="../js/reservation.js"></script>
<script src="../js/admin.js"></script> <script src="../js/admin.js"></script>
<!-- Chart.js et admin.js n'utilisent plus le graphique -->
</body> </body>
</html> </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 mysql = require('mysql2/promise');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
require('dotenv').config(); require('dotenv').config();
@@ -23,9 +13,6 @@ const pool = mysql.createPool({
queueLimit: 0 queueLimit: 0
}); });
// ============================================
// INITIALISATION
// ============================================
async function initDatabase() { async function initDatabase() {
try { try {
@@ -105,7 +92,6 @@ async function initDatabase() {
console.log('✅ Tables vérifiées/créées'); 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']); const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']);
if (rows.length === 0) { if (rows.length === 0) {
const hashed = await bcrypt.hash('admin123', 10); const hashed = await bcrypt.hash('admin123', 10);
@@ -116,7 +102,7 @@ async function initDatabase() {
console.log('✅ Admin par défaut créé'); console.log('✅ Admin par défaut créé');
} }
// 10 places par défaut
const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots'); const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots');
if (spots[0].count === 0) { if (spots[0].count === 0) {
for (let i = 1; i <= 10; i++) { for (let i = 1; i <= 10; i++) {
@@ -134,9 +120,6 @@ async function initDatabase() {
} }
} }
// ============================================
// UTILISATEURS
// ============================================
async function createUser(name, email, phone, hashedPassword, role = 'client') { async function createUser(name, email, phone, hashedPassword, role = 'client') {
const [result] = await pool.query( const [result] = await pool.query(
@@ -180,9 +163,6 @@ async function deleteUser(id) {
return { deleted: result.affectedRows }; return { deleted: result.affectedRows };
} }
// ============================================
// PLACES
// ============================================
async function createSpot(number, sensorId, status = 'free') { async function createSpot(number, sensorId, status = 'free') {
const [result] = await pool.query( const [result] = await pool.query(
@@ -215,9 +195,6 @@ async function deleteAllSpots() {
return { deleted: result.affectedRows }; return { deleted: result.affectedRows };
} }
// ============================================
// RÉSERVATIONS
// ============================================
async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) { async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) {
const [result] = await pool.query( const [result] = await pool.query(
@@ -229,21 +206,7 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat
return { id: result.insertId }; 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) { async function checkReservationConflict(spotId, date, startTime, endTime) {
const [rows] = await pool.query(` const [rows] = await pool.query(`
SELECT id FROM reservations SELECT id FROM reservations
@@ -254,7 +217,7 @@ async function checkReservationConflict(spotId, date, startTime, endTime) {
AND end_time > ? AND end_time > ?
`, [spotId, date, endTime, startTime]); `, [spotId, date, endTime, startTime]);
return rows.length > 0; // true = conflit, false = créneau libre return rows.length > 0;
} }
async function getReservationById(id) { async function getReservationById(id) {
@@ -299,10 +262,7 @@ async function updateReservationStatus(id, status) {
return { changed: result.affectedRows }; return { changed: result.affectedRows };
} }
/**
* Expiration automatique des réservations
* Appelée toutes les 60 secondes par server.js
*/
async function expireReservations() { async function expireReservations() {
const [expiredRows] = await pool.query(` const [expiredRows] = await pool.query(`
SELECT r.id, r.spot_id, r.user_id, s.number AS spot_number SELECT r.id, r.spot_id, r.user_id, s.number AS spot_number
@@ -334,9 +294,36 @@ async function expireReservations() {
return expiredRows.length; 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) { async function addHistory(action, details, userId = null) {
const [result] = await pool.query( const [result] = await pool.query(
@@ -358,9 +345,6 @@ async function getHistory(limit = 50) {
return rows; return rows;
} }
// ============================================
// STATISTIQUES
// ============================================
async function recordStats(total, free, occupied, reserved) { async function recordStats(total, free, occupied, reserved) {
const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0; const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
@@ -381,9 +365,7 @@ async function getStats(days = 7) {
return rows; return rows;
} }
// ============================================
// MQTT
// ============================================
async function recordMqttEvent(topic, message) { async function recordMqttEvent(topic, message) {
const [result] = await pool.query( const [result] = await pool.query(
@@ -393,9 +375,6 @@ async function recordMqttEvent(topic, message) {
return { id: result.insertId }; return { id: result.insertId };
} }
// ============================================
// FERMETURE
// ============================================
async function closeDatabase() { async function closeDatabase() {
await pool.end(); await pool.end();
@@ -411,7 +390,7 @@ module.exports = {
createReservation, checkReservationConflict, createReservation, checkReservationConflict,
getReservationById, getReservationsByUser, getReservationById, getReservationsByUser,
getAllReservations, updateReservationStatus, getAllReservations, updateReservationStatus,
expireReservations, expireReservations, cleanupStaleReservedSpots,
addHistory, getHistory, addHistory, getHistory,
recordStats, getStats, recordStats, getStats,
recordMqttEvent 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 express = require('express');
const router = express.Router(); const router = express.Router();
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const db = require('../db/database'); const db = require('../db/database');
const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth'); const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth');
// ============================================
// AUTHENTIFICATION
// ============================================
router.post('/register', async (req, res) => { router.post('/register', async (req, res) => {
try { try {
@@ -58,9 +46,7 @@ router.post('/login', async (req, res) => {
} }
}); });
// ============================================
// UTILISATEURS
// ============================================
router.get('/users', authenticateToken, requireAdmin, async (req, res) => { router.get('/users', authenticateToken, requireAdmin, async (req, res) => {
try { try {
@@ -80,12 +66,48 @@ router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) =>
} }
}); });
// ============================================ router.put('/users/profile', authenticateToken, async (req, res) => {
// PLACES 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) => { router.get('/spots', authenticateToken, async (req, res) => {
try { try {
await db.cleanupStaleReservedSpots();
const spots = await db.getAllSpots(); 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) { } catch (err) {
@@ -99,16 +121,17 @@ router.put('/spots/:id/status', authenticateToken, async (req, res) => {
if (!['free', 'occupied', 'reserved'].includes(status)) if (!['free', 'occupied', 'reserved'].includes(status))
return res.status(400).json({ success: false, message: 'Statut invalide' }); return res.status(400).json({ success: false, message: 'Statut invalide' });
await db.updateSpotStatus(req.params.id, status); await db.updateSpotStatus(req.params.id, status);
await db.addHistory('Mise à jour place', `Place ${req.params.id} -> ${status}`, req.user.id); await db.addHistory('Mise à jour place', `Place ${req.params.id} ${status}`, req.user.id);
res.json({ success: true, message: 'Statut mis à jour' }); res.json({ success: true, message: 'Statut mis à jour' });
} catch (err) { } catch (err) {
res.status(500).json({ success: false, message: 'Erreur serveur' }); res.status(500).json({ success: false, message: 'Erreur serveur' });
} }
}); });
router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => { router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => {
try { 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(); await db.deleteAllSpots();
for (let i = 1; i <= spotCount; i++) { for (let i = 1; i <= spotCount; i++) {
await db.createSpot(i, `SENSOR_${String(i).padStart(3, '0')}`, 'free'); 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) => { router.get('/reservations', authenticateToken, async (req, res) => {
try { 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) => { router.post('/reservations', authenticateToken, async (req, res) => {
try { try {
const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body; const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body;
@@ -166,16 +174,15 @@ router.post('/reservations', authenticateToken, async (req, res) => {
if (!spot) if (!spot)
return res.status(404).json({ success: false, message: 'Place introuvable' }); return res.status(404).json({ success: false, message: 'Place introuvable' });
// CORRIGÉ : bloquer uniquement si une voiture est physiquement là
if (spot.status === 'occupied') 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); const conflict = await db.checkReservationConflict(spotId, date, startTime, endTime);
if (conflict) if (conflict)
return res.status(409).json({ return res.status(409).json({
success: false, 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); 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 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 const today = new Date().toISOString().split('T')[0];
// et que l'heure de début est maintenant ou dans moins de 30 minutes
const now = new Date(); const now = new Date();
const today = now.toISOString().split('T')[0];
const resStart = new Date(`${date}T${startTime}`); const resStart = new Date(`${date}T${startTime}`);
const diffMin = (resStart - now) / 60000; const diffMin = (resStart - now) / 60000;
if (date === today && diffMin <= 30) { if (date === today && diffMin <= 30) {
await db.updateSpotStatus(spotId, 'reserved'); 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( await db.addHistory(
'Nouvelle réservation', '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) => { router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => {
try { try {
const reservation = await db.getReservationById(req.params.id); 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) => { router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async (req, res) => {
try { try {
const reservation = await db.getReservationById(req.params.id); 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) => { router.get('/stats', authenticateToken, async (req, res) => {
try { try {
const spots = await db.getAllSpots(); const spots = await db.getAllSpots();
@@ -290,7 +283,7 @@ router.get('/history', authenticateToken, requireAdmin, async (req, res) => {
}); });
router.get('/status', (_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; 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 express = require('express');
const cors = require('cors'); const cors = require('cors');
const bodyParser = require('body-parser'); 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('/', (_req, res) => res.sendFile(path.join(__dirname, '..', 'index.html')));
app.get('/dashboard', (_req, res) => res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.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_HOST = process.env.MQTT_HOST || 'localhost';
const MQTT_PORT = process.env.MQTT_PORT || 1883; 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; let mqttClient = null;
@@ -58,7 +34,7 @@ function connectMQTT() {
mqttClient = mqtt.connect(brokerUrl, { mqttClient = mqtt.connect(brokerUrl, {
clientId: 'smartparking-server-' + Math.random().toString(16).slice(3), clientId: 'smartparking-server-' + Math.random().toString(16).slice(3),
keepalive: 60, keepalive: 60,
reconnectPeriod: 5000, // Reconnexion automatique toutes les 5s si coupure reconnectPeriod: 5000,
connectTimeout: 10000 connectTimeout: 10000
}); });
@@ -73,11 +49,11 @@ function connectMQTT() {
}); });
}); });
// Message reçu depuis un capteur Arduino
mqttClient.on('message', async (topic, messageBuffer) => { mqttClient.on('message', async (topic, messageBuffer) => {
const message = messageBuffer.toString().trim(); const message = messageBuffer.toString().trim();
const topicParts = topic.split('/'); // ['smartparking', 'sensor', '1'] const topicParts = topic.split('/');
const spotNumber = parseInt(topicParts[2]); // numéro de place const spotNumber = parseInt(topicParts[2]);
if (isNaN(spotNumber)) { if (isNaN(spotNumber)) {
console.warn(`⚠️ Topic MQTT invalide : ${topic}`); console.warn(`⚠️ Topic MQTT invalide : ${topic}`);
@@ -86,11 +62,11 @@ function connectMQTT() {
console.log(`📩 MQTT reçu → topic: ${topic} | valeur: ${message}`); console.log(`📩 MQTT reçu → topic: ${topic} | valeur: ${message}`);
// "1" = voiture présente → occupée | "0" = libre
const newStatus = message === '1' ? 'occupied' : 'free'; const newStatus = message === '1' ? 'occupied' : 'free';
try { try {
// Récupérer la place par son numéro
const spots = await db.getAllSpots(); const spots = await db.getAllSpots();
const spot = spots.find(s => s.number === spotNumber); const spot = spots.find(s => s.number === spotNumber);
@@ -99,15 +75,14 @@ function connectMQTT() {
return; 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') { if (spot.status === 'reserved' && newStatus === 'occupied') {
console.log(` Place ${spotNumber} déjà réservée — capteur ignoré`); console.log(` Place ${spotNumber} déjà réservée — capteur ignoré`);
await db.recordMqttEvent(topic, message); await db.recordMqttEvent(topic, message);
return; return;
} }
// Mettre à jour en base
await db.updateSpotStatus(spot.id, newStatus); await db.updateSpotStatus(spot.id, newStatus);
await db.recordMqttEvent(topic, message); await db.recordMqttEvent(topic, message);
@@ -130,9 +105,7 @@ function connectMQTT() {
}); });
} }
// ============================================
// DÉMARRAGE DU SERVEUR
// ============================================
async function startServer() { async function startServer() {
try { try {
@@ -153,10 +126,10 @@ async function startServer() {
`); `);
}); });
// 3. Connecter le client MQTT (broker Mosquitto)
connectMQTT(); connectMQTT();
// 4. Enregistrer les statistiques toutes les 5 minutes
setInterval(async () => { setInterval(async () => {
try { try {
const spots = await db.getAllSpots(); const spots = await db.getAllSpots();
@@ -170,8 +143,7 @@ async function startServer() {
} }
}, 5 * 60 * 1000); }, 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 () => { setInterval(async () => {
try { try {
const count = await db.expireReservations(); const count = await db.expireReservations();
@@ -181,7 +153,7 @@ async function startServer() {
} catch (err) { } catch (err) {
console.error('❌ Erreur expiration réservations :', err.message); console.error('❌ Erreur expiration réservations :', err.message);
} }
}, 60 * 1000); // toutes les 60 secondes }, 60 * 1000);
} catch (err) { } catch (err) {
console.error('❌ Erreur au démarrage :', err); console.error('❌ Erreur au démarrage :', err);
@@ -189,7 +161,7 @@ async function startServer() {
} }
} }
// Arrêt propre
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
console.log('\n🛑 Arrêt du serveur...'); console.log('\n🛑 Arrêt du serveur...');
if (mqttClient) mqttClient.end(); if (mqttClient) mqttClient.end();