Reservation de salle

This commit is contained in:
sartron_Northblue
2025-10-13 14:03:21 +04:00
commit 9add6d9ec4
22 changed files with 1974 additions and 0 deletions
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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
+187
View File
@@ -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 ! 🎯**
+371
View File
@@ -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 ! 🚀**
+27
View File
@@ -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
View File
@@ -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"}
+8
View File
@@ -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
+73
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "next/core-web-vitals"
}
+21
View File
@@ -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"]
+26
View File
@@ -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));
}
+23
View File
@@ -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>
)
}
+625
View File
@@ -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>
)
}
+5
View File
@@ -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.
+10
View File
@@ -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
+30
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+15
View File
@@ -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
+28
View File
@@ -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"]
}
+34
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
@echo off
echo ========================================
echo Arret de Dockezr
echo ========================================
echo.
docker-compose down
echo.
echo Services arretes avec succes !
echo.
pause