Mise à jour
This commit is contained in:
19
.env
19
.env
@@ -1,25 +1,10 @@
|
||||
# ============================================================
|
||||
# Smart Parking v2.0 — Variables d'environnement
|
||||
# Copiez ce fichier en .env à la racine du projet
|
||||
# ============================================================
|
||||
|
||||
# ── Base de données ──────────────────────────────────────────
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
DB_USER=smartparking_user
|
||||
DB_PASSWORD=smartparking_pass
|
||||
DB_NAME=smartparking
|
||||
|
||||
# ── JWT ──────────────────────────────────────────────────────
|
||||
# ⚠️ Changez cette valeur ! Mettez une chaîne longue et aléatoire.
|
||||
JWT_SECRET=une_chaine_tres_longue_et_secrete_changez_moi_absolument
|
||||
|
||||
# ── Serveur ──────────────────────────────────────────────────
|
||||
JWT_SECRET=une_chaine_tres_longue_et_secrete
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# ── MQTT (Mosquitto) ─────────────────────────────────────────
|
||||
# Si Mosquitto tourne dans Docker (service "mqtt") → mqtt
|
||||
# Si Mosquitto est installé directement sur le Pi → localhost
|
||||
MQTT_HOST=mqtt
|
||||
MQTT_HOST=172.18.0.1
|
||||
MQTT_PORT=1883
|
||||
375
README.md
375
README.md
@@ -1,208 +1,239 @@
|
||||
# 🅿️ Smart Parking
|
||||
# Smart Parking
|
||||
|
||||
> Système complet de gestion de parking intelligent avec authentification, réservation et paiement QR code
|
||||
Systeme de gestion et de surveillance de parking automatise avec detection en temps reel par capteurs infrarouges, reservation en ligne et administration complete.
|
||||
|
||||
## 📋 Fonctionnalités
|
||||
Projet BTS CIEL IR — APON BARUA — Groupe scolaire La Salle Saint-Denis — 2025/2026
|
||||
|
||||
### 🔐 Authentification
|
||||
- Inscription avec nom, email, téléphone et mot de passe
|
||||
- Connexion sécurisée
|
||||
- Gestion de profil
|
||||
- Deux rôles : Client et Administrateur
|
||||
## Fonctionnalites
|
||||
|
||||
### 🗺️ Carte du Parking
|
||||
- **10 places** visuelles (modifiable par l'admin)
|
||||
- 3 états : Libre ✅, Occupée 🚗, Réservée 📅
|
||||
- Mise à jour en temps réel
|
||||
- Détails de chaque place au clic
|
||||
### Authentification
|
||||
- Inscription et connexion securisees
|
||||
- Mots de passe haches avec BCrypt
|
||||
- Authentification par token JWT (expire apres 24h)
|
||||
- Deux roles : Client et Administrateur
|
||||
|
||||
### 📅 Système de Réservation
|
||||
- Sélection de la place
|
||||
- Choix de la date et heure
|
||||
- Durée : 30min, 1h, 2h, 4h, Journée
|
||||
### Carte du Parking
|
||||
- Affichage en temps reel de l'etat des places
|
||||
- 3 etats : Libre, Occupee, Reservee
|
||||
- Mise a jour automatique toutes les 3 secondes
|
||||
- Details de chaque place au clic
|
||||
|
||||
### Detection automatique
|
||||
- Capteurs infrarouges IR LM393 sur chaque place
|
||||
- ESP32 connecte en WiFi au Raspberry Pi
|
||||
- Communication MQTT vers le serveur
|
||||
- Changement d'etat instantane sur le site
|
||||
|
||||
### Systeme de Reservation
|
||||
- Selection de la place, date, heure et duree
|
||||
- Verification des conflits d'horaire
|
||||
- Saisie de la plaque d'immatriculation
|
||||
- Expiration automatique des reservations
|
||||
|
||||
### 💳 Paiement QR Code
|
||||
- Génération de QR code unique
|
||||
- Code de paiement affiché
|
||||
- Confirmation du paiement
|
||||
### Panel Administrateur
|
||||
- Statistiques globales du parking
|
||||
- Gestion de l'etat de chaque place (1 a 20 places)
|
||||
- Liste des utilisateurs avec suppression
|
||||
- Gestion des reservations (terminer, annuler)
|
||||
- Historique complet des actions
|
||||
|
||||
### 👤 Espace Client
|
||||
- Consulter la carte des places
|
||||
- Voir les tarifs
|
||||
- Faire une réservation
|
||||
- Voir l'historique des réservations
|
||||
- Gérer son profil
|
||||
## Tarifs
|
||||
|
||||
### ⚙️ Panel Admin
|
||||
- Voir toutes les statistiques
|
||||
- Modifier le nombre de places
|
||||
- Gérer l'état de chaque place
|
||||
- Voir tous les utilisateurs
|
||||
- Voir toutes les réservations
|
||||
- Annuler/terminer des réservations
|
||||
- Voir l'historique complet
|
||||
|
||||
## 💰 Tarifs
|
||||
|
||||
| Durée | Prix |
|
||||
| Duree | Prix |
|
||||
|-------|------|
|
||||
| 30 minutes | 2€ |
|
||||
| 1 heure | 3€ |
|
||||
| 2 heures | 5€ |
|
||||
| 4 heures | 8€ |
|
||||
| Journée (8h) | 15€ |
|
||||
| Journee (8h) | 15€ |
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Prérequis
|
||||
- Node.js 18+
|
||||
- npm
|
||||
|
||||
### Étape 1 : Installer les dépendances
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
```
|
||||
|
||||
### Étape 2 : Démarrer le serveur
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Pour le développement (avec redémarrage automatique) :
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Étape 3 : Accéder au site
|
||||
|
||||
Ouvrir un navigateur et aller sur :
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
## 🔑 Compte par défaut
|
||||
|
||||
**Administrateur :**
|
||||
- Email : `admin@smartparking.fr`
|
||||
- Mot de passe : `admin123`
|
||||
|
||||
## 📁 Structure du projet
|
||||
## Architecture technique
|
||||
|
||||
```
|
||||
smart-parking/
|
||||
├── index.html # Page de connexion/inscription
|
||||
├── css/
|
||||
│ ├── style.css # Styles globaux
|
||||
│ ├── auth.css # Styles authentification
|
||||
│ └── dashboard.css # Styles dashboard
|
||||
├── js/
|
||||
│ ├── auth.js # Gestion authentification
|
||||
│ ├── dashboard.js # Gestion dashboard
|
||||
│ ├── map.js # Carte des places
|
||||
│ ├── reservation.js # Système de réservation
|
||||
│ └── admin.js # Panel admin
|
||||
├── pages/
|
||||
│ └── dashboard.html # Dashboard principal
|
||||
├── server/
|
||||
│ ├── package.json # Dépendances Node.js
|
||||
│ ├── server.js # Serveur principal
|
||||
│ ├── db/
|
||||
│ │ └── database.js # Gestion SQLite
|
||||
│ ├── middleware/
|
||||
│ │ └── auth.js # Middleware JWT
|
||||
│ └── routes/
|
||||
│ └── api.js # Routes API
|
||||
└── README.md # Ce fichier
|
||||
[Voiture] → [Capteur IR LM393] → [ESP32 WiFi] → [MQTT] → [Mosquitto]
|
||||
→ [Node.js] → [MariaDB] → [API REST] → [Site web]
|
||||
```
|
||||
|
||||
## 🔌 API REST
|
||||
### Materiel
|
||||
| Composant | Reference | Quantite |
|
||||
|-----------|-----------|----------|
|
||||
| Microcontroleur WiFi | ESP32 Freenove WROVER | 1 |
|
||||
| Capteur infrarouge | Module IR LM393 | 3 |
|
||||
| Serveur central | Raspberry Pi 4 | 1 |
|
||||
| Cables | Dupont F-F et F-M | Plusieurs |
|
||||
|
||||
### Authentification
|
||||
| Méthode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| POST | `/api/register` | Inscription |
|
||||
| POST | `/api/login` | Connexion |
|
||||
### Configuration reseau
|
||||
| Interface | Adresse IP | Utilisation |
|
||||
|-----------|-----------|-------------|
|
||||
| Ethernet | 172.16.60.40 | Reseau ecole |
|
||||
| WiFi (hotspot) | 172.20.10.2 | Communication ESP32 |
|
||||
| Docker bridge | 172.18.0.1 | Reseau interne Docker |
|
||||
|
||||
### Utilisateurs
|
||||
| Méthode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | `/api/users` | Liste des utilisateurs (admin) |
|
||||
| DELETE | `/api/users/:id` | Supprimer un utilisateur (admin) |
|
||||
### Ports
|
||||
| Service | Port |
|
||||
|---------|------|
|
||||
| Node.js (HTTP) | 3000 |
|
||||
| MariaDB | 3306 |
|
||||
| Mosquitto (MQTT) | 1883 |
|
||||
|
||||
### Places
|
||||
| Méthode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | `/api/spots` | Liste des places |
|
||||
| PUT | `/api/spots/:id/status` | Modifier le statut |
|
||||
| POST | `/api/spots/init` | Réinitialiser les places (admin) |
|
||||
## Technologies utilisees
|
||||
|
||||
### Réservations
|
||||
| Méthode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | `/api/reservations` | Mes réservations |
|
||||
| GET | `/api/reservations/all` | Toutes les réservations (admin) |
|
||||
| POST | `/api/reservations` | Créer une réservation |
|
||||
| PUT | `/api/reservations/:id/cancel` | Annuler une réservation |
|
||||
|
||||
### Statistiques
|
||||
| Méthode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | `/api/stats` | Statistiques du parking |
|
||||
| GET | `/api/history` | Historique (admin) |
|
||||
|
||||
## 🛠️ Technologies utilisées
|
||||
|
||||
### Frontend
|
||||
- HTML5
|
||||
- CSS3 (responsive)
|
||||
- JavaScript vanilla
|
||||
- Chart.js (graphiques)
|
||||
- QRCode.js (génération QR)
|
||||
### Hardware
|
||||
- ESP32 Freenove WROVER (WiFi + GPIO)
|
||||
- Capteurs IR LM393 (GPIO 15, 4, 12)
|
||||
- Raspberry Pi 4 (serveur central)
|
||||
|
||||
### Backend
|
||||
- Node.js
|
||||
- Express.js
|
||||
- SQLite3
|
||||
- Node.js + Express.js
|
||||
- MariaDB (base de donnees relationnelle)
|
||||
- Mosquitto (broker MQTT)
|
||||
- Docker + Docker Compose (conteneurisation)
|
||||
- JWT (authentification)
|
||||
- bcryptjs (hashage mots de passe)
|
||||
- BCrypt (hachage mots de passe)
|
||||
|
||||
## 📱 Fonctionnement
|
||||
### Frontend
|
||||
- HTML5 / CSS3 / JavaScript (sans framework)
|
||||
- Design responsive (PC et mobile)
|
||||
|
||||
### Pour les clients :
|
||||
1. Créer un compte ou se connecter
|
||||
2. Consulter la carte des places disponibles
|
||||
3. Choisir une place libre
|
||||
4. Sélectionner date, heure et durée
|
||||
5. Scanner le QR code pour payer
|
||||
6. La place est réservée !
|
||||
### Outils
|
||||
- Arduino IDE (programmation ESP32)
|
||||
- Gitea + GitHub (versioning)
|
||||
- Draw.io (schemas)
|
||||
|
||||
### Pour l'administrateur :
|
||||
1. Se connecter avec le compte admin
|
||||
2. Accéder au panel Admin
|
||||
3. Voir toutes les statistiques
|
||||
4. Gérer les places (cliquer pour changer l'état)
|
||||
5. Modifier le nombre total de places
|
||||
6. Gérer les utilisateurs et réservations
|
||||
## Structure du projet
|
||||
|
||||
## 🔒 Sécurité
|
||||
```
|
||||
Parking/
|
||||
├── index.html
|
||||
├── css/
|
||||
│ ├── style.css
|
||||
│ ├── auth.css
|
||||
│ └── dashboard.css
|
||||
├── js/
|
||||
│ ├── auth.js
|
||||
│ ├── dashboard.js
|
||||
│ ├── map.js
|
||||
│ ├── reservation.js
|
||||
│ └── admin.js
|
||||
├── pages/
|
||||
│ └── dashboard.html
|
||||
├── server/
|
||||
│ ├── server.js
|
||||
│ ├── package.json
|
||||
│ ├── db/
|
||||
│ │ └── database.js
|
||||
│ ├── middleware/
|
||||
│ │ └── auth.js
|
||||
│ └── routes/
|
||||
│ └── api.js
|
||||
├── init.sql
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── mosquitto.conf
|
||||
└── README.md
|
||||
```
|
||||
|
||||
- Mots de passe hashés avec bcrypt
|
||||
- Authentification JWT
|
||||
- Protection des routes sensibles
|
||||
- Validation des données
|
||||
## API REST
|
||||
|
||||
## 📝 Notes
|
||||
### Authentification
|
||||
| Methode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| POST | /api/register | Creer un compte |
|
||||
| POST | /api/login | Se connecter |
|
||||
|
||||
- Les données sont stockées dans SQLite (`server/db/smart-parking.db`)
|
||||
- Le système fonctionne aussi en mode offline (stockage local)
|
||||
- La simulation automatique change l'état des places toutes les 5 secondes
|
||||
### Utilisateurs
|
||||
| Methode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | /api/users | Liste des utilisateurs (admin) |
|
||||
| PUT | /api/users/profile | Modifier mon profil |
|
||||
| DELETE | /api/users/:id | Supprimer un utilisateur (admin) |
|
||||
|
||||
### Places
|
||||
| Methode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | /api/spots | Liste des places |
|
||||
| PUT | /api/spots/:id/status | Modifier le statut |
|
||||
| POST | /api/spots/init | Reinitialiser les places (admin) |
|
||||
|
||||
### Reservations
|
||||
| Methode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | /api/reservations | Mes reservations |
|
||||
| GET | /api/reservations/all | Toutes les reservations (admin) |
|
||||
| POST | /api/reservations | Creer une reservation |
|
||||
| PUT | /api/reservations/:id/cancel | Annuler une reservation |
|
||||
| PUT | /api/reservations/:id/complete | Terminer une reservation (admin) |
|
||||
|
||||
### Statistiques
|
||||
| Methode | Endpoint | Description |
|
||||
|---------|----------|-------------|
|
||||
| GET | /api/stats | Statistiques du parking |
|
||||
| GET | /api/history | Historique (admin) |
|
||||
|
||||
## Base de donnees (6 tables)
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| users | Comptes utilisateurs (id, name, email, password, role) |
|
||||
| spots | Places de parking (id, number, status, sensor_id) |
|
||||
| reservations | Reservations (user_id, spot_id, date, start_time, end_time, vehicle, price) |
|
||||
| history | Historique des actions (action, details, user_id, timestamp) |
|
||||
| stats | Statistiques periodiques (occupation, taux) |
|
||||
| mqtt_events | Messages MQTT recus (topic, message, received_at) |
|
||||
|
||||
## Installation et demarrage
|
||||
|
||||
### 1. Demarrer le hotspot WiFi sur le telephone
|
||||
|
||||
### 2. Sur le Raspberry Pi
|
||||
```bash
|
||||
sudo systemctl start mosquitto
|
||||
sudo iptables -I INPUT -s 172.18.0.0/16 -p tcp --dport 1883 -j ACCEPT
|
||||
sudo iptables -I INPUT -s 172.17.0.0/16 -p tcp --dport 1883 -j ACCEPT
|
||||
cd /home/aponlucas/Parking
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 3. Televerser le code ESP32
|
||||
Ouvrir Arduino IDE, selectionner ESP32 Wrover Module, televerser le code.
|
||||
|
||||
### 4. Acceder au site
|
||||
```
|
||||
http://172.20.10.2:3000
|
||||
```
|
||||
|
||||
## Compte administrateur par defaut
|
||||
|
||||
- Email : admin@smartparking.fr
|
||||
- Mot de passe : admin123
|
||||
|
||||
## Securite
|
||||
|
||||
- Mots de passe haches avec BCrypt (10 rounds)
|
||||
- Authentification JWT avec expiration 24h
|
||||
- Middleware de verification sur toutes les routes protegees
|
||||
- Fichier .env non versionne (secrets exclus de GitHub)
|
||||
- Regles iptables pour le pare-feu Linux
|
||||
|
||||
## Chiffres cles
|
||||
|
||||
| Metrique | Valeur |
|
||||
|----------|--------|
|
||||
| Lignes de code | ~3 900 |
|
||||
| Fichiers | 17 |
|
||||
| Langages | JavaScript, HTML, CSS, C++, SQL |
|
||||
| Endpoints API | 12 |
|
||||
| Tables MariaDB | 6 |
|
||||
| Delai de mise a jour | Moins de 3 secondes |
|
||||
|
||||
## Equipe
|
||||
|
||||
| Membre | Role |
|
||||
|--------|------|
|
||||
| APON BARUA | IoT, Web, Backend, BDD, Docker, Securite |
|
||||
| Lucas | Electronique, Arduino, Barrieres, Schemas |
|
||||
| Mahmoud | Camera de surveillance |
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
🅿️ <strong>Smart Parking - BTS CIEL IR 2025</strong> 🅿️
|
||||
</p>
|
||||
Smart Parking — BTS CIEL IR — APON BARUA — 2025/2026
|
||||
@@ -1,7 +1,3 @@
|
||||
/* ============================================
|
||||
AUTHENTIFICATION - STYLES
|
||||
============================================ */
|
||||
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -149,7 +145,6 @@
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
.auth-box {
|
||||
padding: 24px;
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
/* ============================================
|
||||
DASHBOARD - STYLES SPÉCIFIQUES
|
||||
============================================ */
|
||||
|
||||
/* Reservation form */
|
||||
.reservation-form-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
@@ -35,14 +30,14 @@
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
/* Chart container */
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Admin table actions */
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -53,7 +48,7 @@
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
@@ -73,7 +68,7 @@
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
/* Animation pour les mises à jour */
|
||||
|
||||
@keyframes pulse-update {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
@@ -83,7 +78,6 @@
|
||||
animation: pulse-update 0.3s ease;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -1,45 +1,40 @@
|
||||
/* ============================================
|
||||
SMART PARKING - STYLES GLOBAUX
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Couleurs principales */
|
||||
|
||||
--primary: #6366f1;
|
||||
--primary-dark: #4f46e5;
|
||||
--primary-light: #818cf8;
|
||||
|
||||
/* Couleurs états */
|
||||
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--info: #06b6d4;
|
||||
|
||||
/* Couleurs places */
|
||||
--spot-free: #10b981;
|
||||
--spot-occupied: #ef4444;
|
||||
--spot-reserved: #3b82f6;
|
||||
|
||||
/* Fonds */
|
||||
|
||||
--bg-dark: #0f172a;
|
||||
--bg-darker: #020617;
|
||||
--bg-card: #1e293b;
|
||||
--bg-hover: #334155;
|
||||
|
||||
/* Texte */
|
||||
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-muted: #94a3b8;
|
||||
|
||||
/* Bordures */
|
||||
|
||||
--border: #334155;
|
||||
--border-radius: 12px;
|
||||
--border-radius-sm: 8px;
|
||||
|
||||
/* Ombres */
|
||||
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Transitions */
|
||||
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -67,9 +62,7 @@ body {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HEADER
|
||||
============================================ */
|
||||
|
||||
.header {
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
@@ -110,7 +103,7 @@ body {
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -143,7 +136,7 @@ body {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Header right */
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -166,9 +159,7 @@ body {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MAIN & PAGES
|
||||
============================================ */
|
||||
|
||||
.main {
|
||||
padding: 30px 0;
|
||||
min-height: calc(100vh - 80px);
|
||||
@@ -201,9 +192,7 @@ body {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BOUTONS
|
||||
============================================ */
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -262,9 +251,7 @@ body {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FORMULAIRES
|
||||
============================================ */
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -317,9 +304,6 @@ select.form-control {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CARTES
|
||||
============================================ */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
@@ -328,9 +312,7 @@ select.form-control {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
STATS GRID
|
||||
============================================ */
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@@ -392,9 +374,7 @@ select.form-control {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PARKING MAP
|
||||
============================================ */
|
||||
|
||||
.parking-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
@@ -446,7 +426,7 @@ select.form-control {
|
||||
.parking-spot .spot-number { font-size: 1.1rem; }
|
||||
.parking-spot .spot-icon { font-size: 1.3rem; }
|
||||
|
||||
/* Legend */
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -471,7 +451,7 @@ select.form-control {
|
||||
.legend-color.occupied { background: var(--spot-occupied); }
|
||||
.legend-color.reserved { background: var(--spot-reserved); }
|
||||
|
||||
/* Spot details */
|
||||
|
||||
.spot-details { min-height: 200px; }
|
||||
|
||||
.no-selection {
|
||||
@@ -500,9 +480,7 @@ select.form-control {
|
||||
.spot-status-occupied { color: var(--spot-occupied); }
|
||||
.spot-status-reserved { color: var(--spot-reserved); }
|
||||
|
||||
/* ============================================
|
||||
PRICING
|
||||
============================================ */
|
||||
|
||||
.pricing-section {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
@@ -547,9 +525,7 @@ select.form-control {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MODAL
|
||||
============================================ */
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -597,7 +573,7 @@ select.form-control {
|
||||
|
||||
.modal-body { padding: 24px; }
|
||||
|
||||
/* Récapitulatif réservation */
|
||||
|
||||
.payment-summary {
|
||||
background: var(--bg-dark);
|
||||
border-radius: var(--border-radius-sm);
|
||||
@@ -625,9 +601,7 @@ select.form-control {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MESSAGE DE CONFIRMATION (remplace QR code)
|
||||
============================================ */
|
||||
|
||||
.confirmation-message {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
@@ -658,9 +632,7 @@ select.form-control {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESERVATIONS LIST
|
||||
============================================ */
|
||||
|
||||
.reservations-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -739,9 +711,7 @@ select.form-control {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PROFILE
|
||||
============================================ */
|
||||
|
||||
.profile-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -813,9 +783,7 @@ select.form-control {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ADMIN
|
||||
============================================ */
|
||||
|
||||
.admin-page .admin-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@@ -882,7 +850,7 @@ select.form-control {
|
||||
.admin-place-item.occupied { background: rgba(239, 68, 68, 0.2); color: var(--spot-occupied); }
|
||||
.admin-place-item.reserved { background: rgba(59, 130, 246, 0.2); color: var(--spot-reserved); }
|
||||
|
||||
/* Tables */
|
||||
|
||||
.table-container { overflow-x: auto; }
|
||||
|
||||
.data-table {
|
||||
@@ -908,7 +876,7 @@ select.form-control {
|
||||
|
||||
.data-table tr:hover td { background: var(--bg-hover); }
|
||||
|
||||
/* Log container */
|
||||
|
||||
.log-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
@@ -933,9 +901,7 @@ select.form-control {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TOAST NOTIFICATIONS
|
||||
============================================ */
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
@@ -965,17 +931,13 @@ select.form-control {
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITAIRES
|
||||
============================================ */
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.admin-only { display: none; }
|
||||
.admin-only.visible { display: flex; }
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.parking-section { grid-template-columns: 1fr; }
|
||||
@@ -1004,7 +966,6 @@ select.form-control {
|
||||
.reservation-actions { align-items: flex-start; }
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg-dark); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
# ============================================================
|
||||
# Smart Parking v2.0 — Docker Compose
|
||||
# Services : MariaDB + App Node.js + Mosquitto MQTT
|
||||
# ============================================================
|
||||
|
||||
services:
|
||||
|
||||
# ── Base de données MariaDB ────────────────────────────────
|
||||
db:
|
||||
image: mariadb:10.11
|
||||
container_name: smartparking-db
|
||||
restart: always
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: rootpassword # ⚠️ À changer en production
|
||||
MARIADB_ROOT_PASSWORD: rootpassword
|
||||
MARIADB_DATABASE: smartparking
|
||||
MARIADB_USER: smartparking_user
|
||||
MARIADB_PASSWORD: smartparking_pass # ⚠️ À changer en production
|
||||
MARIADB_PASSWORD: smartparking_pass
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
@@ -27,25 +18,6 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# ── Broker MQTT Mosquitto ──────────────────────────────────
|
||||
# Si vous avez déjà Mosquitto installé directement sur le Pi
|
||||
# (pas dans Docker), commentez ce bloc et mettez
|
||||
# MQTT_HOST=localhost dans la section "app" ci-dessous.
|
||||
mqtt:
|
||||
image: eclipse-mosquitto:2
|
||||
container_name: smartparking-mqtt
|
||||
restart: always
|
||||
ports:
|
||||
- "1883:1883" # Port MQTT (Arduino se connecte ici)
|
||||
- "9001:9001" # Port WebSocket (optionnel)
|
||||
volumes:
|
||||
- ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf
|
||||
- mosquitto_data:/mosquitto/data
|
||||
- mosquitto_log:/mosquitto/log
|
||||
networks:
|
||||
- smartparking-network
|
||||
|
||||
# ── Application Node.js ────────────────────────────────────
|
||||
app:
|
||||
build: .
|
||||
container_name: smartparking-app
|
||||
@@ -55,31 +27,23 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
mqtt:
|
||||
condition: service_started
|
||||
environment:
|
||||
# Base de données
|
||||
DB_HOST: db
|
||||
DB_PORT: 3306
|
||||
DB_USER: smartparking_user
|
||||
DB_HOST: db
|
||||
DB_PORT: 3306
|
||||
DB_USER: smartparking_user
|
||||
DB_PASSWORD: smartparking_pass
|
||||
DB_NAME: smartparking
|
||||
# JWT
|
||||
JWT_SECRET: ${JWT_SECRET:-une_chaine_tres_longue_et_secrete_changez_moi}
|
||||
# MQTT — utiliser "mqtt" si Mosquitto est dans Docker
|
||||
# utiliser "localhost" si Mosquitto est installé directement sur le Pi
|
||||
MQTT_HOST: mqtt
|
||||
MQTT_PORT: 1883
|
||||
# Environnement
|
||||
DB_NAME: smartparking
|
||||
JWT_SECRET: une_chaine_tres_longue_et_secrete
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
MQTT_HOST: 172.18.0.1
|
||||
MQTT_PORT: 1883
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- smartparking-network
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
mosquitto_data:
|
||||
mosquitto_log:
|
||||
|
||||
networks:
|
||||
smartparking-network:
|
||||
@@ -16,7 +16,7 @@
|
||||
<p>Gestion intelligente de parking</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de connexion -->
|
||||
|
||||
<form id="loginForm" class="auth-form">
|
||||
<h2>Connexion</h2>
|
||||
<div class="form-group">
|
||||
@@ -37,7 +37,7 @@
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<!-- Formulaire d'inscription -->
|
||||
|
||||
<form id="registerForm" class="auth-form hidden">
|
||||
<h2>Créer un compte</h2>
|
||||
<div class="form-group">
|
||||
@@ -70,7 +70,7 @@
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<!-- Message d'erreur -->
|
||||
|
||||
<div id="authMessage" class="auth-message hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
134
js/admin.js
134
js/admin.js
@@ -1,14 +1,3 @@
|
||||
/**
|
||||
* ============================================
|
||||
* ADMIN.JS - Panel d'administration
|
||||
* Smart Parking v3.0
|
||||
* CORRIGÉ :
|
||||
* - Suppression du graphique d'occupation
|
||||
* - Historique affiche la date complète
|
||||
* (jour + mois + année + heure + minute)
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('⚙️ Initialisation du panel admin...');
|
||||
if (!isAdmin()) return;
|
||||
@@ -27,7 +16,6 @@ function initAdminPanel() {
|
||||
loadReservationsTable();
|
||||
loadHistoryLog();
|
||||
|
||||
// Rafraîchissement périodique toutes les 10 secondes
|
||||
setInterval(() => {
|
||||
loadAdminStats();
|
||||
loadReservationsTable();
|
||||
@@ -35,9 +23,6 @@ function initAdminPanel() {
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// STATISTIQUES ADMIN
|
||||
// ============================================
|
||||
|
||||
function loadAdminStats() {
|
||||
const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
|
||||
@@ -62,21 +47,44 @@ function loadAdminStats() {
|
||||
document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%';
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GESTION DES PLACES
|
||||
// ============================================
|
||||
|
||||
function initPlacesControl() {
|
||||
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
|
||||
const spotsInput = document.getElementById('adminTotalSpots');
|
||||
if (spotsInput) spotsInput.value = spots.length || 10;
|
||||
|
||||
document.getElementById('updateSpotsBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('updateSpotsBtn')?.addEventListener('click', async () => {
|
||||
const newCount = parseInt(document.getElementById('adminTotalSpots').value);
|
||||
if (newCount < 5 || newCount > 50) {
|
||||
Dashboard.showToast('Le nombre de places doit être entre 5 et 50', 'error');
|
||||
|
||||
if (newCount < 1 || newCount > 20) {
|
||||
Dashboard.showToast('Le nombre de places doit être entre 1 et 20', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
if (token) {
|
||||
try {
|
||||
const response = await fetch('/api/spots/init', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({ count: newCount })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
|
||||
if (window.ParkingMap) await window.ParkingMap.refresh();
|
||||
renderAdminPlacesList();
|
||||
Dashboard.showToast('Nombre de places mis à jour', 'success');
|
||||
return;
|
||||
}
|
||||
} catch (_err) { /* fallback local */ }
|
||||
}
|
||||
|
||||
|
||||
if (window.ParkingMap) {
|
||||
window.ParkingMap.setTotalSpots(newCount);
|
||||
renderAdminPlacesList();
|
||||
@@ -104,27 +112,48 @@ function renderAdminPlacesList() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function toggleSpotStatus(spotId) {
|
||||
|
||||
async function toggleSpotStatus(spotId) {
|
||||
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
|
||||
const spot = spots.find(s => s.id === spotId);
|
||||
if (!spot) return;
|
||||
|
||||
const cycle = ['free', 'occupied', 'reserved'];
|
||||
const nextStatus = cycle[(cycle.indexOf(spot.status) + 1) % cycle.length];
|
||||
spot.status = nextStatus;
|
||||
spot.lastUpdate = new Date().toISOString();
|
||||
const cycle = ['free', 'occupied', 'reserved'];
|
||||
const nextStatus = cycle[(cycle.indexOf(spot.status) + 1) % cycle.length];
|
||||
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
if (token) {
|
||||
try {
|
||||
const response = await fetch(`/api/spots/${spot.id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({ status: nextStatus })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
Dashboard.showToast('Erreur : ' + data.message, 'error');
|
||||
return;
|
||||
}
|
||||
} catch (_err) {
|
||||
console.warn('⚠️ API indisponible, modification locale uniquement');
|
||||
}
|
||||
}
|
||||
|
||||
spot.status = nextStatus;
|
||||
spot.lastUpdate = new Date().toISOString();
|
||||
localStorage.setItem('smart_parking_spots', JSON.stringify(spots));
|
||||
|
||||
renderAdminPlacesList();
|
||||
if (window.ParkingMap) window.ParkingMap.refresh();
|
||||
loadAdminStats();
|
||||
|
||||
Dashboard.showToast(`Place ${spot.number} — ${getStatusLabel(nextStatus)}`, 'success');
|
||||
Dashboard.showToast(`Place ${spot.number} → ${getStatusLabel(nextStatus)}`, 'success');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLEAU UTILISATEURS
|
||||
// ============================================
|
||||
|
||||
|
||||
function loadUsersTable() {
|
||||
const tbody = document.getElementById('adminUsersTable');
|
||||
@@ -176,9 +205,7 @@ function deleteUser(userId) {
|
||||
Dashboard.showToast('Utilisateur supprimé', 'success');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLEAU RÉSERVATIONS
|
||||
// ============================================
|
||||
|
||||
|
||||
function loadReservationsTable() {
|
||||
const tbody = document.getElementById('adminReservationsTable');
|
||||
@@ -218,11 +245,22 @@ function loadReservationsTable() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function completeReservation(reservationId) {
|
||||
async function completeReservation(reservationId) {
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
const reservation = reservations.find(r => r.id === reservationId);
|
||||
if (!reservation) return;
|
||||
|
||||
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`/api/reservations/${reservationId}/complete`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
} catch (_err) { /* fallback local */ }
|
||||
}
|
||||
|
||||
reservation.status = 'completed';
|
||||
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
|
||||
|
||||
@@ -233,13 +271,24 @@ function completeReservation(reservationId) {
|
||||
Dashboard.showToast('Réservation terminée', 'success');
|
||||
}
|
||||
|
||||
function adminCancelReservation(reservationId) {
|
||||
async function adminCancelReservation(reservationId) {
|
||||
if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return;
|
||||
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
const reservation = reservations.find(r => r.id === reservationId);
|
||||
if (!reservation) return;
|
||||
|
||||
// Appeler l'API
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`/api/reservations/${reservationId}/cancel`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
} catch (_err) { /* fallback local */ }
|
||||
}
|
||||
|
||||
reservation.status = 'cancelled';
|
||||
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
|
||||
|
||||
@@ -250,9 +299,6 @@ function adminCancelReservation(reservationId) {
|
||||
Dashboard.showToast('Réservation annulée', 'success');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HISTORIQUE — CORRIGÉ : date complète
|
||||
// ============================================
|
||||
|
||||
function loadHistoryLog() {
|
||||
const container = document.getElementById('adminLogContainer');
|
||||
@@ -273,15 +319,8 @@ function loadHistoryLog() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FONCTIONS DE FORMAT DATE
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* CORRIGÉ — Affiche la date complète dans l'historique
|
||||
* Avant : seulement "14:32"
|
||||
* Après : "12/06/2025 à 14:32"
|
||||
*/
|
||||
|
||||
function formatDateComplete(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
@@ -312,9 +351,6 @@ function getStatusLabel(status) {
|
||||
}[status] || status;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXPORT
|
||||
// ============================================
|
||||
|
||||
window.AdminModule = {
|
||||
refresh: () => {
|
||||
|
||||
130
js/auth.js
130
js/auth.js
@@ -1,57 +1,33 @@
|
||||
/**
|
||||
* ============================================
|
||||
* AUTH.JS - Système d'authentification
|
||||
* Smart Parking - BTS CIEL IR
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
// Configuration
|
||||
const AUTH_CONFIG = {
|
||||
apiUrl: 'http://localhost:3000/api',
|
||||
apiUrl: '/api',
|
||||
tokenKey: 'smart_parking_token',
|
||||
userKey: 'smart_parking_user'
|
||||
};
|
||||
|
||||
// État de l'authentification
|
||||
let authState = {
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
token: null
|
||||
};
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('🔐 Initialisation du système d\'authentification...');
|
||||
|
||||
// Vérifier si déjà connecté
|
||||
checkAuthStatus();
|
||||
|
||||
// Initialiser les écouteurs d'événements
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
/**
|
||||
* Vérifie le statut d'authentification
|
||||
*/
|
||||
function checkAuthStatus() {
|
||||
const token = localStorage.getItem(AUTH_CONFIG.tokenKey);
|
||||
const user = localStorage.getItem(AUTH_CONFIG.userKey);
|
||||
|
||||
if (token && user) {
|
||||
authState.token = token;
|
||||
authState.user = JSON.parse(user);
|
||||
authState.isLoggedIn = true;
|
||||
|
||||
// Rediriger vers le dashboard
|
||||
window.location.href = 'pages/dashboard.html';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les écouteurs d'événements
|
||||
*/
|
||||
function initEventListeners() {
|
||||
// Basculer vers l'inscription
|
||||
const showRegister = document.getElementById('showRegister');
|
||||
if (showRegister) {
|
||||
showRegister.addEventListener('click', (e) => {
|
||||
@@ -60,7 +36,6 @@ function initEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Basculer vers la connexion
|
||||
const showLogin = document.getElementById('showLogin');
|
||||
if (showLogin) {
|
||||
showLogin.addEventListener('click', (e) => {
|
||||
@@ -69,43 +44,27 @@ function initEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Formulaire de connexion
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', handleLogin);
|
||||
}
|
||||
if (loginForm) loginForm.addEventListener('submit', handleLogin);
|
||||
|
||||
// Formulaire d'inscription
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', handleRegister);
|
||||
}
|
||||
if (registerForm) registerForm.addEventListener('submit', handleRegister);
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le formulaire d'inscription
|
||||
*/
|
||||
function showRegisterForm() {
|
||||
document.getElementById('loginForm').classList.add('hidden');
|
||||
document.getElementById('registerForm').classList.remove('hidden');
|
||||
hideMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le formulaire de connexion
|
||||
*/
|
||||
function showLoginForm() {
|
||||
document.getElementById('registerForm').classList.add('hidden');
|
||||
document.getElementById('loginForm').classList.remove('hidden');
|
||||
hideMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la connexion
|
||||
*/
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
@@ -115,43 +74,29 @@ async function handleLogin(e) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Appel API de connexion
|
||||
const response = await fetch(`${AUTH_CONFIG.apiUrl}/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Stocker les informations
|
||||
localStorage.setItem(AUTH_CONFIG.tokenKey, data.token);
|
||||
localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(data.user));
|
||||
|
||||
showMessage('Connexion réussie ! Redirection...', 'success');
|
||||
|
||||
// Rediriger vers le dashboard
|
||||
setTimeout(() => {
|
||||
window.location.href = 'pages/dashboard.html';
|
||||
}, 1000);
|
||||
setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000);
|
||||
} else {
|
||||
showMessage(data.message || 'Email ou mot de passe incorrect', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur connexion:', error);
|
||||
// Mode offline - simulation
|
||||
handleOfflineLogin(email, password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la connexion en mode offline (simulation)
|
||||
*/
|
||||
function handleOfflineLogin(email, password) {
|
||||
// Vérifier dans les utilisateurs stockés localement
|
||||
const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
|
||||
const user = users.find(u => u.email === email && u.password === password);
|
||||
|
||||
@@ -159,71 +104,51 @@ function handleOfflineLogin(email, password) {
|
||||
const token = generateToken();
|
||||
localStorage.setItem(AUTH_CONFIG.tokenKey, token);
|
||||
localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(user));
|
||||
|
||||
showMessage('Connexion réussie ! Redirection...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'pages/dashboard.html';
|
||||
}, 1000);
|
||||
setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000);
|
||||
} else {
|
||||
// Compte admin par défaut
|
||||
if (email === 'admin@smartparking.fr' && password === 'admin123') {
|
||||
const adminUser = {
|
||||
id: 1,
|
||||
name: 'Administrateur',
|
||||
id: 1, name: 'Administrateur',
|
||||
email: 'admin@smartparking.fr',
|
||||
phone: '01 23 45 67 89',
|
||||
role: 'admin'
|
||||
phone: '01 23 45 67 89', role: 'admin'
|
||||
};
|
||||
const token = generateToken();
|
||||
localStorage.setItem(AUTH_CONFIG.tokenKey, token);
|
||||
localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(adminUser));
|
||||
|
||||
showMessage('Connexion admin réussie ! Redirection...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'pages/dashboard.html';
|
||||
}, 1000);
|
||||
setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage('Email ou mot de passe incorrect', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'inscription
|
||||
*/
|
||||
async function handleRegister(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('registerName').value;
|
||||
const email = document.getElementById('registerEmail').value;
|
||||
const phone = document.getElementById('registerPhone').value;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const passwordConfirm = document.getElementById('registerPasswordConfirm').value;
|
||||
|
||||
// Validation
|
||||
if (!name || !email || !phone || !password) {
|
||||
showMessage('Veuillez remplir tous les champs', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
showMessage('Les mots de passe ne correspondent pas', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
showMessage('Le mot de passe doit faire au moins 8 caractères', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Appel API d'inscription
|
||||
const response = await fetch(`${AUTH_CONFIG.apiUrl}/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, phone, password })
|
||||
});
|
||||
|
||||
@@ -233,7 +158,6 @@ async function handleRegister(e) {
|
||||
showMessage('Compte créé avec succès ! Vous pouvez vous connecter.', 'success');
|
||||
setTimeout(() => {
|
||||
showLoginForm();
|
||||
// Pré-remplir l'email
|
||||
document.getElementById('loginEmail').value = email;
|
||||
}, 1500);
|
||||
} else {
|
||||
@@ -241,39 +165,22 @@ async function handleRegister(e) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur inscription:', error);
|
||||
// Mode offline - stockage local
|
||||
handleOfflineRegister(name, email, phone, password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'inscription en mode offline (simulation)
|
||||
*/
|
||||
function handleOfflineRegister(name, email, phone, password) {
|
||||
// Récupérer les utilisateurs existants
|
||||
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
|
||||
|
||||
// Vérifier si l'email existe déjà
|
||||
if (users.find(u => u.email === email)) {
|
||||
showMessage('Cet email est déjà utilisé', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Créer le nouvel utilisateur
|
||||
const newUser = {
|
||||
id: Date.now(),
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
password, // En production: hasher le mot de passe !
|
||||
role: 'client',
|
||||
createdAt: new Date().toISOString()
|
||||
id: Date.now(), name, email, phone, password,
|
||||
role: 'client', createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Ajouter à la liste
|
||||
users.push(newUser);
|
||||
localStorage.setItem('smart_parking_users', JSON.stringify(users));
|
||||
|
||||
showMessage('Compte créé avec succès ! Vous pouvez vous connecter.', 'success');
|
||||
setTimeout(() => {
|
||||
showLoginForm();
|
||||
@@ -281,16 +188,10 @@ function handleOfflineRegister(name, email, phone, password) {
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un token simple
|
||||
*/
|
||||
function generateToken() {
|
||||
return 'token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche un message
|
||||
*/
|
||||
function showMessage(message, type) {
|
||||
const messageEl = document.getElementById('authMessage');
|
||||
messageEl.textContent = message;
|
||||
@@ -298,24 +199,17 @@ function showMessage(message, type) {
|
||||
messageEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache le message
|
||||
*/
|
||||
function hideMessage() {
|
||||
const messageEl = document.getElementById('authMessage');
|
||||
messageEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Déconnexion
|
||||
*/
|
||||
function logout() {
|
||||
localStorage.removeItem(AUTH_CONFIG.tokenKey);
|
||||
localStorage.removeItem(AUTH_CONFIG.userKey);
|
||||
window.location.href = '../index.html';
|
||||
}
|
||||
|
||||
// Exporter les fonctions
|
||||
window.Auth = {
|
||||
logout,
|
||||
getToken: () => localStorage.getItem(AUTH_CONFIG.tokenKey),
|
||||
|
||||
113
js/dashboard.js
113
js/dashboard.js
@@ -1,13 +1,4 @@
|
||||
/**
|
||||
* ============================================
|
||||
* DASHBOARD.JS - Gestion du dashboard
|
||||
* Smart Parking - BTS CIEL IR
|
||||
* CORRIGÉ : cancelReservation utilisait spotNumber
|
||||
* au lieu de spotId pour libérer la place
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
const API_URL = 'http://localhost:3000/api';
|
||||
const API_URL = '/api';
|
||||
|
||||
let dashboardState = {
|
||||
user: null,
|
||||
@@ -93,10 +84,10 @@ function loadUserData() {
|
||||
document.getElementById('userName').textContent = user.name;
|
||||
document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client';
|
||||
|
||||
document.getElementById('profileName').textContent = user.name;
|
||||
document.getElementById('profileNameInput').value = user.name;
|
||||
document.getElementById('profileEmailInput').value = user.email;
|
||||
document.getElementById('profilePhoneInput').value = user.phone || '';
|
||||
document.getElementById('profileName').textContent = user.name;
|
||||
document.getElementById('profileNameInput').value = user.name;
|
||||
document.getElementById('profileEmailInput').value = user.email;
|
||||
document.getElementById('profilePhoneInput').value = user.phone || '';
|
||||
|
||||
const roleBadge = document.getElementById('profileRole');
|
||||
if (roleBadge) {
|
||||
@@ -137,7 +128,7 @@ function loadMyReservations() {
|
||||
<h4>Place ${res.spotNumber}</h4>
|
||||
<div class="reservation-details">
|
||||
<span>📅 ${res.date}</span>
|
||||
<span>🕐 ${res.startTime}</span>
|
||||
<span>🕐 ${res.startTime} - ${res.endTime}</span>
|
||||
<span>⏱️ ${formatDurationLabel(res.duration)}</span>
|
||||
<span>🚗 ${res.vehicle || 'N/A'}</span>
|
||||
</div>
|
||||
@@ -155,22 +146,16 @@ function loadMyReservations() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* CORRIGÉ : utilisait reservation.spotNumber (le numéro visible)
|
||||
* au lieu de reservation.spotId (l'id interne), ce qui empêchait
|
||||
* la place d'être libérée sur la carte.
|
||||
*/
|
||||
function cancelReservation(reservationId) {
|
||||
if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return;
|
||||
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
const reservation = reservations.find(r => r.id === reservationId);
|
||||
|
||||
if (reservation) {
|
||||
reservation.status = 'cancelled';
|
||||
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
|
||||
|
||||
// CORRIGÉ : spotId au lieu de spotNumber
|
||||
if (window.ParkingMap) {
|
||||
window.ParkingMap.setSpotStatus(reservation.spotId, 'free');
|
||||
}
|
||||
@@ -181,26 +166,82 @@ function cancelReservation(reservationId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour du profil
|
||||
document.getElementById('profileForm')?.addEventListener('submit', (e) => {
|
||||
document.getElementById('profileForm')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const phone = document.getElementById('profilePhoneInput').value;
|
||||
const newPassword = document.getElementById('profileNewPassword').value;
|
||||
const confirmPass = document.getElementById('profileNewPassword').value;
|
||||
|
||||
let user = dashboardState.user;
|
||||
user.phone = phone;
|
||||
if (newPassword) user.password = newPassword;
|
||||
|
||||
localStorage.setItem('smart_parking_user', JSON.stringify(user));
|
||||
|
||||
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
|
||||
const idx = users.findIndex(u => u.id === user.id);
|
||||
if (idx !== -1) {
|
||||
users[idx] = user;
|
||||
localStorage.setItem('smart_parking_users', JSON.stringify(users));
|
||||
if (newPassword && newPassword.length < 8) {
|
||||
showToast('Le mot de passe doit faire au moins 8 caractères', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('Profil mis à jour', 'success');
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
|
||||
const updateData = { phone };
|
||||
if (newPassword) {
|
||||
updateData.password = newPassword;
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(`${API_URL}/users/profile`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
|
||||
let user = dashboardState.user;
|
||||
user.phone = phone;
|
||||
dashboardState.user = user;
|
||||
localStorage.setItem('smart_parking_user', JSON.stringify(user));
|
||||
|
||||
|
||||
document.getElementById('profileNewPassword').value = '';
|
||||
|
||||
showToast('Profil mis à jour avec succès !', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Erreur lors de la mise à jour', 'error');
|
||||
}
|
||||
|
||||
} catch (_err) {
|
||||
|
||||
let user = dashboardState.user;
|
||||
user.phone = phone;
|
||||
if (newPassword) user.password = newPassword;
|
||||
dashboardState.user = user;
|
||||
localStorage.setItem('smart_parking_user', JSON.stringify(user));
|
||||
|
||||
let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]');
|
||||
const idx = users.findIndex(u => u.id === user.id);
|
||||
if (idx !== -1) {
|
||||
users[idx] = user;
|
||||
localStorage.setItem('smart_parking_users', JSON.stringify(users));
|
||||
}
|
||||
|
||||
document.getElementById('profileNewPassword').value = '';
|
||||
showToast('Profil mis à jour', 'success');
|
||||
}
|
||||
} else {
|
||||
let user = dashboardState.user;
|
||||
user.phone = phone;
|
||||
if (newPassword) user.password = newPassword;
|
||||
dashboardState.user = user;
|
||||
localStorage.setItem('smart_parking_user', JSON.stringify(user));
|
||||
showToast('Profil mis à jour', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
function getStatusLabel(status) {
|
||||
@@ -208,7 +249,7 @@ function getStatusLabel(status) {
|
||||
}
|
||||
|
||||
function formatDurationLabel(minutes) {
|
||||
if (minutes >= 480) return 'Journée';
|
||||
if (minutes >= 480) return 'Journée (8h)';
|
||||
if (minutes >= 60) return Math.floor(minutes / 60) + 'h' + (minutes % 60 ? (minutes % 60) + 'min' : '');
|
||||
return minutes + ' min';
|
||||
}
|
||||
|
||||
117
js/map.js
117
js/map.js
@@ -1,15 +1,6 @@
|
||||
/**
|
||||
* ============================================
|
||||
* MAP.JS - Carte des places de parking
|
||||
* Smart Parking v2.0
|
||||
* MODIFIÉ : polling API toutes les 3s pour recevoir
|
||||
* les mises à jour en temps réel depuis Arduino
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
const MAP_CONFIG = {
|
||||
totalSpots: 10,
|
||||
updateInterval: 3000 // Refresh depuis l'API toutes les 3 secondes
|
||||
updateInterval: 3000
|
||||
};
|
||||
|
||||
let spotsState = {
|
||||
@@ -19,9 +10,7 @@ let spotsState = {
|
||||
|
||||
const SPOT_STATUS = { FREE: 'free', OCCUPIED: 'occupied', RESERVED: 'reserved' };
|
||||
|
||||
// ============================================
|
||||
// INITIALISATION
|
||||
// ============================================
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('🗺️ Initialisation de la carte...');
|
||||
@@ -29,30 +18,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
async function initParkingMap() {
|
||||
// Essayer d'abord de charger depuis l'API (données MariaDB + Arduino)
|
||||
const loaded = await loadSpotsFromAPI();
|
||||
|
||||
// Si pas d'API disponible, utiliser le localStorage (mode offline)
|
||||
if (!loaded) {
|
||||
loadSpotsFromStorage();
|
||||
}
|
||||
if (!loaded) loadSpotsFromStorage();
|
||||
|
||||
renderMap();
|
||||
updateStats();
|
||||
updateReservationForm();
|
||||
|
||||
// ⭐ Polling toutes les 3 secondes pour recevoir les updates Arduino
|
||||
startAPIPolling();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CHARGEMENT DES PLACES
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Charge les places depuis l'API (MariaDB)
|
||||
* Retourne true si succès, false si hors-ligne
|
||||
*/
|
||||
|
||||
async function loadSpotsFromAPI() {
|
||||
try {
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
@@ -67,7 +43,6 @@ async function loadSpotsFromAPI() {
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.data.length) return false;
|
||||
|
||||
// Convertir le format API → format interne
|
||||
spotsState.spots = data.data.map(s => ({
|
||||
id: s.id,
|
||||
number: s.number,
|
||||
@@ -76,19 +51,14 @@ async function loadSpotsFromAPI() {
|
||||
sensorId: s.sensor_id
|
||||
}));
|
||||
|
||||
// Synchroniser avec localStorage pour le mode offline
|
||||
saveSpots();
|
||||
return true;
|
||||
|
||||
} catch (_err) {
|
||||
// Serveur non joignable → mode offline
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les places depuis le localStorage (mode offline)
|
||||
*/
|
||||
function loadSpotsFromStorage() {
|
||||
const stored = localStorage.getItem('smart_parking_spots');
|
||||
if (stored) {
|
||||
@@ -116,23 +86,22 @@ function saveSpots() {
|
||||
localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ⭐ POLLING TEMPS RÉEL (mises à jour Arduino)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Interroge l'API toutes les 3 secondes.
|
||||
* Si l'Arduino a changé l'état d'une place via MQTT,
|
||||
* la carte se met à jour automatiquement.
|
||||
*/
|
||||
function startAPIPolling() {
|
||||
setInterval(async () => {
|
||||
const loaded = await loadSpotsFromAPI();
|
||||
if (loaded) {
|
||||
renderMap();
|
||||
updateStats();
|
||||
updateReservationForm();
|
||||
// Rafraîchir les détails si une place est sélectionnée
|
||||
|
||||
|
||||
const resSpot = document.getElementById('resSpot');
|
||||
const hasSelection = resSpot && resSpot.value !== '';
|
||||
if (!hasSelection) {
|
||||
updateReservationForm();
|
||||
}
|
||||
|
||||
|
||||
if (spotsState.selectedSpot) {
|
||||
const updated = spotsState.spots.find(s => s.id === spotsState.selectedSpot.id);
|
||||
if (updated) {
|
||||
@@ -144,9 +113,6 @@ function startAPIPolling() {
|
||||
}, MAP_CONFIG.updateInterval);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RENDU DE LA CARTE
|
||||
// ============================================
|
||||
|
||||
function renderMap() {
|
||||
const mapContainer = document.getElementById('parkingMap');
|
||||
@@ -202,21 +168,21 @@ function showSpotDetails(spot) {
|
||||
</div>
|
||||
${reservation ? `
|
||||
<div class="spot-info-row">
|
||||
<span class="spot-info-label">Réservé par</span>
|
||||
<span class="spot-info-value">${reservation.userName}</span>
|
||||
</div>
|
||||
<div class="spot-info-row">
|
||||
<span class="spot-info-label">Jusqu'à</span>
|
||||
<span class="spot-info-label">Réservé jusqu'à</span>
|
||||
<span class="spot-info-value">${reservation.endTime}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${spot.status === SPOT_STATUS.FREE ? `
|
||||
${spot.status !== SPOT_STATUS.OCCUPIED ? `
|
||||
<button class="btn btn-primary btn-block"
|
||||
onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});">
|
||||
<span class="btn-icon">📅</span>
|
||||
Réserver cette place
|
||||
</button>
|
||||
` : ''}
|
||||
` : `
|
||||
<p style="color: var(--danger); text-align: center; margin-top: 12px;">
|
||||
🚗 Une voiture est physiquement sur cette place
|
||||
</p>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -226,9 +192,6 @@ function findReservationForSpot(spotId) {
|
||||
return reservations.find(r => r.spotId === spotId && r.status === 'active');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// STATISTIQUES & FORMULAIRE
|
||||
// ============================================
|
||||
|
||||
function updateStats() {
|
||||
const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length;
|
||||
@@ -241,22 +204,35 @@ function updateStats() {
|
||||
document.getElementById('totalCount').textContent = spotsState.spots.length;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateReservationForm() {
|
||||
const select = document.getElementById('resSpot');
|
||||
if (!select) return;
|
||||
|
||||
|
||||
const currentValue = select.value;
|
||||
|
||||
const firstOption = select.options[0];
|
||||
select.innerHTML = '';
|
||||
select.appendChild(firstOption);
|
||||
|
||||
spotsState.spots
|
||||
.filter(s => s.status === SPOT_STATUS.FREE)
|
||||
.forEach(spot => {
|
||||
const option = document.createElement('option');
|
||||
option.value = spot.id;
|
||||
option.textContent = `Place ${spot.number}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
const availableSpots = spotsState.spots.filter(s => s.status !== SPOT_STATUS.OCCUPIED);
|
||||
|
||||
availableSpots.forEach(spot => {
|
||||
const option = document.createElement('option');
|
||||
option.value = spot.id;
|
||||
|
||||
option.textContent = spot.status === SPOT_STATUS.RESERVED
|
||||
? `Place ${spot.number} (réservée en ce moment)`
|
||||
: `Place ${spot.number}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
if (currentValue) {
|
||||
select.value = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
function selectSpotForReservation(spotId) {
|
||||
@@ -264,9 +240,7 @@ function selectSpotForReservation(spotId) {
|
||||
if (select) select.value = spotId;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ACTIONS ADMIN & SIMULATION
|
||||
// ============================================
|
||||
|
||||
|
||||
async function setSpotStatus(spotId, status) {
|
||||
const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId);
|
||||
@@ -279,7 +253,6 @@ async function setSpotStatus(spotId, status) {
|
||||
updateStats();
|
||||
updateReservationForm();
|
||||
|
||||
// Synchroniser avec l'API si connecté
|
||||
try {
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
if (token) {
|
||||
@@ -315,9 +288,6 @@ function setTotalSpots(count) {
|
||||
updateReservationForm();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UTILITAIRES
|
||||
// ============================================
|
||||
|
||||
function isAdmin() {
|
||||
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
|
||||
@@ -340,9 +310,6 @@ function formatDate(dateString) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXPORT
|
||||
// ============================================
|
||||
|
||||
window.ParkingMap = {
|
||||
refresh: async () => {
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
/**
|
||||
* ============================================
|
||||
* RESERVATION.JS - Système de réservation
|
||||
* Smart Parking v3.0
|
||||
* CORRIGÉ : ne bloque plus une place pour
|
||||
* toujours — vérifie uniquement si
|
||||
* une voiture est physiquement là
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
const PRICING = {
|
||||
30: 2,
|
||||
60: 3,
|
||||
@@ -27,14 +17,46 @@ const TIME_SLOTS = [
|
||||
let currentReservation = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('📅 Initialisation du système de réservation...');
|
||||
cleanupExpiredReservations();
|
||||
initReservationForm();
|
||||
initDatePicker();
|
||||
initTimeSlots();
|
||||
initPricePreview();
|
||||
initConfirmationModal();
|
||||
setInterval(cleanupExpiredReservations, 60000);
|
||||
});
|
||||
|
||||
function cleanupExpiredReservations() {
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
const now = new Date();
|
||||
let cleaned = 0;
|
||||
|
||||
reservations.forEach(r => {
|
||||
if (r.status !== 'active' && r.status !== 'pending') return;
|
||||
const endDateTime = new Date(r.date + 'T' + r.endTime);
|
||||
if (endDateTime < now) {
|
||||
r.status = 'completed';
|
||||
cleaned++;
|
||||
}
|
||||
});
|
||||
|
||||
if (cleaned > 0) {
|
||||
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
|
||||
}
|
||||
}
|
||||
|
||||
function checkLocalConflict(spotId, date, startTime, endTime) {
|
||||
cleanupExpiredReservations();
|
||||
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
|
||||
return reservations.some(r => {
|
||||
if (r.spotId !== spotId) return false;
|
||||
if (r.date !== date) return false;
|
||||
if (r.status !== 'active' && r.status !== 'pending') return false;
|
||||
return r.startTime < endTime && r.endTime > startTime;
|
||||
});
|
||||
}
|
||||
|
||||
function initReservationForm() {
|
||||
const form = document.getElementById('reservationForm');
|
||||
if (!form) return;
|
||||
@@ -52,18 +74,16 @@ function initDatePicker() {
|
||||
function initTimeSlots() {
|
||||
const select = document.getElementById('resStartTime');
|
||||
if (!select) return;
|
||||
|
||||
TIME_SLOTS.forEach(time => {
|
||||
const option = document.createElement('option');
|
||||
option.value = time;
|
||||
option.textContent = time;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMins = now.getMinutes();
|
||||
const nextSlot = TIME_SLOTS.find(t => {
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMins = now.getMinutes();
|
||||
const nextSlot = TIME_SLOTS.find(t => {
|
||||
const [h, m] = t.split(':').map(Number);
|
||||
return h > currentHour || (h === currentHour && m > currentMins);
|
||||
});
|
||||
@@ -83,14 +103,6 @@ function updatePricePreview() {
|
||||
document.getElementById('previewPrice').textContent = price + '€';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire
|
||||
*
|
||||
* CORRIGÉ : on n'empêche plus la réservation si la place
|
||||
* est "reserved" dans le localStorage. On laisse le serveur
|
||||
* vérifier les conflits d'horaire. On bloque uniquement si
|
||||
* une voiture est physiquement détectée (status "occupied").
|
||||
*/
|
||||
async function handleReservationSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -111,25 +123,29 @@ async function handleReservationSubmit(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// CORRIGÉ : on bloque uniquement si une voiture est physiquement là
|
||||
// Une place "reserved" peut quand même être réservée à un autre horaire
|
||||
const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]');
|
||||
const spot = spots.find(s => s.id === spotId);
|
||||
|
||||
if (spot && spot.status === 'occupied') {
|
||||
Dashboard.showToast('Une voiture est déjà sur cette place', 'error');
|
||||
Dashboard.showToast('Une voiture est deja sur cette place', 'error');
|
||||
if (window.ParkingMap) window.ParkingMap.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculer l'heure de fin
|
||||
const endDate = new Date(date + 'T' + startTime);
|
||||
endDate.setMinutes(endDate.getMinutes() + duration);
|
||||
const endTime = endDate.toTimeString().slice(0, 5);
|
||||
|
||||
// Essayer de créer la réservation via l'API
|
||||
// Le serveur vérifiera les conflits d'horaire
|
||||
if (checkLocalConflict(spotId, date, startTime, endTime)) {
|
||||
Dashboard.showToast(
|
||||
'Cette place est deja reservee a cet horaire. Essayez un autre creneau ou une autre date.',
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
let apiSuccess = false;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
@@ -149,12 +165,10 @@ async function handleReservationSubmit(e) {
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
// Le serveur a détecté un conflit ou une erreur
|
||||
Dashboard.showToast(data.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Succès via API
|
||||
currentReservation = {
|
||||
id: data.data.id,
|
||||
userId: user.id,
|
||||
@@ -167,28 +181,34 @@ async function handleReservationSubmit(e) {
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
apiSuccess = true;
|
||||
|
||||
} catch (_err) {
|
||||
// Mode offline : enregistrement local
|
||||
currentReservation = creerReservationLocale(
|
||||
user, spotId, spot, date, startTime, endTime, duration, vehicle
|
||||
);
|
||||
apiSuccess = false;
|
||||
}
|
||||
} else {
|
||||
// Pas de token : mode offline
|
||||
currentReservation = creerReservationLocale(
|
||||
user, spotId, spot, date, startTime, endTime, duration, vehicle
|
||||
);
|
||||
}
|
||||
|
||||
// Sauvegarder dans le localStorage
|
||||
if (!apiSuccess) {
|
||||
currentReservation = {
|
||||
id: Date.now(),
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
spotId: spotId,
|
||||
spotNumber: spot ? spot.number : spotId,
|
||||
date, startTime, endTime, duration,
|
||||
vehicle: vehicle.toUpperCase(),
|
||||
price: PRICING[duration],
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
reservations.push(currentReservation);
|
||||
localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations));
|
||||
|
||||
// Mettre à jour la carte seulement si la réservation est pour aujourd'hui
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const now = new Date();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const now = new Date();
|
||||
const resStart = new Date(date + 'T' + startTime);
|
||||
const diffMin = (resStart - now) / 60000;
|
||||
|
||||
@@ -197,40 +217,16 @@ async function handleReservationSubmit(e) {
|
||||
}
|
||||
|
||||
addToHistory(
|
||||
'Réservation',
|
||||
`Place ${currentReservation.spotNumber} réservée le ${date} de ${startTime} à ${endTime} — ${PRICING[duration]}€`
|
||||
'Reservation',
|
||||
'Place ' + currentReservation.spotNumber + ' reservee le ' + date + ' de ' + startTime + ' a ' + endTime + ' — ' + PRICING[duration] + '€'
|
||||
);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
document.getElementById('reservationForm').reset();
|
||||
initDatePicker();
|
||||
updatePricePreview();
|
||||
|
||||
showConfirmationModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une réservation en mode offline (localStorage)
|
||||
*/
|
||||
function creerReservationLocale(user, spotId, spot, date, startTime, endTime, duration, vehicle) {
|
||||
return {
|
||||
id: Date.now(),
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
spotId: spotId,
|
||||
spotNumber: spot ? spot.number : spotId,
|
||||
date, startTime, endTime, duration,
|
||||
vehicle: vehicle.toUpperCase(),
|
||||
price: PRICING[duration],
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MODAL DE CONFIRMATION
|
||||
// ============================================
|
||||
|
||||
function initConfirmationModal() {
|
||||
document.getElementById('closeConfirmationModal')?.addEventListener('click', hideConfirmationModal);
|
||||
document.getElementById('closeConfirmationBtn')?.addEventListener('click', hideConfirmationModal);
|
||||
@@ -238,60 +234,44 @@ function initConfirmationModal() {
|
||||
|
||||
function showConfirmationModal() {
|
||||
if (!currentReservation) return;
|
||||
|
||||
const modal = document.getElementById('confirmationModal');
|
||||
|
||||
document.getElementById('paySpot').textContent = 'Place ' + currentReservation.spotNumber;
|
||||
document.getElementById('payDate').textContent = formatDate(currentReservation.date);
|
||||
document.getElementById('payTime').textContent = currentReservation.startTime + ' - ' + currentReservation.endTime;
|
||||
document.getElementById('payDuration').textContent = formatDuration(currentReservation.duration);
|
||||
document.getElementById('payTotal').textContent = currentReservation.price + '€';
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideConfirmationModal() {
|
||||
document.getElementById('confirmationModal').classList.add('hidden');
|
||||
|
||||
if (window.Dashboard) {
|
||||
Dashboard.navigateToPage('my-reservations');
|
||||
document.querySelector('[data-page="my-reservations"]')?.classList.add('active');
|
||||
document.querySelector('[data-page="reservation"]')?.classList.remove('active');
|
||||
}
|
||||
|
||||
if (window.Dashboard) Dashboard.refreshStats();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UTILITAIRES
|
||||
// ============================================
|
||||
|
||||
function addToHistory(action, details) {
|
||||
let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]');
|
||||
history.unshift({
|
||||
id: Date.now(),
|
||||
action,
|
||||
details,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
history.unshift({ id: Date.now(), action, details, timestamp: new Date().toISOString() });
|
||||
if (history.length > 100) history = history.slice(0, 100);
|
||||
localStorage.setItem('smart_parking_history', JSON.stringify(history));
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('fr-FR', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
||||
});
|
||||
return new Date(dateString).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
function formatDuration(minutes) {
|
||||
if (minutes >= 480) return 'Journée (8h)';
|
||||
if (minutes >= 480) return 'Journee (8h)';
|
||||
if (minutes >= 60) {
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = minutes % 60;
|
||||
return m > 0 ? `${h}h ${m}min` : `${h}h`;
|
||||
return m > 0 ? h + 'h ' + m + 'min' : h + 'h';
|
||||
}
|
||||
return `${minutes} min`;
|
||||
return minutes + ' min';
|
||||
}
|
||||
|
||||
window.Reservation = { PRICING, TIME_SLOTS, formatDuration, addToHistory };
|
||||
@@ -6,10 +6,8 @@
|
||||
<title>Smart Parking - Dashboard</title>
|
||||
<link rel="stylesheet" href="../css/style.css">
|
||||
<link rel="stylesheet" href="../css/dashboard.css">
|
||||
<!-- Chart.js retiré car le graphique d'occupation a été supprimé -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
@@ -49,7 +47,7 @@
|
||||
|
||||
<main class="main">
|
||||
|
||||
<!-- ═══ PAGE : CARTE DES PLACES ═══ -->
|
||||
|
||||
<section id="map" class="page active">
|
||||
<div class="container">
|
||||
<h2 class="page-title"><span class="icon">🗺️</span>Carte du Parking</h2>
|
||||
@@ -69,17 +67,10 @@
|
||||
<span class="stat-label">Places occupées</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card reserved">
|
||||
<div class="stat-icon">📅</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-value" id="reservedCount">0</span>
|
||||
<span class="stat-label">Places réservées</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card total">
|
||||
<div class="stat-icon">🅿️</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-value" id="totalCount">10</span>
|
||||
<span class="stat-value" id="totalCount">3</span>
|
||||
<span class="stat-label">Total places</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,7 +83,6 @@
|
||||
<div class="legend">
|
||||
<div class="legend-item"><span class="legend-color free"></span><span>Libre</span></div>
|
||||
<div class="legend-item"><span class="legend-color occupied"></span><span>Occupée</span></div>
|
||||
<div class="legend-item"><span class="legend-color reserved"></span><span>Réservée</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spot-details-container">
|
||||
@@ -116,7 +106,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PAGE : RÉSERVATION ═══ -->
|
||||
|
||||
<section id="reservation" class="page hidden">
|
||||
<div class="container">
|
||||
<h2 class="page-title"><span class="icon">📅</span>Réserver une place</h2>
|
||||
@@ -167,7 +157,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal de confirmation -->
|
||||
|
||||
<div id="confirmationModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -198,7 +188,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PAGE : MES RÉSERVATIONS ═══ -->
|
||||
|
||||
<section id="my-reservations" class="page hidden">
|
||||
<div class="container">
|
||||
<h2 class="page-title"><span class="icon">🎫</span>Mes réservations</h2>
|
||||
@@ -211,7 +201,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PAGE : PROFIL ═══ -->
|
||||
|
||||
<section id="profile" class="page hidden">
|
||||
<div class="container">
|
||||
<h2 class="page-title"><span class="icon">👤</span>Mon profil</h2>
|
||||
@@ -266,12 +256,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PAGE : ADMIN ═══ -->
|
||||
|
||||
<section id="admin" class="page hidden admin-page">
|
||||
<div class="container">
|
||||
<h2 class="page-title"><span class="icon">⚙️</span>Administration</h2>
|
||||
|
||||
<!-- Stats admin -->
|
||||
|
||||
<div class="admin-stats-grid">
|
||||
<div class="admin-stat">
|
||||
<span class="admin-stat-value" id="adminTotalUsers">0</span>
|
||||
@@ -291,7 +281,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gestion des places -->
|
||||
|
||||
<div class="admin-section">
|
||||
<h3>🅿️ Gestion des places</h3>
|
||||
<div class="admin-places-control">
|
||||
@@ -299,7 +289,7 @@
|
||||
<label>Nombre total de places</label>
|
||||
<div class="input-group">
|
||||
<input type="number" id="adminTotalSpots" class="form-control"
|
||||
min="5" max="50" value="10">
|
||||
min="1" max="20" value="10">
|
||||
<button id="updateSpotsBtn" class="btn btn-primary">Mettre à jour</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,7 +297,7 @@
|
||||
<div class="admin-places-list" id="adminPlacesList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Utilisateurs -->
|
||||
|
||||
<div class="admin-section">
|
||||
<h3>👥 Utilisateurs</h3>
|
||||
<div class="table-container">
|
||||
@@ -323,7 +313,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Réservations -->
|
||||
|
||||
<div class="admin-section">
|
||||
<h3>📅 Toutes les réservations</h3>
|
||||
<div class="table-container">
|
||||
@@ -339,9 +329,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ GRAPHIQUE D'OCCUPATION SUPPRIMÉ ici ═══ -->
|
||||
|
||||
<!-- Historique -->
|
||||
|
||||
<div class="admin-section">
|
||||
<h3>📜 Historique</h3>
|
||||
<div class="log-container" id="adminLogContainer"></div>
|
||||
@@ -358,6 +347,5 @@
|
||||
<script src="../js/map.js"></script>
|
||||
<script src="../js/reservation.js"></script>
|
||||
<script src="../js/admin.js"></script>
|
||||
<!-- Chart.js et admin.js n'utilisent plus le graphique -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
/**
|
||||
* ============================================
|
||||
* DATABASE.JS - Gestion MariaDB
|
||||
* Smart Parking v3.0
|
||||
* AJOUTÉ : checkReservationConflict()
|
||||
* → vérifie les conflits d'horaire
|
||||
* au lieu de bloquer toute la place
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const bcrypt = require('bcryptjs');
|
||||
require('dotenv').config();
|
||||
@@ -23,9 +13,6 @@ const pool = mysql.createPool({
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// INITIALISATION
|
||||
// ============================================
|
||||
|
||||
async function initDatabase() {
|
||||
try {
|
||||
@@ -105,7 +92,6 @@ async function initDatabase() {
|
||||
|
||||
console.log('✅ Tables vérifiées/créées');
|
||||
|
||||
// Admin par défaut
|
||||
const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']);
|
||||
if (rows.length === 0) {
|
||||
const hashed = await bcrypt.hash('admin123', 10);
|
||||
@@ -116,7 +102,7 @@ async function initDatabase() {
|
||||
console.log('✅ Admin par défaut créé');
|
||||
}
|
||||
|
||||
// 10 places par défaut
|
||||
|
||||
const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots');
|
||||
if (spots[0].count === 0) {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
@@ -134,9 +120,6 @@ async function initDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UTILISATEURS
|
||||
// ============================================
|
||||
|
||||
async function createUser(name, email, phone, hashedPassword, role = 'client') {
|
||||
const [result] = await pool.query(
|
||||
@@ -180,9 +163,6 @@ async function deleteUser(id) {
|
||||
return { deleted: result.affectedRows };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PLACES
|
||||
// ============================================
|
||||
|
||||
async function createSpot(number, sensorId, status = 'free') {
|
||||
const [result] = await pool.query(
|
||||
@@ -215,9 +195,6 @@ async function deleteAllSpots() {
|
||||
return { deleted: result.affectedRows };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RÉSERVATIONS
|
||||
// ============================================
|
||||
|
||||
async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) {
|
||||
const [result] = await pool.query(
|
||||
@@ -229,21 +206,7 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat
|
||||
return { id: result.insertId };
|
||||
}
|
||||
|
||||
/**
|
||||
* ⭐ NOUVELLE FONCTION — Vérification des conflits d'horaire
|
||||
*
|
||||
* Problème corrigé : avant, quand une place était réservée,
|
||||
* elle restait bloquée pour TOUS les jours et TOUTES les heures.
|
||||
*
|
||||
* Maintenant on vérifie uniquement s'il y a une réservation
|
||||
* qui se chevauche sur la MÊME date et le MÊME créneau horaire.
|
||||
*
|
||||
* Exemple :
|
||||
* Place 2 réservée aujourd'hui 10h-11h ✅
|
||||
* Place 2 réservée aujourd'hui 14h-15h ✅ (pas de conflit)
|
||||
* Place 2 réservée demain 10h-11h ✅ (pas de conflit)
|
||||
* Place 2 réservée aujourd'hui 10h30-11h30 ❌ (conflit !)
|
||||
*/
|
||||
|
||||
async function checkReservationConflict(spotId, date, startTime, endTime) {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT id FROM reservations
|
||||
@@ -254,7 +217,7 @@ async function checkReservationConflict(spotId, date, startTime, endTime) {
|
||||
AND end_time > ?
|
||||
`, [spotId, date, endTime, startTime]);
|
||||
|
||||
return rows.length > 0; // true = conflit, false = créneau libre
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
async function getReservationById(id) {
|
||||
@@ -299,10 +262,7 @@ async function updateReservationStatus(id, status) {
|
||||
return { changed: result.affectedRows };
|
||||
}
|
||||
|
||||
/**
|
||||
* Expiration automatique des réservations
|
||||
* Appelée toutes les 60 secondes par server.js
|
||||
*/
|
||||
|
||||
async function expireReservations() {
|
||||
const [expiredRows] = await pool.query(`
|
||||
SELECT r.id, r.spot_id, r.user_id, s.number AS spot_number
|
||||
@@ -334,9 +294,36 @@ async function expireReservations() {
|
||||
return expiredRows.length;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HISTORIQUE
|
||||
// ============================================
|
||||
|
||||
async function cleanupStaleReservedSpots() {
|
||||
try {
|
||||
const [staleSpots] = await pool.query(`
|
||||
SELECT s.id, s.number FROM spots s
|
||||
WHERE s.status = 'reserved'
|
||||
AND s.id NOT IN (
|
||||
SELECT r.spot_id FROM reservations r
|
||||
WHERE r.status IN ('active', 'pending')
|
||||
AND r.date = CURDATE()
|
||||
AND r.start_time <= CURTIME()
|
||||
AND r.end_time > CURTIME()
|
||||
)
|
||||
`);
|
||||
|
||||
for (const spot of staleSpots) {
|
||||
await pool.query(
|
||||
"UPDATE spots SET status = 'free', last_update = NOW() WHERE id = ?",
|
||||
[spot.id]
|
||||
);
|
||||
console.log(`🧹 Place ${spot.number} nettoyée : reserved → free (pas de réservation en cours)`);
|
||||
}
|
||||
|
||||
return staleSpots.length;
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur cleanup stale spots:', err.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function addHistory(action, details, userId = null) {
|
||||
const [result] = await pool.query(
|
||||
@@ -358,9 +345,6 @@ async function getHistory(limit = 50) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// STATISTIQUES
|
||||
// ============================================
|
||||
|
||||
async function recordStats(total, free, occupied, reserved) {
|
||||
const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0;
|
||||
@@ -381,9 +365,7 @@ async function getStats(days = 7) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MQTT
|
||||
// ============================================
|
||||
|
||||
|
||||
async function recordMqttEvent(topic, message) {
|
||||
const [result] = await pool.query(
|
||||
@@ -393,9 +375,6 @@ async function recordMqttEvent(topic, message) {
|
||||
return { id: result.insertId };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FERMETURE
|
||||
// ============================================
|
||||
|
||||
async function closeDatabase() {
|
||||
await pool.end();
|
||||
@@ -411,7 +390,7 @@ module.exports = {
|
||||
createReservation, checkReservationConflict,
|
||||
getReservationById, getReservationsByUser,
|
||||
getAllReservations, updateReservationStatus,
|
||||
expireReservations,
|
||||
expireReservations, cleanupStaleReservedSpots,
|
||||
addHistory, getHistory,
|
||||
recordStats, getStats,
|
||||
recordMqttEvent
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
/**
|
||||
* ============================================
|
||||
* API ROUTES - Routes de l'API REST
|
||||
* Smart Parking v3.0
|
||||
* CORRIGÉ : réservation vérifie les conflits
|
||||
* d'horaire au lieu de bloquer la place
|
||||
* définitivement
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const db = require('../db/database');
|
||||
const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// ============================================
|
||||
// AUTHENTIFICATION
|
||||
// ============================================
|
||||
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
@@ -58,9 +46,7 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// UTILISATEURS
|
||||
// ============================================
|
||||
|
||||
|
||||
router.get('/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
@@ -80,12 +66,48 @@ router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// PLACES
|
||||
// ============================================
|
||||
router.put('/users/profile', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { phone, password } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
const updates = {};
|
||||
if (phone !== undefined) updates.phone = phone;
|
||||
|
||||
if (password && password.length >= 8) {
|
||||
updates.password = await bcrypt.hash(password, 10);
|
||||
} else if (password && password.length < 8) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Le mot de passe doit faire au moins 8 caractères'
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({ success: false, message: 'Aucune modification fournie' });
|
||||
}
|
||||
|
||||
await db.updateUser(userId, updates);
|
||||
const updatedUser = await db.getUserById(userId);
|
||||
|
||||
await db.addHistory('Modification profil', `Utilisateur #${userId} a modifié son profil`, userId);
|
||||
|
||||
res.json({
|
||||
success: true, message: 'Profil mis à jour avec succès',
|
||||
user: { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email, phone: updatedUser.phone, role: updatedUser.role }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur update profile:', err.message);
|
||||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get('/spots', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
|
||||
await db.cleanupStaleReservedSpots();
|
||||
|
||||
const spots = await db.getAllSpots();
|
||||
res.json({ success: true, count: spots.length, data: spots });
|
||||
} catch (err) {
|
||||
@@ -99,16 +121,17 @@ router.put('/spots/:id/status', authenticateToken, async (req, res) => {
|
||||
if (!['free', 'occupied', 'reserved'].includes(status))
|
||||
return res.status(400).json({ success: false, message: 'Statut invalide' });
|
||||
await db.updateSpotStatus(req.params.id, status);
|
||||
await db.addHistory('Mise à jour place', `Place ${req.params.id} -> ${status}`, req.user.id);
|
||||
await db.addHistory('Mise à jour place', `Place ${req.params.id} → ${status}`, req.user.id);
|
||||
res.json({ success: true, message: 'Statut mis à jour' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const spotCount = Math.min(Math.max(parseInt(req.body.count) || 10, 5), 50);
|
||||
const spotCount = Math.min(Math.max(parseInt(req.body.count) || 10, 1), 20);
|
||||
await db.deleteAllSpots();
|
||||
for (let i = 1; i <= spotCount; i++) {
|
||||
await db.createSpot(i, `SENSOR_${String(i).padStart(3, '0')}`, 'free');
|
||||
@@ -120,9 +143,6 @@ router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// RÉSERVATIONS
|
||||
// ============================================
|
||||
|
||||
router.get('/reservations', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
@@ -142,19 +162,7 @@ router.get('/reservations/all', authenticateToken, requireAdmin, async (req, res
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/reservations
|
||||
*
|
||||
* CORRIGÉ : on ne bloque plus la place entière définitivement.
|
||||
* On vérifie uniquement s'il y a un CONFLIT d'horaire sur
|
||||
* la même date et le même créneau.
|
||||
*
|
||||
* Exemple de ce qui est maintenant possible :
|
||||
* Place 2 — 10h-11h aujourd'hui ✅
|
||||
* Place 2 — 14h-15h aujourd'hui ✅ (pas de conflit)
|
||||
* Place 2 — 10h-11h demain ✅ (pas de conflit)
|
||||
* Place 2 — 10h30-11h30 aujourd'hui ❌ (conflit !)
|
||||
*/
|
||||
|
||||
router.post('/reservations', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body;
|
||||
@@ -166,16 +174,15 @@ router.post('/reservations', authenticateToken, async (req, res) => {
|
||||
if (!spot)
|
||||
return res.status(404).json({ success: false, message: 'Place introuvable' });
|
||||
|
||||
// CORRIGÉ : bloquer uniquement si une voiture est physiquement là
|
||||
if (spot.status === 'occupied')
|
||||
return res.status(409).json({ success: false, message: "Une voiture est déjà sur cette place" });
|
||||
return res.status(409).json({ success: false, message: "Une voiture est physiquement sur cette place" });
|
||||
|
||||
|
||||
// CORRIGÉ : vérifier les conflits d'horaire au lieu du statut global
|
||||
const conflict = await db.checkReservationConflict(spotId, date, startTime, endTime);
|
||||
if (conflict)
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: `Cette place est déjà réservée sur ce créneau. Choisissez un autre horaire ou une autre date.`
|
||||
message: 'Cette place est déjà réservée sur ce créneau horaire. Choisissez un autre horaire ou une autre date.'
|
||||
});
|
||||
|
||||
const paymentCode = 'PARK' + Date.now().toString().slice(-8);
|
||||
@@ -183,18 +190,14 @@ router.post('/reservations', authenticateToken, async (req, res) => {
|
||||
req.user.id, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode
|
||||
);
|
||||
|
||||
// On ne change le statut de la place QUE si la réservation est pour aujourd'hui
|
||||
// et que l'heure de début est maintenant ou dans moins de 30 minutes
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split('T')[0];
|
||||
const resStart = new Date(`${date}T${startTime}`);
|
||||
const diffMin = (resStart - now) / 60000;
|
||||
|
||||
if (date === today && diffMin <= 30) {
|
||||
await db.updateSpotStatus(spotId, 'reserved');
|
||||
}
|
||||
// Pour une réservation future, le statut de la place reste inchangé
|
||||
// Le timer d'expiration (server.js) le mettra à jour au bon moment
|
||||
|
||||
await db.addHistory(
|
||||
'Nouvelle réservation',
|
||||
@@ -213,9 +216,6 @@ router.post('/reservations', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/reservations/:id/cancel
|
||||
*/
|
||||
router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const reservation = await db.getReservationById(req.params.id);
|
||||
@@ -238,9 +238,6 @@ router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/reservations/:id/complete (admin)
|
||||
*/
|
||||
router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const reservation = await db.getReservationById(req.params.id);
|
||||
@@ -261,10 +258,6 @@ router.put('/reservations/:id/complete', authenticateToken, requireAdmin, async
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// STATISTIQUES
|
||||
// ============================================
|
||||
|
||||
router.get('/stats', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const spots = await db.getAllSpots();
|
||||
@@ -290,7 +283,7 @@ router.get('/history', authenticateToken, requireAdmin, async (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/status', (_req, res) => {
|
||||
res.json({ success: true, message: 'Smart Parking API opérationnelle', version: '3.0.0', timestamp: new Date().toISOString() });
|
||||
res.json({ success: true, message: 'Smart Parking API opérationnelle', version: '4.1.0', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* ============================================
|
||||
* SERVER.JS - Serveur principal Smart Parking
|
||||
* VERSION 2.0 - MQTT Arduino + Expiration réservations
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
@@ -27,27 +20,10 @@ app.use('/api', apiRoutes);
|
||||
app.get('/', (_req, res) => res.sendFile(path.join(__dirname, '..', 'index.html')));
|
||||
app.get('/dashboard', (_req, res) => res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html')));
|
||||
|
||||
// ============================================
|
||||
// CONNEXION MQTT (Mosquitto sur le Raspberry Pi)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Topics attendus depuis l'Arduino :
|
||||
*
|
||||
* smartparking/sensor/1 → "1" = voiture détectée (occupée)
|
||||
* smartparking/sensor/1 → "0" = place libre
|
||||
*
|
||||
* L'Arduino publie sur ce topic via le shield Ethernet/WiFi.
|
||||
* Le numéro à la fin correspond au numéro de place (1 à N).
|
||||
*
|
||||
* Pour tester sans Arduino (ligne de commande sur le Pi) :
|
||||
* mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "1"
|
||||
* mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "0"
|
||||
*/
|
||||
|
||||
const MQTT_HOST = process.env.MQTT_HOST || 'localhost';
|
||||
const MQTT_PORT = process.env.MQTT_PORT || 1883;
|
||||
const MQTT_TOPIC = 'smartparking/sensor/#'; // # = wildcard, reçoit tous les capteurs
|
||||
const MQTT_TOPIC = 'smartparking/sensor/#';
|
||||
|
||||
let mqttClient = null;
|
||||
|
||||
@@ -58,7 +34,7 @@ function connectMQTT() {
|
||||
mqttClient = mqtt.connect(brokerUrl, {
|
||||
clientId: 'smartparking-server-' + Math.random().toString(16).slice(3),
|
||||
keepalive: 60,
|
||||
reconnectPeriod: 5000, // Reconnexion automatique toutes les 5s si coupure
|
||||
reconnectPeriod: 5000,
|
||||
connectTimeout: 10000
|
||||
});
|
||||
|
||||
@@ -73,11 +49,11 @@ function connectMQTT() {
|
||||
});
|
||||
});
|
||||
|
||||
// Message reçu depuis un capteur Arduino
|
||||
|
||||
mqttClient.on('message', async (topic, messageBuffer) => {
|
||||
const message = messageBuffer.toString().trim();
|
||||
const topicParts = topic.split('/'); // ['smartparking', 'sensor', '1']
|
||||
const spotNumber = parseInt(topicParts[2]); // numéro de place
|
||||
const topicParts = topic.split('/');
|
||||
const spotNumber = parseInt(topicParts[2]);
|
||||
|
||||
if (isNaN(spotNumber)) {
|
||||
console.warn(`⚠️ Topic MQTT invalide : ${topic}`);
|
||||
@@ -86,11 +62,11 @@ function connectMQTT() {
|
||||
|
||||
console.log(`📩 MQTT reçu → topic: ${topic} | valeur: ${message}`);
|
||||
|
||||
// "1" = voiture présente → occupée | "0" = libre
|
||||
|
||||
const newStatus = message === '1' ? 'occupied' : 'free';
|
||||
|
||||
try {
|
||||
// Récupérer la place par son numéro
|
||||
|
||||
const spots = await db.getAllSpots();
|
||||
const spot = spots.find(s => s.number === spotNumber);
|
||||
|
||||
@@ -99,15 +75,14 @@ function connectMQTT() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ne pas écraser une place RÉSERVÉE avec un simple signal capteur
|
||||
// (la réservation prime sur le capteur physique)
|
||||
|
||||
if (spot.status === 'reserved' && newStatus === 'occupied') {
|
||||
console.log(`ℹ️ Place ${spotNumber} déjà réservée — capteur ignoré`);
|
||||
await db.recordMqttEvent(topic, message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mettre à jour en base
|
||||
|
||||
await db.updateSpotStatus(spot.id, newStatus);
|
||||
await db.recordMqttEvent(topic, message);
|
||||
|
||||
@@ -130,9 +105,7 @@ function connectMQTT() {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DÉMARRAGE DU SERVEUR
|
||||
// ============================================
|
||||
|
||||
|
||||
async function startServer() {
|
||||
try {
|
||||
@@ -153,10 +126,10 @@ async function startServer() {
|
||||
`);
|
||||
});
|
||||
|
||||
// 3. Connecter le client MQTT (broker Mosquitto)
|
||||
|
||||
connectMQTT();
|
||||
|
||||
// 4. Enregistrer les statistiques toutes les 5 minutes
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const spots = await db.getAllSpots();
|
||||
@@ -170,8 +143,7 @@ async function startServer() {
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
// 5. ⭐ EXPIRATION AUTOMATIQUE DES RÉSERVATIONS toutes les minutes
|
||||
// Libère les places dont l'heure de fin est dépassée
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const count = await db.expireReservations();
|
||||
@@ -181,7 +153,7 @@ async function startServer() {
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur expiration réservations :', err.message);
|
||||
}
|
||||
}, 60 * 1000); // toutes les 60 secondes
|
||||
}, 60 * 1000);
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur au démarrage :', err);
|
||||
@@ -189,7 +161,7 @@ async function startServer() {
|
||||
}
|
||||
}
|
||||
|
||||
// Arrêt propre
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n🛑 Arrêt du serveur...');
|
||||
if (mqttClient) mqttClient.end();
|
||||
|
||||
Reference in New Issue
Block a user