first commit

This commit is contained in:
2026-03-11 17:18:34 +01:00
commit c6954c1f50
20 changed files with 5049 additions and 0 deletions

15
.env Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

47
docker-compose.yml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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">&times;</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
View 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
View 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
View 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
View 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
View 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();