commit c6954c1f5030e33153cb968dc250809a5afcd758 Author: apon Date: Wed Mar 11 17:18:34 2026 +0100 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..5472096 --- /dev/null +++ b/.env @@ -0,0 +1,15 @@ +# Configuration de la base de données +DB_HOST=db +DB_PORT=3306 +DB_USER=smartparking_user +DB_PASSWORD=smartparking_pass +DB_NAME=smartparking + +# Configuration JWT +JWT_SECRET=une_chaine_tres_longue_et_secrete_à_changer + +# Port du serveur +PORT=3000 + +# Mode environnement +NODE_ENV=production \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6fb6e35 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY server/package*.json ./server/ + +RUN cd server && npm install + +COPY . . + +EXPOSE 3000 + +CMD ["node", "server/server.js"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a2bf24 --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# 🅿️ Smart Parking + +> Système complet de gestion de parking intelligent avec authentification, réservation et paiement QR code + +## 📋 Fonctionnalités + +### 🔐 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 + +### 🗺️ 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 + +### 📅 Système de Réservation +- Sélection de la place +- Choix de la date et heure +- Durée : 30min, 1h, 2h, 4h, Journée +- Saisie de la plaque d'immatriculation + +### 💳 Paiement QR Code +- Génération de QR code unique +- Code de paiement affiché +- Confirmation du paiement + +### 👤 Espace Client +- Consulter la carte des places +- Voir les tarifs +- Faire une réservation +- Voir l'historique des réservations +- Gérer son profil + +### ⚙️ 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 | +|-------|------| +| 30 minutes | 2€ | +| 1 heure | 3€ | +| 2 heures | 5€ | +| 4 heures | 8€ | +| Journée (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 + +``` +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 +``` + +## 🔌 API REST + +### Authentification +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| POST | `/api/register` | Inscription | +| POST | `/api/login` | Connexion | + +### Utilisateurs +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| GET | `/api/users` | Liste des utilisateurs (admin) | +| DELETE | `/api/users/:id` | Supprimer un utilisateur (admin) | + +### 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) | + +### 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) + +### Backend +- Node.js +- Express.js +- SQLite3 +- JWT (authentification) +- bcryptjs (hashage mots de passe) + +## 📱 Fonctionnement + +### 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 ! + +### 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 + +## 🔒 Sécurité + +- Mots de passe hashés avec bcrypt +- Authentification JWT +- Protection des routes sensibles +- Validation des données + +## 📝 Notes + +- 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 + +--- + +

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

diff --git a/css/auth.css b/css/auth.css new file mode 100644 index 0000000..8b48643 --- /dev/null +++ b/css/auth.css @@ -0,0 +1,165 @@ +/* ============================================ + AUTHENTIFICATION - STYLES + ============================================ */ + +.auth-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--bg-darker) 0%, var(--bg-dark) 50%, var(--primary-dark) 100%); + position: relative; + overflow: hidden; +} + +.auth-page::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 50%); + animation: rotate 30s linear infinite; +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.auth-container { + position: relative; + z-index: 1; + width: 100%; + max-width: 420px; + padding: 20px; +} + +.auth-box { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 40px; + border: 1px solid var(--border); + box-shadow: var(--shadow-lg); + backdrop-filter: blur(10px); +} + +.auth-logo { + text-align: center; + margin-bottom: 32px; +} + +.auth-logo .logo-icon { + font-size: 4rem; + display: block; + margin-bottom: 16px; +} + +.auth-logo h1 { + font-size: 1.8rem; + font-weight: 700; + background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 8px; +} + +.auth-logo p { + color: var(--text-muted); + font-size: 0.95rem; +} + +.auth-form h2 { + font-size: 1.3rem; + margin-bottom: 24px; + text-align: center; +} + +.auth-form .form-group { + margin-bottom: 20px; +} + +.auth-form .form-group label { + display: block; + margin-bottom: 8px; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-secondary); +} + +.auth-form .form-control { + width: 100%; + padding: 14px 16px; + background: var(--bg-dark); + border: 1px solid var(--border); + border-radius: var(--border-radius-sm); + color: var(--text-primary); + font-size: 1rem; + transition: var(--transition); +} + +.auth-form .form-control:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); +} + +.auth-form .btn-block { + width: 100%; + padding: 14px; + margin-top: 8px; +} + +.auth-switch { + text-align: center; + margin-top: 20px; + color: var(--text-muted); + font-size: 0.9rem; +} + +.auth-switch a { + color: var(--primary-light); + text-decoration: none; + font-weight: 600; +} + +.auth-switch a:hover { + text-decoration: underline; +} + +.auth-message { + margin-top: 16px; + padding: 12px 16px; + border-radius: var(--border-radius-sm); + font-size: 0.9rem; + text-align: center; +} + +.auth-message.error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid var(--danger); + color: var(--danger); +} + +.auth-message.success { + background: rgba(16, 185, 129, 0.1); + border: 1px solid var(--success); + color: var(--success); +} + +/* Responsive */ +@media (max-width: 480px) { + .auth-box { + padding: 24px; + } + + .auth-logo .logo-icon { + font-size: 3rem; + } + + .auth-logo h1 { + font-size: 1.5rem; + } +} diff --git a/css/dashboard.css b/css/dashboard.css new file mode 100644 index 0000000..06d4367 --- /dev/null +++ b/css/dashboard.css @@ -0,0 +1,108 @@ +/* ============================================ + DASHBOARD - STYLES SPÉCIFIQUES + ============================================ */ + +/* Reservation form */ +.reservation-form-container { + max-width: 600px; + margin: 0 auto; +} + +.reservation-form { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 32px; + border: 1px solid var(--border); +} + +.price-preview { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--bg-dark); + padding: 16px 20px; + border-radius: var(--border-radius-sm); + margin: 20px 0; +} + +.price-preview span:first-child { + color: var(--text-secondary); +} + +.price-amount { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-light); +} + +/* Chart container */ +.chart-container { + position: relative; + height: 300px; + width: 100%; +} + +/* Admin table actions */ +.table-actions { + display: flex; + gap: 8px; +} + +.btn-icon-only { + padding: 6px 10px; + font-size: 1rem; +} + +/* Status badges */ +.badge { + display: inline-block; + padding: 4px 12px; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-admin { + background: rgba(99, 102, 241, 0.2); + color: var(--primary-light); +} + +.badge-client { + background: rgba(6, 182, 212, 0.2); + color: var(--info); +} + +/* Animation pour les mises à jour */ +@keyframes pulse-update { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +.stat-card.updating { + animation: pulse-update 0.3s ease; +} + +/* Loading state */ +.loading { + position: relative; + pointer-events: none; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..b2225ed --- /dev/null +++ b/css/style.css @@ -0,0 +1,1124 @@ +/* ============================================ + 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; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + background: linear-gradient(135deg, var(--bg-darker) 0%, var(--bg-dark) 100%); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* ============================================ + HEADER + ============================================ */ +.header { + background: var(--bg-card); + border-bottom: 1px solid var(--border); + padding: 16px 0; + position: sticky; + top: 0; + z-index: 100; + backdrop-filter: blur(10px); +} + +.header .container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} + +.header-left { + display: flex; + align-items: center; +} + +.logo { + display: flex; + align-items: center; + gap: 12px; +} + +.logo-icon { + font-size: 2rem; +} + +.logo h1 { + font-size: 1.3rem; + font-weight: 700; + background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* Navigation */ +.nav { + display: flex; + gap: 8px; +} + +.nav-link { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: var(--border-radius-sm); + text-decoration: none; + color: var(--text-secondary); + font-weight: 500; + font-size: 0.9rem; + transition: var(--transition); +} + +.nav-link:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.nav-link.active { + background: var(--primary); + color: white; +} + +.nav-icon { + font-size: 1.1rem; +} + +/* Header right */ +.header-right { + display: flex; + align-items: center; + gap: 16px; +} + +.user-info { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.user-info span:first-child { + font-weight: 600; + font-size: 0.9rem; +} + +.user-role { + font-size: 0.75rem; + color: var(--text-muted); +} + +/* ============================================ + MAIN & PAGES + ============================================ */ +.main { + padding: 30px 0; + min-height: calc(100vh - 80px); +} + +.page { + display: none; +} + +.page.active { + display: block; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.page-title { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 24px; + display: flex; + align-items: center; + gap: 12px; +} + +.page-title .icon { + font-size: 1.5rem; +} + +/* ============================================ + BOUTONS + ============================================ */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 24px; + border: none; + border-radius: var(--border-radius-sm); + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition); +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); +} + +.btn-secondary { + background: var(--bg-hover); + color: var(--text-primary); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--border); +} + +.btn-success { + background: linear-gradient(135deg, var(--success) 0%, #059669 100%); + color: white; +} + +.btn-danger { + background: linear-gradient(135deg, var(--danger) 0%, #dc2626 100%); + color: white; +} + +.btn-small { + padding: 8px 16px; + font-size: 0.85rem; +} + +.btn-block { + width: 100%; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* ============================================ + FORMULAIRES + ============================================ */ +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-secondary); +} + +.form-control { + width: 100%; + padding: 12px 16px; + background: var(--bg-dark); + border: 1px solid var(--border); + border-radius: var(--border-radius-sm); + color: var(--text-primary); + font-size: 1rem; + transition: var(--transition); +} + +.form-control:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); +} + +.form-control::placeholder { + color: var(--text-muted); +} + +select.form-control { + cursor: pointer; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.input-group { + display: flex; + gap: 8px; +} + +.input-group .form-control { + flex: 1; +} + +/* ============================================ + CARTES + ============================================ */ +.card { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 24px; + border: 1px solid var(--border); + box-shadow: var(--shadow); +} + +/* ============================================ + STATS GRID + ============================================ */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 30px; +} + +.stat-card { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 20px; + display: flex; + align-items: center; + gap: 16px; + border: 1px solid var(--border); + transition: var(--transition); +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.stat-card.free { + border-left: 4px solid var(--spot-free); +} + +.stat-card.occupied { + border-left: 4px solid var(--spot-occupied); +} + +.stat-card.reserved { + border-left: 4px solid var(--spot-reserved); +} + +.stat-card.total { + border-left: 4px solid var(--primary); +} + +.stat-icon { + font-size: 2rem; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-dark); + border-radius: var(--border-radius-sm); +} + +.stat-content { + display: flex; + flex-direction: column; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + line-height: 1; +} + +.stat-card.free .stat-value { color: var(--spot-free); } +.stat-card.occupied .stat-value { color: var(--spot-occupied); } +.stat-card.reserved .stat-value { color: var(--spot-reserved); } +.stat-card.total .stat-value { color: var(--primary); } + +.stat-label { + font-size: 0.85rem; + color: var(--text-muted); + margin-top: 4px; +} + +/* ============================================ + PARKING MAP + ============================================ */ +.parking-section { + display: grid; + grid-template-columns: 1fr 350px; + gap: 24px; + margin-bottom: 30px; +} + +.parking-map-container, +.spot-details-container { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 24px; + border: 1px solid var(--border); +} + +.parking-map-container h3, +.spot-details-container h3 { + margin-bottom: 20px; + font-size: 1.2rem; +} + +.parking-map { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + margin-bottom: 20px; +} + +.parking-spot { + aspect-ratio: 1; + border-radius: var(--border-radius-sm); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: var(--transition); + border: 2px solid transparent; + font-weight: 600; + background: var(--bg-dark); +} + +.parking-spot:hover { + transform: scale(1.05); +} + +.parking-spot.free { + border-color: var(--spot-free); + color: var(--spot-free); +} + +.parking-spot.occupied { + border-color: var(--spot-occupied); + color: var(--spot-occupied); +} + +.parking-spot.reserved { + border-color: var(--spot-reserved); + color: var(--spot-reserved); +} + +.parking-spot .spot-number { + font-size: 1.1rem; +} + +.parking-spot .spot-icon { + font-size: 1.3rem; +} + +/* Legend */ +.legend { + display: flex; + justify-content: center; + gap: 24px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.legend-color { + width: 16px; + height: 16px; + border-radius: 4px; +} + +.legend-color.free { background: var(--spot-free); } +.legend-color.occupied { background: var(--spot-occupied); } +.legend-color.reserved { background: var(--spot-reserved); } + +/* Spot details */ +.spot-details { + min-height: 200px; +} + +.no-selection { + color: var(--text-muted); + text-align: center; + padding: 40px 0; +} + +.spot-info-detail { + display: flex; + flex-direction: column; + gap: 12px; +} + +.spot-info-row { + display: flex; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid var(--border); +} + +.spot-info-label { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.spot-info-value { + font-weight: 600; +} + +.spot-status-free { color: var(--spot-free); } +.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); + padding: 24px; + border: 1px solid var(--border); +} + +.pricing-section h3 { + margin-bottom: 20px; + font-size: 1.2rem; +} + +.pricing-cards { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; +} + +.pricing-card { + background: var(--bg-dark); + border-radius: var(--border-radius-sm); + padding: 20px; + text-align: center; + border: 1px solid var(--border); + transition: var(--transition); +} + +.pricing-card:hover { + border-color: var(--primary); + transform: translateY(-2px); +} + +.pricing-card h4 { + font-size: 0.9rem; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.pricing-card .price { + font-size: 1.8rem; + font-weight: 700; + color: var(--primary-light); +} + +/* ============================================ + MODAL + ============================================ */ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.modal-content { + background: var(--bg-card); + border-radius: var(--border-radius); + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + border: 1px solid var(--border); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--border); +} + +.modal-header h3 { + margin: 0; +} + +.modal-close { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.5rem; + cursor: pointer; + transition: var(--transition); +} + +.modal-close:hover { + color: var(--text-primary); +} + +.modal-body { + padding: 24px; +} + +/* Payment modal */ +.payment-summary { + background: var(--bg-dark); + border-radius: var(--border-radius-sm); + padding: 16px; + margin-bottom: 20px; +} + +.payment-summary h4 { + margin-bottom: 12px; + font-size: 1rem; +} + +.summary-row { + display: flex; + justify-content: space-between; + padding: 6px 0; + font-size: 0.9rem; +} + +.summary-row.total { + border-top: 1px solid var(--border); + margin-top: 8px; + padding-top: 12px; + font-weight: 700; + font-size: 1.1rem; +} + +.qr-section { + text-align: center; + margin: 20px 0; +} + +.qr-section p { + margin-bottom: 12px; + color: var(--text-secondary); +} + +#qrcode { + display: flex; + justify-content: center; + margin: 16px 0; +} + +#qrcode img { + border-radius: var(--border-radius-sm); +} + +.qr-info { + font-size: 0.85rem; +} + +.payment-actions { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* ============================================ + RESERVATIONS LIST + ============================================ */ +.reservations-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.reservation-card { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 20px; + border: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.reservation-info h4 { + margin-bottom: 8px; + font-size: 1.1rem; +} + +.reservation-details { + display: flex; + gap: 16px; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.reservation-details span { + display: flex; + align-items: center; + gap: 4px; +} + +.reservation-price { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-light); +} + +.reservation-status { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-active { + background: rgba(16, 185, 129, 0.2); + color: var(--success); +} + +.status-completed { + background: rgba(148, 163, 184, 0.2); + color: var(--text-muted); +} + +.status-cancelled { + background: rgba(239, 68, 68, 0.2); + color: var(--danger); +} + +.empty-state { + text-align: center; + padding: 60px 20px; + background: var(--bg-card); + border-radius: var(--border-radius); + border: 1px solid var(--border); +} + +.empty-icon { + font-size: 4rem; + margin-bottom: 16px; + display: block; +} + +.empty-state p { + color: var(--text-muted); + margin-bottom: 20px; +} + +/* ============================================ + PROFILE + ============================================ */ +.profile-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; +} + +.profile-card, +.profile-stats { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 24px; + border: 1px solid var(--border); +} + +.profile-header { + text-align: center; + margin-bottom: 24px; + padding-bottom: 24px; + border-bottom: 1px solid var(--border); +} + +.profile-avatar { + width: 80px; + height: 80px; + background: var(--bg-dark); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 12px; + font-size: 2.5rem; +} + +.profile-header h3 { + margin-bottom: 8px; +} + +.role-badge { + display: inline-block; + padding: 4px 12px; + background: var(--primary); + color: white; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; +} + +.stats-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.stat-box { + background: var(--bg-dark); + border-radius: var(--border-radius-sm); + padding: 20px; + text-align: center; +} + +.stat-box-value { + display: block; + font-size: 1.8rem; + font-weight: 700; + color: var(--primary-light); + margin-bottom: 4px; +} + +.stat-box-label { + font-size: 0.8rem; + color: var(--text-muted); +} + +/* ============================================ + ADMIN + ============================================ */ +.admin-page .admin-stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 30px; +} + +.admin-stat { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 20px; + text-align: center; + border: 1px solid var(--border); +} + +.admin-stat-value { + display: block; + font-size: 2rem; + font-weight: 700; + color: var(--primary-light); +} + +.admin-stat-label { + font-size: 0.85rem; + color: var(--text-muted); +} + +.admin-section { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 24px; + border: 1px solid var(--border); + margin-bottom: 24px; +} + +.admin-section h3 { + margin-bottom: 20px; + font-size: 1.2rem; +} + +.admin-places-control { + max-width: 300px; + margin-bottom: 20px; +} + +.admin-places-list { + display: grid; + grid-template-columns: repeat(10, 1fr); + gap: 8px; +} + +.admin-place-item { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--border-radius-sm); + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; +} + +.admin-place-item.free { background: rgba(16, 185, 129, 0.2); color: var(--spot-free); } +.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 { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.data-table th, +.data-table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.data-table th { + background: var(--bg-dark); + color: var(--text-secondary); + font-weight: 600; + font-size: 0.8rem; + text-transform: uppercase; +} + +.data-table tr:hover td { + background: var(--bg-hover); +} + +/* Log container */ +.log-container { + max-height: 300px; + overflow-y: auto; + background: var(--bg-dark); + border-radius: var(--border-radius-sm); + padding: 12px; +} + +.log-item { + display: flex; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--border); + font-size: 0.85rem; +} + +.log-item:last-child { + border-bottom: none; +} + +.log-time { + color: var(--text-muted); + min-width: 70px; + font-size: 0.75rem; +} + +/* ============================================ + TOAST NOTIFICATIONS + ============================================ */ +.toast-container { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1001; + display: flex; + flex-direction: column; + gap: 8px; +} + +.toast { + background: var(--bg-card); + border-radius: var(--border-radius-sm); + padding: 16px 20px; + border-left: 4px solid var(--primary); + box-shadow: var(--shadow-lg); + animation: slideIn 0.3s ease; + max-width: 350px; +} + +.toast.success { border-left-color: var(--success); } +.toast.error { border-left-color: var(--danger); } +.toast.warning { border-left-color: var(--warning); } + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + 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; + } + + .pricing-cards { + grid-template-columns: repeat(3, 1fr); + } + + .profile-container { + grid-template-columns: 1fr; + } + + .admin-page .admin-stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .header .container { + flex-wrap: wrap; + } + + .nav { + order: 3; + width: 100%; + justify-content: center; + margin-top: 12px; + } + + .nav-link { + padding: 8px 12px; + font-size: 0.8rem; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .form-row { + grid-template-columns: 1fr; + } + + .pricing-cards { + grid-template-columns: repeat(2, 1fr); + } + + .parking-map { + grid-template-columns: repeat(3, 1fr); + } + + .stats-cards { + grid-template-columns: 1fr; + } + + .admin-page .admin-stats-grid { + grid-template-columns: 1fr; + } + + .admin-places-list { + grid-template-columns: repeat(5, 1fr); + } +} + +/* 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; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--secondary); +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4451efd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.8' + +services: + db: + image: mariadb:10.11 + container_name: smartparking-db + restart: always + environment: + MARIADB_ROOT_PASSWORD: rootpassword # À changer + MARIADB_DATABASE: smartparking + MARIADB_USER: smartparking_user + MARIADB_PASSWORD: smartparking_pass # À changer + volumes: + - db_data:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - smartparking-network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 10s + retries: 5 + + app: + build: . + container_name: smartparking-app + restart: always + ports: + - "3000:3000" + depends_on: + db: + condition: service_healthy + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: smartparking_user + DB_PASSWORD: smartparking_pass + DB_NAME: smartparking + JWT_SECRET: ${JWT_SECRET:-une_chaine_tres_longue_et_secrete} + NODE_ENV: production + networks: + - smartparking-network + +volumes: + db_data: + +networks: + smartparking-network: \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..c172a07 --- /dev/null +++ b/index.html @@ -0,0 +1,80 @@ + + + + + + Smart Parking - Connexion + + + + +
+
+ + + +
+

Connexion

+
+ + +
+
+ + +
+ +

+ Pas encore de compte ? + Créer un compte +

+
+ + + + + + +
+
+ + + + diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..b36c08d --- /dev/null +++ b/init.sql @@ -0,0 +1,64 @@ +CREATE DATABASE IF NOT EXISTS smartparking; +USE smartparking; + +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + phone VARCHAR(50), + password VARCHAR(255) NOT NULL, + role ENUM('admin','client') DEFAULT 'client', + status ENUM('active','inactive') DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS spots ( + id INT AUTO_INCREMENT PRIMARY KEY, + number INT UNIQUE NOT NULL, + status ENUM('free','occupied','reserved') DEFAULT 'free', + sensor_id VARCHAR(100), + last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS reservations ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + spot_id INT NOT NULL, + date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + duration INT NOT NULL, + vehicle VARCHAR(20), + price DECIMAL(10,2) NOT NULL, + status ENUM('pending','active','completed','cancelled') DEFAULT 'pending', + payment_code VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (spot_id) REFERENCES spots(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS history ( + id INT AUTO_INCREMENT PRIMARY KEY, + action VARCHAR(255) NOT NULL, + details TEXT, + user_id INT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS stats ( + id INT AUTO_INCREMENT PRIMARY KEY, + total_spots INT, + free_spots INT, + occupied_spots INT, + reserved_spots INT, + occupancy_rate DECIMAL(5,2), + recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS mqtt_events ( + id INT AUTO_INCREMENT PRIMARY KEY, + topic VARCHAR(255) NOT NULL, + message TEXT, + received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..30744ac --- /dev/null +++ b/js/admin.js @@ -0,0 +1,436 @@ +/** + * ============================================ + * ADMIN.JS - Panel d'administration + * Smart Parking - BTS CIEL IR + * ============================================ + */ + +// Initialisation +document.addEventListener('DOMContentLoaded', () => { + console.log('⚙️ Initialisation du panel admin...'); + + // Vérifier si l'utilisateur est admin + if (!isAdmin()) return; + + initAdminPanel(); +}); + +/** + * Vérifie si l'utilisateur est admin + */ +function isAdmin() { + const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null'); + return user && user.role === 'admin'; +} + +/** + * Initialise le panel admin + */ +function initAdminPanel() { + loadAdminStats(); + initPlacesControl(); + loadUsersTable(); + loadReservationsTable(); + initOccupancyChart(); + loadHistoryLog(); + + // Rafraîchissement périodique + setInterval(() => { + loadAdminStats(); + loadReservationsTable(); + }, 10000); +} + +/** + * Charge les statistiques admin + */ +function loadAdminStats() { + const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); + const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); + + // Calculer les stats + const totalUsers = users.length + 1; // +1 pour l'admin par défaut + const totalReservations = reservations.length; + const totalRevenue = reservations + .filter(r => r.status === 'active' || r.status === 'completed') + .reduce((sum, r) => sum + (r.price || 0), 0); + + const occupied = spots.filter(s => s.status === 'occupied').length; + const reserved = spots.filter(s => s.status === 'reserved').length; + const occupancyRate = spots.length > 0 + ? Math.round(((occupied + reserved) / spots.length) * 100) + : 0; + + // Mettre à jour l'affichage + document.getElementById('adminTotalUsers').textContent = totalUsers; + document.getElementById('adminTotalReservations').textContent = totalReservations; + document.getElementById('adminTotalRevenue').textContent = totalRevenue + '€'; + document.getElementById('adminOccupancyRate').textContent = occupancyRate + '%'; +} + +/** + * Initialise le contrôle des places + */ +function initPlacesControl() { + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); + + // Mettre à jour le champ du nombre de places + const spotsInput = document.getElementById('adminTotalSpots'); + if (spotsInput) { + spotsInput.value = spots.length || 10; + } + + // Bouton de mise à jour + document.getElementById('updateSpotsBtn')?.addEventListener('click', () => { + 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'); + return; + } + + if (window.ParkingMap) { + window.ParkingMap.setTotalSpots(newCount); + renderAdminPlacesList(); + Dashboard.showToast('Nombre de places mis à jour', 'success'); + } + }); + + // Rendre la liste des places + renderAdminPlacesList(); +} + +/** + * Rend la liste des places dans l'admin + */ +function renderAdminPlacesList() { + const container = document.getElementById('adminPlacesList'); + if (!container) return; + + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); + + container.innerHTML = spots.map(spot => ` +
+ ${spot.number} +
+ `).join(''); +} + +/** + * Change le statut d'une place (admin) + */ +function toggleSpotStatus(spotId) { + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); + const spot = spots.find(s => s.id === spotId); + + if (!spot) return; + + // Cycle: free -> occupied -> reserved -> free + const cycle = ['free', 'occupied', 'reserved']; + const currentIndex = cycle.indexOf(spot.status); + const nextStatus = cycle[(currentIndex + 1) % cycle.length]; + + spot.status = nextStatus; + spot.lastUpdate = new Date().toISOString(); + + localStorage.setItem('smart_parking_spots', JSON.stringify(spots)); + + // Rafraîchir + renderAdminPlacesList(); + if (window.ParkingMap) { + window.ParkingMap.refresh(); + } + loadAdminStats(); + + Dashboard.showToast(`Place ${spot.number} - ${getStatusLabel(nextStatus)}`, 'success'); +} + +/** + * Charge le tableau des utilisateurs + */ +function loadUsersTable() { + const tbody = document.getElementById('adminUsersTable'); + if (!tbody) return; + + const users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); + + // Ajouter l'admin par défaut + const allUsers = [ + { + id: 0, + name: 'Administrateur', + email: 'admin@smartparking.fr', + phone: '01 23 45 67 89', + role: 'admin' + }, + ...users + ]; + + tbody.innerHTML = allUsers.map(user => ` + + #${user.id} + ${user.name} + ${user.email} + ${user.phone || '-'} + + + ${user.role === 'admin' ? 'Admin' : 'Client'} + + + +
+ ${user.role !== 'admin' ? ` + + ` : '-'} +
+ + + `).join(''); +} + +/** + * Supprime un utilisateur + */ +function deleteUser(userId) { + if (!confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?')) return; + + let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); + users = users.filter(u => u.id !== userId); + localStorage.setItem('smart_parking_users', JSON.stringify(users)); + + // Supprimer aussi ses réservations + let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + reservations = reservations.filter(r => r.userId !== userId); + localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); + + loadUsersTable(); + loadReservationsTable(); + loadAdminStats(); + + Dashboard.showToast('Utilisateur supprimé', 'success'); +} + +/** + * Charge le tableau des réservations + */ +function loadReservationsTable() { + const tbody = document.getElementById('adminReservationsTable'); + if (!tbody) return; + + const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + + if (reservations.length === 0) { + tbody.innerHTML = 'Aucune réservation'; + return; + } + + tbody.innerHTML = reservations.slice().reverse().map(res => ` + + #${res.id} + ${res.userName} + Place ${res.spotNumber} + ${formatDate(res.date)} + ${res.startTime} - ${res.endTime} + ${res.price}€ + + + ${getStatusLabel(res.status)} + + + +
+ ${res.status === 'active' ? ` + + + ` : '-'} +
+ + + `).join(''); +} + +/** + * Termine une réservation + */ +function completeReservation(reservationId) { + let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + const reservation = reservations.find(r => r.id === reservationId); + + if (reservation) { + reservation.status = 'completed'; + localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); + + // Libérer la place + if (window.ParkingMap) { + window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); + } + + loadReservationsTable(); + loadAdminStats(); + Dashboard.showToast('Réservation terminée', 'success'); + } +} + +/** + * Annule une réservation (admin) + */ +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) { + reservation.status = 'cancelled'; + localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); + + // Libérer la place + if (window.ParkingMap) { + window.ParkingMap.setSpotStatus(reservation.spotId, 'free'); + } + + loadReservationsTable(); + loadAdminStats(); + Dashboard.showToast('Réservation annulée', 'success'); + } +} + +/** + * Initialise le graphique d'occupation + */ +function initOccupancyChart() { + const ctx = document.getElementById('adminOccupancyChart'); + if (!ctx) return; + + // Générer des données d'exemple + const labels = []; + const data = []; + + for (let i = 6; i >= 0; i--) { + const date = new Date(); + date.setDate(date.getDate() - i); + labels.push(date.toLocaleDateString('fr-FR', { weekday: 'short' })); + data.push(Math.floor(Math.random() * 40) + 30); // 30-70% d'occupation + } + + new Chart(ctx, { + type: 'bar', + data: { + labels: labels, + datasets: [{ + label: 'Taux d\'occupation (%)', + data: data, + backgroundColor: 'rgba(99, 102, 241, 0.5)', + borderColor: 'rgba(99, 102, 241, 1)', + borderWidth: 1, + borderRadius: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + labels: { color: '#f1f5f9' } + } + }, + scales: { + x: { + grid: { color: '#334155' }, + ticks: { color: '#94a3b8' } + }, + y: { + min: 0, + max: 100, + grid: { color: '#334155' }, + ticks: { + color: '#94a3b8', + callback: value => value + '%' + } + } + } + } + }); +} + +/** + * Charge l'historique + */ +function loadHistoryLog() { + const container = document.getElementById('adminLogContainer'); + if (!container) return; + + const history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]'); + + if (history.length === 0) { + container.innerHTML = '

Aucun historique

'; + return; + } + + container.innerHTML = history.slice(0, 20).map(item => ` +
+ ${formatTime(item.timestamp)} + ${item.action}: ${item.details} +
+ `).join(''); +} + +/** + * Formate une date + */ +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit' + }); +} + +/** + * Formate une heure + */ +function formatTime(dateString) { + const date = new Date(dateString); + return date.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit' + }); +} + +/** + * Retourne le label du statut + */ +function getStatusLabel(status) { + const labels = { + 'pending': 'En attente', + 'active': 'Active', + 'completed': 'Terminée', + 'cancelled': 'Annulée' + }; + return labels[status] || status; +} + +// Exporter les fonctions +window.AdminModule = { + refresh: () => { + loadAdminStats(); + renderAdminPlacesList(); + loadUsersTable(); + loadReservationsTable(); + loadHistoryLog(); + }, + toggleSpotStatus, + deleteUser, + completeReservation, + adminCancelReservation +}; diff --git a/js/auth.js b/js/auth.js new file mode 100644 index 0000000..0a87995 --- /dev/null +++ b/js/auth.js @@ -0,0 +1,327 @@ +/** + * ============================================ + * AUTH.JS - Système d'authentification + * Smart Parking - BTS CIEL IR + * ============================================ + */ + +// Configuration +const AUTH_CONFIG = { + apiUrl: 'http://localhost:3000/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) => { + e.preventDefault(); + showRegisterForm(); + }); + } + + // Basculer vers la connexion + const showLogin = document.getElementById('showLogin'); + if (showLogin) { + showLogin.addEventListener('click', (e) => { + e.preventDefault(); + showLoginForm(); + }); + } + + // Formulaire de connexion + const loginForm = document.getElementById('loginForm'); + if (loginForm) { + loginForm.addEventListener('submit', handleLogin); + } + + // Formulaire d'inscription + const registerForm = document.getElementById('registerForm'); + 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' + }, + 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); + } 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); + } else { + // Compte admin par défaut + if (email === 'admin@smartparking.fr' && password === 'admin123') { + const adminUser = { + id: 1, + name: 'Administrateur', + email: 'admin@smartparking.fr', + 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); + 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' + }, + 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 { + showMessage(data.message || 'Erreur lors de la création du compte', 'error'); + } + } 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() + }; + + // 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(); + document.getElementById('loginEmail').value = email; + }, 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; + messageEl.className = `auth-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), + getUser: () => JSON.parse(localStorage.getItem(AUTH_CONFIG.userKey) || 'null'), + isAdmin: () => { + const user = JSON.parse(localStorage.getItem(AUTH_CONFIG.userKey) || 'null'); + return user && user.role === 'admin'; + } +}; diff --git a/js/dashboard.js b/js/dashboard.js new file mode 100644 index 0000000..cf91dfe --- /dev/null +++ b/js/dashboard.js @@ -0,0 +1,300 @@ +/** + * ============================================ + * DASHBOARD.JS - Gestion du dashboard + * Smart Parking - BTS CIEL IR + * ============================================ + */ + +// Configuration +const API_URL = 'http://localhost:3000/api'; + +// État global +let dashboardState = { + user: null, + currentPage: 'map' +}; + +// Initialisation +document.addEventListener('DOMContentLoaded', () => { + console.log('🚗 Initialisation du dashboard...'); + + // Vérifier l'authentification + checkAuthentication(); + + // Initialiser la navigation + initNavigation(); + + // Initialiser la déconnexion + initLogout(); + + // Charger les données utilisateur + loadUserData(); +}); + +/** + * Vérifie que l'utilisateur est authentifié + */ +function checkAuthentication() { + const token = localStorage.getItem('smart_parking_token'); + const user = localStorage.getItem('smart_parking_user'); + + if (!token || !user) { + // Rediriger vers la page de connexion + window.location.href = '../index.html'; + return; + } + + dashboardState.user = JSON.parse(user); +} + +/** + * Initialise la navigation + */ +function initNavigation() { + const navLinks = document.querySelectorAll('.nav-link'); + + navLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const page = link.getAttribute('data-page'); + navigateToPage(page); + + // Mettre à jour la classe active + navLinks.forEach(l => l.classList.remove('active')); + link.classList.add('active'); + }); + }); + + // Afficher le lien admin si l'utilisateur est admin + if (dashboardState.user && dashboardState.user.role === 'admin') { + const adminLink = document.querySelector('.admin-only'); + if (adminLink) { + adminLink.classList.remove('hidden'); + adminLink.classList.add('visible'); + } + } +} + +/** + * Navigation entre les pages + */ +function navigateToPage(page) { + // Cacher toutes les pages + const pages = document.querySelectorAll('.page'); + pages.forEach(p => { + p.classList.add('hidden'); + p.classList.remove('active'); + }); + + // Afficher la page demandée + const targetPage = document.getElementById(page); + if (targetPage) { + targetPage.classList.remove('hidden'); + targetPage.classList.add('active'); + dashboardState.currentPage = page; + + // Rafraîchir les données selon la page + if (page === 'map' && window.ParkingMap) { + window.ParkingMap.refresh(); + } else if (page === 'my-reservations') { + loadMyReservations(); + } else if (page === 'admin' && window.AdminModule) { + window.AdminModule.refresh(); + } + } +} + +/** + * Initialise la déconnexion + */ +function initLogout() { + const logoutBtn = document.getElementById('logoutBtn'); + if (logoutBtn) { + logoutBtn.addEventListener('click', () => { + localStorage.removeItem('smart_parking_token'); + localStorage.removeItem('smart_parking_user'); + window.location.href = '../index.html'; + }); + } +} + +/** + * Charge les données utilisateur + */ +function loadUserData() { + const user = dashboardState.user; + if (!user) return; + + // Mettre à jour l'affichage + document.getElementById('userName').textContent = user.name; + document.getElementById('userRole').textContent = user.role === 'admin' ? 'Administrateur' : 'Client'; + + // Page profil + document.getElementById('profileName').textContent = user.name; + document.getElementById('profileNameInput').value = user.name; + document.getElementById('profileEmailInput').value = user.email; + document.getElementById('profilePhoneInput').value = user.phone || ''; + + // Badge rôle + const roleBadge = document.getElementById('profileRole'); + if (roleBadge) { + roleBadge.textContent = user.role === 'admin' ? 'Administrateur' : 'Client'; + roleBadge.className = 'role-badge ' + (user.role === 'admin' ? 'badge-admin' : 'badge-client'); + } + + // Charger les statistiques + loadUserStats(); +} + +/** + * Charge les statistiques utilisateur + */ +function loadUserStats() { + const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + const userReservations = reservations.filter(r => r.userId === dashboardState.user.id); + + const totalReservations = userReservations.length; + const activeReservations = userReservations.filter(r => r.status === 'active').length; + const totalSpent = userReservations.reduce((sum, r) => sum + (r.price || 0), 0); + + document.getElementById('totalReservations').textContent = totalReservations; + document.getElementById('activeReservations').textContent = activeReservations; + document.getElementById('totalSpent').textContent = totalSpent + '€'; +} + +/** + * Charge les réservations de l'utilisateur + */ +function loadMyReservations() { + const container = document.getElementById('myReservationsList'); + const emptyState = document.getElementById('noReservations'); + + if (!container || !emptyState) return; + + const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + const userReservations = reservations.filter(r => r.userId === dashboardState.user.id); + + if (userReservations.length === 0) { + container.innerHTML = ''; + emptyState.classList.remove('hidden'); + return; + } + + emptyState.classList.add('hidden'); + + container.innerHTML = userReservations.map(res => ` +
+
+

Place ${res.spotNumber}

+
+ 📅 ${res.date} + 🕐 ${res.startTime} + ⏱️ ${res.duration} min + 🚗 ${res.vehicle || 'N/A'} +
+
+
+ ${res.price}€ + ${getStatusLabel(res.status)} + ${res.status === 'active' ? ` + + ` : ''} +
+
+ `).join(''); +} + +/** + * Annule une réservation + */ +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') || '[]'); + const reservation = reservations.find(r => r.id === reservationId); + + if (reservation) { + // Mettre à jour le statut + reservation.status = 'cancelled'; + localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); + + // Libérer la place + if (window.ParkingMap) { + window.ParkingMap.setSpotStatus(reservation.spotNumber, 'free'); + } + + showToast('Réservation annulée', 'success'); + loadMyReservations(); + loadUserStats(); + } +} + +/** + * Met à jour le profil + */ +document.getElementById('profileForm')?.addEventListener('submit', (e) => { + e.preventDefault(); + + const phone = document.getElementById('profilePhoneInput').value; + const newPassword = document.getElementById('profileNewPassword').value; + + // Mettre à jour l'utilisateur + let user = dashboardState.user; + user.phone = phone; + + if (newPassword) { + user.password = newPassword; + } + + // Sauvegarder + localStorage.setItem('smart_parking_user', JSON.stringify(user)); + + // Mettre à jour aussi dans la liste des utilisateurs + let users = JSON.parse(localStorage.getItem('smart_parking_users') || '[]'); + const userIndex = users.findIndex(u => u.id === user.id); + if (userIndex !== -1) { + users[userIndex] = user; + localStorage.setItem('smart_parking_users', JSON.stringify(users)); + } + + showToast('Profil mis à jour', 'success'); +}); + +/** + * Retourne le label du statut + */ +function getStatusLabel(status) { + const labels = { + 'active': 'Active', + 'completed': 'Terminée', + 'cancelled': 'Annulée' + }; + return labels[status] || status; +} + +/** + * Affiche une notification toast + */ +function showToast(message, type = 'info') { + const container = document.getElementById('toastContainer'); + + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.textContent = message; + + container.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, 3000); +} + +// Exporter les fonctions +window.Dashboard = { + navigateToPage, + showToast, + getUser: () => dashboardState.user, + refreshStats: loadUserStats +}; diff --git a/js/map.js b/js/map.js new file mode 100644 index 0000000..1b71d88 --- /dev/null +++ b/js/map.js @@ -0,0 +1,367 @@ +/** + * ============================================ + * MAP.JS - Carte des places de parking + * Smart Parking - BTS CIEL IR + * ============================================ + */ + +// Configuration +const MAP_CONFIG = { + totalSpots: 10, // Nombre total de places + updateInterval: 5000 // Intervalle de mise à jour +}; + +// État des places +let spotsState = { + spots: [], + selectedSpot: null +}; + +// Types de places +const SPOT_STATUS = { + FREE: 'free', + OCCUPIED: 'occupied', + RESERVED: 'reserved' +}; + +// Initialisation +document.addEventListener('DOMContentLoaded', () => { + console.log('🗺️ Initialisation de la carte...'); + initParkingMap(); +}); + +/** + * Initialise la carte du parking + */ +function initParkingMap() { + // Charger les places depuis le stockage local ou créer les places par défaut + loadSpots(); + + // Rendre la carte + renderMap(); + + // Mettre à jour les statistiques + updateStats(); + + // Mettre à jour le formulaire de réservation + updateReservationForm(); + + // Démarrer la simulation (si pas admin) + if (!isAdmin()) { + startSimulation(); + } +} + +/** + * Charge les places + */ +function loadSpots() { + const stored = localStorage.getItem('smart_parking_spots'); + + if (stored) { + spotsState.spots = JSON.parse(stored); + } else { + // Créer les places par défaut + createDefaultSpots(); + } +} + +/** + * Crée les places par défaut + */ +function createDefaultSpots() { + spotsState.spots = []; + + for (let i = 1; i <= MAP_CONFIG.totalSpots; i++) { + // Distribution: 60% libre, 25% occupé, 15% réservé + const rand = Math.random(); + let status = SPOT_STATUS.FREE; + + if (rand > 0.85) { + status = SPOT_STATUS.RESERVED; + } else if (rand > 0.60) { + status = SPOT_STATUS.OCCUPIED; + } + + spotsState.spots.push({ + id: i, + number: i, + status: status, + lastUpdate: new Date().toISOString(), + sensorId: `SENSOR_${String(i).padStart(3, '0')}` + }); + } + + saveSpots(); +} + +/** + * Sauvegarde les places + */ +function saveSpots() { + localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots)); +} + +/** + * Rend la carte + */ +function renderMap() { + const mapContainer = document.getElementById('parkingMap'); + if (!mapContainer) return; + + mapContainer.innerHTML = spotsState.spots.map(spot => ` +
+ ${spot.number} + ${getStatusIcon(spot.status)} +
+ `).join(''); +} + +/** + * Gère le clic sur une place + */ +function handleSpotClick(spotId) { + const spot = spotsState.spots.find(s => s.id === spotId); + if (!spot) return; + + spotsState.selectedSpot = spot; + showSpotDetails(spot); +} + +/** + * Affiche les détails d'une place + */ +function showSpotDetails(spot) { + const container = document.getElementById('spotDetails'); + if (!container) return; + + const isReserved = spot.status === SPOT_STATUS.RESERVED; + const reservation = isReserved ? findReservationForSpot(spot.id) : null; + + container.innerHTML = ` +
+
+ Numéro + Place ${spot.number} +
+
+ État + + ${getStatusLabel(spot.status)} + +
+
+ Capteur + ${spot.sensorId} +
+
+ Dernière mise à jour + ${formatDate(spot.lastUpdate)} +
+ ${reservation ? ` +
+ Réservé par + ${reservation.userName} +
+
+ Jusqu'à + ${reservation.endTime} +
+ ` : ''} + ${spot.status === SPOT_STATUS.FREE ? ` + + ` : ''} +
+ `; +} + +/** + * Trouve la réservation pour une place + */ +function findReservationForSpot(spotId) { + const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + return reservations.find(r => r.spotId === spotId && r.status === 'active'); +} + +/** + * Met à jour les statistiques + */ +function updateStats() { + const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length; + const occupied = spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length; + const reserved = spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length; + + document.getElementById('freeCount').textContent = free; + document.getElementById('occupiedCount').textContent = occupied; + document.getElementById('reservedCount').textContent = reserved; + document.getElementById('totalCount').textContent = spotsState.spots.length; +} + +/** + * Met à jour le formulaire de réservation + */ +function updateReservationForm() { + const select = document.getElementById('resSpot'); + if (!select) return; + + // Garder la première option + const firstOption = select.options[0]; + select.innerHTML = ''; + select.appendChild(firstOption); + + // Ajouter uniquement les places libres + const freeSpots = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE); + + freeSpots.forEach(spot => { + const option = document.createElement('option'); + option.value = spot.id; + option.textContent = `Place ${spot.number}`; + select.appendChild(option); + }); +} + +/** + * Sélectionne une place pour la réservation + */ +function selectSpotForReservation(spotId) { + const select = document.getElementById('resSpot'); + if (select) { + select.value = spotId; + } +} + +/** + * Définit le statut d'une place + */ +function setSpotStatus(spotId, status) { + const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId); + if (spot) { + spot.status = status; + spot.lastUpdate = new Date().toISOString(); + saveSpots(); + renderMap(); + updateStats(); + updateReservationForm(); + } +} + +/** + * Change le nombre total de places + */ +function setTotalSpots(count) { + MAP_CONFIG.totalSpots = count; + + // Ajuster le tableau des places + if (count > spotsState.spots.length) { + // Ajouter des places + for (let i = spotsState.spots.length + 1; i <= count; i++) { + spotsState.spots.push({ + id: i, + number: i, + status: SPOT_STATUS.FREE, + lastUpdate: new Date().toISOString(), + sensorId: `SENSOR_${String(i).padStart(3, '0')}` + }); + } + } else if (count < spotsState.spots.length) { + // Supprimer des places + spotsState.spots = spotsState.spots.slice(0, count); + } + + saveSpots(); + renderMap(); + updateStats(); + updateReservationForm(); +} + +/** + * Simulation automatique + */ +function startSimulation() { + setInterval(() => { + // 20% de chance de changer une place + if (Math.random() > 0.8) { + const randomSpot = spotsState.spots[Math.floor(Math.random() * spotsState.spots.length)]; + + if (randomSpot.status === SPOT_STATUS.FREE && Math.random() > 0.5) { + setSpotStatus(randomSpot.id, SPOT_STATUS.OCCUPIED); + } else if (randomSpot.status === SPOT_STATUS.OCCUPIED && Math.random() > 0.3) { + setSpotStatus(randomSpot.id, SPOT_STATUS.FREE); + } + } + }, MAP_CONFIG.updateInterval); +} + +/** + * Vérifie si l'utilisateur est admin + */ +function isAdmin() { + const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null'); + return user && user.role === 'admin'; +} + +/** + * Retourne le label du statut + */ +function getStatusLabel(status) { + const labels = { + [SPOT_STATUS.FREE]: 'Libre', + [SPOT_STATUS.OCCUPIED]: 'Occupée', + [SPOT_STATUS.RESERVED]: 'Réservée' + }; + return labels[status] || 'Inconnu'; +} + +/** + * Retourne l'icône du statut + */ +function getStatusIcon(status) { + const icons = { + [SPOT_STATUS.FREE]: '✓', + [SPOT_STATUS.OCCUPIED]: '🚗', + [SPOT_STATUS.RESERVED]: '📅' + }; + return icons[status] || '?'; +} + +/** + * Formate une date + */ +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +// Exporter les fonctions +window.ParkingMap = { + refresh: () => { + loadSpots(); + renderMap(); + updateStats(); + updateReservationForm(); + }, + setSpotStatus, + setTotalSpots, + getSpots: () => spotsState.spots, + getFreeSpots: () => spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE), + getStats: () => ({ + total: spotsState.spots.length, + free: spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length, + occupied: spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length, + reserved: spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length + }) +}; diff --git a/js/reservation.js b/js/reservation.js new file mode 100644 index 0000000..680ae1d --- /dev/null +++ b/js/reservation.js @@ -0,0 +1,351 @@ +/** + * ============================================ + * RESERVATION.JS - Système de réservation + * Smart Parking - BTS CIEL IR + * ============================================ + */ + +// Tarifs +const PRICING = { + 30: 2, // 30 min = 2€ + 60: 3, // 1h = 3€ + 120: 5, // 2h = 5€ + 240: 8, // 4h = 8€ + 480: 15 // 8h (journée) = 15€ +}; + +// Horaires disponibles +const TIME_SLOTS = [ + '06:00', '06:30', '07:00', '07:30', '08:00', '08:30', '09:00', '09:30', + '10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00', '13:30', + '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', + '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', + '22:00' +]; + +// État de la réservation en cours +let currentReservation = null; + +// Initialisation +document.addEventListener('DOMContentLoaded', () => { + console.log('📅 Initialisation du système de réservation...'); + initReservationForm(); + initDatePicker(); + initTimeSlots(); + initPricePreview(); + initPaymentModal(); +}); + +/** + * Initialise le formulaire de réservation + */ +function initReservationForm() { + const form = document.getElementById('reservationForm'); + if (!form) return; + + form.addEventListener('submit', handleReservationSubmit); +} + +/** + * Initialise le sélecteur de date + */ +function initDatePicker() { + const dateInput = document.getElementById('resDate'); + if (!dateInput) return; + + // Date minimum = aujourd'hui + const today = new Date().toISOString().split('T')[0]; + dateInput.min = today; + dateInput.value = today; +} + +/** + * Initialise les créneaux horaires + */ +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); + }); + + // Sélectionner l'heure actuelle + 1h + const now = new Date(); + const currentHour = now.getHours(); + const currentMinutes = now.getMinutes(); + const nextSlot = TIME_SLOTS.find(t => { + const [h, m] = t.split(':').map(Number); + return h > currentHour || (h === currentHour && m > currentMinutes); + }); + + if (nextSlot) { + select.value = nextSlot; + } +} + +/** + * Initialise la prévisualisation du prix + */ +function initPricePreview() { + const durationSelect = document.getElementById('resDuration'); + if (!durationSelect) return; + + durationSelect.addEventListener('change', updatePricePreview); + + // Prix initial + updatePricePreview(); +} + +/** + * Met à jour la prévisualisation du prix + */ +function updatePricePreview() { + const duration = parseInt(document.getElementById('resDuration').value); + const price = PRICING[duration] || 0; + + document.getElementById('previewPrice').textContent = price + '€'; +} + +/** + * Gère la soumission du formulaire + */ +function handleReservationSubmit(e) { + e.preventDefault(); + + const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null'); + if (!user) { + Dashboard.showToast('Veuillez vous connecter', 'error'); + return; + } + + const spotId = parseInt(document.getElementById('resSpot').value); + const date = document.getElementById('resDate').value; + const startTime = document.getElementById('resStartTime').value; + const duration = parseInt(document.getElementById('resDuration').value); + const vehicle = document.getElementById('resVehicle').value; + + if (!spotId || !date || !startTime || !vehicle) { + Dashboard.showToast('Veuillez remplir tous les champs', 'error'); + return; + } + + // Vérifier que la place est toujours libre + const spots = JSON.parse(localStorage.getItem('smart_parking_spots') || '[]'); + const spot = spots.find(s => s.id === spotId); + + if (!spot || spot.status !== 'free') { + Dashboard.showToast('Cette place n\'est plus disponible', 'error'); + // Rafraîchir la carte + if (window.ParkingMap) { + window.ParkingMap.refresh(); + } + return; + } + + // Calculer l'heure de fin + const [hours, minutes] = startTime.split(':').map(Number); + const endDate = new Date(date + 'T' + startTime); + endDate.setMinutes(endDate.getMinutes() + duration); + const endTime = endDate.toTimeString().slice(0, 5); + + // Créer la réservation + currentReservation = { + id: Date.now(), + userId: user.id, + userName: user.name, + spotId: spotId, + spotNumber: spot.number, + date: date, + startTime: startTime, + endTime: endTime, + duration: duration, + vehicle: vehicle.toUpperCase(), + price: PRICING[duration], + status: 'pending', + createdAt: new Date().toISOString() + }; + + // Afficher le modal de paiement + showPaymentModal(); +} + +/** + * Initialise le modal de paiement + */ +function initPaymentModal() { + // Fermer le modal + document.getElementById('closePaymentModal')?.addEventListener('click', hidePaymentModal); + document.getElementById('cancelPaymentBtn')?.addEventListener('click', hidePaymentModal); + + // Confirmer le paiement + document.getElementById('confirmPaymentBtn')?.addEventListener('click', confirmPayment); +} + +/** + * Affiche le modal de paiement + */ +function showPaymentModal() { + if (!currentReservation) return; + + const modal = document.getElementById('paymentModal'); + + // Remplir le récapitulatif + 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 + '€'; + + // Générer le QR code + generateQRCode(); + + // Afficher le modal + modal.classList.remove('hidden'); +} + +/** + * Cache le modal de paiement + */ +function hidePaymentModal() { + document.getElementById('paymentModal').classList.add('hidden'); +} + +/** + * Génère le QR code + */ +function generateQRCode() { + const container = document.getElementById('qrcode'); + container.innerHTML = ''; + + // Générer un code de paiement unique + const paymentCode = 'PARK' + Date.now().toString().slice(-8); + document.getElementById('paymentCode').textContent = paymentCode; + + // Créer le QR code + const qrData = JSON.stringify({ + type: 'parking_payment', + reservationId: currentReservation.id, + amount: currentReservation.price, + code: paymentCode + }); + + // Utiliser QRCode.js si disponible, sinon afficher un faux QR + if (typeof QRCode !== 'undefined') { + new QRCode(container, { + text: qrData, + width: 200, + height: 200, + colorDark: '#000000', + colorLight: '#ffffff', + correctLevel: QRCode.CorrectLevel.M + }); + } else { + // Fallback - afficher un QR code simulé + container.innerHTML = ` +
+
📱
+
QR Code de paiement
+
+ `; + } +} + +/** + * Confirme le paiement + */ +function confirmPayment() { + if (!currentReservation) return; + + // Mettre à jour le statut + currentReservation.status = 'active'; + + // Sauvegarder la réservation + let reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); + reservations.push(currentReservation); + localStorage.setItem('smart_parking_reservations', JSON.stringify(reservations)); + + // Mettre à jour le statut de la place + if (window.ParkingMap) { + window.ParkingMap.setSpotStatus(currentReservation.spotId, 'reserved'); + } + + // Ajouter à l'historique admin + addToHistory('Réservation', `Place ${currentReservation.spotNumber} réservée par ${currentReservation.userName} - ${currentReservation.price}€`); + + // Fermer le modal + hidePaymentModal(); + + // Réinitialiser le formulaire + document.getElementById('reservationForm').reset(); + initDatePicker(); + updatePricePreview(); + + // Afficher confirmation + Dashboard.showToast('Réservation confirmée !', 'success'); + + // Rediriger vers mes réservations + setTimeout(() => { + Dashboard.navigateToPage('my-reservations'); + document.querySelector('[data-page="my-reservations"]').classList.add('active'); + document.querySelector('[data-page="reservation"]').classList.remove('active'); + }, 1500); +} + +/** + * Ajoute à l'historique + */ +function addToHistory(action, details) { + let history = JSON.parse(localStorage.getItem('smart_parking_history') || '[]'); + history.unshift({ + id: Date.now(), + action: action, + details: details, + timestamp: new Date().toISOString() + }); + + // Garder seulement les 100 dernières entrées + if (history.length > 100) { + history = history.slice(0, 100); + } + + localStorage.setItem('smart_parking_history', JSON.stringify(history)); +} + +/** + * Formate une date + */ +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); +} + +/** + * Formate une durée + */ +function formatDuration(minutes) { + if (minutes >= 480) { + return 'Journée (8h)'; + } else if (minutes >= 60) { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`; + } else { + return `${minutes} min`; + } +} + +// Exporter les fonctions +window.Reservation = { + PRICING, + TIME_SLOTS, + formatDuration, + addToHistory +}; diff --git a/pages/dashboard.html b/pages/dashboard.html new file mode 100644 index 0000000..6c5b3ef --- /dev/null +++ b/pages/dashboard.html @@ -0,0 +1,474 @@ + + + + + + Smart Parking - Dashboard + + + + + + + +
+
+
+ +
+ +
+ + +
+
+
+ + +
+ +
+
+

+ 🗺️ + Carte du Parking +

+ + +
+
+
+
+ 0 + Places libres +
+
+
+
🚗
+
+ 0 + Places occupées +
+
+
+
📅
+
+ 0 + Places réservées +
+
+
+
🅿️
+
+ 10 + Total places +
+
+
+ + +
+
+

Vue du parking

+
+ +
+
+
+ + Libre +
+
+ + Occupée +
+
+ + Réservée +
+
+
+ + +
+

Détails de la place

+
+

Cliquez sur une place pour voir les détails

+
+
+
+ + +
+

💰 Nos tarifs

+
+
+

30 minutes

+ 2€ +
+
+

1 heure

+ 3€ +
+
+

2 heures

+ 5€ +
+
+

4 heures

+ 8€ +
+
+

Journée

+ 15€ +
+
+
+
+
+ + + + + + + + + + + + +
+ + +
+ + + + + + + diff --git a/server/db/database.js b/server/db/database.js new file mode 100644 index 0000000..c6ba7dd --- /dev/null +++ b/server/db/database.js @@ -0,0 +1,340 @@ +const mysql = require('mysql2/promise'); +const bcrypt = require('bcryptjs'); +require('dotenv').config(); + +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'smartparking_user', + password: process.env.DB_PASSWORD || 'smartparking_pass', + database: process.env.DB_NAME || 'smartparking', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +async function initDatabase() { + try { + await pool.query(` + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + phone VARCHAR(50), + password VARCHAR(255) NOT NULL, + role ENUM('admin','client') DEFAULT 'client', + status ENUM('active','inactive') DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS spots ( + id INT AUTO_INCREMENT PRIMARY KEY, + number INT UNIQUE NOT NULL, + status ENUM('free','occupied','reserved') DEFAULT 'free', + sensor_id VARCHAR(100), + last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS reservations ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + spot_id INT NOT NULL, + date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + duration INT NOT NULL, + vehicle VARCHAR(20), + price DECIMAL(10,2) NOT NULL, + status ENUM('pending','active','completed','cancelled') DEFAULT 'pending', + payment_code VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (spot_id) REFERENCES spots(id) ON DELETE CASCADE + ) + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS history ( + id INT AUTO_INCREMENT PRIMARY KEY, + action VARCHAR(255) NOT NULL, + details TEXT, + user_id INT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL + ) + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS stats ( + id INT AUTO_INCREMENT PRIMARY KEY, + total_spots INT, + free_spots INT, + occupied_spots INT, + reserved_spots INT, + occupancy_rate DECIMAL(5,2), + recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS mqtt_events ( + id INT AUTO_INCREMENT PRIMARY KEY, + topic VARCHAR(255) NOT NULL, + message TEXT, + received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + console.log('✅ Tables vérifiées/créées avec succès'); + + const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', ['admin@smartparking.fr']); + if (rows.length === 0) { + const hashedPassword = await bcrypt.hash('admin123', 10); + await pool.query( + 'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)', + ['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashedPassword, 'admin'] + ); + console.log('✅ Administrateur par défaut créé'); + } + + const [spots] = await pool.query('SELECT COUNT(*) as count FROM spots'); + if (spots[0].count === 0) { + for (let i = 1; i <= 10; i++) { + let status = 'free'; + const rand = Math.random(); + if (rand > 0.85) status = 'reserved'; + else if (rand > 0.60) status = 'occupied'; + await pool.query( + 'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)', + [i, `SENSOR_${String(i).padStart(3, '0')}`, status] + ); + } + console.log('✅ 10 places par défaut créées'); + } + + } catch (err) { + console.error('❌ Erreur lors de l\'initialisation de la base :', err.message); + throw err; + } +} + +// ============================================ +// UTILISATEURS +// ============================================ + +async function createUser(name, email, phone, hashedPassword, role = 'client') { + const [result] = await pool.query( + 'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)', + [name, email, phone, hashedPassword, role] + ); + return { id: result.insertId }; +} + +async function getUserByEmail(email) { + const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]); + return rows[0]; +} + +async function getUserById(id) { + const [rows] = await pool.query( + 'SELECT id, name, email, phone, role, status, created_at FROM users WHERE id = ?', + [id] + ); + return rows[0]; +} + +async function getAllUsers() { + const [rows] = await pool.query( + 'SELECT id, name, email, phone, role, status, created_at FROM users ORDER BY name' + ); + return rows; +} + +async function updateUser(id, updates) { + const fields = Object.keys(updates).map(k => `${k} = ?`).join(', '); + const values = Object.values(updates); + const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]); + return { changed: result.affectedRows }; +} + +async function deleteUser(id) { + const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]); + return { deleted: result.affectedRows }; +} + +// ============================================ +// PLACES +// ============================================ + +async function createSpot(number, sensorId, status = 'free') { + const [result] = await pool.query( + 'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)', + [number, sensorId, status] + ); + return { id: result.insertId }; +} + +async function getAllSpots() { + const [rows] = await pool.query('SELECT * FROM spots ORDER BY number'); + return rows; +} + +async function getSpotById(id) { + const [rows] = await pool.query('SELECT * FROM spots WHERE id = ?', [id]); + return rows[0]; +} + +async function updateSpotStatus(id, status) { + const [result] = await pool.query( + 'UPDATE spots SET status = ?, last_update = CURRENT_TIMESTAMP WHERE id = ?', + [status, id] + ); + return { changed: result.affectedRows }; +} + +async function deleteAllSpots() { + const [result] = await pool.query('DELETE FROM spots'); + return { deleted: result.affectedRows }; +} + +// ============================================ +// RÉSERVATIONS +// ============================================ + +async function createReservation(userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode) { + const [result] = await pool.query( + `INSERT INTO reservations + (user_id, spot_id, date, start_time, end_time, duration, vehicle, price, payment_code, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`, + [userId, spotId, date, startTime, endTime, duration, vehicle, price, paymentCode] + ); + return { id: result.insertId }; +} + +async function getReservationsByUser(userId) { + const [rows] = await pool.query( + `SELECT r.*, s.number as spot_number + FROM reservations r + JOIN spots s ON r.spot_id = s.id + WHERE r.user_id = ? + ORDER BY r.created_at DESC`, + [userId] + ); + return rows; +} + +async function getAllReservations() { + const [rows] = await pool.query( + `SELECT r.*, s.number as spot_number, u.name as user_name + FROM reservations r + JOIN spots s ON r.spot_id = s.id + JOIN users u ON r.user_id = u.id + ORDER BY r.created_at DESC` + ); + return rows; +} + +async function updateReservationStatus(id, status) { + const [result] = await pool.query( + 'UPDATE reservations SET status = ? WHERE id = ?', + [status, id] + ); + return { changed: result.affectedRows }; +} + +// ============================================ +// HISTORIQUE +// ============================================ + +async function addHistory(action, details, userId = null) { + const [result] = await pool.query( + 'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)', + [action, details, userId] + ); + return { id: result.insertId }; +} + +async function getHistory(limit = 50) { + const [rows] = await pool.query( + `SELECT h.*, u.name as user_name + FROM history h + LEFT JOIN users u ON h.user_id = u.id + ORDER BY h.timestamp DESC + LIMIT ?`, + [limit] + ); + return rows; +} + +// ============================================ +// STATISTIQUES +// ============================================ + +async function recordStats(total, free, occupied, reserved) { + const rate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0; + const [result] = await pool.query( + 'INSERT INTO stats (total_spots, free_spots, occupied_spots, reserved_spots, occupancy_rate) VALUES (?, ?, ?, ?, ?)', + [total, free, occupied, reserved, rate] + ); + return { id: result.insertId }; +} + +async function getStats(days = 7) { + const [rows] = await pool.query( + `SELECT * FROM stats + WHERE recorded_at > DATE_SUB(NOW(), INTERVAL ? DAY) + ORDER BY recorded_at DESC`, + [days] + ); + return rows; +} + +// ============================================ +// MQTT +// ============================================ + +async function recordMqttEvent(topic, message) { + const [result] = await pool.query( + 'INSERT INTO mqtt_events (topic, message) VALUES (?, ?)', + [topic, message] + ); + return { id: result.insertId }; +} + +// ============================================ +// FERMETURE DU POOL +// ============================================ + +async function closeDatabase() { + await pool.end(); + console.log('🔌 Connexions à la base fermées'); +} + +module.exports = { + initDatabase, + closeDatabase, + createUser, + getUserByEmail, + getUserById, + getAllUsers, + updateUser, + deleteUser, + createSpot, + getAllSpots, + getSpotById, + updateSpotStatus, + deleteAllSpots, + createReservation, + getReservationsByUser, + getAllReservations, + updateReservationStatus, + addHistory, + getHistory, + recordStats, + getStats, + recordMqttEvent +}; \ No newline at end of file diff --git a/server/middleware/auth.js b/server/middleware/auth.js new file mode 100644 index 0000000..5a77ee7 --- /dev/null +++ b/server/middleware/auth.js @@ -0,0 +1,53 @@ +const jwt = require('jsonwebtoken'); +const JWT_SECRET = process.env.JWT_SECRET || 'smart-parking-secret-key-bts-ciel-2025'; + +function generateToken(user) { + return jwt.sign( + { + id: user.id, + email: user.email, + role: user.role + }, + JWT_SECRET, + { expiresIn: '24h' } + ); +} + +function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ + success: false, + message: 'Token manquant' + }); + } + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({ + success: false, + message: 'Token invalide' + }); + } + req.user = user; + next(); + }); +} + +function requireAdmin(req, res, next) { + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + message: 'Accès réservé aux administrateurs' + }); + } + next(); +} + +module.exports = { + generateToken, + authenticateToken, + requireAdmin +}; \ No newline at end of file diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..3c164bf --- /dev/null +++ b/server/package.json @@ -0,0 +1,24 @@ +{ + "name": "smart-parking-server", + "version": "1.0.0", + "description": "Backend Smart Parking avec MariaDB et Docker", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "init-db": "node db/init.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "body-parser": "^1.20.2", + "mysql2": "^3.6.0", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "dotenv": "^16.3.1", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} \ No newline at end of file diff --git a/server/routes/api.js b/server/routes/api.js new file mode 100644 index 0000000..127dd32 --- /dev/null +++ b/server/routes/api.js @@ -0,0 +1,483 @@ +/** + * ============================================ + * API ROUTES - Routes de l'API REST + * Smart Parking - BTS CIEL IR + * ============================================ + */ + +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcryptjs'); +const { v4: uuidv4 } = require('uuid'); +const db = require('../db/database'); +const { generateToken, authenticateToken, requireAdmin } = require('../middleware/auth'); + +// ============================================ +// AUTHENTIFICATION +// ============================================ + +/** + * POST /api/register + * Inscription d'un nouvel utilisateur + */ +router.post('/register', async (req, res) => { + try { + const { name, email, phone, password } = req.body; + + if (!name || !email || !password) { + return res.status(400).json({ + success: false, + message: 'Tous les champs sont requis' + }); + } + + // Vérifier si l'email existe déjà + const existingUser = await db.getUserByEmail(email); + if (existingUser) { + return res.status(409).json({ + success: false, + message: 'Cet email est déjà utilisé' + }); + } + + // Hasher le mot de passe + const hashedPassword = await bcrypt.hash(password, 10); + + // Créer l'utilisateur + const result = await db.createUser(name, email, phone, hashedPassword, 'client'); + + // Générer le token + const user = await db.getUserById(result.id); + const token = generateToken(user); + + res.status(201).json({ + success: true, + message: 'Compte créé avec succès', + token, + user: { + id: user.id, + name: user.name, + email: user.email, + phone: user.phone, + role: user.role + } + }); + } catch (err) { + console.error('❌ Erreur register:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +/** + * POST /api/login + * Connexion + */ +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + success: false, + message: 'Email et mot de passe requis' + }); + } + + // Récupérer l'utilisateur + const user = await db.getUserByEmail(email); + if (!user) { + return res.status(401).json({ + success: false, + message: 'Email ou mot de passe incorrect' + }); + } + + // Vérifier le mot de passe + const validPassword = await bcrypt.compare(password, user.password); + if (!validPassword) { + return res.status(401).json({ + success: false, + message: 'Email ou mot de passe incorrect' + }); + } + + // Générer le token + const token = generateToken(user); + + res.json({ + success: true, + message: 'Connexion réussie', + token, + user: { + id: user.id, + name: user.name, + email: user.email, + phone: user.phone, + role: user.role + } + }); + } catch (err) { + console.error('❌ Erreur login:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +// ============================================ +// UTILISATEURS +// ============================================ + +/** + * GET /api/users + * Liste tous les utilisateurs (admin uniquement) + */ +router.get('/users', authenticateToken, requireAdmin, async (req, res) => { + try { + const users = await db.getAllUsers(); + res.json({ + success: true, + count: users.length, + data: users + }); + } catch (err) { + console.error('❌ Erreur get users:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +/** + * DELETE /api/users/:id + * Supprime un utilisateur (admin uniquement) + */ +router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) => { + try { + await db.deleteUser(req.params.id); + res.json({ + success: true, + message: 'Utilisateur supprimé' + }); + } catch (err) { + console.error('❌ Erreur delete user:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +// ============================================ +// PLACES +// ============================================ + +/** + * GET /api/spots + * Liste toutes les places + */ +router.get('/spots', authenticateToken, async (req, res) => { + try { + const spots = await db.getAllSpots(); + res.json({ + success: true, + count: spots.length, + data: spots + }); + } catch (err) { + console.error('❌ Erreur get spots:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +/** + * PUT /api/spots/:id/status + * Met à jour le statut d'une place + */ +router.put('/spots/:id/status', authenticateToken, async (req, res) => { + try { + const { status } = req.body; + const validStatuses = ['free', 'occupied', 'reserved']; + + if (!status || !validStatuses.includes(status)) { + return res.status(400).json({ + success: false, + message: 'Statut invalide' + }); + } + + await db.updateSpotStatus(req.params.id, status); + + // Ajouter à l'historique + await db.addHistory('Mise à jour place', `Place ${req.params.id} - ${status}`, req.user.id); + + res.json({ + success: true, + message: 'Statut mis à jour' + }); + } catch (err) { + console.error('❌ Erreur update spot:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +/** + * POST /api/spots/init + * Réinitialise les places (admin uniquement) + */ +router.post('/spots/init', authenticateToken, requireAdmin, async (req, res) => { + try { + const { count } = req.body; + const spotCount = count || 10; + + // Supprimer les places existantes + await db.deleteAllSpots(); + + // Créer les nouvelles places + for (let i = 1; i <= spotCount; i++) { + let status = 'free'; + const rand = Math.random(); + if (rand > 0.85) status = 'reserved'; + else if (rand > 0.60) status = 'occupied'; + + await db.createSpot(i, `SENSOR_${String(i).padStart(3, '0')}`, status); + } + + await db.addHistory('Réinitialisation places', `${spotCount} places créées`, req.user.id); + + res.json({ + success: true, + message: `${spotCount} places créées` + }); + } catch (err) { + console.error('❌ Erreur init spots:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +// ============================================ +// RÉSERVATIONS +// ============================================ + +/** + * GET /api/reservations + * Liste les réservations de l'utilisateur connecté + */ +router.get('/reservations', authenticateToken, async (req, res) => { + try { + const reservations = await db.getReservationsByUser(req.user.id); + res.json({ + success: true, + count: reservations.length, + data: reservations + }); + } catch (err) { + console.error('❌ Erreur get reservations:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +/** + * GET /api/reservations/all + * Liste toutes les réservations (admin uniquement) + */ +router.get('/reservations/all', authenticateToken, requireAdmin, async (req, res) => { + try { + const reservations = await db.getAllReservations(); + res.json({ + success: true, + count: reservations.length, + data: reservations + }); + } catch (err) { + console.error('❌ Erreur get all reservations:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +/** + * POST /api/reservations + * Crée une nouvelle réservation + */ +router.post('/reservations', authenticateToken, async (req, res) => { + try { + const { spotId, date, startTime, endTime, duration, vehicle, price } = req.body; + + if (!spotId || !date || !startTime || !endTime || !duration || !price) { + return res.status(400).json({ + success: false, + message: 'Tous les champs sont requis' + }); + } + + // Vérifier que la place est libre + const spot = await db.getSpotById(spotId); + if (!spot || spot.status !== 'free') { + return res.status(409).json({ + success: false, + message: 'Cette place n\'est plus disponible' + }); + } + + // Générer un code de paiement + const paymentCode = 'PARK' + Date.now().toString().slice(-8); + + // Créer la réservation + const result = await db.createReservation( + req.user.id, + spotId, + date, + startTime, + endTime, + duration, + vehicle, + price, + paymentCode + ); + + // Mettre à jour le statut de la place + await db.updateSpotStatus(spotId, 'reserved'); + + // Ajouter à l'historique + await db.addHistory('Nouvelle réservation', `Place ${spot.number} - ${price}€`, req.user.id); + + res.status(201).json({ + success: true, + message: 'Réservation créée', + data: { + id: result.id, + paymentCode + } + }); + } catch (err) { + console.error('❌ Erreur create reservation:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +/** + * PUT /api/reservations/:id/cancel + * Annule une réservation + */ +router.put('/reservations/:id/cancel', authenticateToken, async (req, res) => { + try { + await db.updateReservationStatus(req.params.id, 'cancelled'); + + // TODO: Libérer la place associée + + res.json({ + success: true, + message: 'Réservation annulée' + }); + } catch (err) { + console.error('❌ Erreur cancel reservation:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +// ============================================ +// STATISTIQUES +// ============================================ + +/** + * GET /api/stats + * Récupère les statistiques + */ +router.get('/stats', authenticateToken, async (req, res) => { + try { + const spots = await db.getAllSpots(); + const total = spots.length; + const free = spots.filter(s => s.status === 'free').length; + const occupied = spots.filter(s => s.status === 'occupied').length; + const reserved = spots.filter(s => s.status === 'reserved').length; + const occupancyRate = total > 0 ? Math.round(((occupied + reserved) / total) * 100) : 0; + + res.json({ + success: true, + data: { + total, + free, + occupied, + reserved, + occupancyRate + } + }); + } catch (err) { + console.error('❌ Erreur get stats:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +// ============================================ +// HISTORIQUE +// ============================================ + +/** + * GET /api/history + * Récupère l'historique + */ +router.get('/history', authenticateToken, requireAdmin, async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 50; + const history = await db.getHistory(limit); + res.json({ + success: true, + count: history.length, + data: history + }); + } catch (err) { + console.error('❌ Erreur get history:', err.message); + res.status(500).json({ + success: false, + message: 'Erreur serveur' + }); + } +}); + +// ============================================ +// STATUS +// ============================================ + +/** + * GET /api/status + * Vérifie le statut du serveur + */ +router.get('/status', (req, res) => { + res.json({ + success: true, + message: 'Smart Parking API opérationnelle', + version: '1.0.0', + timestamp: new Date().toISOString() + }); +}); + +module.exports = router; diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..45d8d9d --- /dev/null +++ b/server/server.js @@ -0,0 +1,70 @@ +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const path = require('path'); +require('dotenv').config(); + +const db = require('./db/database'); +const apiRoutes = require('./routes/api'); + +const PORT = process.env.PORT || 3000; + +const app = express(); + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.use(express.static(path.join(__dirname, '..'))); + +app.use('/api', apiRoutes); + +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '..', 'index.html')); +}); + +app.get('/dashboard', (req, res) => { + res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html')); +}); + +async function startServer() { + try { + await db.initDatabase(); + app.listen(PORT, () => { + console.log(` +╔══════════════════════════════════════════════════╗ +║ 🅿️ SMART PARKING SERVER - PRÊT POUR DOCKER ║ +╠══════════════════════════════════════════════════╣ +║ 🌐 Port : ${PORT} +║ 🗄️ Base : MariaDB (${process.env.DB_HOST}) +║ 🔐 JWT sécurisé +╚══════════════════════════════════════════════════╝ + `); + }); + + setInterval(async () => { + try { + const spots = await db.getAllSpots(); + const total = spots.length; + const free = spots.filter(s => s.status === 'free').length; + const occupied = spots.filter(s => s.status === 'occupied').length; + const reserved = spots.filter(s => s.status === 'reserved').length; + await db.recordStats(total, free, occupied, reserved); + } catch (err) { + console.error('❌ Erreur stats:', err.message); + } + }, 5 * 60 * 1000); + + } catch (err) { + console.error('❌ Erreur au démarrage :', err); + process.exit(1); + } +} + +process.on('SIGINT', async () => { + console.log('\n🛑 Arrêt du serveur...'); + await db.closeDatabase(); + process.exit(0); +}); + +startServer(); \ No newline at end of file