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

- 🅿️ Smart Parking - BTS CIEL IR 2025 🅿️ -

+Smart Parking — BTS CIEL IR — APON BARUA — 2025/2026 \ No newline at end of file diff --git a/css/auth.css b/css/auth.css index 8b48643..1a928ce 100644 --- a/css/auth.css +++ b/css/auth.css @@ -1,7 +1,3 @@ -/* ============================================ - AUTHENTIFICATION - STYLES - ============================================ */ - .auth-page { min-height: 100vh; display: flex; @@ -149,7 +145,6 @@ color: var(--success); } -/* Responsive */ @media (max-width: 480px) { .auth-box { padding: 24px; diff --git a/css/dashboard.css b/css/dashboard.css index 06d4367..aa2530f 100644 --- a/css/dashboard.css +++ b/css/dashboard.css @@ -1,8 +1,3 @@ -/* ============================================ - DASHBOARD - STYLES SPÉCIFIQUES - ============================================ */ - -/* Reservation form */ .reservation-form-container { max-width: 600px; margin: 0 auto; @@ -35,14 +30,14 @@ color: var(--primary-light); } -/* Chart container */ + .chart-container { position: relative; height: 300px; width: 100%; } -/* Admin table actions */ + .table-actions { display: flex; gap: 8px; @@ -53,7 +48,7 @@ font-size: 1rem; } -/* Status badges */ + .badge { display: inline-block; padding: 4px 12px; @@ -73,7 +68,7 @@ color: var(--info); } -/* Animation pour les mises à jour */ + @keyframes pulse-update { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } @@ -83,7 +78,6 @@ animation: pulse-update 0.3s ease; } -/* Loading state */ .loading { position: relative; pointer-events: none; diff --git a/css/style.css b/css/style.css index 0130158..7fd16cc 100644 --- a/css/style.css +++ b/css/style.css @@ -1,45 +1,40 @@ -/* ============================================ - SMART PARKING - STYLES GLOBAUX - ============================================ */ - :root { - /* Couleurs principales */ + --primary: #6366f1; --primary-dark: #4f46e5; --primary-light: #818cf8; - /* Couleurs états */ + --success: #10b981; --danger: #ef4444; --warning: #f59e0b; --info: #06b6d4; - /* Couleurs places */ --spot-free: #10b981; --spot-occupied: #ef4444; --spot-reserved: #3b82f6; - /* Fonds */ + --bg-dark: #0f172a; --bg-darker: #020617; --bg-card: #1e293b; --bg-hover: #334155; - /* Texte */ + --text-primary: #f8fafc; --text-secondary: #cbd5e1; --text-muted: #94a3b8; - /* Bordures */ + --border: #334155; --border-radius: 12px; --border-radius-sm: 8px; - /* Ombres */ + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); --shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.4); - /* Transitions */ + --transition: all 0.3s ease; } @@ -67,9 +62,7 @@ body { padding: 0 20px; } -/* ============================================ - HEADER - ============================================ */ + .header { background: var(--bg-card); border-bottom: 1px solid var(--border); @@ -110,7 +103,7 @@ body { -webkit-text-fill-color: transparent; } -/* Navigation */ + .nav { display: flex; gap: 8px; @@ -143,7 +136,7 @@ body { font-size: 1.1rem; } -/* Header right */ + .header-right { display: flex; align-items: center; @@ -166,9 +159,7 @@ body { color: var(--text-muted); } -/* ============================================ - MAIN & PAGES - ============================================ */ + .main { padding: 30px 0; min-height: calc(100vh - 80px); @@ -201,9 +192,7 @@ body { font-size: 1.5rem; } -/* ============================================ - BOUTONS - ============================================ */ + .btn { display: inline-flex; align-items: center; @@ -262,9 +251,7 @@ body { cursor: not-allowed; } -/* ============================================ - FORMULAIRES - ============================================ */ + .form-group { margin-bottom: 16px; } @@ -317,9 +304,6 @@ select.form-control { flex: 1; } -/* ============================================ - CARTES - ============================================ */ .card { background: var(--bg-card); border-radius: var(--border-radius); @@ -328,9 +312,7 @@ select.form-control { box-shadow: var(--shadow); } -/* ============================================ - STATS GRID - ============================================ */ + .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); @@ -392,9 +374,7 @@ select.form-control { margin-top: 4px; } -/* ============================================ - PARKING MAP - ============================================ */ + .parking-section { display: grid; grid-template-columns: 1fr 350px; @@ -446,7 +426,7 @@ select.form-control { .parking-spot .spot-number { font-size: 1.1rem; } .parking-spot .spot-icon { font-size: 1.3rem; } -/* Legend */ + .legend { display: flex; justify-content: center; @@ -471,7 +451,7 @@ select.form-control { .legend-color.occupied { background: var(--spot-occupied); } .legend-color.reserved { background: var(--spot-reserved); } -/* Spot details */ + .spot-details { min-height: 200px; } .no-selection { @@ -500,9 +480,7 @@ select.form-control { .spot-status-occupied { color: var(--spot-occupied); } .spot-status-reserved { color: var(--spot-reserved); } -/* ============================================ - PRICING - ============================================ */ + .pricing-section { background: var(--bg-card); border-radius: var(--border-radius); @@ -547,9 +525,7 @@ select.form-control { color: var(--primary-light); } -/* ============================================ - MODAL - ============================================ */ + .modal { position: fixed; top: 0; @@ -597,7 +573,7 @@ select.form-control { .modal-body { padding: 24px; } -/* Récapitulatif réservation */ + .payment-summary { background: var(--bg-dark); border-radius: var(--border-radius-sm); @@ -625,9 +601,7 @@ select.form-control { font-size: 1.1rem; } -/* ============================================ - MESSAGE DE CONFIRMATION (remplace QR code) - ============================================ */ + .confirmation-message { text-align: center; padding: 24px 16px; @@ -658,9 +632,7 @@ select.form-control { color: var(--text-primary); } -/* ============================================ - RESERVATIONS LIST - ============================================ */ + .reservations-list { display: flex; flex-direction: column; @@ -739,9 +711,7 @@ select.form-control { margin-bottom: 20px; } -/* ============================================ - PROFILE - ============================================ */ + .profile-container { display: grid; grid-template-columns: 1fr 1fr; @@ -813,9 +783,7 @@ select.form-control { color: var(--text-muted); } -/* ============================================ - ADMIN - ============================================ */ + .admin-page .admin-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); @@ -882,7 +850,7 @@ select.form-control { .admin-place-item.occupied { background: rgba(239, 68, 68, 0.2); color: var(--spot-occupied); } .admin-place-item.reserved { background: rgba(59, 130, 246, 0.2); color: var(--spot-reserved); } -/* Tables */ + .table-container { overflow-x: auto; } .data-table { @@ -908,7 +876,7 @@ select.form-control { .data-table tr:hover td { background: var(--bg-hover); } -/* Log container */ + .log-container { max-height: 300px; overflow-y: auto; @@ -933,9 +901,7 @@ select.form-control { font-size: 0.75rem; } -/* ============================================ - TOAST NOTIFICATIONS - ============================================ */ + .toast-container { position: fixed; bottom: 20px; @@ -965,17 +931,13 @@ select.form-control { to { transform: translateX(0); opacity: 1; } } -/* ============================================ - UTILITAIRES - ============================================ */ + .hidden { display: none !important; } .admin-only { display: none; } .admin-only.visible { display: flex; } -/* ============================================ - RESPONSIVE - ============================================ */ + @media (max-width: 1024px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } .parking-section { grid-template-columns: 1fr; } @@ -1004,7 +966,6 @@ select.form-control { .reservation-actions { align-items: flex-start; } } -/* Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--bg-dark); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } diff --git a/docker-compose.yml b/docker-compose.yml index 4544c92..7df63d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,13 @@ -version: '3.8' - -# ============================================================ -# Smart Parking v2.0 — Docker Compose -# Services : MariaDB + App Node.js + Mosquitto MQTT -# ============================================================ - services: - - # ── Base de données MariaDB ──────────────────────────────── db: image: mariadb:10.11 container_name: smartparking-db restart: always environment: - MARIADB_ROOT_PASSWORD: rootpassword # ⚠️ À changer en production + MARIADB_ROOT_PASSWORD: rootpassword MARIADB_DATABASE: smartparking MARIADB_USER: smartparking_user - MARIADB_PASSWORD: smartparking_pass # ⚠️ À changer en production + MARIADB_PASSWORD: smartparking_pass volumes: - db_data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql @@ -27,25 +18,6 @@ services: timeout: 10s retries: 5 - # ── Broker MQTT Mosquitto ────────────────────────────────── - # Si vous avez déjà Mosquitto installé directement sur le Pi - # (pas dans Docker), commentez ce bloc et mettez - # MQTT_HOST=localhost dans la section "app" ci-dessous. - mqtt: - image: eclipse-mosquitto:2 - container_name: smartparking-mqtt - restart: always - ports: - - "1883:1883" # Port MQTT (Arduino se connecte ici) - - "9001:9001" # Port WebSocket (optionnel) - volumes: - - ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf - - mosquitto_data:/mosquitto/data - - mosquitto_log:/mosquitto/log - networks: - - smartparking-network - - # ── Application Node.js ──────────────────────────────────── app: build: . container_name: smartparking-app @@ -55,31 +27,23 @@ services: depends_on: db: condition: service_healthy - mqtt: - condition: service_started environment: - # Base de données - DB_HOST: db - DB_PORT: 3306 - DB_USER: smartparking_user + DB_HOST: db + DB_PORT: 3306 + DB_USER: smartparking_user DB_PASSWORD: smartparking_pass - DB_NAME: smartparking - # JWT - JWT_SECRET: ${JWT_SECRET:-une_chaine_tres_longue_et_secrete_changez_moi} - # MQTT — utiliser "mqtt" si Mosquitto est dans Docker - # utiliser "localhost" si Mosquitto est installé directement sur le Pi - MQTT_HOST: mqtt - MQTT_PORT: 1883 - # Environnement + DB_NAME: smartparking + JWT_SECRET: une_chaine_tres_longue_et_secrete NODE_ENV: production - PORT: 3000 + MQTT_HOST: 172.18.0.1 + MQTT_PORT: 1883 + extra_hosts: + - "host.docker.internal:host-gateway" networks: - smartparking-network volumes: db_data: - mosquitto_data: - mosquitto_log: networks: smartparking-network: \ No newline at end of file diff --git a/index.html b/index.html index c172a07..24a8afe 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@

Gestion intelligente de parking

- +

Connexion

@@ -37,7 +37,7 @@

- +
diff --git a/js/admin.js b/js/admin.js index 8e1f93d..d8cbf17 100644 --- a/js/admin.js +++ b/js/admin.js @@ -1,14 +1,3 @@ -/** - * ============================================ - * ADMIN.JS - Panel d'administration - * Smart Parking v3.0 - * CORRIGÉ : - * - Suppression du graphique d'occupation - * - Historique affiche la date complète - * (jour + mois + année + heure + minute) - * ============================================ - */ - document.addEventListener('DOMContentLoaded', () => { console.log('⚙️ Initialisation du panel admin...'); if (!isAdmin()) return; @@ -27,7 +16,6 @@ function initAdminPanel() { loadReservationsTable(); loadHistoryLog(); - // Rafraîchissement périodique toutes les 10 secondes setInterval(() => { loadAdminStats(); loadReservationsTable(); @@ -35,9 +23,6 @@ function initAdminPanel() { }, 10000); } -// ============================================ -// STATISTIQUES ADMIN -// ============================================ function loadAdminStats() { const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); @@ -62,21 +47,44 @@ function loadAdminStats() { document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%'; } -// ============================================ -// GESTION DES PLACES -// ============================================ function initPlacesControl() { const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); const spotsInput = document.getElementById('adminTotalSpots'); if (spotsInput) spotsInput.value = spots.length || 10; - document.getElementById('updateSpotsBtn')?.addEventListener('click', () => { + document.getElementById('updateSpotsBtn')?.addEventListener('click', async () => { const newCount = parseInt(document.getElementById('adminTotalSpots').value); - if (newCount < 5 || newCount > 50) { - Dashboard.showToast('Le nombre de places doit être entre 5 et 50', 'error'); + + if (newCount < 1 || newCount > 20) { + Dashboard.showToast('Le nombre de places doit être entre 1 et 20', 'error'); return; } + + + const token = localStorage.getItem('smart_parking_token'); + if (token) { + try { + const response = await fetch('/api/spots/init', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ count: newCount }) + }); + const data = await response.json(); + if (data.success) { + + if (window.ParkingMap) await window.ParkingMap.refresh(); + renderAdminPlacesList(); + Dashboard.showToast('Nombre de places mis à jour', 'success'); + return; + } + } catch (_err) { /* fallback local */ } + } + + if (window.ParkingMap) { window.ParkingMap.setTotalSpots(newCount); renderAdminPlacesList(); @@ -104,27 +112,48 @@ function renderAdminPlacesList() { `).join(''); } -function toggleSpotStatus(spotId) { + +async function toggleSpotStatus(spotId) { const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); const spot = spots.find(s => s.id === spotId); if (!spot) return; - const cycle = ['free', 'occupied', 'reserved']; - const nextStatus = cycle[(cycle.indexOf(spot.status) + 1) % cycle.length]; - spot.status = nextStatus; - spot.lastUpdate = new Date().toISOString(); + const cycle = ['free', 'occupied', 'reserved']; + const nextStatus = cycle[(cycle.indexOf(spot.status) + 1) % cycle.length]; + const token = localStorage.getItem('smart_parking_token'); + if (token) { + try { + const response = await fetch(`/api/spots/${spot.id}/status`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ status: nextStatus }) + }); + const data = await response.json(); + if (!data.success) { + Dashboard.showToast('Erreur : ' + data.message, 'error'); + return; + } + } catch (_err) { + console.warn('⚠️ API indisponible, modification locale uniquement'); + } + } + + spot.status = nextStatus; + spot.lastUpdate = new Date().toISOString(); localStorage.setItem('smart_parking_spots', JSON.stringify(spots)); + renderAdminPlacesList(); if (window.ParkingMap) window.ParkingMap.refresh(); loadAdminStats(); - Dashboard.showToast(`Place ${spot.number} — ${getStatusLabel(nextStatus)}`, 'success'); + Dashboard.showToast(`Place ${spot.number} → ${getStatusLabel(nextStatus)}`, 'success'); } -// ============================================ -// TABLEAU UTILISATEURS -// ============================================ + function loadUsersTable() { const tbody = document.getElementById('adminUsersTable'); @@ -176,9 +205,7 @@ function deleteUser(userId) { Dashboard.showToast('Utilisateur supprimé', 'success'); } -// ============================================ -// TABLEAU RÉSERVATIONS -// ============================================ + function loadReservationsTable() { const tbody = document.getElementById('adminReservationsTable'); @@ -218,11 +245,22 @@ function loadReservationsTable() { `).join(''); } -function completeReservation(reservationId) { +async function completeReservation(reservationId) { let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservation = reservations.find(r => r.id === reservationId); if (!reservation) return; + + const token = localStorage.getItem('smart_parking_token'); + if (token) { + try { + await fetch(`/api/reservations/${reservationId}/complete`, { + method: 'PUT', + headers: { 'Authorization': 'Bearer ' + token } + }); + } catch (_err) { /* fallback local */ } + } + reservation.status = 'completed'; localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); @@ -233,13 +271,24 @@ function completeReservation(reservationId) { Dashboard.showToast('Réservation terminée', 'success'); } -function adminCancelReservation(reservationId) { +async function adminCancelReservation(reservationId) { if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return; let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservation = reservations.find(r => r.id === reservationId); if (!reservation) return; + // Appeler l'API + const token = localStorage.getItem('smart_parking_token'); + if (token) { + try { + await fetch(`/api/reservations/${reservationId}/cancel`, { + method: 'PUT', + headers: { 'Authorization': 'Bearer ' + token } + }); + } catch (_err) { /* fallback local */ } + } + reservation.status = 'cancelled'; localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); @@ -250,9 +299,6 @@ function adminCancelReservation(reservationId) { Dashboard.showToast('Réservation annulée', 'success'); } -// ============================================ -// HISTORIQUE — CORRIGÉ : date complète -// ============================================ function loadHistoryLog() { const container = document.getElementById('adminLogContainer'); @@ -273,15 +319,8 @@ function loadHistoryLog() { `).join(''); } -// ============================================ -// FONCTIONS DE FORMAT DATE -// ============================================ -/** - * CORRIGÉ — Affiche la date complète dans l'historique - * Avant : seulement "14:32" - * Après : "12/06/2025 à 14:32" - */ + function formatDateComplete(dateString) { if (!dateString) return 'N/A'; const date = new Date(dateString); @@ -312,9 +351,6 @@ function getStatusLabel(status) { }[status] || status; } -// ============================================ -// EXPORT -// ============================================ window.AdminModule = { refresh: () => { diff --git a/js/auth.js b/js/auth.js index 0a87995..6d4872d 100644 --- a/js/auth.js +++ b/js/auth.js @@ -1,57 +1,33 @@ -/** - * ============================================ - * AUTH.JS - Système d'authentification - * Smart Parking - BTS CIEL IR - * ============================================ - */ - -// Configuration const AUTH_CONFIG = { - apiUrl: 'http://localhost:3000/api', + apiUrl: '/api', tokenKey: 'smart_parking_token', userKey: 'smart_parking_user' }; -// État de l'authentification let authState = { isLoggedIn: false, user: null, token: null }; -// Initialisation document.addEventListener('DOMContentLoaded', () => { console.log('🔐 Initialisation du système d\'authentification...'); - - // Vérifier si déjà connecté checkAuthStatus(); - - // Initialiser les écouteurs d'événements initEventListeners(); }); -/** - * Vérifie le statut d'authentification - */ function checkAuthStatus() { const token = localStorage.getItem(AUTH_CONFIG.tokenKey); const user = localStorage.getItem(AUTH_CONFIG.userKey); - if (token && user) { authState.token = token; authState.user = JSON.parse(user); authState.isLoggedIn = true; - - // Rediriger vers le dashboard window.location.href = 'pages/dashboard.html'; } } -/** - * Initialise les écouteurs d'événements - */ function initEventListeners() { - // Basculer vers l'inscription const showRegister = document.getElementById('showRegister'); if (showRegister) { showRegister.addEventListener('click', (e) => { @@ -59,8 +35,7 @@ function initEventListeners() { showRegisterForm(); }); } - - // Basculer vers la connexion + const showLogin = document.getElementById('showLogin'); if (showLogin) { showLogin.addEventListener('click', (e) => { @@ -68,172 +43,121 @@ function initEventListeners() { showLoginForm(); }); } - - // Formulaire de connexion + const loginForm = document.getElementById('loginForm'); - if (loginForm) { - loginForm.addEventListener('submit', handleLogin); - } - - // Formulaire d'inscription + if (loginForm) loginForm.addEventListener('submit', handleLogin); + const registerForm = document.getElementById('registerForm'); - if (registerForm) { - registerForm.addEventListener('submit', handleRegister); - } + if (registerForm) registerForm.addEventListener('submit', handleRegister); } -/** - * Affiche le formulaire d'inscription - */ function showRegisterForm() { document.getElementById('loginForm').classList.add('hidden'); document.getElementById('registerForm').classList.remove('hidden'); hideMessage(); } -/** - * Affiche le formulaire de connexion - */ function showLoginForm() { document.getElementById('registerForm').classList.add('hidden'); document.getElementById('loginForm').classList.remove('hidden'); hideMessage(); } -/** - * Gère la connexion - */ async function handleLogin(e) { e.preventDefault(); - const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; - + if (!email || !password) { showMessage('Veuillez remplir tous les champs', 'error'); return; } - + try { - // Appel API de connexion const response = await fetch(`${AUTH_CONFIG.apiUrl}/login`, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); - + const data = await response.json(); - + if (data.success) { - // Stocker les informations localStorage.setItem(AUTH_CONFIG.tokenKey, data.token); localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(data.user)); - showMessage('Connexion réussie ! Redirection...', 'success'); - - // Rediriger vers le dashboard - setTimeout(() => { - window.location.href = 'pages/dashboard.html'; - }, 1000); + setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000); } else { showMessage(data.message || 'Email ou mot de passe incorrect', 'error'); } } catch (error) { console.error('Erreur connexion:', error); - // Mode offline - simulation handleOfflineLogin(email, password); } } -/** - * Gère la connexion en mode offline (simulation) - */ function handleOfflineLogin(email, password) { - // Vérifier dans les utilisateurs stockés localement const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); const user = users.find(u => u.email === email && u.password === password); - + if (user) { const token = generateToken(); localStorage.setItem(AUTH_CONFIG.tokenKey, token); localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(user)); - showMessage('Connexion réussie ! Redirection...', 'success'); - setTimeout(() => { - window.location.href = 'pages/dashboard.html'; - }, 1000); + setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000); } else { - // Compte admin par défaut if (email === 'admin@smartparking.fr' && password === 'admin123') { const adminUser = { - id: 1, - name: 'Administrateur', + id: 1, name: 'Administrateur', email: 'admin@smartparking.fr', - phone: '01 23 45 67 89', - role: 'admin' + phone: '01 23 45 67 89', role: 'admin' }; const token = generateToken(); localStorage.setItem(AUTH_CONFIG.tokenKey, token); localStorage.setItem(AUTH_CONFIG.userKey, JSON.stringify(adminUser)); - showMessage('Connexion admin réussie ! Redirection...', 'success'); - setTimeout(() => { - window.location.href = 'pages/dashboard.html'; - }, 1000); + setTimeout(() => { window.location.href = 'pages/dashboard.html'; }, 1000); return; } - showMessage('Email ou mot de passe incorrect', 'error'); } } -/** - * Gère l'inscription - */ async function handleRegister(e) { e.preventDefault(); - const name = document.getElementById('registerName').value; const email = document.getElementById('registerEmail').value; const phone = document.getElementById('registerPhone').value; const password = document.getElementById('registerPassword').value; const passwordConfirm = document.getElementById('registerPasswordConfirm').value; - - // Validation + if (!name || !email || !phone || !password) { showMessage('Veuillez remplir tous les champs', 'error'); return; } - if (password !== passwordConfirm) { showMessage('Les mots de passe ne correspondent pas', 'error'); return; } - if (password.length < 8) { showMessage('Le mot de passe doit faire au moins 8 caractères', 'error'); return; } - + try { - // Appel API d'inscription const response = await fetch(`${AUTH_CONFIG.apiUrl}/register`, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, phone, password }) }); - + const data = await response.json(); - + if (data.success) { showMessage('Compte créé avec succès ! Vous pouvez vous connecter.', 'success'); setTimeout(() => { showLoginForm(); - // Pré-remplir l'email document.getElementById('loginEmail').value = email; }, 1500); } else { @@ -241,39 +165,22 @@ async function handleRegister(e) { } } catch (error) { console.error('Erreur inscription:', error); - // Mode offline - stockage local handleOfflineRegister(name, email, phone, password); } } -/** - * Gère l'inscription en mode offline (simulation) - */ function handleOfflineRegister(name, email, phone, password) { - // Récupérer les utilisateurs existants let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); - - // Vérifier si l'email existe déjà if (users.find(u => u.email === email)) { showMessage('Cet email est déjà utilisé', 'error'); return; } - - // Créer le nouvel utilisateur const newUser = { - id: Date.now(), - name, - email, - phone, - password, // En production: hasher le mot de passe ! - role: 'client', - createdAt: new Date().toISOString() + id: Date.now(), name, email, phone, password, + role: 'client', createdAt: new Date().toISOString() }; - - // Ajouter à la liste users.push(newUser); localStorage.setItem('smart_parking_users', JSON.stringify(users)); - showMessage('Compte créé avec succès ! Vous pouvez vous connecter.', 'success'); setTimeout(() => { showLoginForm(); @@ -281,16 +188,10 @@ function handleOfflineRegister(name, email, phone, password) { }, 1500); } -/** - * Génère un token simple - */ function generateToken() { return 'token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } -/** - * Affiche un message - */ function showMessage(message, type) { const messageEl = document.getElementById('authMessage'); messageEl.textContent = message; @@ -298,24 +199,17 @@ function showMessage(message, type) { messageEl.classList.remove('hidden'); } -/** - * Cache le message - */ function hideMessage() { const messageEl = document.getElementById('authMessage'); messageEl.classList.add('hidden'); } -/** - * Déconnexion - */ function logout() { localStorage.removeItem(AUTH_CONFIG.tokenKey); localStorage.removeItem(AUTH_CONFIG.userKey); window.location.href = '../index.html'; } -// Exporter les fonctions window.Auth = { logout, getToken: () => localStorage.getItem(AUTH_CONFIG.tokenKey), @@ -324,4 +218,4 @@ window.Auth = { const user = JSON.parse(localStorage.getItem(AUTH_CONFIG.userKey) || 'null'); return user && user.role === 'admin'; } -}; +}; \ No newline at end of file diff --git a/js/dashboard.js b/js/dashboard.js index 3aaea1d..5019ccc 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -1,13 +1,4 @@ -/** - * ============================================ - * DASHBOARD.JS - Gestion du dashboard - * Smart Parking - BTS CIEL IR - * CORRIGÉ : cancelReservation utilisait spotNumber - * au lieu de spotId pour libérer la place - * ============================================ - */ - -const API_URL = 'http://localhost:3000/api'; +const API_URL = '/api'; let dashboardState = { user: null, @@ -93,10 +84,10 @@ function loadUserData() { document.getElementById('userName').textContent = user.name; document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client'; - document.getElementById('profileName').textContent = user.name; - document.getElementById('profileNameInput').value = user.name; - document.getElementById('profileEmailInput').value = user.email; - document.getElementById('profilePhoneInput').value = user.phone || ''; + document.getElementById('profileName').textContent = user.name; + document.getElementById('profileNameInput').value = user.name; + document.getElementById('profileEmailInput').value = user.email; + document.getElementById('profilePhoneInput').value = user.phone || ''; const roleBadge = document.getElementById('profileRole'); if (roleBadge) { @@ -137,7 +128,7 @@ function loadMyReservations() {

Place ${res.spotNumber}

📅 ${res.date} - 🕐 ${res.startTime} + 🕐 ${res.startTime} - ${res.endTime} ⏱️ ${formatDurationLabel(res.duration)} 🚗 ${res.vehicle || 'N/A'}
@@ -155,22 +146,16 @@ function loadMyReservations() { `).join(''); } -/** - * CORRIGÉ : utilisait reservation.spotNumber (le numéro visible) - * au lieu de reservation.spotId (l'id interne), ce qui empêchait - * la place d'être libérée sur la carte. - */ function cancelReservation(reservationId) { if (!confirm('Êtes-vous sûr de vouloir annuler cette réservation ?')) return; - let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservation = reservations.find(r => r.id === reservationId); if (reservation) { reservation.status = 'cancelled'; localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); - // CORRIGÉ : spotId au lieu de spotNumber if (window.ParkingMap) { window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); } @@ -181,26 +166,82 @@ function cancelReservation(reservationId) { } } -// Mise à jour du profil -document.getElementById('profileForm')?.addEventListener('submit', (e) => { +document.getElementById('profileForm')?.addEventListener('submit', async (e) => { e.preventDefault(); + const phone = document.getElementById('profilePhoneInput').value; const newPassword = document.getElementById('profileNewPassword').value; + const confirmPass = document.getElementById('profileNewPassword').value; - let user = dashboardState.user; - user.phone = phone; - if (newPassword) user.password = newPassword; - - localStorage.setItem('smart_parking_user', JSON.stringify(user)); - - let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); - const idx = users.findIndex(u => u.id === user.id); - if (idx !== -1) { - users[idx] = user; - localStorage.setItem('smart_parking_users', JSON.stringify(users)); + + if (newPassword && newPassword.length < 8) { + showToast('Le mot de passe doit faire au moins 8 caractères', 'error'); + return; } - showToast('Profil mis à jour', 'success'); + const token = localStorage.getItem('smart_parking_token'); + + if (token) { + try { + + const updateData = { phone }; + if (newPassword) { + updateData.password = newPassword; + } + + + const response = await fetch(`${API_URL}/users/profile`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify(updateData) + }); + + const data = await response.json(); + + if (data.success) { + + let user = dashboardState.user; + user.phone = phone; + dashboardState.user = user; + localStorage.setItem('smart_parking_user', JSON.stringify(user)); + + + document.getElementById('profileNewPassword').value = ''; + + showToast('Profil mis à jour avec succès !', 'success'); + } else { + showToast(data.message || 'Erreur lors de la mise à jour', 'error'); + } + + } catch (_err) { + + let user = dashboardState.user; + user.phone = phone; + if (newPassword) user.password = newPassword; + dashboardState.user = user; + localStorage.setItem('smart_parking_user', JSON.stringify(user)); + + let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); + const idx = users.findIndex(u => u.id === user.id); + if (idx !== -1) { + users[idx] = user; + localStorage.setItem('smart_parking_users', JSON.stringify(users)); + } + + document.getElementById('profileNewPassword').value = ''; + showToast('Profil mis à jour', 'success'); + } + } else { + let user = dashboardState.user; + user.phone = phone; + if (newPassword) user.password = newPassword; + dashboardState.user = user; + localStorage.setItem('smart_parking_user', JSON.stringify(user)); + showToast('Profil mis à jour', 'success'); + } }); function getStatusLabel(status) { @@ -208,7 +249,7 @@ function getStatusLabel(status) { } function formatDurationLabel(minutes) { - if (minutes >= 480) return 'Journée'; + if (minutes >= 480) return 'Journée (8h)'; if (minutes >= 60) return Math.floor(minutes / 60) + 'h' + (minutes % 60 ? (minutes % 60) + 'min' : ''); return minutes + ' min'; } diff --git a/js/map.js b/js/map.js index 0319154..d8d27dc 100644 --- a/js/map.js +++ b/js/map.js @@ -1,15 +1,6 @@ -/** - * ============================================ - * MAP.JS - Carte des places de parking - * Smart Parking v2.0 - * MODIFIÉ : polling API toutes les 3s pour recevoir - * les mises à jour en temps réel depuis Arduino - * ============================================ - */ - const MAP_CONFIG = { totalSpots: 10, - updateInterval: 3000 // Refresh depuis l'API toutes les 3 secondes + updateInterval: 3000 }; let spotsState = { @@ -19,9 +10,7 @@ let spotsState = { const SPOT_STATUS = { FREE: 'free', OCCUPIED: 'occupied', RESERVED: 'reserved' }; -// ============================================ -// INITIALISATION -// ============================================ + document.addEventListener('DOMContentLoaded', () => { console.log('🗺️ Initialisation de la carte...'); @@ -29,30 +18,17 @@ document.addEventListener('DOMContentLoaded', () => { }); async function initParkingMap() { - // Essayer d'abord de charger depuis l'API (données MariaDB + Arduino) const loaded = await loadSpotsFromAPI(); - - // Si pas d'API disponible, utiliser le localStorage (mode offline) - if (!loaded) { - loadSpotsFromStorage(); - } + if (!loaded) loadSpotsFromStorage(); renderMap(); updateStats(); updateReservationForm(); - - // ⭐ Polling toutes les 3 secondes pour recevoir les updates Arduino startAPIPolling(); } -// ============================================ -// CHARGEMENT DES PLACES -// ============================================ -/** - * Charge les places depuis l'API (MariaDB) - * Retourne true si succès, false si hors-ligne - */ + async function loadSpotsFromAPI() { try { const token = localStorage.getItem('smart_parking_token'); @@ -67,7 +43,6 @@ async function loadSpotsFromAPI() { const data = await response.json(); if (!data.success || !data.data.length) return false; - // Convertir le format API → format interne spotsState.spots = data.data.map(s => ({ id: s.id, number: s.number, @@ -76,19 +51,14 @@ async function loadSpotsFromAPI() { sensorId: s.sensor_id })); - // Synchroniser avec localStorage pour le mode offline saveSpots(); return true; } catch (_err) { - // Serveur non joignable → mode offline return false; } } -/** - * Charge les places depuis le localStorage (mode offline) - */ function loadSpotsFromStorage() { const stored = localStorage.getItem('smart_parking_spots'); if (stored) { @@ -116,23 +86,22 @@ function saveSpots() { localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots)); } -// ============================================ -// ⭐ POLLING TEMPS RÉEL (mises à jour Arduino) -// ============================================ -/** - * Interroge l'API toutes les 3 secondes. - * Si l'Arduino a changé l'état d'une place via MQTT, - * la carte se met à jour automatiquement. - */ function startAPIPolling() { setInterval(async () => { const loaded = await loadSpotsFromAPI(); if (loaded) { renderMap(); updateStats(); - updateReservationForm(); - // Rafraîchir les détails si une place est sélectionnée + + + const resSpot = document.getElementById('resSpot'); + const hasSelection = resSpot && resSpot.value !== ''; + if (!hasSelection) { + updateReservationForm(); + } + + if (spotsState.selectedSpot) { const updated = spotsState.spots.find(s => s.id === spotsState.selectedSpot.id); if (updated) { @@ -144,9 +113,6 @@ function startAPIPolling() { }, MAP_CONFIG.updateInterval); } -// ============================================ -// RENDU DE LA CARTE -// ============================================ function renderMap() { const mapContainer = document.getElementById('parkingMap'); @@ -202,21 +168,21 @@ function showSpotDetails(spot) { ${reservation ? `
- Réservé par - ${reservation.userName} -
-
- Jusqu'à + Réservé jusqu'à ${reservation.endTime}
` : ''} - ${spot.status === SPOT_STATUS.FREE ? ` + ${spot.status !== SPOT_STATUS.OCCUPIED ? ` - ` : ''} + ` : ` +

+ 🚗 Une voiture est physiquement sur cette place +

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

🗺️Carte du Parking

@@ -69,17 +67,10 @@ Places occupées
-
-
📅
-
- 0 - Places réservées -
-
🅿️
- 10 + 3 Total places
@@ -92,7 +83,6 @@
Libre
Occupée
-
Réservée
@@ -116,7 +106,7 @@
- + - + - + - +