Reservation de salle
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Node
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
# 🔧 Guide de Dépannage - Expernet
|
||||||
|
|
||||||
|
## Problème : Les onglets "Réserver une salle" et "Mes réservations" ne fonctionnent pas
|
||||||
|
|
||||||
|
### Solution 1 : Vider le cache du navigateur (Recommandé)
|
||||||
|
|
||||||
|
1. **Ouvrez votre navigateur** sur http://localhost:3000
|
||||||
|
2. **Appuyez sur** `Ctrl + Shift + R` (Windows/Linux) ou `Cmd + Shift + R` (Mac)
|
||||||
|
- Cela force le rechargement de la page sans cache
|
||||||
|
3. **Ou utilisez le mode navigation privée** :
|
||||||
|
- Chrome/Edge : `Ctrl + Shift + N`
|
||||||
|
- Firefox : `Ctrl + Shift + P`
|
||||||
|
|
||||||
|
### Solution 2 : Vider complètement le cache
|
||||||
|
|
||||||
|
#### Chrome / Edge
|
||||||
|
1. `Ctrl + Shift + Delete`
|
||||||
|
2. Sélectionnez "Images et fichiers en cache"
|
||||||
|
3. Cliquez sur "Effacer les données"
|
||||||
|
4. Rechargez http://localhost:3000
|
||||||
|
|
||||||
|
#### Firefox
|
||||||
|
1. `Ctrl + Shift + Delete`
|
||||||
|
2. Sélectionnez "Cache"
|
||||||
|
3. Cliquez sur "Effacer maintenant"
|
||||||
|
4. Rechargez http://localhost:3000
|
||||||
|
|
||||||
|
### Solution 3 : Reconstruire complètement le frontend
|
||||||
|
|
||||||
|
Si le problème persiste :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arrêter tous les services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Reconstruire uniquement le frontend
|
||||||
|
docker-compose up -d --build frontend
|
||||||
|
|
||||||
|
# Ou reconstruire tout
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution 4 : Vérifier la console du navigateur
|
||||||
|
|
||||||
|
1. Appuyez sur `F12` pour ouvrir les outils de développement
|
||||||
|
2. Allez dans l'onglet **Console**
|
||||||
|
3. Recherchez des erreurs en rouge
|
||||||
|
4. Si vous voyez des erreurs, notez-les et contactez le support
|
||||||
|
|
||||||
|
### Solution 5 : Vérifier que les services fonctionnent
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier l'état des conteneurs
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Tous doivent afficher "Up" dans la colonne STATUS
|
||||||
|
|
||||||
|
# Vérifier les logs du frontend
|
||||||
|
docker-compose logs frontend --tail=20
|
||||||
|
|
||||||
|
# Vérifier les logs du backend
|
||||||
|
docker-compose logs backend --tail=20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vérification Rapide
|
||||||
|
|
||||||
|
### Test 1 : API Backend
|
||||||
|
Ouvrez dans votre navigateur : http://localhost:8000/rooms
|
||||||
|
- ✅ Vous devez voir un JSON avec la liste des salles
|
||||||
|
|
||||||
|
### Test 2 : Frontend
|
||||||
|
Ouvrez dans votre navigateur : http://localhost:3000
|
||||||
|
- ✅ Vous devez voir la page avec 3 onglets
|
||||||
|
|
||||||
|
### Test 3 : Planning
|
||||||
|
Sur http://localhost:3000 :
|
||||||
|
- ✅ L'onglet "Planning des salles" doit être actif par défaut
|
||||||
|
- ✅ Vous devez voir une grille avec les 5 salles
|
||||||
|
|
||||||
|
## Problèmes Courants
|
||||||
|
|
||||||
|
### Les onglets sont visibles mais ne répondent pas aux clics
|
||||||
|
➡️ **Solution** : Videz le cache (`Ctrl + Shift + R`)
|
||||||
|
|
||||||
|
### La page est blanche
|
||||||
|
➡️ **Solution** :
|
||||||
|
```bash
|
||||||
|
docker-compose restart frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur "Cannot connect to server"
|
||||||
|
➡️ **Solution** :
|
||||||
|
```bash
|
||||||
|
# Vérifier que tous les services sont up
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Redémarrer si nécessaire
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Les données ne se chargent pas
|
||||||
|
➡️ **Solution** :
|
||||||
|
1. Vérifiez que le backend fonctionne : http://localhost:8000/docs
|
||||||
|
2. Ouvrez F12 > Network pour voir les requêtes
|
||||||
|
3. Vérifiez qu'il n'y a pas d'erreurs CORS
|
||||||
|
|
||||||
|
## Redémarrage Complet (Solution ultime)
|
||||||
|
|
||||||
|
Si rien ne fonctionne :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Tout arrêter et nettoyer
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# 2. Reconstruire tout
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# 3. Attendre 30 secondes que tout démarre
|
||||||
|
|
||||||
|
# 4. Ouvrir en mode privé
|
||||||
|
# Chrome: Ctrl + Shift + N
|
||||||
|
# Puis aller sur http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accès Direct aux Fonctionnalités
|
||||||
|
|
||||||
|
- **Planning** : http://localhost:3000 (onglet par défaut)
|
||||||
|
- **API Salles** : http://localhost:8000/rooms
|
||||||
|
- **API Réservations** : http://localhost:8000/reservations
|
||||||
|
- **Documentation API** : http://localhost:8000/docs
|
||||||
|
|
||||||
|
## Contact Support
|
||||||
|
|
||||||
|
Si le problème persiste après toutes ces étapes :
|
||||||
|
1. Notez le message d'erreur exact
|
||||||
|
2. Faites une capture d'écran de la console (F12)
|
||||||
|
3. Exécutez : `docker-compose logs > logs.txt`
|
||||||
|
4. Envoyez ces informations au support
|
||||||
|
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# 📖 Guide d'Utilisation - Expernet Réservation de Salles
|
||||||
|
|
||||||
|
## 🚀 Démarrage Rapide
|
||||||
|
|
||||||
|
### 1. Lancer l'application
|
||||||
|
|
||||||
|
**Windows :**
|
||||||
|
```bash
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/Mac :**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Accéder à l'interface
|
||||||
|
|
||||||
|
Ouvrez votre navigateur : **http://localhost:3000**
|
||||||
|
|
||||||
|
## 📊 Consulter le Planning
|
||||||
|
|
||||||
|
### Vue d'ensemble
|
||||||
|
|
||||||
|
1. Cliquez sur l'onglet **"📊 Planning des salles"** (onglet par défaut)
|
||||||
|
2. Sélectionnez une date avec le sélecteur de date
|
||||||
|
3. Le planning affiche :
|
||||||
|
- ✅ **Cases vertes** : Créneaux disponibles
|
||||||
|
- ❌ **Cases rouges** : Créneaux réservés (avec le nom de l'utilisateur)
|
||||||
|
4. Passez la souris sur une case réservée pour voir les détails
|
||||||
|
5. En bas du planning, la liste détaillée des réservations du jour
|
||||||
|
|
||||||
|
### Avantages du Planning
|
||||||
|
|
||||||
|
- 🔍 Vision globale de toutes les salles
|
||||||
|
- ⏰ Horaires de 8h à 22h
|
||||||
|
- 📅 Navigation par date
|
||||||
|
- 👤 Identification rapide de qui a réservé
|
||||||
|
|
||||||
|
## 📅 Comment Réserver une Salle
|
||||||
|
|
||||||
|
### Étape 1 : Consulter le planning (recommandé)
|
||||||
|
|
||||||
|
1. Allez dans **"📊 Planning des salles"**
|
||||||
|
2. Vérifiez les disponibilités pour votre date
|
||||||
|
3. Notez les créneaux libres
|
||||||
|
|
||||||
|
### Étape 2 : Sélectionner une salle
|
||||||
|
|
||||||
|
1. Cliquez sur l'onglet **"📅 Réserver une salle"**
|
||||||
|
2. Vous verrez la liste des 5 salles disponibles à gauche
|
||||||
|
3. Chaque salle affiche :
|
||||||
|
- 👥 Sa capacité
|
||||||
|
- 🔧 Ses équipements
|
||||||
|
- 📝 Sa description
|
||||||
|
4. Cliquez sur la salle souhaitée (elle sera surlignée en bleu)
|
||||||
|
|
||||||
|
### Étape 3 : Remplir le formulaire
|
||||||
|
|
||||||
|
1. **Nom** : Votre nom complet *(obligatoire)*
|
||||||
|
2. **Email** : Votre adresse email *(optionnel)*
|
||||||
|
3. **Date** : Sélectionnez la date de réservation *(obligatoire)*
|
||||||
|
4. **Heure de début** : Heure de début souhaitée *(obligatoire)*
|
||||||
|
5. **Heure de fin** : Heure de fin souhaitée *(obligatoire)*
|
||||||
|
6. **Objet** : Décrivez le but de la réservation *(obligatoire)*
|
||||||
|
|
||||||
|
### Étape 4 : Vérification automatique
|
||||||
|
|
||||||
|
⚡ **Nouveau !** Le système vérifie automatiquement la disponibilité :
|
||||||
|
- ✅ **Message vert** : "Ce créneau est disponible !"
|
||||||
|
- ❌ **Message rouge** : "Ce créneau est déjà réservé. Choisissez un autre horaire."
|
||||||
|
|
||||||
|
Le bouton de confirmation est **automatiquement désactivé** si le créneau est occupé !
|
||||||
|
|
||||||
|
### Étape 5 : Confirmer
|
||||||
|
|
||||||
|
Cliquez sur **"✅ Confirmer la réservation"** (uniquement si le créneau est disponible)
|
||||||
|
|
||||||
|
### ⚠️ Gestion des Conflits
|
||||||
|
|
||||||
|
Le système empêche **automatiquement** les doubles réservations :
|
||||||
|
- ❌ Le bouton de réservation est désactivé si le créneau est occupé
|
||||||
|
- 🔒 Impossible de réserver sur un créneau déjà pris
|
||||||
|
- ✅ Vérification en temps réel pendant la saisie
|
||||||
|
|
||||||
|
## 📋 Consulter vos Réservations
|
||||||
|
|
||||||
|
1. Cliquez sur l'onglet **"📋 Mes réservations"**
|
||||||
|
2. Vous verrez toutes les réservations actuelles avec :
|
||||||
|
- 🏛️ Le nom de la salle
|
||||||
|
- 👤 Le nom de l'utilisateur
|
||||||
|
- 📅 La date
|
||||||
|
- ⏰ Les horaires
|
||||||
|
- 📧 L'email de contact
|
||||||
|
- 📝 L'objet de la réservation
|
||||||
|
|
||||||
|
## ❌ Annuler une Réservation
|
||||||
|
|
||||||
|
1. Dans l'onglet **"Mes réservations"**
|
||||||
|
2. Cliquez sur **"❌ Annuler"** à côté de la réservation
|
||||||
|
3. Confirmez l'annulation
|
||||||
|
|
||||||
|
## 🏛️ Les 5 Salles Expernet
|
||||||
|
|
||||||
|
### 🎓 Salle Atlas (30 personnes)
|
||||||
|
- Vidéoprojecteur, Tableau blanc, WiFi
|
||||||
|
- **Usage** : Grande formation, séminaire
|
||||||
|
|
||||||
|
### 🤝 Salle Horizon (15 personnes)
|
||||||
|
- Écran TV, Tableau blanc, WiFi
|
||||||
|
- **Usage** : Réunion, petit groupe
|
||||||
|
|
||||||
|
### 🎤 Salle Innovation (50 personnes)
|
||||||
|
- Vidéoprojecteur, Sono, WiFi, Climatisation
|
||||||
|
- **Usage** : Conférence, grand événement
|
||||||
|
|
||||||
|
### 💼 Salle Connect (8 personnes)
|
||||||
|
- Écran TV, Visioconférence, WiFi
|
||||||
|
- **Usage** : Réunion d'équipe, visio
|
||||||
|
|
||||||
|
### 💻 Salle Digital (20 personnes)
|
||||||
|
- 20 postes informatiques, Vidéoprojecteur, WiFi
|
||||||
|
- **Usage** : Formation pratique, TP informatique
|
||||||
|
|
||||||
|
## 🔧 Administration
|
||||||
|
|
||||||
|
### Ajouter une nouvelle salle
|
||||||
|
|
||||||
|
Accédez à la documentation API : **http://localhost:8000/docs**
|
||||||
|
|
||||||
|
1. Trouvez l'endpoint `POST /rooms`
|
||||||
|
2. Cliquez sur "Try it out"
|
||||||
|
3. Remplissez les informations :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Salle Créative",
|
||||||
|
"capacity": 12,
|
||||||
|
"equipment": "Tableau interactif, WiFi",
|
||||||
|
"description": "Salle pour ateliers créatifs"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. Cliquez sur "Execute"
|
||||||
|
|
||||||
|
### Consulter les statistiques
|
||||||
|
|
||||||
|
Via l'API :
|
||||||
|
- **Toutes les réservations** : http://localhost:8000/reservations
|
||||||
|
- **Réservations par salle** : http://localhost:8000/reservations/room/{id}
|
||||||
|
- **Réservations par date** : http://localhost:8000/reservations/date/2025-10-15
|
||||||
|
|
||||||
|
## 🛠️ Dépannage
|
||||||
|
|
||||||
|
### L'application ne se charge pas
|
||||||
|
```bash
|
||||||
|
# Vérifier l'état des conteneurs
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur de connexion à la base
|
||||||
|
```bash
|
||||||
|
# Redémarrer tous les services
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Réinitialiser complètement
|
||||||
|
```bash
|
||||||
|
# Arrêter et supprimer tout (⚠️ perd les données)
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Relancer
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
Pour toute question ou problème, consultez :
|
||||||
|
- 📖 **Documentation API** : http://localhost:8000/docs
|
||||||
|
- 📋 **README** : Voir le fichier README.md
|
||||||
|
- 🔧 **Logs** : `docker-compose logs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Bon usage du système de réservation Expernet ! 🎯**
|
||||||
|
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
# 🏢 Expernet - Système de Réservation de Salles
|
||||||
|
|
||||||
|
Système complet de réservation de salles pour le centre de formation **Expernet**, développé avec **FastAPI** (Backend), **Next.js** (Frontend) et **PostgreSQL** (Base de données), orchestré avec Docker Compose.
|
||||||
|
|
||||||
|
## 📋 Stack Technique
|
||||||
|
|
||||||
|
- **Backend**: FastAPI (Python) avec AsyncPG
|
||||||
|
- **Frontend**: Next.js 14 + TypeScript + Tailwind CSS
|
||||||
|
- **Base de données**: PostgreSQL 16
|
||||||
|
- **Containerisation**: Docker & Docker Compose
|
||||||
|
|
||||||
|
## ✨ Fonctionnalités
|
||||||
|
|
||||||
|
### 🏛️ Gestion des Salles
|
||||||
|
- 5 salles pré-configurées (Atlas, Horizon, Innovation, Connect, Digital)
|
||||||
|
- Affichage des capacités et équipements
|
||||||
|
- Interface intuitive de sélection
|
||||||
|
|
||||||
|
### 📅 Réservations
|
||||||
|
- Formulaire de réservation complet
|
||||||
|
- Champ email optionnel
|
||||||
|
- Sélection de date et horaires
|
||||||
|
- **Vérification en temps réel** de la disponibilité
|
||||||
|
- Détection automatique des conflits d'horaires
|
||||||
|
- **Bouton désactivé** si créneau occupé
|
||||||
|
- Validation des créneaux disponibles
|
||||||
|
- Annulation de réservations
|
||||||
|
|
||||||
|
### 📊 Planning Visuel
|
||||||
|
- **Grille de planning par jour** (8h-22h)
|
||||||
|
- Vue d'ensemble de toutes les salles
|
||||||
|
- Code couleur : ✅ Disponible / ❌ Réservé
|
||||||
|
- Nom de l'utilisateur affiché sur les créneaux réservés
|
||||||
|
- Navigation par date
|
||||||
|
- Liste détaillée des réservations du jour
|
||||||
|
|
||||||
|
### 📋 Suivi
|
||||||
|
- Vue d'ensemble de toutes les réservations
|
||||||
|
- Filtrage par salle et par date
|
||||||
|
- Historique complet
|
||||||
|
|
||||||
|
## 🚀 Démarrage Rapide
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Docker et Docker Compose installés sur votre machine
|
||||||
|
- Ports 3000, 8000 et 5432 disponibles
|
||||||
|
|
||||||
|
### Installation et lancement
|
||||||
|
|
||||||
|
1. **Cloner le projet** (si applicable)
|
||||||
|
```bash
|
||||||
|
git clone <votre-repo>
|
||||||
|
cd dockezr
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Lancer tous les services avec Docker Compose**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
ou utilisez le script Windows :
|
||||||
|
```bash
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette commande va :
|
||||||
|
- Créer le réseau Docker `dockezr_network`
|
||||||
|
- Démarrer PostgreSQL sur le port 5432
|
||||||
|
- Créer automatiquement les tables et les 5 salles par défaut
|
||||||
|
- Démarrer le backend FastAPI sur le port 8000
|
||||||
|
- Démarrer le frontend Next.js sur le port 3000
|
||||||
|
|
||||||
|
3. **Accéder à l'application**
|
||||||
|
- **Frontend (Interface de réservation)**: http://localhost:3000
|
||||||
|
- **Backend API**: http://localhost:8000
|
||||||
|
- **Documentation API (Swagger)**: http://localhost:8000/docs
|
||||||
|
- **Documentation API (ReDoc)**: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
## 📁 Structure du Projet
|
||||||
|
|
||||||
|
```
|
||||||
|
dockezr/
|
||||||
|
├── backend/ # API FastAPI
|
||||||
|
│ ├── main.py # Point d'entrée de l'API
|
||||||
|
│ ├── requirements.txt # Dépendances Python
|
||||||
|
│ └── Dockerfile # Configuration Docker
|
||||||
|
│
|
||||||
|
├── frontend/ # Application Next.js
|
||||||
|
│ ├── app/ # Pages et composants Next.js 14
|
||||||
|
│ │ ├── layout.tsx
|
||||||
|
│ │ ├── page.tsx
|
||||||
|
│ │ └── globals.css
|
||||||
|
│ ├── package.json # Dépendances Node.js
|
||||||
|
│ ├── tsconfig.json # Configuration TypeScript
|
||||||
|
│ ├── tailwind.config.ts # Configuration Tailwind
|
||||||
|
│ └── Dockerfile # Configuration Docker
|
||||||
|
│
|
||||||
|
├── docker-compose.yml # Orchestration des services
|
||||||
|
├── .dockerignore # Fichiers ignorés par Docker
|
||||||
|
├── .gitignore # Fichiers ignorés par Git
|
||||||
|
└── README.md # Ce fichier
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Commandes Utiles
|
||||||
|
|
||||||
|
### Démarrer les services
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrêter les services
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voir les logs
|
||||||
|
```bash
|
||||||
|
# Tous les services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Service spécifique
|
||||||
|
docker-compose logs -f backend
|
||||||
|
docker-compose logs -f frontend
|
||||||
|
docker-compose logs -f db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reconstruire les images
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrêter et supprimer les volumes (⚠️ supprime les données)
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accéder à un conteneur
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
docker exec -it dockezr_backend sh
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
docker exec -it dockezr_frontend sh
|
||||||
|
|
||||||
|
# Base de données
|
||||||
|
docker exec -it dockezr_db psql -U user -d dockezr
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏛️ Salles Pré-configurées
|
||||||
|
|
||||||
|
Le système est livré avec 5 salles de formation :
|
||||||
|
|
||||||
|
| Salle | Capacité | Équipements |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| **Salle Atlas** | 30 personnes | Vidéoprojecteur, Tableau blanc, WiFi |
|
||||||
|
| **Salle Horizon** | 15 personnes | Écran TV, Tableau blanc, WiFi |
|
||||||
|
| **Salle Innovation** | 50 personnes | Vidéoprojecteur, Sono, WiFi, Climatisation |
|
||||||
|
| **Salle Connect** | 8 personnes | Écran TV, Visioconférence, WiFi |
|
||||||
|
| **Salle Digital** | 20 personnes | 20 postes informatiques, Vidéoprojecteur, WiFi |
|
||||||
|
|
||||||
|
## 🌐 API Endpoints
|
||||||
|
|
||||||
|
### Routes des Salles
|
||||||
|
|
||||||
|
- `GET /rooms` - Liste toutes les salles
|
||||||
|
- `GET /rooms/{room_id}` - Récupère une salle spécifique
|
||||||
|
- `POST /rooms` - Crée une nouvelle salle
|
||||||
|
- `DELETE /rooms/{room_id}` - Supprime une salle
|
||||||
|
|
||||||
|
### Routes des Réservations
|
||||||
|
|
||||||
|
- `GET /reservations` - Liste toutes les réservations
|
||||||
|
- `GET /reservations/room/{room_id}` - Réservations d'une salle spécifique
|
||||||
|
- `GET /reservations/date/{date}` - Réservations pour une date donnée
|
||||||
|
- `POST /reservations` - Crée une nouvelle réservation
|
||||||
|
- `DELETE /reservations/{reservation_id}` - Annule une réservation
|
||||||
|
|
||||||
|
### Validation automatique
|
||||||
|
|
||||||
|
L'API vérifie automatiquement :
|
||||||
|
- ✅ La disponibilité de la salle
|
||||||
|
- ✅ Les conflits d'horaires
|
||||||
|
- ✅ La cohérence des horaires (début < fin)
|
||||||
|
- ✅ L'existence de la salle
|
||||||
|
|
||||||
|
### Documentation interactive
|
||||||
|
- **Swagger UI**: http://localhost:8000/docs
|
||||||
|
- **ReDoc**: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
## 🗄️ Base de Données
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- **Host**: localhost (ou `db` depuis les conteneurs)
|
||||||
|
- **Port**: 5432
|
||||||
|
- **Utilisateur**: user
|
||||||
|
- **Mot de passe**: password
|
||||||
|
- **Base de données**: dockezr
|
||||||
|
|
||||||
|
### Structure des tables
|
||||||
|
|
||||||
|
**Table `rooms`**
|
||||||
|
- id (SERIAL PRIMARY KEY)
|
||||||
|
- name (VARCHAR UNIQUE)
|
||||||
|
- capacity (INTEGER)
|
||||||
|
- equipment (TEXT)
|
||||||
|
- description (TEXT)
|
||||||
|
|
||||||
|
**Table `reservations`**
|
||||||
|
- id (SERIAL PRIMARY KEY)
|
||||||
|
- room_id (INTEGER FOREIGN KEY)
|
||||||
|
- user_name (VARCHAR)
|
||||||
|
- user_email (VARCHAR)
|
||||||
|
- reservation_date (DATE)
|
||||||
|
- start_time (TIME)
|
||||||
|
- end_time (TIME)
|
||||||
|
- purpose (TEXT)
|
||||||
|
- created_at (TIMESTAMP)
|
||||||
|
|
||||||
|
### Connexion à PostgreSQL
|
||||||
|
```bash
|
||||||
|
docker exec -it dockezr_db psql -U user -d dockezr
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Frontend
|
||||||
|
|
||||||
|
L'interface utilise :
|
||||||
|
- **Next.js 14** avec App Router
|
||||||
|
- **TypeScript** pour le typage statique
|
||||||
|
- **Tailwind CSS** pour le styling moderne et responsive
|
||||||
|
- **Axios** pour les requêtes HTTP vers l'API
|
||||||
|
|
||||||
|
### Fonctionnalités de l'interface
|
||||||
|
|
||||||
|
**Onglet Planning (par défaut)**
|
||||||
|
- 📊 Grille de planning visuelle (8h-22h)
|
||||||
|
- 🎨 Code couleur : vert (disponible) / rouge (réservé)
|
||||||
|
- 📅 Sélecteur de date
|
||||||
|
- 👤 Affichage du nom de l'utilisateur sur les créneaux
|
||||||
|
- 📋 Liste détaillée des réservations du jour sélectionné
|
||||||
|
- 🖱️ Info-bulle au survol des créneaux réservés
|
||||||
|
|
||||||
|
**Onglet Réservation**
|
||||||
|
- Liste visuelle des salles disponibles avec détails
|
||||||
|
- Sélection interactive de salle
|
||||||
|
- Formulaire de réservation complet
|
||||||
|
- **Email optionnel** (pas obligatoire)
|
||||||
|
- ⚡ **Vérification en temps réel** de la disponibilité
|
||||||
|
- ✅/❌ Indicateur visuel de disponibilité du créneau
|
||||||
|
- 🔒 Bouton désactivé automatiquement si créneau occupé
|
||||||
|
|
||||||
|
**Onglet Mes Réservations**
|
||||||
|
- Affichage de toutes les réservations
|
||||||
|
- Informations détaillées (salle, date, horaire, objet)
|
||||||
|
- Possibilité d'annuler une réservation
|
||||||
|
|
||||||
|
## 🔄 Développement
|
||||||
|
|
||||||
|
### Mode développement avec hot-reload
|
||||||
|
|
||||||
|
Les deux services (backend et frontend) sont configurés en mode développement avec rechargement automatique :
|
||||||
|
|
||||||
|
- **Backend**: Uvicorn avec `--reload`
|
||||||
|
- **Frontend**: Next.js avec `npm run dev`
|
||||||
|
|
||||||
|
Les modifications de code sont automatiquement détectées et appliquées.
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
- `DATABASE_URL`: URL de connexion PostgreSQL
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- `NEXT_PUBLIC_API_URL`: URL de l'API backend
|
||||||
|
|
||||||
|
## 🐛 Dépannage
|
||||||
|
|
||||||
|
### Les conteneurs ne démarrent pas
|
||||||
|
```bash
|
||||||
|
# Vérifier les logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Reconstruire les images
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### La base de données n'est pas prête
|
||||||
|
Le backend attend que PostgreSQL soit complètement démarré grâce au healthcheck.
|
||||||
|
|
||||||
|
### Erreurs de connexion à l'API
|
||||||
|
Vérifiez que :
|
||||||
|
- Le backend est démarré : `docker-compose ps`
|
||||||
|
- L'URL de l'API est correcte dans le frontend
|
||||||
|
- Le réseau Docker fonctionne : `docker network ls`
|
||||||
|
|
||||||
|
## 📝 Personnalisation
|
||||||
|
|
||||||
|
### Ajouter de nouvelles salles
|
||||||
|
|
||||||
|
Via l'API :
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/rooms \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Salle Formation",
|
||||||
|
"capacity": 25,
|
||||||
|
"equipment": "Vidéoprojecteur, WiFi",
|
||||||
|
"description": "Salle de formation polyvalente"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou directement dans la base de données :
|
||||||
|
```sql
|
||||||
|
INSERT INTO rooms (name, capacity, equipment, description)
|
||||||
|
VALUES ('Ma Salle', 40, 'Équipements', 'Description');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifier les ports
|
||||||
|
Éditez le fichier `docker-compose.yml` et changez les mappings de ports :
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "VOTRE_PORT:PORT_INTERNE"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ajouter des dépendances
|
||||||
|
|
||||||
|
**Backend (Python)**:
|
||||||
|
1. Ajoutez la dépendance dans `backend/requirements.txt`
|
||||||
|
2. Reconstruisez : `docker-compose up -d --build backend`
|
||||||
|
|
||||||
|
**Frontend (Node.js)**:
|
||||||
|
1. Ajoutez la dépendance dans `frontend/package.json`
|
||||||
|
2. Reconstruisez : `docker-compose up -d --build frontend`
|
||||||
|
|
||||||
|
## 🎯 Cas d'Usage
|
||||||
|
|
||||||
|
Ce système est idéal pour :
|
||||||
|
- ✅ Centres de formation
|
||||||
|
- ✅ Espaces de coworking
|
||||||
|
- ✅ Entreprises avec salles de réunion
|
||||||
|
- ✅ Universités et écoles
|
||||||
|
- ✅ Bibliothèques avec salles d'étude
|
||||||
|
|
||||||
|
## 📦 Production
|
||||||
|
|
||||||
|
Pour un déploiement en production, modifiez :
|
||||||
|
|
||||||
|
1. Les mots de passe et secrets dans `docker-compose.yml`
|
||||||
|
2. Désactivez le mode debug/reload
|
||||||
|
3. Utilisez des variables d'environnement sécurisées
|
||||||
|
4. Configurez HTTPS/SSL
|
||||||
|
5. Ajoutez un reverse proxy (Nginx, Traefik)
|
||||||
|
6. Mettez en place des sauvegardes de la base de données
|
||||||
|
|
||||||
|
## 🔐 Sécurité
|
||||||
|
|
||||||
|
⚠️ **Important pour la production** :
|
||||||
|
- Changez les identifiants PostgreSQL par défaut
|
||||||
|
- Utilisez des secrets Docker pour les mots de passe
|
||||||
|
- Activez l'authentification utilisateur
|
||||||
|
- Configurez CORS correctement
|
||||||
|
- Utilisez HTTPS
|
||||||
|
|
||||||
|
## 📄 Licence
|
||||||
|
|
||||||
|
Ce projet est développé pour **Expernet** - Centre de Formation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🏢 Système de réservation Expernet - Simplifions la gestion des salles ! 🚀**
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Image de base Python
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Définir le répertoire de travail
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installer les dépendances système nécessaires pour PostgreSQL
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
postgresql-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copier les fichiers de requirements
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Installer les dépendances Python
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copier le code de l'application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Exposer le port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Commande pour démarrer l'application
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
|
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional
|
||||||
|
import asyncpg
|
||||||
|
from datetime import datetime, date, time
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Configuration de la base de données
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@db:5432/dockezr")
|
||||||
|
|
||||||
|
# Pool de connexions
|
||||||
|
pool = None
|
||||||
|
|
||||||
|
# Modèles Pydantic pour les Salles
|
||||||
|
class RoomCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
capacity: int
|
||||||
|
equipment: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class Room(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
capacity: int
|
||||||
|
equipment: Optional[str]
|
||||||
|
description: Optional[str]
|
||||||
|
|
||||||
|
# Modèles Pydantic pour les Réservations
|
||||||
|
class ReservationCreate(BaseModel):
|
||||||
|
room_id: int
|
||||||
|
user_name: str
|
||||||
|
user_email: Optional[str] = None
|
||||||
|
reservation_date: date
|
||||||
|
start_time: time
|
||||||
|
end_time: time
|
||||||
|
purpose: str
|
||||||
|
|
||||||
|
class Reservation(BaseModel):
|
||||||
|
id: int
|
||||||
|
room_id: int
|
||||||
|
room_name: Optional[str] = None
|
||||||
|
user_name: str
|
||||||
|
user_email: Optional[str] = None
|
||||||
|
reservation_date: date
|
||||||
|
start_time: time
|
||||||
|
end_time: time
|
||||||
|
purpose: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
# Application FastAPI
|
||||||
|
app = FastAPI(
|
||||||
|
title="Expernet - Système de Réservation de Salles",
|
||||||
|
version="1.0.0",
|
||||||
|
description="API de gestion des réservations de salles pour le centre de formation Expernet"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["http://localhost:3000", "http://frontend:3000"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup():
|
||||||
|
global pool
|
||||||
|
pool = await asyncpg.create_pool(DATABASE_URL)
|
||||||
|
# Créer les tables si elles n'existent pas
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
# Table des salles
|
||||||
|
await conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS rooms (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
capacity INTEGER NOT NULL,
|
||||||
|
equipment TEXT,
|
||||||
|
description TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Table des réservations
|
||||||
|
await conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS reservations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
||||||
|
user_name VARCHAR(255) NOT NULL,
|
||||||
|
user_email VARCHAR(255),
|
||||||
|
reservation_date DATE NOT NULL,
|
||||||
|
start_time TIME NOT NULL,
|
||||||
|
end_time TIME NOT NULL,
|
||||||
|
purpose TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Insérer des salles par défaut si la table est vide
|
||||||
|
count = await conn.fetchval('SELECT COUNT(*) FROM rooms')
|
||||||
|
if count == 0:
|
||||||
|
await conn.execute('''
|
||||||
|
INSERT INTO rooms (name, capacity, equipment, description) VALUES
|
||||||
|
('Salle Atlas', 30, 'Vidéoprojecteur, Tableau blanc, WiFi', 'Grande salle de formation idéale pour les cours'),
|
||||||
|
('Salle Horizon', 15, 'Écran TV, Tableau blanc, WiFi', 'Salle de réunion moyenne capacité'),
|
||||||
|
('Salle Innovation', 50, 'Vidéoprojecteur, Sono, WiFi, Climatisation', 'Salle de conférence grande capacité'),
|
||||||
|
('Salle Connect', 8, 'Écran TV, Visioconférence, WiFi', 'Petite salle pour réunions d''équipe'),
|
||||||
|
('Salle Digital', 20, '20 postes informatiques, Vidéoprojecteur, WiFi', 'Salle informatique pour formations pratiques')
|
||||||
|
''')
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown():
|
||||||
|
await pool.close()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "Bienvenue sur le système de réservation de salles Expernet!", "status": "running"}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "healthy", "database": "connected"}
|
||||||
|
|
||||||
|
# === ROUTES POUR LES SALLES ===
|
||||||
|
|
||||||
|
@app.get("/rooms", response_model=List[Room])
|
||||||
|
async def get_rooms():
|
||||||
|
"""Récupérer toutes les salles"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch('SELECT * FROM rooms ORDER BY name')
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
@app.get("/rooms/{room_id}", response_model=Room)
|
||||||
|
async def get_room(room_id: int):
|
||||||
|
"""Récupérer une salle spécifique"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow('SELECT * FROM rooms WHERE id = $1', room_id)
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Salle non trouvée")
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
@app.post("/rooms", response_model=Room)
|
||||||
|
async def create_room(room: RoomCreate):
|
||||||
|
"""Créer une nouvelle salle"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
try:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
'INSERT INTO rooms (name, capacity, equipment, description) VALUES ($1, $2, $3, $4) RETURNING *',
|
||||||
|
room.name, room.capacity, room.equipment, room.description
|
||||||
|
)
|
||||||
|
return dict(row)
|
||||||
|
except asyncpg.UniqueViolationError:
|
||||||
|
raise HTTPException(status_code=400, detail="Une salle avec ce nom existe déjà")
|
||||||
|
|
||||||
|
@app.delete("/rooms/{room_id}")
|
||||||
|
async def delete_room(room_id: int):
|
||||||
|
"""Supprimer une salle"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
result = await conn.execute('DELETE FROM rooms WHERE id = $1', room_id)
|
||||||
|
if result == "DELETE 0":
|
||||||
|
raise HTTPException(status_code=404, detail="Salle non trouvée")
|
||||||
|
return {"message": "Salle supprimée avec succès"}
|
||||||
|
|
||||||
|
# === ROUTES POUR LES RÉSERVATIONS ===
|
||||||
|
|
||||||
|
@app.get("/reservations", response_model=List[Reservation])
|
||||||
|
async def get_reservations():
|
||||||
|
"""Récupérer toutes les réservations avec les informations des salles"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch('''
|
||||||
|
SELECT r.*, ro.name as room_name
|
||||||
|
FROM reservations r
|
||||||
|
JOIN rooms ro ON r.room_id = ro.id
|
||||||
|
ORDER BY r.reservation_date DESC, r.start_time DESC
|
||||||
|
''')
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
@app.get("/reservations/room/{room_id}", response_model=List[Reservation])
|
||||||
|
async def get_reservations_by_room(room_id: int):
|
||||||
|
"""Récupérer les réservations d'une salle spécifique"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch('''
|
||||||
|
SELECT r.*, ro.name as room_name
|
||||||
|
FROM reservations r
|
||||||
|
JOIN rooms ro ON r.room_id = ro.id
|
||||||
|
WHERE r.room_id = $1
|
||||||
|
ORDER BY r.reservation_date, r.start_time
|
||||||
|
''', room_id)
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
@app.get("/reservations/date/{reservation_date}")
|
||||||
|
async def get_reservations_by_date(reservation_date: date):
|
||||||
|
"""Récupérer les réservations pour une date spécifique"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch('''
|
||||||
|
SELECT r.*, ro.name as room_name
|
||||||
|
FROM reservations r
|
||||||
|
JOIN rooms ro ON r.room_id = ro.id
|
||||||
|
WHERE r.reservation_date = $1
|
||||||
|
ORDER BY r.start_time
|
||||||
|
''', reservation_date)
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
@app.post("/reservations", response_model=Reservation)
|
||||||
|
async def create_reservation(reservation: ReservationCreate):
|
||||||
|
"""Créer une nouvelle réservation"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
# Vérifier que la salle existe
|
||||||
|
room = await conn.fetchrow('SELECT * FROM rooms WHERE id = $1', reservation.room_id)
|
||||||
|
if room is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Salle non trouvée")
|
||||||
|
|
||||||
|
# Vérifier que l'heure de fin est après l'heure de début
|
||||||
|
if reservation.end_time <= reservation.start_time:
|
||||||
|
raise HTTPException(status_code=400, detail="L'heure de fin doit être après l'heure de début")
|
||||||
|
|
||||||
|
# Vérifier les conflits de réservation
|
||||||
|
conflicts = await conn.fetch('''
|
||||||
|
SELECT * FROM reservations
|
||||||
|
WHERE room_id = $1
|
||||||
|
AND reservation_date = $2
|
||||||
|
AND (
|
||||||
|
(start_time < $4 AND end_time > $3)
|
||||||
|
OR (start_time >= $3 AND start_time < $4)
|
||||||
|
)
|
||||||
|
''', reservation.room_id, reservation.reservation_date,
|
||||||
|
reservation.start_time, reservation.end_time)
|
||||||
|
|
||||||
|
if conflicts:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="La salle est déjà réservée pour ce créneau horaire"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Créer la réservation
|
||||||
|
row = await conn.fetchrow('''
|
||||||
|
INSERT INTO reservations
|
||||||
|
(room_id, user_name, user_email, reservation_date, start_time, end_time, purpose, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
RETURNING *
|
||||||
|
''', reservation.room_id, reservation.user_name, reservation.user_email,
|
||||||
|
reservation.reservation_date, reservation.start_time, reservation.end_time,
|
||||||
|
reservation.purpose, datetime.utcnow())
|
||||||
|
|
||||||
|
result = dict(row)
|
||||||
|
result['room_name'] = room['name']
|
||||||
|
return result
|
||||||
|
|
||||||
|
@app.delete("/reservations/{reservation_id}")
|
||||||
|
async def delete_reservation(reservation_id: int):
|
||||||
|
"""Annuler une réservation"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
result = await conn.execute('DELETE FROM reservations WHERE id = $1', reservation_id)
|
||||||
|
if result == "DELETE 0":
|
||||||
|
raise HTTPException(status_code=404, detail="Réservation non trouvée")
|
||||||
|
return {"message": "Réservation annulée avec succès"}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
sqlalchemy==1.4.51
|
||||||
|
asyncpg==0.29.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
pydantic==2.5.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Base de données PostgreSQL
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: dockezr_db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: user
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_DB: dockezr
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- dockezr_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U user -d dockezr"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Backend FastAPI
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: dockezr_backend
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://user:password@db:5432/dockezr
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- dockezr_network
|
||||||
|
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
# Frontend Next.js
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: dockezr_frontend
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
NEXT_PUBLIC_API_URL: http://localhost:8000
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/.next
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- dockezr_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dockezr_network:
|
||||||
|
driver: bridge
|
||||||
|
name: dockezr_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
name: dockezr_postgres_data
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Image de base Node.js
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Définir le répertoire de travail
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copier les fichiers package
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Installer les dépendances
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copier le reste des fichiers
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Exposer le port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Commande pour démarrer l'application
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 0, 0, 0;
|
||||||
|
--background-start-rgb: 214, 219, 220;
|
||||||
|
--background-end-rgb: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: rgb(var(--foreground-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import './globals.css'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Expernet - Réservation de Salles',
|
||||||
|
description: 'Système de réservation de salles pour le centre de formation Expernet',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="fr">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,625 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
BuildingOffice2Icon,
|
||||||
|
CalendarIcon,
|
||||||
|
ClipboardDocumentListIcon,
|
||||||
|
ChartBarIcon,
|
||||||
|
UserGroupIcon,
|
||||||
|
WrenchScrewdriverIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
EnvelopeIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
TrashIcon,
|
||||||
|
CheckIcon,
|
||||||
|
XMarkIcon
|
||||||
|
} from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
|
interface Room {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
capacity: number
|
||||||
|
equipment: string | null
|
||||||
|
description: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Reservation {
|
||||||
|
id: number
|
||||||
|
room_id: number
|
||||||
|
room_name?: string
|
||||||
|
user_name: string
|
||||||
|
user_email: string
|
||||||
|
reservation_date: string
|
||||||
|
start_time: string
|
||||||
|
end_time: string
|
||||||
|
purpose: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [rooms, setRooms] = useState<Room[]>([])
|
||||||
|
const [reservations, setReservations] = useState<Reservation[]>([])
|
||||||
|
const [selectedRoom, setSelectedRoom] = useState<number | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState<'rooms' | 'reservations' | 'planning'>('planning')
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0])
|
||||||
|
|
||||||
|
// Formulaire de réservation
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
user_name: '',
|
||||||
|
user_email: '',
|
||||||
|
reservation_date: '',
|
||||||
|
start_time: '',
|
||||||
|
end_time: '',
|
||||||
|
purpose: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRooms()
|
||||||
|
fetchReservations()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchRooms = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/rooms`)
|
||||||
|
setRooms(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du chargement des salles:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchReservations = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/reservations`)
|
||||||
|
setReservations(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du chargement des réservations:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchReservationsByDate = async (date: string) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/reservations/date/${date}`)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du chargement des réservations par date:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReservationsForRoom = (roomId: number, date: string) => {
|
||||||
|
return reservations.filter(r =>
|
||||||
|
r.room_id === roomId &&
|
||||||
|
r.reservation_date === date
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSlotAvailable = (roomId: number, date: string, startTime: string, endTime: string) => {
|
||||||
|
const roomReservations = getReservationsForRoom(roomId, date)
|
||||||
|
|
||||||
|
for (const reservation of roomReservations) {
|
||||||
|
const resStart = reservation.start_time
|
||||||
|
const resEnd = reservation.end_time
|
||||||
|
|
||||||
|
// Vérifie s'il y a chevauchement
|
||||||
|
if ((startTime < resEnd && endTime > resStart)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReservation = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!selectedRoom) {
|
||||||
|
alert('Veuillez sélectionner une salle')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await axios.post(`${API_URL}/reservations`, {
|
||||||
|
room_id: selectedRoom,
|
||||||
|
...formData
|
||||||
|
})
|
||||||
|
|
||||||
|
alert('Réservation effectuée avec succès !')
|
||||||
|
setFormData({
|
||||||
|
user_name: '',
|
||||||
|
user_email: '',
|
||||||
|
reservation_date: '',
|
||||||
|
start_time: '',
|
||||||
|
end_time: '',
|
||||||
|
purpose: ''
|
||||||
|
})
|
||||||
|
setSelectedRoom(null)
|
||||||
|
fetchReservations()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 409) {
|
||||||
|
alert('Cette salle est déjà réservée pour ce créneau horaire')
|
||||||
|
} else {
|
||||||
|
alert('Erreur lors de la réservation: ' + (error.response?.data?.detail || error.message))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteReservation = async (id: number) => {
|
||||||
|
if (!confirm('Voulez-vous vraiment annuler cette réservation ?')) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`${API_URL}/reservations/${id}`)
|
||||||
|
fetchReservations()
|
||||||
|
alert('Réservation annulée avec succès')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de l\'annulation:', error)
|
||||||
|
alert('Erreur lors de l\'annulation de la réservation')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="flex items-center justify-center gap-3 mb-2">
|
||||||
|
<BuildingOffice2Icon className="w-12 h-12 text-indigo-900" />
|
||||||
|
<h1 className="text-5xl font-bold text-indigo-900">
|
||||||
|
Expernet
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-xl text-gray-600">
|
||||||
|
Centre de Formation - Réservation de Salles
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-1 inline-flex">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('planning')}
|
||||||
|
className={`px-6 py-2 rounded-md font-medium transition-colors flex items-center gap-2 ${
|
||||||
|
activeTab === 'planning'
|
||||||
|
? 'bg-indigo-600 text-white'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChartBarIcon className="w-5 h-5" />
|
||||||
|
Planning des salles
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('rooms')}
|
||||||
|
className={`px-6 py-2 rounded-md font-medium transition-colors flex items-center gap-2 ${
|
||||||
|
activeTab === 'rooms'
|
||||||
|
? 'bg-indigo-600 text-white'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="w-5 h-5" />
|
||||||
|
Réserver une salle
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('reservations')}
|
||||||
|
className={`px-6 py-2 rounded-md font-medium transition-colors flex items-center gap-2 ${
|
||||||
|
activeTab === 'reservations'
|
||||||
|
? 'bg-indigo-600 text-white'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ClipboardDocumentListIcon className="w-5 h-5" />
|
||||||
|
Mes réservations
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Onglet Planning */}
|
||||||
|
{activeTab === 'planning' && (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-800">
|
||||||
|
Planning des Salles
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Date :
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={(e) => setSelectedDate(e.target.value)}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grille de planning */}
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-indigo-50">
|
||||||
|
<th className="border border-gray-300 px-4 py-2 text-left font-semibold text-gray-700">
|
||||||
|
Salle
|
||||||
|
</th>
|
||||||
|
{Array.from({ length: 14 }, (_, i) => i + 8).map((hour) => (
|
||||||
|
<th key={hour} className="border border-gray-300 px-2 py-2 text-center text-sm font-medium text-gray-700">
|
||||||
|
{hour}h
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rooms.map((room) => {
|
||||||
|
const roomReservations = getReservationsForRoom(room.id, selectedDate)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={room.id} className="hover:bg-gray-50">
|
||||||
|
<td className="border border-gray-300 px-4 py-2 font-medium text-gray-900 whitespace-nowrap">
|
||||||
|
{room.name}
|
||||||
|
<div className="text-xs text-gray-500 flex items-center gap-1">
|
||||||
|
<UserGroupIcon className="w-3 h-3" />
|
||||||
|
{room.capacity} pers.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{Array.from({ length: 14 }, (_, i) => i + 8).map((hour) => {
|
||||||
|
const hourStart = `${hour.toString().padStart(2, '0')}:00`
|
||||||
|
const hourEnd = `${(hour + 1).toString().padStart(2, '0')}:00`
|
||||||
|
|
||||||
|
// Trouver les réservations qui couvrent cette heure
|
||||||
|
const overlappingReservations = roomReservations.filter(res => {
|
||||||
|
return res.start_time < hourEnd && res.end_time > hourStart
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOccupied = overlappingReservations.length > 0
|
||||||
|
const reservation = overlappingReservations[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={hour}
|
||||||
|
className={`border border-gray-300 px-1 py-2 text-center text-xs ${
|
||||||
|
isOccupied ? 'bg-red-100' : 'bg-green-50'
|
||||||
|
}`}
|
||||||
|
title={
|
||||||
|
reservation
|
||||||
|
? `${reservation.user_name} - ${reservation.purpose}\n${reservation.start_time} - ${reservation.end_time}`
|
||||||
|
: 'Disponible'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isOccupied ? (
|
||||||
|
<div className="text-red-700 font-medium">
|
||||||
|
<XMarkIcon className="w-4 h-4 mx-auto" />
|
||||||
|
<div className="text-[10px] mt-1 truncate">
|
||||||
|
{reservation.user_name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-green-600">
|
||||||
|
<CheckIcon className="w-4 h-4 mx-auto" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Légende */}
|
||||||
|
<div className="mt-6 flex gap-6 justify-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 bg-green-50 border border-gray-300 flex items-center justify-center text-green-600">
|
||||||
|
<CheckIcon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-700">Disponible</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 bg-red-100 border border-gray-300 flex items-center justify-center text-red-700">
|
||||||
|
<XMarkIcon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-700">Réservé</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste des réservations du jour */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-gray-800">
|
||||||
|
Réservations du {new Date(selectedDate).toLocaleDateString('fr-FR')}
|
||||||
|
</h3>
|
||||||
|
{reservations.filter(r => r.reservation_date === selectedDate).length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-4">Aucune réservation pour cette date</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{reservations
|
||||||
|
.filter(r => r.reservation_date === selectedDate)
|
||||||
|
.sort((a, b) => a.start_time.localeCompare(b.start_time))
|
||||||
|
.map((reservation) => (
|
||||||
|
<div
|
||||||
|
key={reservation.id}
|
||||||
|
className="border border-gray-200 rounded-lg p-3 bg-gray-50"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-indigo-900">{reservation.room_name}</span>
|
||||||
|
<span className="mx-2">•</span>
|
||||||
|
<span className="text-gray-700">{reservation.start_time} - {reservation.end_time}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-sm">
|
||||||
|
<div className="font-medium text-gray-900">{reservation.user_name}</div>
|
||||||
|
<div className="text-gray-600">{reservation.purpose}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Onglet Réservation */}
|
||||||
|
{activeTab === 'rooms' && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* Liste des salles */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-800">
|
||||||
|
Nos Salles Disponibles
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 max-h-[600px] overflow-y-auto">
|
||||||
|
{rooms.map((room) => (
|
||||||
|
<div
|
||||||
|
key={room.id}
|
||||||
|
onClick={() => setSelectedRoom(room.id)}
|
||||||
|
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
|
||||||
|
selectedRoom === room.id
|
||||||
|
? 'border-indigo-600 bg-indigo-50'
|
||||||
|
: 'border-gray-200 hover:border-indigo-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{room.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 mt-1 flex items-center gap-1">
|
||||||
|
<UserGroupIcon className="w-4 h-4" />
|
||||||
|
Capacité: {room.capacity} personnes
|
||||||
|
</p>
|
||||||
|
{room.equipment && (
|
||||||
|
<p className="text-sm text-gray-600 mt-1 flex items-center gap-1">
|
||||||
|
<WrenchScrewdriverIcon className="w-4 h-4" />
|
||||||
|
{room.equipment}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{room.description && (
|
||||||
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
|
{room.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedRoom === room.id && (
|
||||||
|
<div className="ml-2">
|
||||||
|
<span className="inline-block bg-indigo-600 text-white rounded-full p-2">
|
||||||
|
<CheckIcon className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulaire de réservation */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-800">
|
||||||
|
Formulaire de Réservation
|
||||||
|
</h2>
|
||||||
|
{!selectedRoom ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<BuildingOffice2Icon className="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
||||||
|
<p className="text-lg">Sélectionnez une salle pour continuer</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleReservation} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Votre nom *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.user_name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, user_name: e.target.value })}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
placeholder="Jean Dupont"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email (optionnel)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={formData.user_email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, user_email: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
placeholder="jean.dupont@expernet.fr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Date *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={formData.reservation_date}
|
||||||
|
onChange={(e) => setFormData({ ...formData, reservation_date: e.target.value })}
|
||||||
|
required
|
||||||
|
min={new Date().toISOString().split('T')[0]}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Heure de début *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={formData.start_time}
|
||||||
|
onChange={(e) => setFormData({ ...formData, start_time: e.target.value })}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Heure de fin *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={formData.end_time}
|
||||||
|
onChange={(e) => setFormData({ ...formData, end_time: e.target.value })}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Objet de la réservation *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.purpose}
|
||||||
|
onChange={(e) => setFormData({ ...formData, purpose: e.target.value })}
|
||||||
|
required
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
placeholder="Formation Python, Réunion d'équipe, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vérification de disponibilité */}
|
||||||
|
{selectedRoom && formData.reservation_date && formData.start_time && formData.end_time && (
|
||||||
|
<div className={`p-3 rounded-lg ${
|
||||||
|
isSlotAvailable(selectedRoom, formData.reservation_date, formData.start_time, formData.end_time)
|
||||||
|
? 'bg-green-50 border border-green-200'
|
||||||
|
: 'bg-red-50 border border-red-200'
|
||||||
|
}`}>
|
||||||
|
{isSlotAvailable(selectedRoom, formData.reservation_date, formData.start_time, formData.end_time) ? (
|
||||||
|
<div className="flex items-center gap-2 text-green-700">
|
||||||
|
<CheckCircleIcon className="w-6 h-6" />
|
||||||
|
<span className="font-medium">Ce créneau est disponible !</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 text-red-700">
|
||||||
|
<XCircleIcon className="w-6 h-6" />
|
||||||
|
<span className="font-medium">Ce créneau est déjà réservé. Choisissez un autre horaire.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || (selectedRoom && formData.reservation_date && formData.start_time && formData.end_time && !isSlotAvailable(selectedRoom, formData.reservation_date, formData.start_time, formData.end_time))}
|
||||||
|
className="w-full bg-indigo-600 text-white py-3 px-4 rounded-lg hover:bg-indigo-700 transition-colors disabled:bg-gray-400 font-medium text-lg flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<ClockIcon className="w-5 h-5 animate-spin" />
|
||||||
|
Réservation en cours...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircleIcon className="w-5 h-5" />
|
||||||
|
Confirmer la réservation
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Onglet Liste des réservations */}
|
||||||
|
{activeTab === 'reservations' && (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-6 text-gray-800">
|
||||||
|
Liste des Réservations
|
||||||
|
</h2>
|
||||||
|
{reservations.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<p className="text-lg">Aucune réservation pour le moment</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{reservations.map((reservation) => (
|
||||||
|
<div
|
||||||
|
key={reservation.id}
|
||||||
|
className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h3 className="text-lg font-bold text-indigo-900">
|
||||||
|
{reservation.room_name}
|
||||||
|
</h3>
|
||||||
|
<span className="px-2 py-1 bg-indigo-100 text-indigo-800 text-xs rounded-full">
|
||||||
|
{reservation.user_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600">
|
||||||
|
<p className="flex items-center gap-1">
|
||||||
|
<CalendarIcon className="w-4 h-4" />
|
||||||
|
{new Date(reservation.reservation_date).toLocaleDateString('fr-FR')}
|
||||||
|
</p>
|
||||||
|
<p className="flex items-center gap-1">
|
||||||
|
<ClockIcon className="w-4 h-4" />
|
||||||
|
{reservation.start_time} - {reservation.end_time}
|
||||||
|
</p>
|
||||||
|
{reservation.user_email && (
|
||||||
|
<p className="col-span-2 flex items-center gap-1">
|
||||||
|
<EnvelopeIcon className="w-4 h-4" />
|
||||||
|
{reservation.user_email}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="col-span-2 flex items-center gap-1">
|
||||||
|
<DocumentTextIcon className="w-4 h-4" />
|
||||||
|
{reservation.purpose}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteReservation(reservation.id)}
|
||||||
|
className="ml-4 px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors font-medium flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-5 h-5" />
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "dockezr-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"next": "^14.0.3",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"@heroicons/react": "^2.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"@types/react": "^18.2.42",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"typescript": "^5.3.2",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"eslint-config-next": "^14.0.3",
|
||||||
|
"tailwindcss": "^3.3.5",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
export default config
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
@echo off
|
||||||
|
echo ========================================
|
||||||
|
echo Demarrage de Dockezr
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo Verification de Docker...
|
||||||
|
docker --version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERREUR] Docker n'est pas installe ou n'est pas dans le PATH
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Docker est installe !
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo Demarrage des conteneurs...
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Services demarres avec succes !
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo Frontend: http://localhost:3000
|
||||||
|
echo Backend API: http://localhost:8000
|
||||||
|
echo API Docs: http://localhost:8000/docs
|
||||||
|
echo.
|
||||||
|
echo Pour voir les logs: docker-compose logs -f
|
||||||
|
echo Pour arreter: docker-compose down
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
|
||||||
Reference in New Issue
Block a user