first commit
This commit is contained in:
15
.env
Normal file
15
.env
Normal file
@@ -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
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -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"]
|
||||||
208
README.md
Normal file
208
README.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
🅿️ <strong>Smart Parking - BTS CIEL IR 2025</strong> 🅿️
|
||||||
|
</p>
|
||||||
165
css/auth.css
Normal file
165
css/auth.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
css/dashboard.css
Normal file
108
css/dashboard.css
Normal file
@@ -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); }
|
||||||
|
}
|
||||||
1124
css/style.css
Normal file
1124
css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
47
docker-compose.yml
Normal file
47
docker-compose.yml
Normal file
@@ -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:
|
||||||
80
index.html
Normal file
80
index.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Smart Parking - Connexion</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="css/auth.css">
|
||||||
|
</head>
|
||||||
|
<body class="auth-page">
|
||||||
|
<div class="auth-container">
|
||||||
|
<div class="auth-box">
|
||||||
|
<div class="auth-logo">
|
||||||
|
<span class="logo-icon">🅿️</span>
|
||||||
|
<h1>Smart Parking</h1>
|
||||||
|
<p>Gestion intelligente de parking</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire de connexion -->
|
||||||
|
<form id="loginForm" class="auth-form">
|
||||||
|
<h2>Connexion</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="loginEmail">Email</label>
|
||||||
|
<input type="email" id="loginEmail" class="form-control" placeholder="votre@email.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="loginPassword">Mot de passe</label>
|
||||||
|
<input type="password" id="loginPassword" class="form-control" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
|
<span class="btn-icon">🔑</span>
|
||||||
|
Se connecter
|
||||||
|
</button>
|
||||||
|
<p class="auth-switch">
|
||||||
|
Pas encore de compte ?
|
||||||
|
<a href="#" id="showRegister">Créer un compte</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Formulaire d'inscription -->
|
||||||
|
<form id="registerForm" class="auth-form hidden">
|
||||||
|
<h2>Créer un compte</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registerName">Nom complet</label>
|
||||||
|
<input type="text" id="registerName" class="form-control" placeholder="Jean Dupont" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registerEmail">Email</label>
|
||||||
|
<input type="email" id="registerEmail" class="form-control" placeholder="votre@email.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registerPhone">Téléphone</label>
|
||||||
|
<input type="tel" id="registerPhone" class="form-control" placeholder="06 12 34 56 78" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registerPassword">Mot de passe</label>
|
||||||
|
<input type="password" id="registerPassword" class="form-control" placeholder="8 caractères minimum" minlength="8" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registerPasswordConfirm">Confirmer le mot de passe</label>
|
||||||
|
<input type="password" id="registerPasswordConfirm" class="form-control" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
|
<span class="btn-icon">✨</span>
|
||||||
|
Créer mon compte
|
||||||
|
</button>
|
||||||
|
<p class="auth-switch">
|
||||||
|
Déjà un compte ?
|
||||||
|
<a href="#" id="showLogin">Se connecter</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Message d'erreur -->
|
||||||
|
<div id="authMessage" class="auth-message hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
init.sql
Normal file
64
init.sql
Normal file
@@ -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
|
||||||
|
);
|
||||||
436
js/admin.js
Normal file
436
js/admin.js
Normal file
@@ -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 => `
|
||||||
|
<div
|
||||||
|
class="admin-place-item ${spot.status}"
|
||||||
|
onclick="toggleSpotStatus(${spot.id})"
|
||||||
|
title="Place ${spot.number} - Cliquez pour changer"
|
||||||
|
>
|
||||||
|
${spot.number}
|
||||||
|
</div>
|
||||||
|
`).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 => `
|
||||||
|
<tr>
|
||||||
|
<td>#${user.id}</td>
|
||||||
|
<td>${user.name}</td>
|
||||||
|
<td>${user.email}</td>
|
||||||
|
<td>${user.phone || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge ${user.role === 'admin' ? 'badge-admin' : 'badge-client'}">
|
||||||
|
${user.role === 'admin' ? 'Admin' : 'Client'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
|
${user.role !== 'admin' ? `
|
||||||
|
<button class="btn btn-danger btn-small btn-icon-only" onclick="deleteUser(${user.id})" title="Supprimer">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
` : '-'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).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 = '<tr><td colspan="8" style="text-align: center; color: var(--text-muted);">Aucune réservation</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = reservations.slice().reverse().map(res => `
|
||||||
|
<tr>
|
||||||
|
<td>#${res.id}</td>
|
||||||
|
<td>${res.userName}</td>
|
||||||
|
<td>Place ${res.spotNumber}</td>
|
||||||
|
<td>${formatDate(res.date)}</td>
|
||||||
|
<td>${res.startTime} - ${res.endTime}</td>
|
||||||
|
<td>${res.price}€</td>
|
||||||
|
<td>
|
||||||
|
<span class="reservation-status status-${res.status}">
|
||||||
|
${getStatusLabel(res.status)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
|
${res.status === 'active' ? `
|
||||||
|
<button class="btn btn-success btn-small btn-icon-only" onclick="completeReservation(${res.id})" title="Terminer">
|
||||||
|
✓
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-small btn-icon-only" onclick="adminCancelReservation(${res.id})" title="Annuler">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
` : '-'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).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 = '<p style="color: var(--text-muted); text-align: center;">Aucun historique</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = history.slice(0, 20).map(item => `
|
||||||
|
<div class="log-item">
|
||||||
|
<span class="log-time">${formatTime(item.timestamp)}</span>
|
||||||
|
<span><strong>${item.action}:</strong> ${item.details}</span>
|
||||||
|
</div>
|
||||||
|
`).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
|
||||||
|
};
|
||||||
327
js/auth.js
Normal file
327
js/auth.js
Normal file
@@ -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';
|
||||||
|
}
|
||||||
|
};
|
||||||
300
js/dashboard.js
Normal file
300
js/dashboard.js
Normal file
@@ -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 => `
|
||||||
|
<div class="reservation-card">
|
||||||
|
<div class="reservation-info">
|
||||||
|
<h4>Place ${res.spotNumber}</h4>
|
||||||
|
<div class="reservation-details">
|
||||||
|
<span>📅 ${res.date}</span>
|
||||||
|
<span>🕐 ${res.startTime}</span>
|
||||||
|
<span>⏱️ ${res.duration} min</span>
|
||||||
|
<span>🚗 ${res.vehicle || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reservation-actions">
|
||||||
|
<span class="reservation-price">${res.price}€</span>
|
||||||
|
<span class="reservation-status status-${res.status}">${getStatusLabel(res.status)}</span>
|
||||||
|
${res.status === 'active' ? `
|
||||||
|
<button class="btn btn-danger btn-small" onclick="cancelReservation(${res.id})">
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).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
|
||||||
|
};
|
||||||
367
js/map.js
Normal file
367
js/map.js
Normal file
@@ -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 => `
|
||||||
|
<div
|
||||||
|
class="parking-spot ${spot.status}"
|
||||||
|
data-id="${spot.id}"
|
||||||
|
onclick="handleSpotClick(${spot.id})"
|
||||||
|
title="Place ${spot.number} - ${getStatusLabel(spot.status)}"
|
||||||
|
>
|
||||||
|
<span class="spot-number">${spot.number}</span>
|
||||||
|
<span class="spot-icon">${getStatusIcon(spot.status)}</span>
|
||||||
|
</div>
|
||||||
|
`).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 = `
|
||||||
|
<div class="spot-info-detail">
|
||||||
|
<div class="spot-info-row">
|
||||||
|
<span class="spot-info-label">Numéro</span>
|
||||||
|
<span class="spot-info-value">Place ${spot.number}</span>
|
||||||
|
</div>
|
||||||
|
<div class="spot-info-row">
|
||||||
|
<span class="spot-info-label">État</span>
|
||||||
|
<span class="spot-info-value spot-status-${spot.status}">
|
||||||
|
${getStatusLabel(spot.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="spot-info-row">
|
||||||
|
<span class="spot-info-label">Capteur</span>
|
||||||
|
<span class="spot-info-value">${spot.sensorId}</span>
|
||||||
|
</div>
|
||||||
|
<div class="spot-info-row">
|
||||||
|
<span class="spot-info-label">Dernière mise à jour</span>
|
||||||
|
<span class="spot-info-value">${formatDate(spot.lastUpdate)}</span>
|
||||||
|
</div>
|
||||||
|
${reservation ? `
|
||||||
|
<div class="spot-info-row">
|
||||||
|
<span class="spot-info-label">Réservé par</span>
|
||||||
|
<span class="spot-info-value">${reservation.userName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="spot-info-row">
|
||||||
|
<span class="spot-info-label">Jusqu'à</span>
|
||||||
|
<span class="spot-info-value">${reservation.endTime}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${spot.status === SPOT_STATUS.FREE ? `
|
||||||
|
<button class="btn btn-primary btn-block" onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});">
|
||||||
|
<span class="btn-icon">📅</span>
|
||||||
|
Réserver cette place
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
})
|
||||||
|
};
|
||||||
351
js/reservation.js
Normal file
351
js/reservation.js
Normal file
@@ -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 = `
|
||||||
|
<div style="width: 200px; height: 200px; background: white; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
|
||||||
|
<div style="font-size: 80px;">📱</div>
|
||||||
|
<div style="color: #333; font-size: 12px; margin-top: 10px;">QR Code de paiement</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
};
|
||||||
474
pages/dashboard.html
Normal file
474
pages/dashboard.html
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Smart Parking - Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="../css/style.css">
|
||||||
|
<link rel="stylesheet" href="../css/dashboard.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🅿️</span>
|
||||||
|
<h1>Smart Parking</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="#map" class="nav-link active" data-page="map">
|
||||||
|
<span class="nav-icon">🗺️</span>
|
||||||
|
Carte
|
||||||
|
</a>
|
||||||
|
<a href="#reservation" class="nav-link" data-page="reservation">
|
||||||
|
<span class="nav-icon">📅</span>
|
||||||
|
Réservation
|
||||||
|
</a>
|
||||||
|
<a href="#my-reservations" class="nav-link" data-page="my-reservations">
|
||||||
|
<span class="nav-icon">🎫</span>
|
||||||
|
Mes réservations
|
||||||
|
</a>
|
||||||
|
<a href="#profile" class="nav-link" data-page="profile">
|
||||||
|
<span class="nav-icon">👤</span>
|
||||||
|
Profil
|
||||||
|
</a>
|
||||||
|
<!-- Lien admin (visible uniquement pour admin) -->
|
||||||
|
<a href="#admin" class="nav-link admin-only hidden" data-page="admin">
|
||||||
|
<span class="nav-icon">⚙️</span>
|
||||||
|
Admin
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="user-info">
|
||||||
|
<span id="userName">Utilisateur</span>
|
||||||
|
<span id="userRole" class="user-role">Client</span>
|
||||||
|
</div>
|
||||||
|
<button id="logoutBtn" class="btn btn-secondary btn-small">
|
||||||
|
<span class="btn-icon">🚪</span>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main">
|
||||||
|
<!-- PAGE: CARTE DES PLACES -->
|
||||||
|
<section id="map" class="page active">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="page-title">
|
||||||
|
<span class="icon">🗺️</span>
|
||||||
|
Carte du Parking
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Statistiques des places -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card free">
|
||||||
|
<div class="stat-icon">✅</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<span class="stat-value" id="freeCount">0</span>
|
||||||
|
<span class="stat-label">Places libres</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card occupied">
|
||||||
|
<div class="stat-icon">🚗</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<span class="stat-value" id="occupiedCount">0</span>
|
||||||
|
<span class="stat-label">Places occupées</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card reserved">
|
||||||
|
<div class="stat-icon">📅</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<span class="stat-value" id="reservedCount">0</span>
|
||||||
|
<span class="stat-label">Places réservées</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card total">
|
||||||
|
<div class="stat-icon">🅿️</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<span class="stat-value" id="totalCount">10</span>
|
||||||
|
<span class="stat-label">Total places</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte des places -->
|
||||||
|
<div class="parking-section">
|
||||||
|
<div class="parking-map-container">
|
||||||
|
<h3>Vue du parking</h3>
|
||||||
|
<div class="parking-map" id="parkingMap">
|
||||||
|
<!-- Les places seront générées par JS -->
|
||||||
|
</div>
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-color free"></span>
|
||||||
|
<span>Libre</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-color occupied"></span>
|
||||||
|
<span>Occupée</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-color reserved"></span>
|
||||||
|
<span>Réservée</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Détails de la place sélectionnée -->
|
||||||
|
<div class="spot-details-container">
|
||||||
|
<h3>Détails de la place</h3>
|
||||||
|
<div id="spotDetails" class="spot-details">
|
||||||
|
<p class="no-selection">Cliquez sur une place pour voir les détails</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tarifs -->
|
||||||
|
<div class="pricing-section">
|
||||||
|
<h3>💰 Nos tarifs</h3>
|
||||||
|
<div class="pricing-cards">
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h4>30 minutes</h4>
|
||||||
|
<span class="price">2€</span>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h4>1 heure</h4>
|
||||||
|
<span class="price">3€</span>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h4>2 heures</h4>
|
||||||
|
<span class="price">5€</span>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h4>4 heures</h4>
|
||||||
|
<span class="price">8€</span>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h4>Journée</h4>
|
||||||
|
<span class="price">15€</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PAGE: RÉSERVATION -->
|
||||||
|
<section id="reservation" class="page hidden">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="page-title">
|
||||||
|
<span class="icon">📅</span>
|
||||||
|
Réserver une place
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="reservation-form-container">
|
||||||
|
<form id="reservationForm" class="reservation-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resSpot">Place à réserver</label>
|
||||||
|
<select id="resSpot" class="form-control" required>
|
||||||
|
<option value="">Choisir une place</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resDate">Date</label>
|
||||||
|
<input type="date" id="resDate" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resStartTime">Heure d'arrivée</label>
|
||||||
|
<select id="resStartTime" class="form-control" required>
|
||||||
|
<option value="">Choisir</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resDuration">Durée</label>
|
||||||
|
<select id="resDuration" class="form-control" required>
|
||||||
|
<option value="30">30 min - 2€</option>
|
||||||
|
<option value="60">1h - 3€</option>
|
||||||
|
<option value="120" selected>2h - 5€</option>
|
||||||
|
<option value="240">4h - 8€</option>
|
||||||
|
<option value="480">Journée (8h) - 15€</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resVehicle">Plaque d'immatriculation</label>
|
||||||
|
<input type="text" id="resVehicle" class="form-control" placeholder="AB-123-CD" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="price-preview">
|
||||||
|
<span>Prix total:</span>
|
||||||
|
<span id="previewPrice" class="price-amount">5€</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
|
<span class="btn-icon">💳</span>
|
||||||
|
Procéder au paiement
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de paiement QR Code -->
|
||||||
|
<div id="paymentModal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>💳 Paiement</h3>
|
||||||
|
<button class="modal-close" id="closePaymentModal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="payment-summary">
|
||||||
|
<h4>Récapitulatif</h4>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>Place:</span>
|
||||||
|
<span id="paySpot">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>Date:</span>
|
||||||
|
<span id="payDate">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>Heure:</span>
|
||||||
|
<span id="payTime">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>Durée:</span>
|
||||||
|
<span id="payDuration">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row total">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span id="payTotal">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-section">
|
||||||
|
<p>Scannez ce QR code pour payer</p>
|
||||||
|
<div id="qrcode"></div>
|
||||||
|
<p class="qr-info">Ou utilisez le code: <strong id="paymentCode">-</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="payment-actions">
|
||||||
|
<button id="confirmPaymentBtn" class="btn btn-success btn-block">
|
||||||
|
<span class="btn-icon">✅</span>
|
||||||
|
J'ai payé
|
||||||
|
</button>
|
||||||
|
<button id="cancelPaymentBtn" class="btn btn-secondary btn-block">
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PAGE: MES RÉSERVATIONS -->
|
||||||
|
<section id="my-reservations" class="page hidden">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="page-title">
|
||||||
|
<span class="icon">🎫</span>
|
||||||
|
Mes réservations
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div id="myReservationsList" class="reservations-list">
|
||||||
|
<!-- Généré par JS -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="noReservations" class="empty-state">
|
||||||
|
<span class="empty-icon">📭</span>
|
||||||
|
<p>Vous n'avez aucune réservation active</p>
|
||||||
|
<a href="#reservation" class="btn btn-primary">Faire une réservation</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PAGE: PROFIL -->
|
||||||
|
<section id="profile" class="page hidden">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="page-title">
|
||||||
|
<span class="icon">👤</span>
|
||||||
|
Mon profil
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="profile-container">
|
||||||
|
<div class="profile-card">
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="profile-avatar">
|
||||||
|
<span id="profileAvatar">👤</span>
|
||||||
|
</div>
|
||||||
|
<h3 id="profileName">-</h3>
|
||||||
|
<span id="profileRole" class="role-badge">Client</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="profileForm" class="profile-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nom complet</label>
|
||||||
|
<input type="text" id="profileNameInput" class="form-control" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="email" id="profileEmailInput" class="form-control" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Téléphone</label>
|
||||||
|
<input type="tel" id="profilePhoneInput" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nouveau mot de passe</label>
|
||||||
|
<input type="password" id="profileNewPassword" class="form-control" placeholder="Laisser vide pour ne pas changer">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="btn-icon">💾</span>
|
||||||
|
Mettre à jour
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-stats">
|
||||||
|
<h3>Statistiques</h3>
|
||||||
|
<div class="stats-cards">
|
||||||
|
<div class="stat-box">
|
||||||
|
<span class="stat-box-value" id="totalReservations">0</span>
|
||||||
|
<span class="stat-box-label">Réservations totales</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<span class="stat-box-value" id="activeReservations">0</span>
|
||||||
|
<span class="stat-box-label">Réservations actives</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<span class="stat-box-value" id="totalSpent">0€</span>
|
||||||
|
<span class="stat-box-label">Total dépensé</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PAGE: ADMIN (visible uniquement pour admin) -->
|
||||||
|
<section id="admin" class="page hidden admin-page">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="page-title">
|
||||||
|
<span class="icon">⚙️</span>
|
||||||
|
Administration
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Stats admin -->
|
||||||
|
<div class="admin-stats-grid">
|
||||||
|
<div class="admin-stat">
|
||||||
|
<span class="admin-stat-value" id="adminTotalUsers">0</span>
|
||||||
|
<span class="admin-stat-label">Utilisateurs</span>
|
||||||
|
</div>
|
||||||
|
<div class="admin-stat">
|
||||||
|
<span class="admin-stat-value" id="adminTotalReservations">0</span>
|
||||||
|
<span class="admin-stat-label">Réservations</span>
|
||||||
|
</div>
|
||||||
|
<div class="admin-stat">
|
||||||
|
<span class="admin-stat-value" id="adminTotalRevenue">0€</span>
|
||||||
|
<span class="admin-stat-label">Revenus</span>
|
||||||
|
</div>
|
||||||
|
<div class="admin-stat">
|
||||||
|
<span class="admin-stat-value" id="adminOccupancyRate">0%</span>
|
||||||
|
<span class="admin-stat-label">Taux occupation</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gestion des places -->
|
||||||
|
<div class="admin-section">
|
||||||
|
<h3>🅿️ Gestion des places</h3>
|
||||||
|
<div class="admin-places-control">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nombre total de places</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" id="adminTotalSpots" class="form-control" min="5" max="50" value="10">
|
||||||
|
<button id="updateSpotsBtn" class="btn btn-primary">Mettre à jour</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="admin-places-list" id="adminPlacesList">
|
||||||
|
<!-- Généré par JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gestion des utilisateurs -->
|
||||||
|
<div class="admin-section">
|
||||||
|
<h3>👥 Utilisateurs</h3>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Téléphone</th>
|
||||||
|
<th>Rôle</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="adminUsersTable">
|
||||||
|
<!-- Généré par JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toutes les réservations -->
|
||||||
|
<div class="admin-section">
|
||||||
|
<h3>📅 Toutes les réservations</h3>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Client</th>
|
||||||
|
<th>Place</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Horaire</th>
|
||||||
|
<th>Prix</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="adminReservationsTable">
|
||||||
|
<!-- Généré par JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Graphique d'occupation -->
|
||||||
|
<div class="admin-section">
|
||||||
|
<h3>📊 Statistiques d'occupation</h3>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="adminOccupancyChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Historique -->
|
||||||
|
<div class="admin-section">
|
||||||
|
<h3>📜 Historique</h3>
|
||||||
|
<div class="log-container" id="adminLogContainer">
|
||||||
|
<!-- Généré par JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast notifications -->
|
||||||
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
|
|
||||||
|
<script src="../js/dashboard.js"></script>
|
||||||
|
<script src="../js/map.js"></script>
|
||||||
|
<script src="../js/reservation.js"></script>
|
||||||
|
<script src="../js/admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
340
server/db/database.js
Normal file
340
server/db/database.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
53
server/middleware/auth.js
Normal file
53
server/middleware/auth.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
24
server/package.json
Normal file
24
server/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
483
server/routes/api.js
Normal file
483
server/routes/api.js
Normal file
@@ -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;
|
||||||
70
server/server.js
Normal file
70
server/server.js
Normal file
@@ -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();
|
||||||
Reference in New Issue
Block a user