From 9add6d9ec4214e573aea349765dfaaf51950cbfc Mon Sep 17 00:00:00 2001 From: sartron_Northblue Date: Mon, 13 Oct 2025 14:03:21 +0400 Subject: [PATCH] Reservation de salle --- .dockerignore | 34 ++ .gitignore | 38 +++ DEPANNAGE.md | 139 ++++++++ GUIDE_UTILISATION.md | 187 +++++++++++ README.md | 371 +++++++++++++++++++++ backend/Dockerfile | 27 ++ backend/main.py | 256 +++++++++++++++ backend/requirements.txt | 8 + docker-compose.yml | 73 +++++ frontend/.eslintrc.json | 4 + frontend/Dockerfile | 21 ++ frontend/app/globals.css | 26 ++ frontend/app/layout.tsx | 23 ++ frontend/app/page.tsx | 625 ++++++++++++++++++++++++++++++++++++ frontend/next-env.d.ts | 5 + frontend/next.config.js | 10 + frontend/package.json | 30 ++ frontend/postcss.config.js | 7 + frontend/tailwind.config.ts | 15 + frontend/tsconfig.json | 28 ++ start.bat | 34 ++ stop.bat | 13 + 22 files changed, 1974 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 DEPANNAGE.md create mode 100644 GUIDE_UTILISATION.md create mode 100644 README.md create mode 100644 backend/Dockerfile create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 docker-compose.yml create mode 100644 frontend/.eslintrc.json create mode 100644 frontend/Dockerfile create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.js create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 start.bat create mode 100644 stop.bat diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..35f3217 --- /dev/null +++ b/.dockerignore @@ -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 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0edbee9 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/DEPANNAGE.md b/DEPANNAGE.md new file mode 100644 index 0000000..3599655 --- /dev/null +++ b/DEPANNAGE.md @@ -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 + diff --git a/GUIDE_UTILISATION.md b/GUIDE_UTILISATION.md new file mode 100644 index 0000000..1cc8338 --- /dev/null +++ b/GUIDE_UTILISATION.md @@ -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 ! 🎯** + diff --git a/README.md b/README.md new file mode 100644 index 0000000..189113c --- /dev/null +++ b/README.md @@ -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 +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 ! 🚀** + diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..84061b0 --- /dev/null +++ b/backend/Dockerfile @@ -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"] + diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..06575dc --- /dev/null +++ b/backend/main.py @@ -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"} + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..e084dc4 --- /dev/null +++ b/backend/requirements.txt @@ -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 + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2519640 --- /dev/null +++ b/docker-compose.yml @@ -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 + diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..f18272b --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": "next/core-web-vitals" +} + diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..262be09 --- /dev/null +++ b/frontend/Dockerfile @@ -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"] + diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..6f4cb36 --- /dev/null +++ b/frontend/app/globals.css @@ -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)); +} + diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..4d2901f --- /dev/null +++ b/frontend/app/layout.tsx @@ -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 ( + + {children} + + ) +} + diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..ada56e9 --- /dev/null +++ b/frontend/app/page.tsx @@ -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([]) + const [reservations, setReservations] = useState([]) + const [selectedRoom, setSelectedRoom] = useState(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 ( +
+
+ {/* Header */} +
+
+ +

+ Expernet +

+
+

+ Centre de Formation - Réservation de Salles +

+
+ + {/* Tabs */} +
+
+ + + +
+
+ + {/* Onglet Planning */} + {activeTab === 'planning' && ( +
+
+

+ Planning des Salles +

+
+ + 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" + /> +
+
+ + {/* Grille de planning */} +
+ + + + + {Array.from({ length: 14 }, (_, i) => i + 8).map((hour) => ( + + ))} + + + + {rooms.map((room) => { + const roomReservations = getReservationsForRoom(room.id, selectedDate) + + return ( + + + {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 ( + + ) + })} + + ) + })} + +
+ Salle + + {hour}h +
+ {room.name} +
+ + {room.capacity} pers. +
+
+ {isOccupied ? ( +
+ +
+ {reservation.user_name} +
+
+ ) : ( +
+ +
+ )} +
+
+ + {/* Légende */} +
+
+
+ +
+ Disponible +
+
+
+ +
+ Réservé +
+
+ + {/* Liste des réservations du jour */} +
+

+ Réservations du {new Date(selectedDate).toLocaleDateString('fr-FR')} +

+ {reservations.filter(r => r.reservation_date === selectedDate).length === 0 ? ( +

Aucune réservation pour cette date

+ ) : ( +
+ {reservations + .filter(r => r.reservation_date === selectedDate) + .sort((a, b) => a.start_time.localeCompare(b.start_time)) + .map((reservation) => ( +
+
+
+ {reservation.room_name} + ‱ + {reservation.start_time} - {reservation.end_time} +
+
+
{reservation.user_name}
+
{reservation.purpose}
+
+
+
+ ))} +
+ )} +
+
+ )} + + {/* Onglet Réservation */} + {activeTab === 'rooms' && ( +
+ {/* Liste des salles */} +
+

+ Nos Salles Disponibles +

+
+ {rooms.map((room) => ( +
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' + }`} + > +
+
+

+ {room.name} +

+

+ + Capacité: {room.capacity} personnes +

+ {room.equipment && ( +

+ + {room.equipment} +

+ )} + {room.description && ( +

+ {room.description} +

+ )} +
+ {selectedRoom === room.id && ( +
+ + + +
+ )} +
+
+ ))} +
+
+ + {/* Formulaire de réservation */} +
+

+ Formulaire de Réservation +

+ {!selectedRoom ? ( +
+ +

Sélectionnez une salle pour continuer

+
+ ) : ( +
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ +
+ +