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