feat: Implement Historique and Upcoming Votes pages with styling and data fetching
- Added HistoriquePage component to display user's voting history with detailed statistics and vote cards. - Created UpcomingVotesPage component to show upcoming elections with a similar layout. - Developed CSS styles for both pages to enhance visual appeal and responsiveness. - Integrated API calls to fetch user's votes and upcoming elections. - Added a rebuild script for Docker environment setup and data restoration. - Created a Python script to populate the database with sample data for testing.
This commit is contained in:
parent
4b3da56c40
commit
8baabf528c
143
e-voting-system/.claude/BUILD_SOLUTION.md
Normal file
143
e-voting-system/.claude/BUILD_SOLUTION.md
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
# 🔧 Notes de Développement
|
||||||
|
|
||||||
|
## ✅ Solution: Build Frontend AVANT Docker
|
||||||
|
|
||||||
|
**Inspiré par:** L_Onomathoppee project
|
||||||
|
|
||||||
|
### Le Problème (Résolu ✅)
|
||||||
|
- **Ancien problème:** Docker build avec cache → changements React non visibles
|
||||||
|
- **Cause:** CRA buildait le React à chaque `docker-compose up --build`, mais le cache Docker gardait l'ancien résultat
|
||||||
|
- **Solution:** Build React **AVANT** Docker avec `npm run build`
|
||||||
|
|
||||||
|
### 🚀 Workflow Recommandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Éditer le code
|
||||||
|
vim frontend/src/pages/DashboardPage.jsx
|
||||||
|
|
||||||
|
# 2. Build et deploy (TOUT EN UN)
|
||||||
|
make build
|
||||||
|
|
||||||
|
# ✨ Les changements sont visibles immédiatement!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Comment ça fonctionne
|
||||||
|
|
||||||
|
1. ✅ `npm run build` dans `frontend/` → crée `frontend/build/`
|
||||||
|
2. ✅ Copie le build dans `build/frontend/`
|
||||||
|
3. ✅ Crée un Dockerfile qui utilise **Nginx** pour servir le build statique
|
||||||
|
4. ✅ Docker-compose lance les conteneurs avec le build frais
|
||||||
|
5. ✅ **BONUS:** Nginx optimise le cache des assets et gère React Router
|
||||||
|
|
||||||
|
### 📁 Structure après `make build`
|
||||||
|
|
||||||
|
```
|
||||||
|
build/
|
||||||
|
├── docker-compose.yml # Orchestration
|
||||||
|
├── init.sql # Init MariaDB
|
||||||
|
├── frontend/
|
||||||
|
│ ├── Dockerfile # Nginx + static files
|
||||||
|
│ ├── nginx.conf # Config React SPA (try_files)
|
||||||
|
│ └── [fichiers React compilés]
|
||||||
|
└── backend/
|
||||||
|
├── Dockerfile # Python FastAPI
|
||||||
|
├── pyproject.toml
|
||||||
|
└── [fichiers Python]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔑 Commandes principales
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build complet (recommandé après changements au code)
|
||||||
|
make build # Clean + npm build + docker build + deploy
|
||||||
|
|
||||||
|
# Redémarrage sans rebuild (si rien n'a changé au code)
|
||||||
|
make up # Juste redémarrer les conteneurs existants
|
||||||
|
|
||||||
|
# Arrêter les services
|
||||||
|
make down
|
||||||
|
|
||||||
|
# Voir les logs en temps réel
|
||||||
|
make logs-frontend # Logs du frontend (Nginx)
|
||||||
|
make logs-backend # Logs du backend (FastAPI)
|
||||||
|
|
||||||
|
# Nettoyer complètement
|
||||||
|
make clean # Supprime build/, frontend/build/, images Docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📝 Exemple: Corriger la Navigation Dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Éditer le fichier
|
||||||
|
vim frontend/src/pages/DashboardPage.jsx
|
||||||
|
# → Ajoute useLocation pour détecter les changements de route
|
||||||
|
|
||||||
|
# 2. Sauvegarder et builder
|
||||||
|
make build
|
||||||
|
# → npm run build → docker build → docker-compose up -d
|
||||||
|
|
||||||
|
# 3. Vérifier dans le navigateur
|
||||||
|
# http://localhost:3000/dashboard/actifs
|
||||||
|
# ✅ Le filtre change maintenant correctement!
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚙️ Scripts et Fichiers
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `build.sh` | Script de build complet (npm build + docker) |
|
||||||
|
| `Makefile` | Commandes pratiques (make build, make up, etc) |
|
||||||
|
| `build/docker-compose.yml` | Généré automatiquement, orchestration |
|
||||||
|
| `.claude/` | Documentation (ce fichier) |
|
||||||
|
|
||||||
|
### 🌐 URLs d'accès après `make build`
|
||||||
|
|
||||||
|
- **Frontend:** http://localhost:3000
|
||||||
|
- **Backend:** http://localhost:8000
|
||||||
|
- **Database:** localhost:3306
|
||||||
|
|
||||||
|
### ✨ Avantages de cette approche
|
||||||
|
|
||||||
|
✅ **Pas de cache Docker** → changements visibles **immédiatement**
|
||||||
|
✅ **Build production réel** → `npm run build` (pas de dev server)
|
||||||
|
✅ **Nginx optimisé** → Cache des assets, gestion React Router
|
||||||
|
✅ **Simple et rapide** → Une commande: `make build`
|
||||||
|
✅ **Production-ready** → Comme en production réelle
|
||||||
|
|
||||||
|
### ⚠️ Points importants
|
||||||
|
|
||||||
|
1. **Après modifier le frontend** → Toujours faire `make build`
|
||||||
|
2. **Après modifier le backend** → `make build` (ou `make up` si pas de changement à la structure)
|
||||||
|
3. **Pour nettoyer** → `make clean` (supprime tout, build à zéro)
|
||||||
|
4. **Les fichiers `build/`** → À .gitignore (fichiers générés)
|
||||||
|
|
||||||
|
### 🔍 Troubleshooting
|
||||||
|
|
||||||
|
**Les changements React ne sont pas visibles?**
|
||||||
|
```bash
|
||||||
|
make clean # Nettoie tout
|
||||||
|
make build # Rebuild from scratch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Port déjà utilisé?**
|
||||||
|
```bash
|
||||||
|
make down # Arrête les conteneurs
|
||||||
|
make up # Redémarre
|
||||||
|
```
|
||||||
|
|
||||||
|
**Voir ce qui se passe?**
|
||||||
|
```bash
|
||||||
|
cd build
|
||||||
|
docker-compose logs -f frontend # Voir tous les logs Nginx
|
||||||
|
docker-compose logs -f backend # Voir tous les logs FastAPI
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📚 Référence: Inspiré par L_Onomathoppee
|
||||||
|
|
||||||
|
Ce workflow est basé sur le projet L_Onomathoppee qui:
|
||||||
|
- Build le frontend React AVANT Docker
|
||||||
|
- Utilise Nginx pour servir les fichiers statiques
|
||||||
|
- Gère correctement React Router avec `try_files`
|
||||||
|
- Cache optimisé pour les assets
|
||||||
|
|
||||||
|
Voir: ~/L_Onomathoppee/build.sh pour la version complète
|
||||||
122
e-voting-system/.claude/DEV_NOTES.md
Normal file
122
e-voting-system/.claude/DEV_NOTES.md
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# 🔧 Notes de Développement
|
||||||
|
|
||||||
|
## Problème: Build statique vs Développement
|
||||||
|
|
||||||
|
### Le Problème
|
||||||
|
- **Production:** `docker-compose.yml` → build statique avec `npm run build` (fichiers pré-compilés)
|
||||||
|
- **Issue:** Les changements au frontend ne sont pas visibles car le build est en cache
|
||||||
|
- **Symptôme:** L'URL change (`/dashboard/actifs`) mais le contenu ne change pas
|
||||||
|
|
||||||
|
### Solution: Script de Rebuild Complet ⚡
|
||||||
|
|
||||||
|
Le problème du cache Docker est **résolu** avec un script qui:
|
||||||
|
1. ✅ Arrête tous les conteneurs
|
||||||
|
2. ✅ Supprime les images Docker en cache
|
||||||
|
3. ✅ Nettoie le build précédent
|
||||||
|
4. ✅ Rebuild tout avec `docker-compose up -d --build`
|
||||||
|
|
||||||
|
#### <20> Utilisation
|
||||||
|
|
||||||
|
**Option 1: Script direct (recommandé)**
|
||||||
|
```bash
|
||||||
|
./rebuild.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Makefile**
|
||||||
|
```bash
|
||||||
|
make rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux font exactement la même chose!
|
||||||
|
|
||||||
|
### 📊 Modes de Déploiement
|
||||||
|
|
||||||
|
#### 1️⃣ **Production (Build Statique)** ← À UTILISER pour le dev aussi
|
||||||
|
```bash
|
||||||
|
make rebuild # Rebuild complet, force le cache
|
||||||
|
make up # Simple redémarrage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Utiliser pour:**
|
||||||
|
- Tests finaux ✅
|
||||||
|
- Déploiement réel ✅
|
||||||
|
- Déploiement Docker ✅
|
||||||
|
|
||||||
|
#### 2️⃣ **Développement (Hot Reload)** ← Si vraiment tu veux npm start
|
||||||
|
```bash
|
||||||
|
make up-dev # npm start avec auto-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
**Utiliser pour:**
|
||||||
|
- Dev ultra-rapide (mais pas de build production)
|
||||||
|
- Testing local rapide
|
||||||
|
- Debugging React
|
||||||
|
|
||||||
|
### 📁 Fichiers de Configuration
|
||||||
|
|
||||||
|
| Fichier | Mode | Frontend | Backend |
|
||||||
|
|---------|------|----------|---------|
|
||||||
|
| `docker-compose.yml` | Production | `npm run build` + serve | `--reload` |
|
||||||
|
| `docker-compose.dev.yml` | Dev | `npm start` (hot reload) | `--reload` |
|
||||||
|
| `rebuild.sh` | Production | Force rebuild complet | N/A |
|
||||||
|
|
||||||
|
### 🚀 Workflow Recommandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Éditer le code
|
||||||
|
# vim frontend/src/pages/DashboardPage.jsx
|
||||||
|
|
||||||
|
# 2. Rebuild complet
|
||||||
|
make rebuild
|
||||||
|
|
||||||
|
# 3. Test dans le navigateur
|
||||||
|
# http://localhost:3000/dashboard/actifs
|
||||||
|
|
||||||
|
# ✅ Les changements sont appliqués!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔍 Debugging
|
||||||
|
|
||||||
|
**Voir les logs:**
|
||||||
|
```bash
|
||||||
|
make logs-frontend # Logs du frontend
|
||||||
|
make logs-backend # Logs du backend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nettoyer complètement:**
|
||||||
|
```bash
|
||||||
|
make clean # Prune + supprime les images
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Notes Importantes
|
||||||
|
|
||||||
|
1. **Script `rebuild.sh`:** Nettoie complètement et recompile
|
||||||
|
- Plus lent (~30-60s) mais garantit une build fraîche
|
||||||
|
- Idéal après changements majeurs
|
||||||
|
|
||||||
|
2. **`make up` simple:** Redémarrage rapide
|
||||||
|
- Utilise l'image précédente en cache
|
||||||
|
- Plus rapide mais peut avoir du cache résiduel
|
||||||
|
|
||||||
|
3. **En cas de problème:**
|
||||||
|
```bash
|
||||||
|
make clean # Nettoie tout
|
||||||
|
make rebuild # Rebuild du zéro
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📝 Exemple: Corriger la Navigation du Dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Éditer DashboardPage.jsx
|
||||||
|
vim frontend/src/pages/DashboardPage.jsx
|
||||||
|
|
||||||
|
# 2. Rebuild complet
|
||||||
|
make rebuild
|
||||||
|
|
||||||
|
# 3. Vérifier dans le navigateur
|
||||||
|
# http://localhost:3000/dashboard/actifs
|
||||||
|
# → Les changements sont visibles! ✨
|
||||||
|
|
||||||
|
# ✅ Le filtre change maintenant correctement
|
||||||
|
```
|
||||||
|
|
||||||
@ -1,21 +1,57 @@
|
|||||||
.PHONY: help up down test logs
|
.PHONY: help build up down logs logs-frontend logs-backend test clean restore-db
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "E-Voting System - Post-Quantum Cryptography"
|
@echo "E-Voting System - Post-Quantum Cryptography"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make up Démarrer (docker-compose up -d)"
|
@echo "🚀 MAIN COMMAND"
|
||||||
@echo " make down Arrêter (docker-compose down)"
|
@echo " make build 🔨 Build frontend + deploy (RECOMMANDÉ)"
|
||||||
@echo " make logs Voir les logs"
|
@echo ""
|
||||||
@echo " make test Tester (pytest)"
|
@echo "📦 PRODUCTION"
|
||||||
|
@echo " make up 🔄 Redémarrer (sans rebuild)"
|
||||||
|
@echo " make down ⏹️ Arrêter les conteneurs"
|
||||||
|
@echo " make restore-db 🗄️ Restaurer les données de test"
|
||||||
|
@echo ""
|
||||||
|
@echo "📊 LOGS"
|
||||||
|
@echo " make logs-frontend 📝 Logs du frontend"
|
||||||
|
@echo " make logs-backend 📝 Logs du backend"
|
||||||
|
@echo ""
|
||||||
|
@echo "🧪 TESTS"
|
||||||
|
@echo " make test 🧪 Tester (pytest)"
|
||||||
|
@echo " make clean 🗑️ Nettoyer complètement"
|
||||||
|
|
||||||
|
# Commande principale: build + deploy + restore DB
|
||||||
|
build:
|
||||||
|
./build.sh
|
||||||
|
@echo "⏳ Attendre le démarrage de la base de données (5 secondes)..."
|
||||||
|
@sleep 5
|
||||||
|
@echo "🗄️ Restauration des données de test..."
|
||||||
|
@python3 restore_data.py
|
||||||
|
|
||||||
|
# Redémarrage simple (utilise l'image existante)
|
||||||
up:
|
up:
|
||||||
docker-compose up -d
|
cd build && docker-compose up -d 2>/dev/null || docker-compose up -d
|
||||||
|
|
||||||
down:
|
down:
|
||||||
docker-compose down
|
cd build && docker-compose down 2>/dev/null || docker-compose down
|
||||||
|
|
||||||
logs:
|
# Restauration des données de test uniquement
|
||||||
docker-compose logs -f backend
|
restore-db:
|
||||||
|
@echo "🗄️ Restauration des données de test..."
|
||||||
|
@python3 restore_data.py
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs-frontend:
|
||||||
|
cd build && docker-compose logs -f frontend 2>/dev/null || echo "Conteneurs non trouvés. Lancez: make build"
|
||||||
|
|
||||||
|
logs-backend:
|
||||||
|
cd build && docker-compose logs -f backend 2>/dev/null || echo "Conteneurs non trouvés. Lancez: make build"
|
||||||
|
|
||||||
|
# Tests
|
||||||
test:
|
test:
|
||||||
pytest tests/ -v
|
pytest tests/ -v
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
clean:
|
||||||
|
rm -rf build/ frontend/build/ frontend/node_modules/.cache/ 2>/dev/null || true
|
||||||
|
docker system prune -f 2>/dev/null || true
|
||||||
|
docker image rm build-frontend build-backend 2>/dev/null || true
|
||||||
|
|||||||
@ -11,19 +11,20 @@ from ..models import Voter
|
|||||||
router = APIRouter(prefix="/api/elections", tags=["elections"])
|
router = APIRouter(prefix="/api/elections", tags=["elections"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/active", response_model=schemas.ElectionResponse)
|
@router.get("/active", response_model=list[schemas.ElectionResponse])
|
||||||
def get_active_election(db: Session = Depends(get_db)):
|
def get_active_elections(db: Session = Depends(get_db)):
|
||||||
"""Récupérer l'élection active en cours"""
|
"""Récupérer toutes les élections actives en cours (limité aux vraies élections actives)"""
|
||||||
|
from datetime import datetime
|
||||||
|
from .. import models
|
||||||
|
|
||||||
election = services.ElectionService.get_active_election(db)
|
now = datetime.utcnow()
|
||||||
|
active = db.query(models.Election).filter(
|
||||||
|
(models.Election.start_date <= now) &
|
||||||
|
(models.Election.end_date >= now) &
|
||||||
|
(models.Election.is_active == True) # Vérifier que is_active=1
|
||||||
|
).order_by(models.Election.id.asc()).limit(10).all() # Limiter à 10 max
|
||||||
|
|
||||||
if not election:
|
return active
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="No active election"
|
|
||||||
)
|
|
||||||
|
|
||||||
return election
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/completed")
|
@router.get("/completed")
|
||||||
@ -68,6 +69,21 @@ def get_active_election_results(db: Session = Depends(get_db)):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{election_id}/candidates")
|
||||||
|
def get_election_candidates(election_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Récupérer les candidats d'une élection"""
|
||||||
|
|
||||||
|
election = services.ElectionService.get_election(db, election_id)
|
||||||
|
|
||||||
|
if not election:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Election not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return election.candidates
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{election_id}", response_model=schemas.ElectionResponse)
|
@router.get("/{election_id}", response_model=schemas.ElectionResponse)
|
||||||
def get_election(election_id: int, db: Session = Depends(get_db)):
|
def get_election(election_id: int, db: Session = Depends(get_db)):
|
||||||
"""Récupérer une élection par son ID"""
|
"""Récupérer une élection par son ID"""
|
||||||
|
|||||||
@ -13,7 +13,84 @@ from ..crypto.hashing import SecureHash
|
|||||||
router = APIRouter(prefix="/api/votes", tags=["votes"])
|
router = APIRouter(prefix="/api/votes", tags=["votes"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/submit", response_model=schemas.VoteResponse)
|
@router.post("")
|
||||||
|
async def submit_simple_vote(
|
||||||
|
vote_data: dict,
|
||||||
|
current_voter: Voter = Depends(get_current_voter),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
request: Request = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Soumettre un vote simple avec just élection_id et nom du candidat.
|
||||||
|
Interface simplifiée pour l'application web.
|
||||||
|
"""
|
||||||
|
from .. import models
|
||||||
|
|
||||||
|
election_id = vote_data.get('election_id')
|
||||||
|
candidate_name = vote_data.get('choix')
|
||||||
|
|
||||||
|
if not election_id or not candidate_name:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="election_id and choix are required"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Vérifier que l'électeur n'a pas déjà voté
|
||||||
|
if services.VoteService.has_voter_voted(db, current_voter.id, election_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Voter has already voted in this election"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Vérifier que l'élection existe
|
||||||
|
election = services.ElectionService.get_election(db, election_id)
|
||||||
|
if not election:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Election not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trouver le candidat par nom
|
||||||
|
candidate = db.query(models.Candidate).filter(
|
||||||
|
models.Candidate.name == candidate_name,
|
||||||
|
models.Candidate.election_id == election_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not candidate:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Candidate not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enregistrer le vote (sans chiffrement pour l'MVP)
|
||||||
|
import time
|
||||||
|
from ..crypto.hashing import SecureHash
|
||||||
|
|
||||||
|
ballot_hash = SecureHash.hash_bulletin(
|
||||||
|
vote_id=current_voter.id,
|
||||||
|
candidate_id=candidate.id,
|
||||||
|
timestamp=int(time.time())
|
||||||
|
)
|
||||||
|
|
||||||
|
vote = services.VoteService.record_vote(
|
||||||
|
db=db,
|
||||||
|
voter_id=current_voter.id,
|
||||||
|
election_id=election_id,
|
||||||
|
candidate_id=candidate.id,
|
||||||
|
encrypted_vote=b"", # Empty for MVP
|
||||||
|
ballot_hash=ballot_hash,
|
||||||
|
ip_address=request.client.host if request else None
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Vote recorded successfully",
|
||||||
|
"id": vote.id,
|
||||||
|
"ballot_hash": ballot_hash,
|
||||||
|
"timestamp": vote.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def submit_vote(
|
async def submit_vote(
|
||||||
vote_bulletin: schemas.VoteBulletin,
|
vote_bulletin: schemas.VoteBulletin,
|
||||||
current_voter: Voter = Depends(get_current_voter),
|
current_voter: Voter = Depends(get_current_voter),
|
||||||
@ -141,13 +218,21 @@ def get_voter_history(
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if election:
|
if election:
|
||||||
|
# Déterminer le statut de l'élection
|
||||||
|
if election.start_date > datetime.utcnow():
|
||||||
|
status = "upcoming"
|
||||||
|
elif election.end_date < datetime.utcnow():
|
||||||
|
status = "closed"
|
||||||
|
else:
|
||||||
|
status = "active"
|
||||||
|
|
||||||
history.append({
|
history.append({
|
||||||
"vote_id": vote.id,
|
"vote_id": vote.id,
|
||||||
"election_id": election.id,
|
"election_id": election.id,
|
||||||
"election_name": election.name,
|
"election_name": election.name,
|
||||||
"candidate_name": candidate.name if candidate else "Unknown",
|
"candidate_name": candidate.name if candidate else "Unknown",
|
||||||
"vote_date": vote.timestamp,
|
"vote_date": vote.timestamp,
|
||||||
"election_status": "completed" if election.end_date < datetime.utcnow() else "active"
|
"election_status": status
|
||||||
})
|
})
|
||||||
|
|
||||||
return history
|
return history
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from typing import Optional, List
|
|||||||
|
|
||||||
class VoterRegister(BaseModel):
|
class VoterRegister(BaseModel):
|
||||||
"""Enregistrement d'un électeur"""
|
"""Enregistrement d'un électeur"""
|
||||||
email: EmailStr
|
email: str
|
||||||
password: str = Field(..., min_length=8)
|
password: str = Field(..., min_length=8)
|
||||||
first_name: str
|
first_name: str
|
||||||
last_name: str
|
last_name: str
|
||||||
@ -18,7 +18,7 @@ class VoterRegister(BaseModel):
|
|||||||
|
|
||||||
class VoterLogin(BaseModel):
|
class VoterLogin(BaseModel):
|
||||||
"""Authentification"""
|
"""Authentification"""
|
||||||
email: EmailStr
|
email: str
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,182 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
Script de réinitialisation et de peuplement de la base de données pour tests.
|
SEED SCRIPT SUPPRIMÉ
|
||||||
|
|
||||||
Usage (depuis la racine du projet ou à l'intérieur du conteneur):
|
La base de données n'est plus peuplée automatiquement.
|
||||||
python -m backend.scripts.seed_db
|
Seul l'utilisateur paul.roost@epita.fr existe en base.
|
||||||
|
|
||||||
Ce script supprime/crée les tables et insère des électeurs, élections,
|
Pour ajouter des données:
|
||||||
candidats et votes d'exemple selon la demande de l'utilisateur.
|
- Utilisez l'API REST
|
||||||
|
- Ou des scripts SQL personnalisés
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from ..database import engine, SessionLocal
|
|
||||||
from ..models import Base, Voter, Election, Candidate, Vote
|
|
||||||
from ..auth import hash_password
|
|
||||||
|
|
||||||
|
|
||||||
def reset_db():
|
|
||||||
print("Resetting database: dropping and recreating all tables...")
|
|
||||||
Base.metadata.drop_all(bind=engine)
|
|
||||||
Base.metadata.create_all(bind=engine)
|
|
||||||
|
|
||||||
|
|
||||||
def seed():
|
|
||||||
print("Seeding database with sample voters, elections, candidates and votes...")
|
|
||||||
db: Session = SessionLocal()
|
|
||||||
try:
|
|
||||||
now = datetime.utcnow()
|
|
||||||
|
|
||||||
# Create voters with the requested CNI values
|
|
||||||
voters = []
|
|
||||||
for cid in ["1234", "12345", "123456"]:
|
|
||||||
v = Voter(
|
|
||||||
email=f"user{cid}@example.com",
|
|
||||||
password_hash=hash_password("Password123"),
|
|
||||||
first_name=f"User{cid}",
|
|
||||||
last_name="Seed",
|
|
||||||
citizen_id=cid
|
|
||||||
)
|
|
||||||
db.add(v)
|
|
||||||
voters.append(v)
|
|
||||||
db.commit()
|
|
||||||
for v in voters:
|
|
||||||
db.refresh(v)
|
|
||||||
|
|
||||||
# Create a few elections: past, present, future
|
|
||||||
elections = []
|
|
||||||
|
|
||||||
# Past elections (ended 1 month ago)
|
|
||||||
for i in range(1, 4):
|
|
||||||
e = Election(
|
|
||||||
name=f"Past Election {i}",
|
|
||||||
description="Election seed (past)",
|
|
||||||
start_date=now - timedelta(days=60 + i),
|
|
||||||
end_date=now - timedelta(days=30 + i),
|
|
||||||
is_active=False,
|
|
||||||
results_published=True
|
|
||||||
)
|
|
||||||
db.add(e)
|
|
||||||
elections.append(e)
|
|
||||||
|
|
||||||
# Current election (ends in ~3 months)
|
|
||||||
current = Election(
|
|
||||||
name="Ongoing Election",
|
|
||||||
description="Election seed (present)",
|
|
||||||
start_date=now - timedelta(days=1),
|
|
||||||
end_date=now + timedelta(days=90),
|
|
||||||
is_active=True,
|
|
||||||
results_published=False
|
|
||||||
)
|
|
||||||
db.add(current)
|
|
||||||
elections.append(current)
|
|
||||||
|
|
||||||
# Future elections
|
|
||||||
for i in range(1, 4):
|
|
||||||
e = Election(
|
|
||||||
name=f"Future Election {i}",
|
|
||||||
description="Election seed (future)",
|
|
||||||
start_date=now + timedelta(days=30 * i),
|
|
||||||
end_date=now + timedelta(days=30 * i + 14),
|
|
||||||
is_active=False,
|
|
||||||
results_published=False
|
|
||||||
)
|
|
||||||
db.add(e)
|
|
||||||
elections.append(e)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
for e in elections:
|
|
||||||
db.refresh(e)
|
|
||||||
|
|
||||||
# Create 2-3 candidates per election
|
|
||||||
all_candidates = {}
|
|
||||||
for e in elections:
|
|
||||||
cands = []
|
|
||||||
for j in range(1, 4):
|
|
||||||
c = Candidate(
|
|
||||||
election_id=e.id,
|
|
||||||
name=f"Candidate {j} for {e.name}",
|
|
||||||
description="Candidate from seed",
|
|
||||||
order=j
|
|
||||||
)
|
|
||||||
db.add(c)
|
|
||||||
cands.append(c)
|
|
||||||
db.commit()
|
|
||||||
for c in cands:
|
|
||||||
db.refresh(c)
|
|
||||||
all_candidates[e.id] = cands
|
|
||||||
|
|
||||||
# Insert votes: Distribute votes among past/present/future
|
|
||||||
# For the CNI '1234' -> 3 past, 1 present, 2 future
|
|
||||||
# For '12345' and '123456' do similar but not the exact same elections
|
|
||||||
def create_vote(voter_obj, election_obj, candidate_obj, when):
|
|
||||||
vote = Vote(
|
|
||||||
voter_id=voter_obj.id,
|
|
||||||
election_id=election_obj.id,
|
|
||||||
candidate_id=candidate_obj.id,
|
|
||||||
encrypted_vote=b"seeded_encrypted",
|
|
||||||
zero_knowledge_proof=b"seeded_proof",
|
|
||||||
ballot_hash=f"seed-{voter_obj.citizen_id}-{election_obj.id}-{candidate_obj.id}",
|
|
||||||
timestamp=when
|
|
||||||
)
|
|
||||||
db.add(vote)
|
|
||||||
|
|
||||||
# Helper to pick candidate
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Map citizen to required vote counts
|
|
||||||
plan = {
|
|
||||||
"1234": {"past": 3, "present": 1, "future": 2},
|
|
||||||
"12345": {"past": 2, "present": 1, "future": 2},
|
|
||||||
"123456": {"past": 1, "present": 1, "future": 3},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect elections by status
|
|
||||||
past_elections = [e for e in elections if e.end_date < now]
|
|
||||||
present_elections = [e for e in elections if e.start_date <= now < e.end_date]
|
|
||||||
future_elections = [e for e in elections if e.start_date > now]
|
|
||||||
|
|
||||||
for voter in voters:
|
|
||||||
p = plan.get(voter.citizen_id, {"past": 1, "present": 0, "future": 1})
|
|
||||||
|
|
||||||
# Past votes
|
|
||||||
for _ in range(p.get("past", 0)):
|
|
||||||
if not past_elections:
|
|
||||||
break
|
|
||||||
e = random.choice(past_elections)
|
|
||||||
c = random.choice(all_candidates[e.id])
|
|
||||||
create_vote(voter, e, c, when=e.end_date - timedelta(days=5))
|
|
||||||
|
|
||||||
# Present votes
|
|
||||||
for _ in range(p.get("present", 0)):
|
|
||||||
if not present_elections:
|
|
||||||
break
|
|
||||||
e = random.choice(present_elections)
|
|
||||||
c = random.choice(all_candidates[e.id])
|
|
||||||
create_vote(voter, e, c, when=now)
|
|
||||||
|
|
||||||
# Future votes (we still create them as scheduled/placeholder votes)
|
|
||||||
for _ in range(p.get("future", 0)):
|
|
||||||
if not future_elections:
|
|
||||||
break
|
|
||||||
e = random.choice(future_elections)
|
|
||||||
c = random.choice(all_candidates[e.id])
|
|
||||||
create_vote(voter, e, c, when=e.start_date + timedelta(days=1))
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Report
|
|
||||||
total_voters = db.query(Voter).count()
|
|
||||||
total_elections = db.query(Election).count()
|
|
||||||
total_votes = db.query(Vote).count()
|
|
||||||
print(f"Seeded: voters={total_voters}, elections={total_elections}, votes={total_votes}")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
reset_db()
|
print("ℹ️ Ce script a été supprimé.")
|
||||||
seed()
|
print("La base de données est maintenant gérée manuellement.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
229
e-voting-system/build.sh
Executable file
229
e-voting-system/build.sh
Executable file
@ -0,0 +1,229 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# E-Voting System - Build Script
|
||||||
|
# Build le frontend React AVANT Docker pour éviter les problèmes de cache
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${BLUE}ℹ️ $1${NC}"; }
|
||||||
|
log_success() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||||
|
log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
|
||||||
|
log_error() { echo -e "${RED}❌ $1${NC}"; }
|
||||||
|
log_title() { echo -e "${BLUE}\n=== $1 ===${NC}\n"; }
|
||||||
|
|
||||||
|
error_exit() {
|
||||||
|
log_error "$1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérifier qu'on est dans le bon répertoire
|
||||||
|
if [ ! -d "frontend" ] || [ ! -d "backend" ]; then
|
||||||
|
error_exit "Ce script doit être exécuté depuis la racine du projet (e-voting-system/)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(pwd)"
|
||||||
|
BUILD_DIR="$PROJECT_ROOT/build"
|
||||||
|
|
||||||
|
log_title "🏗️ Build E-Voting System - Frontend & Backend"
|
||||||
|
|
||||||
|
# 1. Arrêter les conteneurs actuels
|
||||||
|
log_info "Arrêt des conteneurs existants..."
|
||||||
|
docker-compose down 2>/dev/null || true
|
||||||
|
|
||||||
|
# 2. Nettoyer les builds précédents
|
||||||
|
log_info "Nettoyage des builds précédents..."
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
mkdir -p "$BUILD_DIR"/{frontend,backend}
|
||||||
|
rm -rf frontend/build frontend/node_modules/.cache
|
||||||
|
|
||||||
|
# 3. Installer les dépendances frontend
|
||||||
|
log_info "Installation des dépendances frontend..."
|
||||||
|
cd "$PROJECT_ROOT/frontend"
|
||||||
|
npm install --legacy-peer-deps || error_exit "Échec de l'installation frontend"
|
||||||
|
|
||||||
|
# 4. Build React (production)
|
||||||
|
log_info "Build du frontend React..."
|
||||||
|
npm run build || error_exit "Échec du build React"
|
||||||
|
|
||||||
|
# 5. Copier le build dans le répertoire de déploiement
|
||||||
|
log_success "Frontend buildé"
|
||||||
|
cp -r "$PROJECT_ROOT/frontend/build"/* "$BUILD_DIR/frontend/" || error_exit "Erreur lors de la copie du build"
|
||||||
|
log_success "Build frontend copié vers $BUILD_DIR/frontend/"
|
||||||
|
|
||||||
|
# 6. Créer le Dockerfile pour le frontend (serveur avec serve)
|
||||||
|
log_info "Création du Dockerfile frontend..."
|
||||||
|
cat > "$BUILD_DIR/frontend/Dockerfile" << 'EOF'
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installer serve pour servir les fichiers statiques
|
||||||
|
RUN npm install -g serve
|
||||||
|
|
||||||
|
# Copier les fichiers buildés
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Servir les fichiers sur le port 3000
|
||||||
|
CMD ["serve", "-s", ".", "-l", "3000"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_success "Dockerfile frontend créé"
|
||||||
|
|
||||||
|
# 6.5 Configuration Nginx pour React SPA - SUPPRIMÉE (on utilise serve à la place)
|
||||||
|
|
||||||
|
# 8. Copier les fichiers backend
|
||||||
|
log_info "Préparation du backend..."
|
||||||
|
cp -r "$PROJECT_ROOT/backend"/* "$BUILD_DIR/backend/" || true
|
||||||
|
|
||||||
|
# Copier pyproject.toml et poetry.lock
|
||||||
|
cp "$PROJECT_ROOT/pyproject.toml" "$BUILD_DIR/backend/" 2>/dev/null || true
|
||||||
|
cp "$PROJECT_ROOT/poetry.lock" "$BUILD_DIR/backend/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Créer le Dockerfile pour le backend adapté à cette structure
|
||||||
|
log_info "Création du Dockerfile backend..."
|
||||||
|
cat > "$BUILD_DIR/backend/Dockerfile" << 'EOF'
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installer les dépendances système
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Installer Poetry
|
||||||
|
RUN pip install --no-cache-dir poetry
|
||||||
|
|
||||||
|
# Copier les fichiers de configuration
|
||||||
|
COPY pyproject.toml poetry.lock* ./
|
||||||
|
|
||||||
|
# Installer les dépendances Python
|
||||||
|
RUN poetry config virtualenvs.create false && \
|
||||||
|
poetry install --no-interaction --no-ansi --no-root
|
||||||
|
|
||||||
|
# Copier le code backend
|
||||||
|
COPY . ./backend/
|
||||||
|
|
||||||
|
# Exposer le port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Démarrer l'application
|
||||||
|
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_success "Backend copié vers $BUILD_DIR/backend/"
|
||||||
|
|
||||||
|
# 9. Mettre à jour docker-compose.yml pour utiliser les Dockerfiles depuis build/
|
||||||
|
log_info "Création du docker-compose.yml..."
|
||||||
|
cat > "$BUILD_DIR/docker-compose.yml" << 'EOF'
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: evoting_db
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpass123}
|
||||||
|
MYSQL_DATABASE: ${DB_NAME:-evoting_db}
|
||||||
|
MYSQL_USER: ${DB_USER:-evoting_user}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-3306}:3306"
|
||||||
|
volumes:
|
||||||
|
- evoting_data:/var/lib/mysql
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- evoting_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "--silent"]
|
||||||
|
timeout: 20s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: evoting_backend
|
||||||
|
environment:
|
||||||
|
DB_HOST: mariadb
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_NAME: ${DB_NAME:-evoting_db}
|
||||||
|
DB_USER: ${DB_USER:-evoting_user}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
|
||||||
|
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production}
|
||||||
|
DEBUG: ${DEBUG:-false}
|
||||||
|
ports:
|
||||||
|
- "${BACKEND_PORT:-8000}:8000"
|
||||||
|
depends_on:
|
||||||
|
mariadb:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- evoting_network
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
container_name: evoting_frontend
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT:-3000}:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- evoting_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
evoting_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
evoting_network:
|
||||||
|
driver: bridge
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_success "docker-compose.yml créé"
|
||||||
|
|
||||||
|
cd "$BUILD_DIR"
|
||||||
|
|
||||||
|
# Copier init.sql pour MariaDB
|
||||||
|
cp "$PROJECT_ROOT/docker/init.sql" "$BUILD_DIR/init.sql" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 10. Docker build
|
||||||
|
log_info "Build des images Docker..."
|
||||||
|
docker-compose build || error_exit "Erreur lors du build Docker"
|
||||||
|
|
||||||
|
log_success "Images Docker buildées"
|
||||||
|
|
||||||
|
# 11. Démarrer les conteneurs
|
||||||
|
log_info "Démarrage des conteneurs..."
|
||||||
|
docker-compose up -d || error_exit "Erreur lors du démarrage"
|
||||||
|
|
||||||
|
log_success "Conteneurs démarrés"
|
||||||
|
|
||||||
|
# 12. Afficher le résumé
|
||||||
|
echo ""
|
||||||
|
log_title "✅ BUILD COMPLET!"
|
||||||
|
echo ""
|
||||||
|
log_info "📊 État des services:"
|
||||||
|
docker-compose ps
|
||||||
|
echo ""
|
||||||
|
log_info "🌐 URLs d'accès:"
|
||||||
|
echo " Frontend: http://localhost:3000"
|
||||||
|
echo " Backend: http://localhost:8000"
|
||||||
|
echo ""
|
||||||
|
log_info "📝 Pour voir les logs:"
|
||||||
|
echo " docker-compose logs -f frontend"
|
||||||
|
echo " docker-compose logs -f backend"
|
||||||
|
echo ""
|
||||||
|
log_warning "📁 Les fichiers sont dans: $BUILD_DIR/"
|
||||||
|
echo " Pour redémarrer: cd $BUILD_DIR && docker-compose up -d"
|
||||||
73
e-voting-system/docker-compose.dev.yml
Normal file
73
e-voting-system/docker-compose.dev.yml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: evoting_db
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpass123}
|
||||||
|
MYSQL_DATABASE: ${DB_NAME:-evoting_db}
|
||||||
|
MYSQL_USER: ${DB_USER:-evoting_user}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-3306}:3306"
|
||||||
|
volumes:
|
||||||
|
- evoting_data:/var/lib/mysql
|
||||||
|
- ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- evoting_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "--silent"]
|
||||||
|
timeout: 20s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.backend
|
||||||
|
container_name: evoting_backend
|
||||||
|
environment:
|
||||||
|
DB_HOST: mariadb
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_NAME: ${DB_NAME:-evoting_db}
|
||||||
|
DB_USER: ${DB_USER:-evoting_user}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
|
||||||
|
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production}
|
||||||
|
DEBUG: ${DEBUG:-false}
|
||||||
|
ports:
|
||||||
|
- "${BACKEND_PORT:-8000}:8000"
|
||||||
|
depends_on:
|
||||||
|
mariadb:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app/backend
|
||||||
|
networks:
|
||||||
|
- evoting_network
|
||||||
|
command: uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.frontend.dev
|
||||||
|
args:
|
||||||
|
REACT_APP_API_URL: http://backend:8000
|
||||||
|
container_name: evoting_frontend
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT:-3000}:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
volumes:
|
||||||
|
- ./frontend/src:/app/src
|
||||||
|
- ./frontend/public:/app/public
|
||||||
|
networks:
|
||||||
|
- evoting_network
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
evoting_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
evoting_network:
|
||||||
|
driver: bridge
|
||||||
21
e-voting-system/docker/Dockerfile.frontend.dev
Normal file
21
e-voting-system/docker/Dockerfile.frontend.dev
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copier package.json
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
|
||||||
|
# Installer dépendances
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
# Copier code source
|
||||||
|
COPY frontend/ .
|
||||||
|
|
||||||
|
# Build argument for API URL
|
||||||
|
ARG REACT_APP_API_URL=http://backend:8000
|
||||||
|
ENV REACT_APP_API_URL=${REACT_APP_API_URL}
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Mode développement avec hot reload (npm start)
|
||||||
|
CMD ["npm", "start"]
|
||||||
420
e-voting-system/docker/populate_past_elections.sql
Normal file
420
e-voting-system/docker/populate_past_elections.sql
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
-- ================================================================
|
||||||
|
-- Population de la base de données avec élections passées
|
||||||
|
-- 10 élections passées avec ~600 utilisateurs et leurs votes
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
-- Désactiver les contraintes temporairement
|
||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- 1. Créer les 10 élections passées
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
INSERT INTO elections (name, description, start_date, end_date, elgamal_p, elgamal_g, is_active, results_published)
|
||||||
|
VALUES
|
||||||
|
('Présidentielle 2020', 'Election présidentielle - Tour 1', '2020-04-10 08:00:00', '2020-04-10 20:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Législatives 2020', 'Elections législatives nationales', '2020-06-07 08:00:00', '2020-06-07 19:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Européennes 2019', 'Elections au Parlement européen', '2019-05-26 08:00:00', '2019-05-26 20:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Régionales 2021', 'Elections régionales et départementales', '2021-06-20 08:00:00', '2021-06-20 20:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Municipales 2020', 'Elections municipales', '2020-03-15 08:00:00', '2020-03-15 19:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Référendum 2022', 'Référendum constitutionnel', '2022-09-04 08:00:00', '2022-09-04 20:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Sénatoriales 2020', 'Election du Sénat', '2020-09-27 08:00:00', '2020-09-27 19:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Présidentielle 2022', 'Election présidentielle - 2022', '2022-04-10 08:00:00', '2022-04-10 20:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Locales 2023', 'Elections locales complémentaires', '2023-02-12 08:00:00', '2023-02-12 19:00:00', 23, 5, FALSE, TRUE),
|
||||||
|
('Référendum 2023', 'Référendum sur la réforme', '2023-07-09 08:00:00', '2023-07-09 20:00:00', 23, 5, FALSE, TRUE);
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- 2. Créer des candidats simples pour chaque élection (3-5 par élection)
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
-- Election 1: Présidentielle 2020
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(1, 'Candidat A', 'Parti de gauche', 1),
|
||||||
|
(1, 'Candidat B', 'Parti de centre', 2),
|
||||||
|
(1, 'Candidat C', 'Parti de droite', 3),
|
||||||
|
(1, 'Candidat D', 'Parti écologiste', 4);
|
||||||
|
|
||||||
|
-- Election 2: Législatives 2020
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(2, 'Liste 1', 'Coalition de gauche', 1),
|
||||||
|
(2, 'Liste 2', 'Majorité sortante', 2),
|
||||||
|
(2, 'Liste 3', 'Opposition', 3);
|
||||||
|
|
||||||
|
-- Election 3: Européennes 2019
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(3, 'Liste PS', 'Socialistes', 1),
|
||||||
|
(3, 'Liste LREM', 'Libéraux', 2),
|
||||||
|
(3, 'Liste LR', 'Conservateurs', 3),
|
||||||
|
(3, 'Liste Verts', 'Écologistes', 4),
|
||||||
|
(3, 'Liste RN', 'Populistes', 5);
|
||||||
|
|
||||||
|
-- Election 4: Régionales 2021
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(4, 'Région 1 - Liste A', 'Sortante', 1),
|
||||||
|
(4, 'Région 1 - Liste B', 'Opposition', 2),
|
||||||
|
(4, 'Région 1 - Liste C', 'Alternative', 3);
|
||||||
|
|
||||||
|
-- Election 5: Municipales 2020
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(5, 'Ville - Liste Sortante', 'Équipe en place', 1),
|
||||||
|
(5, 'Ville - Liste A', 'Opposition', 2),
|
||||||
|
(5, 'Ville - Liste B', 'Alternatif', 3);
|
||||||
|
|
||||||
|
-- Election 6: Référendum 2022
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(6, 'OUI', 'Pour la réforme', 1),
|
||||||
|
(6, 'NON', 'Contre la réforme', 2);
|
||||||
|
|
||||||
|
-- Election 7: Sénatoriales 2020
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(7, 'Sénateur 1', 'Parti A', 1),
|
||||||
|
(7, 'Sénateur 2', 'Parti B', 2),
|
||||||
|
(7, 'Sénateur 3', 'Parti C', 3);
|
||||||
|
|
||||||
|
-- Election 8: Présidentielle 2022
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(8, 'Sortant', 'Président sortant', 1),
|
||||||
|
(8, 'Challenger 1', 'Candidat A', 2),
|
||||||
|
(8, 'Challenger 2', 'Candidat B', 3),
|
||||||
|
(8, 'Challenger 3', 'Candidat C', 4);
|
||||||
|
|
||||||
|
-- Election 9: Locales 2023
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(9, 'Commune A', 'Liste 1', 1),
|
||||||
|
(9, 'Commune A', 'Liste 2', 2);
|
||||||
|
|
||||||
|
-- Election 10: Référendum 2023
|
||||||
|
INSERT INTO candidates (election_id, name, description, `order`)
|
||||||
|
VALUES
|
||||||
|
(10, 'OUI', 'Approbation', 1),
|
||||||
|
(10, 'NON', 'Rejet', 2),
|
||||||
|
(10, 'BLANC', 'Vote blanc', 3);
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- 3. Créer ~600 utilisateurs et leurs votes
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 1 (100 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e1_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election1_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E1_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2020-04-10 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
|
||||||
|
(SELECT 0 UNION SELECT 1) t4,
|
||||||
|
(SELECT @row:=-1) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 100;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 2 (100 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e2_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election2_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E2_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2020-06-07 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
|
||||||
|
(SELECT 0 UNION SELECT 1) t4,
|
||||||
|
(SELECT @row:=99) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 200;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 3 (100 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e3_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election3_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E3_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2019-05-26 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
|
||||||
|
(SELECT 0 UNION SELECT 1) t4,
|
||||||
|
(SELECT @row:=199) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 300;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 4 (100 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e4_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election4_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E4_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2021-06-20 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
|
||||||
|
(SELECT 0 UNION SELECT 1) t4,
|
||||||
|
(SELECT @row:=299) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 400;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 5 (100 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e5_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election5_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E5_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2020-03-15 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
|
||||||
|
(SELECT 0 UNION SELECT 1) t4,
|
||||||
|
(SELECT @row:=399) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 500;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 6 (60 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e6_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election6_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E6_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2022-09-04 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t3,
|
||||||
|
(SELECT @row:=499) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 560;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 7 (50 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e7_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election7_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E7_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2020-09-27 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
|
||||||
|
(SELECT @row:=559) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 610;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 8 (50 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e8_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election8_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E8_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2022-04-10 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
|
||||||
|
(SELECT @row:=609) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 660;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 9 (50 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e9_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election9_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E9_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2023-02-12 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
|
||||||
|
(SELECT @row:=659) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 710;
|
||||||
|
|
||||||
|
-- Utilisateurs pour l'élection 10 (50 utilisateurs)
|
||||||
|
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
|
||||||
|
SELECT
|
||||||
|
CONCAT('user_e10_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
|
||||||
|
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
|
||||||
|
CONCAT('FirstName', seq.id) as first_name,
|
||||||
|
CONCAT('Election10_', seq.id) as last_name,
|
||||||
|
CONCAT('CNI_E10_', LPAD(seq.id, 4, '0')) as citizen_id,
|
||||||
|
TRUE as has_voted,
|
||||||
|
DATE_SUB('2023-07-09 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
|
||||||
|
FROM (
|
||||||
|
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
|
||||||
|
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
|
||||||
|
(SELECT @row:=709) t0
|
||||||
|
) seq
|
||||||
|
WHERE seq.id < 760;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- 4. Créer les votes pour chaque utilisateur
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
-- Votes pour l'élection 1
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
1 as election_id,
|
||||||
|
(1 + (v.id % 4)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 1))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_1'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2020-04-10 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e1_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 2
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
2 as election_id,
|
||||||
|
(5 + (v.id % 3)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 2))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_2'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2020-06-07 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e2_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 3
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
3 as election_id,
|
||||||
|
(8 + (v.id % 5)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 3))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_3'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2019-05-26 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e3_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 4
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
4 as election_id,
|
||||||
|
(13 + (v.id % 3)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 4))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_4'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2021-06-20 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e4_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 5
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
5 as election_id,
|
||||||
|
(16 + (v.id % 3)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 5))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_5'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2020-03-15 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e5_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 6
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
6 as election_id,
|
||||||
|
(19 + (v.id % 2)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 6))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_6'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2022-09-04 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e6_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 7
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
7 as election_id,
|
||||||
|
(21 + (v.id % 3)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 7))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_7'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2020-09-27 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e7_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 8
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
8 as election_id,
|
||||||
|
(24 + (v.id % 4)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 8))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_8'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2022-04-10 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e8_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 9
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
9 as election_id,
|
||||||
|
(28 + (v.id % 2)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 9))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_9'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2023-02-12 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e9_%';
|
||||||
|
|
||||||
|
-- Votes pour l'élection 10
|
||||||
|
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
|
||||||
|
SELECT
|
||||||
|
v.id as voter_id,
|
||||||
|
10 as election_id,
|
||||||
|
(30 + (v.id % 3)) as candidate_id,
|
||||||
|
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 10))) as encrypted_vote,
|
||||||
|
SHA2(CONCAT('ballot_', v.id, '_10'), 256) as ballot_hash,
|
||||||
|
DATE_SUB('2023-07-09 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
|
||||||
|
FROM voters v
|
||||||
|
WHERE v.email LIKE 'user_e10_%';
|
||||||
|
|
||||||
|
-- Réactiver les contraintes
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- Confirmation
|
||||||
|
-- ================================================================
|
||||||
|
SELECT 'Population complète!' as status;
|
||||||
|
SELECT COUNT(*) as total_voters FROM voters;
|
||||||
|
SELECT COUNT(*) as total_elections FROM elections;
|
||||||
|
SELECT COUNT(*) as total_votes FROM votes;
|
||||||
@ -15,6 +15,10 @@ import RegisterPage from './pages/RegisterPage';
|
|||||||
import DashboardPage from './pages/DashboardPage';
|
import DashboardPage from './pages/DashboardPage';
|
||||||
import VotingPage from './pages/VotingPage';
|
import VotingPage from './pages/VotingPage';
|
||||||
import ArchivesPage from './pages/ArchivesPage';
|
import ArchivesPage from './pages/ArchivesPage';
|
||||||
|
import ElectionDetailsPage from './pages/ElectionDetailsPage';
|
||||||
|
import HistoriquePage from './pages/HistoriquePage';
|
||||||
|
import ActiveVotesPage from './pages/ActiveVotesPage';
|
||||||
|
import UpcomingVotesPage from './pages/UpcomingVotesPage';
|
||||||
import ProfilePage from './pages/ProfilePage';
|
import ProfilePage from './pages/ProfilePage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -77,6 +81,7 @@ function App() {
|
|||||||
<LoginPage onLogin={handleLogin} />
|
<LoginPage onLogin={handleLogin} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* Dashboard Routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
@ -85,42 +90,56 @@ function App() {
|
|||||||
<Navigate to="/login" replace />
|
<Navigate to="/login" replace />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/dashboard/actifs"
|
|
||||||
element={
|
|
||||||
voter ?
|
|
||||||
<DashboardPage voter={voter} /> :
|
|
||||||
<Navigate to="/login" replace />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/dashboard/futurs"
|
|
||||||
element={
|
|
||||||
voter ?
|
|
||||||
<DashboardPage voter={voter} /> :
|
|
||||||
<Navigate to="/login" replace />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard/historique"
|
path="/dashboard/historique"
|
||||||
element={
|
element={
|
||||||
voter ?
|
voter ?
|
||||||
<DashboardPage voter={voter} /> :
|
<HistoriquePage voter={voter} /> :
|
||||||
<Navigate to="/login" replace />
|
<Navigate to="/login" replace />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/vote/:id"
|
path="/dashboard/actifs"
|
||||||
element={
|
element={
|
||||||
voter ?
|
voter ?
|
||||||
<VotingPage /> :
|
<ActiveVotesPage voter={voter} /> :
|
||||||
<Navigate to="/login" replace />
|
<Navigate to="/login" replace />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/dashboard/futurs"
|
||||||
|
element={
|
||||||
|
voter ?
|
||||||
|
<UpcomingVotesPage voter={voter} /> :
|
||||||
|
<Navigate to="/login" replace />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Voting Route */}
|
||||||
|
<Route
|
||||||
|
path="/voting/:id"
|
||||||
|
element={
|
||||||
|
voter ?
|
||||||
|
<VotingPage voter={voter} /> :
|
||||||
|
<Navigate to="/login" replace />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Archives Routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/archives"
|
path="/archives"
|
||||||
element={<ArchivesPage />}
|
element={<ArchivesPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/archives/election/:id"
|
||||||
|
element={<ElectionDetailsPage type="archives" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Legacy route - redirect to archives */}
|
||||||
|
<Route
|
||||||
|
path="/election/:id"
|
||||||
|
element={<Navigate to="/archives/election/:id" replace />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/profile"
|
path="/profile"
|
||||||
element={
|
element={
|
||||||
|
|||||||
281
e-voting-system/frontend/src/components/ElectionDetailsModal.css
Normal file
281
e-voting-system/frontend/src/components/ElectionDetailsModal.css
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 20px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
animation: modalSlideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-section h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dates {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dates > div {
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dates label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #999;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dates p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-candidates {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-candidate {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9ff;
|
||||||
|
border-left: 3px solid #667eea;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-candidate h4 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-candidate p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-total-votes {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f0f7ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-total-votes svg {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-total-votes strong {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-result-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-result-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-result-percentage {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-result-bar {
|
||||||
|
height: 20px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-result-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-result-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-loading,
|
||||||
|
.modal-error {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-error {
|
||||||
|
color: #d32f2f;
|
||||||
|
background: #ffebee;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.modal-content {
|
||||||
|
max-height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dates {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
161
e-voting-system/frontend/src/components/ElectionDetailsModal.jsx
Normal file
161
e-voting-system/frontend/src/components/ElectionDetailsModal.jsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { X, Users, BarChart3, CheckCircle } from 'lucide-react';
|
||||||
|
import './ElectionDetailsModal.css';
|
||||||
|
|
||||||
|
export default function ElectionDetailsModal({ electionId, isOpen, onClose, voter = null, type = 'historique' }) {
|
||||||
|
const [election, setElection] = useState(null);
|
||||||
|
const [results, setResults] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [userVote, setUserVote] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && electionId) {
|
||||||
|
fetchElectionDetails();
|
||||||
|
}
|
||||||
|
}, [isOpen, electionId]);
|
||||||
|
|
||||||
|
const fetchElectionDetails = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Récupérer les détails de l'élection
|
||||||
|
const electionResponse = await fetch(`http://localhost:8000/api/elections/${electionId}`);
|
||||||
|
if (!electionResponse.ok) {
|
||||||
|
throw new Error('Élection non trouvée');
|
||||||
|
}
|
||||||
|
const electionData = await electionResponse.json();
|
||||||
|
setElection(electionData);
|
||||||
|
|
||||||
|
// Récupérer le vote de l'utilisateur si connecté et type = historique
|
||||||
|
if (voter && type === 'historique') {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const userVoteResponse = await fetch(`http://localhost:8000/api/votes/election/${electionId}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (userVoteResponse.ok) {
|
||||||
|
const userVoteData = await userVoteResponse.json();
|
||||||
|
setUserVote(userVoteData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Impossible de récupérer le vote utilisateur');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les résultats si l'élection est terminée
|
||||||
|
if (electionData.results_published) {
|
||||||
|
try {
|
||||||
|
const resultsResponse = await fetch(`http://localhost:8000/api/elections/${electionId}/results`);
|
||||||
|
if (resultsResponse.ok) {
|
||||||
|
const resultsData = await resultsResponse.json();
|
||||||
|
setResults(resultsData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Résultats non disponibles');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Erreur de chargement');
|
||||||
|
console.error('Erreur:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>{election?.name || 'Détails de l\'élection'}</h2>
|
||||||
|
<button className="modal-close" onClick={onClose}>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <div className="modal-loading">Chargement...</div>}
|
||||||
|
{error && <div className="modal-error">{error}</div>}
|
||||||
|
|
||||||
|
{election && !loading && (
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="modal-section">
|
||||||
|
<h3>📋 Informations</h3>
|
||||||
|
<p className="modal-description">{election.description}</p>
|
||||||
|
<div className="modal-dates">
|
||||||
|
<div>
|
||||||
|
<label>Ouverture</label>
|
||||||
|
<p>{formatDate(election.start_date)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Fermeture</label>
|
||||||
|
<p>{formatDate(election.end_date)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-section">
|
||||||
|
<h3>👥 Candidats ({election.candidates?.length || 0})</h3>
|
||||||
|
<div className="modal-candidates">
|
||||||
|
{election.candidates?.map((candidate, index) => (
|
||||||
|
<div key={candidate.id} className="modal-candidate">
|
||||||
|
<span className="candidate-number">{candidate.order || index + 1}</span>
|
||||||
|
<div>
|
||||||
|
<h4>{candidate.name}</h4>
|
||||||
|
{candidate.description && <p>{candidate.description}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{results && election.results_published && (
|
||||||
|
<div className="modal-section">
|
||||||
|
<h3>📊 Résultats</h3>
|
||||||
|
<p className="modal-total-votes">
|
||||||
|
<Users size={18} />
|
||||||
|
Total: <strong>{results.total_votes} vote(s)</strong>
|
||||||
|
</p>
|
||||||
|
<div className="modal-results">
|
||||||
|
{results.results?.map((result, index) => (
|
||||||
|
<div key={index} className="modal-result-item">
|
||||||
|
<div className="modal-result-header">
|
||||||
|
<span className="modal-result-name">
|
||||||
|
{userVote && userVote.candidate_name === result.candidate_name && (
|
||||||
|
<CheckCircle size={16} style={{ display: 'inline', marginRight: '8px', color: '#4CAF50' }} />
|
||||||
|
)}
|
||||||
|
{result.candidate_name}
|
||||||
|
</span>
|
||||||
|
<span className="modal-result-percentage">{result.percentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="modal-result-bar">
|
||||||
|
<div
|
||||||
|
className="modal-result-bar-fill"
|
||||||
|
style={{ width: `${result.percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="modal-result-count">{result.vote_count} vote(s)</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
import { Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
import './VoteCard.css';
|
import './VoteCard.css';
|
||||||
|
|
||||||
export default function VoteCard({ vote, onVote, userVote = null, showResult = false }) {
|
export default function VoteCard({ vote, onVote, userVote = null, showResult = false, context = 'archives', onShowDetails = null }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const getStatusBadge = () => {
|
const getStatusBadge = () => {
|
||||||
if (vote.status === 'actif') {
|
if (vote.status === 'actif') {
|
||||||
return (
|
return (
|
||||||
@ -119,20 +121,50 @@ export default function VoteCard({ vote, onVote, userVote = null, showResult = f
|
|||||||
VOTER MAINTENANT
|
VOTER MAINTENANT
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{vote.status === 'actif' && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => {
|
||||||
|
if (onShowDetails) {
|
||||||
|
onShowDetails(vote.id);
|
||||||
|
} else {
|
||||||
|
navigate(`/archives/election/${vote.id}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Voir les Détails
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{userVote && (
|
{userVote && (
|
||||||
<button className="btn btn-success btn-lg" disabled>
|
<button className="btn btn-success btn-lg" disabled>
|
||||||
<CheckCircle size={20} />
|
<CheckCircle size={20} />
|
||||||
DÉJÀ VOTÉ
|
DÉJÀ VOTÉ
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{vote.status === 'ferme' && (
|
{(vote.status === 'ferme' || vote.status === 'fermé') && (
|
||||||
<a href={`/vote/${vote.id}`} className="btn btn-ghost">
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => {
|
||||||
|
if (context === 'historique' && onShowDetails) {
|
||||||
|
onShowDetails(vote.id);
|
||||||
|
} else if (context === 'archives') {
|
||||||
|
navigate(`/archives/election/${vote.id}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
Voir les Détails
|
Voir les Détails
|
||||||
</a>
|
</button>
|
||||||
)}
|
)}
|
||||||
{vote.status === 'futur' && (
|
{vote.status === 'futur' && (
|
||||||
<button className="btn btn-secondary">
|
<button
|
||||||
M'alerter
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
if (onShowDetails) {
|
||||||
|
onShowDetails(vote.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Voir les Détails
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
183
e-voting-system/frontend/src/pages/ActiveVotesPage.css
Normal file
183
e-voting-system/frontend/src/pages/ActiveVotesPage.css
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
.active-votes-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .page-header {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .page-header h1 {
|
||||||
|
margin: 0.5rem 0 0.5rem 0;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .page-header p {
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .back-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .back-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .stats-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .stat {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .votes-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .vote-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .vote-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .vote-card-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .vote-card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .vote-card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .vote-card-body p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .status-badge.active {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .empty-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .empty-state h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .empty-state p {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .button-group .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .btn-vote {
|
||||||
|
background-color: #10b981;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-votes-page .btn-vote:hover {
|
||||||
|
background-color: #059669;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
146
e-voting-system/frontend/src/pages/ActiveVotesPage.jsx
Normal file
146
e-voting-system/frontend/src/pages/ActiveVotesPage.jsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import LoadingSpinner from '../components/LoadingSpinner';
|
||||||
|
import ElectionDetailsModal from '../components/ElectionDetailsModal';
|
||||||
|
import './ActiveVotesPage.css';
|
||||||
|
|
||||||
|
export default function ActiveVotesPage({ voter }) {
|
||||||
|
const [activeElections, setActiveElections] = useState([]);
|
||||||
|
const [userVotedElectionIds, setUserVotedElectionIds] = useState(new Set());
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedElectionId, setSelectedElectionId] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// Fetch all active elections
|
||||||
|
const electionsResponse = await fetch('http://localhost:8000/api/elections/active', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!electionsResponse.ok) throw new Error('Erreur de chargement');
|
||||||
|
|
||||||
|
const electionsData = await electionsResponse.json();
|
||||||
|
// /api/elections/active retourne un objet unique, pas un array
|
||||||
|
// On l'enveloppe dans un array pour la cohérence
|
||||||
|
const electionsArray = Array.isArray(electionsData) ? electionsData : [electionsData];
|
||||||
|
setActiveElections(electionsArray);
|
||||||
|
|
||||||
|
// Fetch user's votes to know which ones they already voted for
|
||||||
|
const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (votesResponse.ok) {
|
||||||
|
const votesData = await votesResponse.json();
|
||||||
|
// Créer un Set des election_id où l'utilisateur a déjà voté (et qui sont actives)
|
||||||
|
const votedIds = new Set(
|
||||||
|
votesData
|
||||||
|
.filter(vote => vote.election_status === 'active')
|
||||||
|
.map(vote => vote.election_id)
|
||||||
|
);
|
||||||
|
setUserVotedElectionIds(votedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner fullscreen />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="active-votes-page">
|
||||||
|
<div className="container">
|
||||||
|
<div className="page-header">
|
||||||
|
<button className="back-btn" onClick={() => navigate('/dashboard')}>
|
||||||
|
← Retour au Tableau de Bord
|
||||||
|
</button>
|
||||||
|
<h1>🔴 Votes en Cours</h1>
|
||||||
|
<p>Élections en cours auxquelles vous participez</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeElections.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<div className="empty-icon">📭</div>
|
||||||
|
<h3>Aucun vote en cours</h3>
|
||||||
|
<p>Il n'y a actuellement aucune élection active.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="stats-bar">
|
||||||
|
<div className="stat">
|
||||||
|
<span className="stat-label">Élections en cours</span>
|
||||||
|
<span className="stat-value">{activeElections.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="votes-grid">
|
||||||
|
{activeElections.map(election => {
|
||||||
|
const hasVoted = userVotedElectionIds.has(election.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={election.id} className="vote-card active">
|
||||||
|
<div className="vote-card-header">
|
||||||
|
<h3>{election.name}</h3>
|
||||||
|
<span className="status-badge active">
|
||||||
|
🔴 En cours
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="vote-card-body">
|
||||||
|
{hasVoted ? (
|
||||||
|
<>
|
||||||
|
<p><strong>Statut :</strong> Vous avez voté ✓</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setSelectedElectionId(election.id)}
|
||||||
|
>
|
||||||
|
Voir les détails
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>{election.description || 'Votez pour cette élection'}</p>
|
||||||
|
<div className="button-group">
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setSelectedElectionId(election.id)}
|
||||||
|
>
|
||||||
|
Voir les détails
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-vote"
|
||||||
|
onClick={() => navigate(`/vote/${election.id}`)}
|
||||||
|
>
|
||||||
|
✓ Voter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ElectionDetailsModal
|
||||||
|
electionId={selectedElectionId}
|
||||||
|
isOpen={!!selectedElectionId}
|
||||||
|
onClose={() => setSelectedElectionId(null)}
|
||||||
|
voter={voter}
|
||||||
|
type="actif"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -105,6 +105,7 @@ export default function ArchivesPage() {
|
|||||||
key={vote.id}
|
key={vote.id}
|
||||||
vote={vote}
|
vote={vote}
|
||||||
showResult={true}
|
showResult={true}
|
||||||
|
context="archives"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { BarChart3, Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
import { BarChart3, Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
import VoteCard from '../components/VoteCard';
|
import VoteCard from '../components/VoteCard';
|
||||||
|
import ElectionDetailsModal from '../components/ElectionDetailsModal';
|
||||||
import LoadingSpinner from '../components/LoadingSpinner';
|
import LoadingSpinner from '../components/LoadingSpinner';
|
||||||
import './DashboardPage.css';
|
import './DashboardPage.css';
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ export default function DashboardPage({ voter }) {
|
|||||||
const [votes, setVotes] = useState([]);
|
const [votes, setVotes] = useState([]);
|
||||||
const [userVotes, setUserVotes] = useState([]);
|
const [userVotes, setUserVotes] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filter, setFilter] = useState('all'); // all, actifs, futurs, historique
|
const [selectedElectionId, setSelectedElectionId] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchVotes();
|
fetchVotes();
|
||||||
@ -18,17 +19,24 @@ export default function DashboardPage({ voter }) {
|
|||||||
const fetchVotes = async () => {
|
const fetchVotes = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const response = await fetch('http://localhost:8000/elections/', {
|
const [activeRes, upcomingRes, completedRes] = await Promise.all([
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
fetch('http://localhost:8000/api/elections/active', { headers: { 'Authorization': `Bearer ${token}` } }),
|
||||||
});
|
fetch('http://localhost:8000/api/elections/upcoming', { headers: { 'Authorization': `Bearer ${token}` } }),
|
||||||
|
fetch('http://localhost:8000/api/elections/completed', { headers: { 'Authorization': `Bearer ${token}` } })
|
||||||
|
]);
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Erreur de chargement');
|
const active = activeRes.ok ? await activeRes.json() : [];
|
||||||
|
const upcoming = upcomingRes.ok ? await upcomingRes.json() : [];
|
||||||
|
const completed = completedRes.ok ? await completedRes.json() : [];
|
||||||
|
|
||||||
const data = await response.json();
|
const allVotes = [
|
||||||
setVotes(data);
|
...((Array.isArray(active) ? active : [active]).map(v => ({ ...v, status: 'actif' }))),
|
||||||
|
...upcoming.map(v => ({ ...v, status: 'futur' })),
|
||||||
|
...completed.map(v => ({ ...v, status: 'ferme' }))
|
||||||
|
];
|
||||||
|
setVotes(allVotes);
|
||||||
|
|
||||||
// Fetch user's votes
|
const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
|
||||||
const votesResponse = await fetch('http://localhost:8000/votes/my-votes', {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,66 +51,50 @@ export default function DashboardPage({ voter }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveVotes = () => votes.filter(v => v.status === 'actif');
|
const activeVotes = votes.filter(v => v.status === 'actif');
|
||||||
const getFutureVotes = () => votes.filter(v => v.status === 'futur');
|
const futureVotes = votes.filter(v => v.status === 'futur');
|
||||||
const getHistoryVotes = () => votes.filter(v => v.status === 'ferme');
|
const historyVotes = votes.filter(v => v.status === 'ferme');
|
||||||
|
|
||||||
const activeVotes = getActiveVotes();
|
|
||||||
const futureVotes = getFutureVotes();
|
|
||||||
const historyVotes = getHistoryVotes();
|
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner fullscreen />;
|
if (loading) return <LoadingSpinner fullscreen />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-page">
|
<div className="dashboard-page">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{/* Welcome Section */}
|
|
||||||
<div className="dashboard-header">
|
<div className="dashboard-header">
|
||||||
<div>
|
<h1>Bienvenue, {voter?.nom}! 👋</h1>
|
||||||
<h1>Bienvenue, {voter?.nom}! 👋</h1>
|
<p>Voici votre tableau de bord personnel</p>
|
||||||
<p>Voici votre tableau de bord personnel</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Section */}
|
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-icon active">
|
<div className="stat-icon active"><AlertCircle size={24} /></div>
|
||||||
<AlertCircle size={24} />
|
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
<div className="stat-content">
|
||||||
<div className="stat-value">{activeVotes.length}</div>
|
<div className="stat-value">{activeVotes.length}</div>
|
||||||
<div className="stat-label">Votes Actifs</div>
|
<div className="stat-label">Votes Actifs</div>
|
||||||
<a href="#actifs" className="stat-link">Voir →</a>
|
<Link to="/dashboard/actifs" className="stat-link">Voir →</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-icon future">
|
<div className="stat-icon future"><Clock size={24} /></div>
|
||||||
<Clock size={24} />
|
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
<div className="stat-content">
|
||||||
<div className="stat-value">{futureVotes.length}</div>
|
<div className="stat-value">{futureVotes.length}</div>
|
||||||
<div className="stat-label">À Venir</div>
|
<div className="stat-label">À Venir</div>
|
||||||
<a href="#futurs" className="stat-link">Voir →</a>
|
<Link to="/dashboard/futurs" className="stat-link">Voir →</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-icon completed">
|
<div className="stat-icon completed"><CheckCircle size={24} /></div>
|
||||||
<CheckCircle size={24} />
|
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
<div className="stat-content">
|
||||||
<div className="stat-value">{historyVotes.length}</div>
|
<div className="stat-value">{historyVotes.length}</div>
|
||||||
<div className="stat-label">Votes Terminés</div>
|
<div className="stat-label">Votes Terminés</div>
|
||||||
<a href="#historique" className="stat-link">Voir →</a>
|
<Link to="/dashboard/historique" className="stat-link">Voir →</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-icon total">
|
<div className="stat-icon total"><BarChart3 size={24} /></div>
|
||||||
<BarChart3 size={24} />
|
|
||||||
</div>
|
|
||||||
<div className="stat-content">
|
<div className="stat-content">
|
||||||
<div className="stat-value">{userVotes.length}</div>
|
<div className="stat-value">{userVotes.length}</div>
|
||||||
<div className="stat-label">Votes Effectués</div>
|
<div className="stat-label">Votes Effectués</div>
|
||||||
@ -111,39 +103,10 @@ export default function DashboardPage({ voter }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Tabs */}
|
{activeVotes.length > 0 && (
|
||||||
<div className="dashboard-tabs">
|
<div className="votes-section">
|
||||||
<button
|
<h2>⚡ Votes Actifs</h2>
|
||||||
className={`tab ${filter === 'all' ? 'active' : ''}`}
|
<p className="section-subtitle">Votes en cours - Participez maintenant!</p>
|
||||||
onClick={() => setFilter('all')}
|
|
||||||
>
|
|
||||||
Tous les votes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`tab ${filter === 'actifs' ? 'active' : ''}`}
|
|
||||||
onClick={() => setFilter('actifs')}
|
|
||||||
>
|
|
||||||
Actifs ({activeVotes.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`tab ${filter === 'futurs' ? 'active' : ''}`}
|
|
||||||
onClick={() => setFilter('futurs')}
|
|
||||||
>
|
|
||||||
À venir ({futureVotes.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`tab ${filter === 'historique' ? 'active' : ''}`}
|
|
||||||
onClick={() => setFilter('historique')}
|
|
||||||
>
|
|
||||||
Historique ({historyVotes.length})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Required Section - Only for Active Votes */}
|
|
||||||
{filter === 'all' && activeVotes.length > 0 && (
|
|
||||||
<div className="action-section">
|
|
||||||
<h2>⚡ Action Requise</h2>
|
|
||||||
<p className="section-subtitle">Votes en attente de votre participation</p>
|
|
||||||
<div className="votes-grid">
|
<div className="votes-grid">
|
||||||
{activeVotes.slice(0, 2).map(vote => (
|
{activeVotes.slice(0, 2).map(vote => (
|
||||||
<VoteCard
|
<VoteCard
|
||||||
@ -156,57 +119,75 @@ export default function DashboardPage({ voter }) {
|
|||||||
</div>
|
</div>
|
||||||
{activeVotes.length > 2 && (
|
{activeVotes.length > 2 && (
|
||||||
<Link to="/dashboard/actifs" className="btn btn-secondary">
|
<Link to="/dashboard/actifs" className="btn btn-secondary">
|
||||||
Voir tous les votes actifs
|
Voir tous les votes actifs ({activeVotes.length})
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Votes Display */}
|
{futureVotes.length > 0 && (
|
||||||
<div className="votes-section">
|
<div className="votes-section">
|
||||||
<h2>
|
<h2>🔮 Votes à Venir</h2>
|
||||||
{filter === 'all' && 'Tous les votes'}
|
<p className="section-subtitle">Élections qui démarreront bientôt</p>
|
||||||
{filter === 'actifs' && 'Votes Actifs'}
|
<div className="votes-grid">
|
||||||
{filter === 'futurs' && 'Votes à Venir'}
|
{futureVotes.slice(0, 2).map(vote => (
|
||||||
{filter === 'historique' && 'Mon Historique'}
|
<VoteCard
|
||||||
</h2>
|
key={vote.id}
|
||||||
|
vote={vote}
|
||||||
|
context="futur"
|
||||||
|
onShowDetails={(id) => setSelectedElectionId(id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{futureVotes.length > 2 && (
|
||||||
|
<Link to="/dashboard/futurs" className="btn btn-secondary">
|
||||||
|
Voir tous les votes à venir ({futureVotes.length})
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{(() => {
|
{historyVotes.length > 0 && (
|
||||||
let displayVotes = votes;
|
<div className="votes-section">
|
||||||
if (filter === 'actifs') displayVotes = activeVotes;
|
<h2>📋 Mon Historique</h2>
|
||||||
if (filter === 'futurs') displayVotes = futureVotes;
|
<p className="section-subtitle">Vos 2 derniers votes</p>
|
||||||
if (filter === 'historique') displayVotes = historyVotes;
|
<div className="votes-grid">
|
||||||
|
{historyVotes.slice(0, 2).map(vote => (
|
||||||
|
<VoteCard
|
||||||
|
key={vote.id}
|
||||||
|
vote={vote}
|
||||||
|
userVote={userVotes.find(v => v.election_id === vote.id)?.choix}
|
||||||
|
showResult={true}
|
||||||
|
context="historique"
|
||||||
|
onShowDetails={(id) => setSelectedElectionId(id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{historyVotes.length > 2 && (
|
||||||
|
<Link to="/dashboard/historique" className="btn btn-secondary">
|
||||||
|
Voir tout mon historique ({historyVotes.length})
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
if (displayVotes.length === 0) {
|
{activeVotes.length === 0 && futureVotes.length === 0 && historyVotes.length === 0 && (
|
||||||
return (
|
<div className="votes-section">
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<div className="empty-icon">📭</div>
|
<div className="empty-icon">📭</div>
|
||||||
<h3>Aucun vote</h3>
|
<h3>Aucun vote disponible</h3>
|
||||||
<p>
|
<p>Il n'y a pas encore de votes disponibles.</p>
|
||||||
{filter === 'all' && 'Il n\'y a pas encore de votes disponibles.'}
|
</div>
|
||||||
{filter === 'actifs' && 'Aucun vote actif pour le moment.'}
|
</div>
|
||||||
{filter === 'futurs' && 'Aucun vote à venir.'}
|
)}
|
||||||
{filter === 'historique' && 'Vous n\'avez pas encore voté.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<ElectionDetailsModal
|
||||||
<div className="votes-grid">
|
electionId={selectedElectionId}
|
||||||
{displayVotes.map(vote => (
|
isOpen={!!selectedElectionId}
|
||||||
<VoteCard
|
onClose={() => setSelectedElectionId(null)}
|
||||||
key={vote.id}
|
voter={voter}
|
||||||
vote={vote}
|
type="futur"
|
||||||
userVote={userVotes.find(v => v.election_id === vote.id)?.choix}
|
/>
|
||||||
showResult={filter === 'historique' || vote.status === 'ferme'}
|
|
||||||
onVote={(id) => window.location.href = `/vote/${id}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
332
e-voting-system/frontend/src/pages/ElectionDetailsPage.css
Normal file
332
e-voting-system/frontend/src/pages/ElectionDetailsPage.css
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
.election-details-page {
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-details-page .container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: white;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-header {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-header h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-closed {
|
||||||
|
background: #9E9E9E;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-future {
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 25px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.details-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-card {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-card h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-card .description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item svg {
|
||||||
|
color: #667eea;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item div label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #999;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item div p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Candidats */
|
||||||
|
.candidates-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9ff;
|
||||||
|
border-left: 3px solid #667eea;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-item:hover {
|
||||||
|
background: #f0f2ff;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-info h3 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Résultats */
|
||||||
|
.results-card {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-votes {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f0f7ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-votes svg {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-votes strong {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-percentage {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-bar {
|
||||||
|
height: 24px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results,
|
||||||
|
.info-message {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-message {
|
||||||
|
background: #f0f7ff;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.election-details-page {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-card h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
253
e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx
Normal file
253
e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { ArrowLeft, Calendar, Users, BarChart3, CheckCircle } from 'lucide-react';
|
||||||
|
import LoadingSpinner from '../components/LoadingSpinner';
|
||||||
|
import Alert from '../components/Alert';
|
||||||
|
import './ElectionDetailsPage.css';
|
||||||
|
|
||||||
|
export default function ElectionDetailsPage({ voter = null, type = 'archives' }) {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [election, setElection] = useState(null);
|
||||||
|
const [results, setResults] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [candidates, setCandidates] = useState([]);
|
||||||
|
const [userVote, setUserVote] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchElectionDetails();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const fetchElectionDetails = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Récupérer les détails de l'élection
|
||||||
|
const electionResponse = await fetch(`http://localhost:8000/api/elections/${id}`);
|
||||||
|
if (!electionResponse.ok) {
|
||||||
|
throw new Error('Élection non trouvée');
|
||||||
|
}
|
||||||
|
const electionData = await electionResponse.json();
|
||||||
|
setElection(electionData);
|
||||||
|
setCandidates(electionData.candidates || []);
|
||||||
|
|
||||||
|
// Récupérer le vote de l'utilisateur si connecté et type = historique
|
||||||
|
if (voter && type === 'historique') {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const userVoteResponse = await fetch(`http://localhost:8000/api/votes/election/${id}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (userVoteResponse.ok) {
|
||||||
|
const userVoteData = await userVoteResponse.json();
|
||||||
|
setUserVote(userVoteData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Impossible de récupérer le vote utilisateur');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les résultats si l'élection est terminée
|
||||||
|
if (electionData.results_published) {
|
||||||
|
try {
|
||||||
|
const resultsResponse = await fetch(`http://localhost:8000/api/elections/${id}/results`);
|
||||||
|
if (resultsResponse.ok) {
|
||||||
|
const resultsData = await resultsResponse.json();
|
||||||
|
setResults(resultsData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Résultats non disponibles');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Erreur de chargement');
|
||||||
|
console.error('Erreur:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getElectionStatus = () => {
|
||||||
|
if (!election) return '';
|
||||||
|
const now = new Date();
|
||||||
|
const start = new Date(election.start_date);
|
||||||
|
const end = new Date(election.end_date);
|
||||||
|
|
||||||
|
if (now < start) return 'À venir';
|
||||||
|
if (now > end) return 'Terminée';
|
||||||
|
return 'En cours';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner fullscreen />;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="election-details-page">
|
||||||
|
<div className="container">
|
||||||
|
<button className="btn btn-ghost" onClick={() => navigate(-1)}>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
<Alert type="error" title="Erreur" message={error} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!election) {
|
||||||
|
return (
|
||||||
|
<div className="election-details-page">
|
||||||
|
<div className="container">
|
||||||
|
<button className="btn btn-ghost" onClick={() => navigate(-1)}>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
<Alert type="error" title="Erreur" message="Élection non trouvée" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = getElectionStatus();
|
||||||
|
const statusColor = status === 'Terminée' ? 'closed' : status === 'En cours' ? 'active' : 'future';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="election-details-page">
|
||||||
|
<div className="container">
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-back"
|
||||||
|
onClick={() => {
|
||||||
|
if (type === 'historique') {
|
||||||
|
navigate('/dashboard/historique');
|
||||||
|
} else if (type === 'futur') {
|
||||||
|
navigate('/dashboard/futurs');
|
||||||
|
} else {
|
||||||
|
navigate('/archives');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="details-header">
|
||||||
|
<div>
|
||||||
|
<h1>{election.name}</h1>
|
||||||
|
<span className={`status-badge status-${statusColor}`}>{status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="details-grid">
|
||||||
|
{/* Section Informations */}
|
||||||
|
<div className="details-card">
|
||||||
|
<h2>📋 Informations</h2>
|
||||||
|
<p className="description">{election.description}</p>
|
||||||
|
|
||||||
|
<div className="info-section">
|
||||||
|
<div className="info-item">
|
||||||
|
<Calendar size={20} />
|
||||||
|
<div>
|
||||||
|
<label>Ouverture</label>
|
||||||
|
<p>{formatDate(election.start_date)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="info-item">
|
||||||
|
<Calendar size={20} />
|
||||||
|
<div>
|
||||||
|
<label>Fermeture</label>
|
||||||
|
<p>{formatDate(election.end_date)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Candidats */}
|
||||||
|
<div className="details-card">
|
||||||
|
<h2>👥 Candidats ({candidates.length})</h2>
|
||||||
|
<div className="candidates-list">
|
||||||
|
{candidates.map((candidate, index) => (
|
||||||
|
<div key={candidate.id} className="candidate-item">
|
||||||
|
<div className="candidate-number">{candidate.order || index + 1}</div>
|
||||||
|
<div className="candidate-info">
|
||||||
|
<h3>{candidate.name}</h3>
|
||||||
|
{candidate.description && <p>{candidate.description}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Résultats */}
|
||||||
|
{results && election.results_published && (
|
||||||
|
<div className="details-card results-card">
|
||||||
|
<h2>📊 Résultats</h2>
|
||||||
|
<div className="results-section">
|
||||||
|
{results.results && results.results.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<p className="total-votes">
|
||||||
|
<Users size={18} />
|
||||||
|
Total: <strong>{results.total_votes} vote(s)</strong>
|
||||||
|
</p>
|
||||||
|
<div className="results-list">
|
||||||
|
{results.results.map((result, index) => (
|
||||||
|
<div key={index} className="result-item">
|
||||||
|
<div className="result-header">
|
||||||
|
<span className="result-name">
|
||||||
|
{userVote && userVote.candidate_name === result.candidate_name && (
|
||||||
|
<CheckCircle size={16} style={{ display: 'inline', marginRight: '8px', color: '#4CAF50' }} />
|
||||||
|
)}
|
||||||
|
{result.candidate_name}
|
||||||
|
</span>
|
||||||
|
<span className="result-percentage">{result.percentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="result-bar">
|
||||||
|
<div
|
||||||
|
className="result-bar-fill"
|
||||||
|
style={{ width: `${result.percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="result-count">{result.vote_count} vote(s)</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="no-results">Aucun résultat disponible</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!election.results_published && status === 'Terminée' && (
|
||||||
|
<div className="details-card">
|
||||||
|
<p className="info-message">
|
||||||
|
📊 Les résultats de cette élection n'ont pas encore été publiés.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status !== 'Terminée' && (
|
||||||
|
<div className="details-card">
|
||||||
|
<p className="info-message">
|
||||||
|
⏳ Les résultats seront disponibles une fois l'élection terminée.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
198
e-voting-system/frontend/src/pages/HistoriquePage.css
Normal file
198
e-voting-system/frontend/src/pages/HistoriquePage.css
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
.historique-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historique-page .container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historique-header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historique-header h1 {
|
||||||
|
color: white;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historique-header p {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.votes-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card.historique {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card-header {
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.active {
|
||||||
|
background: #ff6b6b;
|
||||||
|
border-color: #ff5252;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.ferme {
|
||||||
|
background: #51cf66;
|
||||||
|
border-color: #40c057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card-body p {
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-card-body strong {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 60px 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.historique-header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historique-header p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-bar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.votes-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
e-voting-system/frontend/src/pages/HistoriquePage.jsx
Normal file
114
e-voting-system/frontend/src/pages/HistoriquePage.jsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import VoteCard from '../components/VoteCard';
|
||||||
|
import ElectionDetailsModal from '../components/ElectionDetailsModal';
|
||||||
|
import LoadingSpinner from '../components/LoadingSpinner';
|
||||||
|
import './HistoriquePage.css';
|
||||||
|
|
||||||
|
export default function HistoriquePage({ voter }) {
|
||||||
|
const [elections, setElections] = useState([]);
|
||||||
|
const [userVotes, setUserVotes] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedElectionId, setSelectedElectionId] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// Fetch user's votes (c'est la source de vérité)
|
||||||
|
const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!votesResponse.ok) throw new Error('Erreur de chargement de l\'historique');
|
||||||
|
|
||||||
|
const votesData = await votesResponse.json();
|
||||||
|
|
||||||
|
// Filtrer SEULEMENT les votes pour les élections TERMINÉES (status: "closed")
|
||||||
|
const closedVotes = votesData.filter(vote => vote.election_status === 'closed');
|
||||||
|
setUserVotes(closedVotes);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Les votes retournés par /api/votes/history contiennent déjà les informations nécessaires
|
||||||
|
const filteredElections = userVotes;
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner fullscreen />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="historique-page">
|
||||||
|
<div className="container">
|
||||||
|
<div className="historique-header">
|
||||||
|
<button className="back-btn" onClick={() => navigate('/dashboard')}>
|
||||||
|
← Retour au Tableau de Bord
|
||||||
|
</button>
|
||||||
|
<h1>📋 Mon Historique de Votes</h1>
|
||||||
|
<p>Toutes les élections passées auxquelles vous avez participé</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredElections.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<div className="empty-icon">📭</div>
|
||||||
|
<h3>Aucun vote enregistré</h3>
|
||||||
|
<p>Vous n'avez pas encore participé à des élections terminées.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="stats-bar">
|
||||||
|
<div className="stat">
|
||||||
|
<span className="stat-label">Total de votes</span>
|
||||||
|
<span className="stat-value">{filteredElections.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="stat">
|
||||||
|
<span className="stat-label">Élections auxquelles vous avez participé</span>
|
||||||
|
<span className="stat-value">{filteredElections.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="votes-grid">
|
||||||
|
{filteredElections.map(vote => (
|
||||||
|
<div key={vote.vote_id} className="vote-card historique">
|
||||||
|
<div className="vote-card-header">
|
||||||
|
<h3>{vote.election_name}</h3>
|
||||||
|
<span className="status-badge closed">
|
||||||
|
✅ Terminée
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="vote-card-body">
|
||||||
|
<p><strong>Votre choix :</strong> {vote.candidate_name}</p>
|
||||||
|
<p><strong>Date du vote :</strong> {new Date(vote.vote_date).toLocaleDateString('fr-FR')}</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setSelectedElectionId(vote.election_id)}
|
||||||
|
>
|
||||||
|
Voir les détails
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Election Details Modal */}
|
||||||
|
<ElectionDetailsModal
|
||||||
|
electionId={selectedElectionId}
|
||||||
|
isOpen={!!selectedElectionId}
|
||||||
|
onClose={() => setSelectedElectionId(null)}
|
||||||
|
voter={voter}
|
||||||
|
type="historique"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
138
e-voting-system/frontend/src/pages/LoginPage.jsx
Normal file
138
e-voting-system/frontend/src/pages/LoginPage.jsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { Mail, Lock, LogIn } from 'lucide-react';
|
||||||
|
import Alert from '../components/Alert';
|
||||||
|
import { API_ENDPOINTS } from '../config/api';
|
||||||
|
import './AuthPage.css';
|
||||||
|
|
||||||
|
export default function LoginPage({ onLogin }) {
|
||||||
|
console.log('🔴 LoginPage MONTÉE!');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
console.log('🔴 handleSubmit APPELÉ!');
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_ENDPOINTS.LOGIN, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.detail || 'Email ou mot de passe incorrect');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('✅ Data reçue:', data);
|
||||||
|
|
||||||
|
const voterData = {
|
||||||
|
id: data.id,
|
||||||
|
email: data.email,
|
||||||
|
first_name: data.first_name,
|
||||||
|
last_name: data.last_name,
|
||||||
|
};
|
||||||
|
console.log('✅ voterData préparé:', voterData);
|
||||||
|
|
||||||
|
localStorage.setItem('voter', JSON.stringify(voterData));
|
||||||
|
console.log('✅ localStorage voter set');
|
||||||
|
localStorage.setItem('token', data.access_token);
|
||||||
|
console.log('✅ localStorage token set');
|
||||||
|
|
||||||
|
console.log('✅ Appel onLogin');
|
||||||
|
onLogin(voterData);
|
||||||
|
console.log('✅ onLogin appelé, navigation...');
|
||||||
|
navigate('/dashboard');
|
||||||
|
console.log('✅ navigate appelé');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ CATCH ERROR:', err);
|
||||||
|
console.error('❌ Error message:', err.message);
|
||||||
|
setError(err.message || 'Erreur de connexion');
|
||||||
|
} finally {
|
||||||
|
console.log('✅ Finally: setLoading(false)');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-page">
|
||||||
|
<div className="auth-container">
|
||||||
|
<div className="auth-card">
|
||||||
|
<div className="auth-header">
|
||||||
|
<h1>Se Connecter</h1>
|
||||||
|
<p>Accédez à votre tableau de bord</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert type="error" message={error} onClose={() => setError('')} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="auth-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="email">Email</label>
|
||||||
|
<div className="input-wrapper">
|
||||||
|
<Mail size={20} className="input-icon" />
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="password">Mot de passe</label>
|
||||||
|
<div className="input-wrapper">
|
||||||
|
<Lock size={20} className="input-icon" />
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-footer">
|
||||||
|
<a href="#forgot" className="forgot-link">Mot de passe oublié ?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary btn-lg btn-block" disabled={loading} onClick={() => console.log('🔴 BOUTON CLIQUÉ')}>
|
||||||
|
<LogIn size={20} />
|
||||||
|
{loading ? 'Connexion...' : 'Se Connecter'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="auth-divider">ou</div>
|
||||||
|
|
||||||
|
<p className="auth-switch">
|
||||||
|
Pas encore de compte ? <Link to="/register">S'inscrire</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="auth-illustration">
|
||||||
|
<div className="illustration-box">
|
||||||
|
<div className="illustration-icon">🗳️</div>
|
||||||
|
<h3>Bienvenue</h3>
|
||||||
|
<p>Votez en toute confiance sur notre plateforme sécurisée</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
e-voting-system/frontend/src/pages/UpcomingVotesPage.css
Normal file
155
e-voting-system/frontend/src/pages/UpcomingVotesPage.css
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
.upcoming-votes-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .page-header {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .page-header h1 {
|
||||||
|
margin: 0.5rem 0 0.5rem 0;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .page-header p {
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .back-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .back-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .stats-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .stat {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .elections-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .election-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .election-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .election-card-header {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .election-card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .election-card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .election-card-body p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .status-badge.upcoming {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .empty-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .empty-state h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-votes-page .empty-state p {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
101
e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx
Normal file
101
e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import LoadingSpinner from '../components/LoadingSpinner';
|
||||||
|
import ElectionDetailsModal from '../components/ElectionDetailsModal';
|
||||||
|
import './UpcomingVotesPage.css';
|
||||||
|
|
||||||
|
export default function UpcomingVotesPage({ voter }) {
|
||||||
|
const [upcomingElections, setUpcomingElections] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedElectionId, setSelectedElectionId] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// Fetch upcoming elections
|
||||||
|
const electionsResponse = await fetch('http://localhost:8000/api/elections/upcoming', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!electionsResponse.ok) throw new Error('Erreur de chargement');
|
||||||
|
|
||||||
|
const electionsData = await electionsResponse.json();
|
||||||
|
setUpcomingElections(electionsData || []);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner fullscreen />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="upcoming-votes-page">
|
||||||
|
<div className="container">
|
||||||
|
<div className="page-header">
|
||||||
|
<button className="back-btn" onClick={() => navigate('/dashboard')}>
|
||||||
|
← Retour au Tableau de Bord
|
||||||
|
</button>
|
||||||
|
<h1>⏳ Votes à Venir</h1>
|
||||||
|
<p>Élections qui arriveront prochainement</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{upcomingElections.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<div className="empty-icon">📭</div>
|
||||||
|
<h3>Aucun vote à venir</h3>
|
||||||
|
<p>Aucune élection prévue pour le moment.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="stats-bar">
|
||||||
|
<div className="stat">
|
||||||
|
<span className="stat-label">Élections à venir</span>
|
||||||
|
<span className="stat-value">{upcomingElections.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="elections-grid">
|
||||||
|
{upcomingElections.map(election => (
|
||||||
|
<div key={election.id} className="election-card upcoming">
|
||||||
|
<div className="election-card-header">
|
||||||
|
<h3>{election.name}</h3>
|
||||||
|
<span className="status-badge upcoming">
|
||||||
|
⏳ À venir
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="election-card-body">
|
||||||
|
<p><strong>Description :</strong> {election.description || 'N/A'}</p>
|
||||||
|
<p><strong>Début :</strong> {new Date(election.start_date).toLocaleDateString('fr-FR')}</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setSelectedElectionId(election.id)}
|
||||||
|
>
|
||||||
|
Voir les détails
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ElectionDetailsModal
|
||||||
|
electionId={selectedElectionId}
|
||||||
|
isOpen={!!selectedElectionId}
|
||||||
|
onClose={() => setSelectedElectionId(null)}
|
||||||
|
voter={voter}
|
||||||
|
type="futur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { ArrowLeft, CheckCircle, AlertCircle } from 'lucide-react';
|
import { ArrowLeft, CheckCircle, AlertCircle, BarChart3 } from 'lucide-react';
|
||||||
import Alert from '../components/Alert';
|
import Alert from '../components/Alert';
|
||||||
import Modal from '../components/Modal';
|
import Modal from '../components/Modal';
|
||||||
import LoadingSpinner from '../components/LoadingSpinner';
|
import LoadingSpinner from '../components/LoadingSpinner';
|
||||||
@ -9,7 +9,10 @@ import './VotingPage.css';
|
|||||||
export default function VotingPage() {
|
export default function VotingPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [vote, setVote] = useState(null);
|
const [election, setElection] = useState(null);
|
||||||
|
const [candidates, setCandidates] = useState([]);
|
||||||
|
const [userVote, setUserVote] = useState(null);
|
||||||
|
const [results, setResults] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedOption, setSelectedOption] = useState('');
|
const [selectedOption, setSelectedOption] = useState('');
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
@ -19,20 +22,56 @@ export default function VotingPage() {
|
|||||||
const [voting, setVoting] = useState(false);
|
const [voting, setVoting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchVote();
|
fetchElectionData();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const fetchVote = async () => {
|
const fetchElectionData = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const response = await fetch(`http://localhost:8000/elections/${id}`, {
|
|
||||||
|
// Fetch election details
|
||||||
|
const electionResponse = await fetch(`http://localhost:8000/elections/${id}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Vote non trouvé');
|
if (!electionResponse.ok) throw new Error('Élection non trouvée');
|
||||||
|
|
||||||
|
const electionData = await electionResponse.json();
|
||||||
|
setElection(electionData);
|
||||||
|
|
||||||
const data = await response.json();
|
// Fetch candidates for this election
|
||||||
setVote(data);
|
const candidatesResponse = await fetch(`http://localhost:8000/elections/${id}/candidates`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (candidatesResponse.ok) {
|
||||||
|
const candidatesData = await candidatesResponse.json();
|
||||||
|
setCandidates(candidatesData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user's vote if it exists
|
||||||
|
const userVoteResponse = await fetch(`http://localhost:8000/votes/my-votes?election_id=${id}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userVoteResponse.ok) {
|
||||||
|
const userVoteData = await userVoteResponse.json();
|
||||||
|
if (Array.isArray(userVoteData) && userVoteData.length > 0) {
|
||||||
|
setUserVote(userVoteData[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch results if election is closed
|
||||||
|
if (electionData.results_published) {
|
||||||
|
const resultsResponse = await fetch(`http://localhost:8000/elections/${id}/results`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resultsResponse.ok) {
|
||||||
|
const resultsData = await resultsResponse.json();
|
||||||
|
setResults(resultsData);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@ -51,14 +90,14 @@ export default function VotingPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const response = await fetch('http://localhost:8000/votes/submit', {
|
const response = await fetch('http://localhost:8000/votes', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
election_id: vote.id,
|
election_id: election.id,
|
||||||
choix: selectedOption,
|
choix: selectedOption,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -79,11 +118,11 @@ export default function VotingPage() {
|
|||||||
|
|
||||||
if (loading) return <LoadingSpinner fullscreen />;
|
if (loading) return <LoadingSpinner fullscreen />;
|
||||||
|
|
||||||
if (!vote) {
|
if (!election) {
|
||||||
return (
|
return (
|
||||||
<div className="voting-page">
|
<div className="voting-page">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Alert type="error" message={error || 'Vote introuvable'} />
|
<Alert type="error" message={error || 'Élection non trouvée'} />
|
||||||
<button onClick={() => navigate('/dashboard')} className="btn btn-secondary">
|
<button onClick={() => navigate('/dashboard')} className="btn btn-secondary">
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Retour au dashboard
|
Retour au dashboard
|
||||||
@ -102,7 +141,7 @@ export default function VotingPage() {
|
|||||||
<h1>Merci!</h1>
|
<h1>Merci!</h1>
|
||||||
<p>Votre vote a été enregistré avec succès.</p>
|
<p>Votre vote a été enregistré avec succès.</p>
|
||||||
<div className="success-details">
|
<div className="success-details">
|
||||||
<p><strong>Vote:</strong> {vote.titre}</p>
|
<p><strong>Vote:</strong> {election.name}</p>
|
||||||
<p><strong>Votre choix:</strong> {selectedOption}</p>
|
<p><strong>Votre choix:</strong> {selectedOption}</p>
|
||||||
<p className="success-note">
|
<p className="success-note">
|
||||||
Redirection vers le tableau de bord...
|
Redirection vers le tableau de bord...
|
||||||
@ -114,6 +153,10 @@ export default function VotingPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isHistorique = election.results_published;
|
||||||
|
const isActif = election.is_active;
|
||||||
|
const isFutur = new Date(election.start_date) > new Date();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="voting-page">
|
<div className="voting-page">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@ -126,117 +169,74 @@ export default function VotingPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="vote-container">
|
<div className="vote-container">
|
||||||
{/* Left Column - Vote Info */}
|
{/* Vote Header */}
|
||||||
<div className="vote-info-section">
|
<div className="vote-header-section">
|
||||||
<div className="vote-header">
|
<h1>{election.name}</h1>
|
||||||
<h1>{vote.titre}</h1>
|
<p className="vote-description">{election.description}</p>
|
||||||
<div className="vote-meta">
|
|
||||||
<span className="meta-item">
|
{/* Status Badge */}
|
||||||
<strong>Statut:</strong> {vote.status === 'actif' ? '🟢 OUVERT' : '🔴 TERMINÉ'}
|
<div className="vote-status">
|
||||||
</span>
|
{isHistorique && <span className="badge badge-closed">📊 Terminé - Résultats disponibles</span>}
|
||||||
<span className="meta-item">
|
{isActif && <span className="badge badge-active">🟢 Élection en cours</span>}
|
||||||
<strong>Participants:</strong> {vote.total_votes || 0} votes
|
{isFutur && <span className="badge badge-future">⏰ À venir</span>}
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
|
{/* User's vote if already voted */}
|
||||||
|
{userVote && (
|
||||||
|
<div className="user-vote-info">
|
||||||
|
<CheckCircle size={20} color="green" />
|
||||||
|
<span>Vous avez voté pour: <strong>{userVote.choix}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="vote-description">
|
{/* Candidates Section */}
|
||||||
<h2>Description</h2>
|
<div className="candidates-section">
|
||||||
<p>{vote.description}</p>
|
{candidates.length > 0 ? (
|
||||||
|
<>
|
||||||
{vote.context && (
|
<h2>{isHistorique ? '📊 Résultats' : 'Candidats'}</h2>
|
||||||
<div className="vote-context">
|
<div className="candidates-list">
|
||||||
<h3>Contexte</h3>
|
{candidates.map((candidate) => (
|
||||||
<p>{vote.context}</p>
|
<div key={candidate.id} className="candidate-card">
|
||||||
</div>
|
<div className="candidate-info">
|
||||||
)}
|
<h3>{candidate.name}</h3>
|
||||||
|
{candidate.description && <p>{candidate.description}</p>}
|
||||||
{vote.resultats && vote.status === 'ferme' && (
|
|
||||||
<div className="vote-results">
|
|
||||||
<h3>Résultats Finaux</h3>
|
|
||||||
<div className="results-display">
|
|
||||||
{Object.entries(vote.resultats).map(([option, count]) => (
|
|
||||||
<div key={option} className="result-row">
|
|
||||||
<span className="result-option">{option}</span>
|
|
||||||
<div className="result-bar">
|
|
||||||
<div
|
|
||||||
className="result-bar-fill"
|
|
||||||
style={{
|
|
||||||
width: `${(count / (vote.total_votes || 1)) * 100}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="result-percent">
|
|
||||||
{Math.round((count / (vote.total_votes || 1)) * 100)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
{isActif && !userVote && (
|
||||||
</div>
|
<button
|
||||||
</div>
|
className={`btn btn-primary ${selectedOption === candidate.name ? 'active' : ''}`}
|
||||||
)}
|
onClick={() => {
|
||||||
</div>
|
setSelectedOption(candidate.name);
|
||||||
</div>
|
setShowConfirmModal(true);
|
||||||
|
}}
|
||||||
{/* Right Column - Voting Section */}
|
>
|
||||||
<div className="vote-voting-section">
|
Voter
|
||||||
<div className="voting-card">
|
</button>
|
||||||
<h2>Voter</h2>
|
)}
|
||||||
|
{isHistorique && results && (
|
||||||
{error && (
|
<div className="candidate-result">
|
||||||
<Alert type="error" message={error} onClose={() => setError('')} />
|
<span className="result-percent">
|
||||||
)}
|
{results[candidate.name] || 0}%
|
||||||
|
</span>
|
||||||
{vote.status !== 'actif' && (
|
</div>
|
||||||
<Alert
|
)}
|
||||||
type="warning"
|
|
||||||
title="Vote fermé"
|
|
||||||
message="Ce vote n'est plus actif. Vous ne pouvez plus voter."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{vote.status === 'actif' && (
|
|
||||||
<>
|
|
||||||
<div className="voting-form">
|
|
||||||
<p className="voting-question">
|
|
||||||
<strong>Sélectionnez votre option:</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="voting-options">
|
|
||||||
{vote.options && vote.options.map((option) => (
|
|
||||||
<label key={option} className="voting-option">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="vote"
|
|
||||||
value={option}
|
|
||||||
checked={selectedOption === option}
|
|
||||||
onChange={(e) => setSelectedOption(e.target.value)}
|
|
||||||
disabled={voting}
|
|
||||||
/>
|
|
||||||
<span className="option-text">{option}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<button
|
</div>
|
||||||
className="btn btn-primary btn-lg btn-block"
|
</>
|
||||||
onClick={() => setShowConfirmModal(true)}
|
) : (
|
||||||
disabled={!selectedOption || voting}
|
<Alert type="info" message="Aucun candidat pour cette élection" />
|
||||||
>
|
)}
|
||||||
<CheckCircle size={20} />
|
|
||||||
{voting ? 'Envoi en cours...' : 'Soumettre mon vote'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="voting-info">
|
|
||||||
<AlertCircle size={18} />
|
|
||||||
<p>
|
|
||||||
Votre vote est <strong>final</strong> et ne peut pas être modifié après la soumission.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Future Election Message */}
|
||||||
|
{isFutur && (
|
||||||
|
<div className="future-election-message">
|
||||||
|
<AlertCircle size={40} />
|
||||||
|
<h3>Élection à venir</h3>
|
||||||
|
<p>Les détails et la date d'ouverture seront bientôt disponibles.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,4 +4,5 @@ export { default as RegisterPage } from './RegisterPage';
|
|||||||
export { default as DashboardPage } from './DashboardPage';
|
export { default as DashboardPage } from './DashboardPage';
|
||||||
export { default as VotingPage } from './VotingPage';
|
export { default as VotingPage } from './VotingPage';
|
||||||
export { default as ArchivesPage } from './ArchivesPage';
|
export { default as ArchivesPage } from './ArchivesPage';
|
||||||
|
export { default as ElectionDetailsPage } from './ElectionDetailsPage';
|
||||||
export { default as ProfilePage } from './ProfilePage';
|
export { default as ProfilePage } from './ProfilePage';
|
||||||
|
|||||||
35
e-voting-system/rebuild.sh
Executable file
35
e-voting-system/rebuild.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script complet de rebuild
|
||||||
|
# Usage: ./rebuild.sh
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
echo "🧹 Nettoyage complet..."
|
||||||
|
docker-compose down 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "🗑️ Suppression des images..."
|
||||||
|
docker image rm evoting-frontend evoting-backend 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "🧼 Prune Docker..."
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
|
echo "📦 Suppression du build frontend..."
|
||||||
|
rm -rf frontend/build/
|
||||||
|
rm -rf frontend/node_modules/.cache/
|
||||||
|
|
||||||
|
echo "🔨 Rebuild complet avec docker-compose..."
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Rebuild complet terminé!"
|
||||||
|
echo ""
|
||||||
|
echo "📊 État des services:"
|
||||||
|
docker-compose ps
|
||||||
|
echo ""
|
||||||
|
echo "🌐 URLs:"
|
||||||
|
echo " Frontend: http://localhost:3000"
|
||||||
|
echo " Backend: http://localhost:8000"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Pour voir les logs:"
|
||||||
|
echo " docker-compose logs -f frontend"
|
||||||
|
echo " docker-compose logs -f backend"
|
||||||
412
e-voting-system/restore_data.py
Normal file
412
e-voting-system/restore_data.py
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script pour repeupler la base de données avec :
|
||||||
|
- 1700 utilisateurs
|
||||||
|
- 10 élections passées (historique)
|
||||||
|
- 5 élections actives
|
||||||
|
- 15 élections futures
|
||||||
|
- Votes distribués aléatoirement (pas tous les users dans chaque election)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import random
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import bcrypt
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DB_USER = "evoting_user"
|
||||||
|
DB_PASSWORD = "evoting_pass123"
|
||||||
|
DB_NAME = "evoting_db"
|
||||||
|
HASHED_PASSWORD = bcrypt.hashpw(b"epita1234", b"$2b$12$zxCiC3MJpa32FfpX8u7Lx.").decode('utf-8')
|
||||||
|
|
||||||
|
def run_sql(sql):
|
||||||
|
"""Exécuter du SQL via docker exec"""
|
||||||
|
cmd = f'docker exec -i evoting_db mariadb -u {DB_USER} -p{DB_PASSWORD} {DB_NAME}'
|
||||||
|
result = subprocess.run(cmd, shell=True, input=sql, capture_output=True, text=True)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def log(msg, emoji="📝"):
|
||||||
|
"""Afficher un message formaté"""
|
||||||
|
print(f"{emoji} {msg}")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PHASE 0: NETTOYER LA BASE DE DONNÉES
|
||||||
|
# ============================================================================
|
||||||
|
log("PHASE 0: Nettoyage de la base de données...", "🧹")
|
||||||
|
|
||||||
|
# Utiliser mysql client directement pour avoir plus de control
|
||||||
|
cleanup_sql = """
|
||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
DROP TABLE IF EXISTS votes;
|
||||||
|
DROP TABLE IF EXISTS candidates;
|
||||||
|
DROP TABLE IF EXISTS elections;
|
||||||
|
DROP TABLE IF EXISTS voters;
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Écrire dans un fichier temporaire et exécuter
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.sql', delete=False) as f:
|
||||||
|
f.write(cleanup_sql)
|
||||||
|
temp_file = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = f'docker exec -i evoting_db mariadb -u {DB_USER} -p{DB_PASSWORD} {DB_NAME} < {temp_file}'
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log("✓ Anciennes tables supprimées", "✅")
|
||||||
|
else:
|
||||||
|
log(f"⚠️ Nettoyage échoué: {result.stderr}", "⚠️")
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file)
|
||||||
|
|
||||||
|
# Recréer les tables
|
||||||
|
create_tables_sql = """
|
||||||
|
CREATE TABLE voters (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
first_name VARCHAR(100),
|
||||||
|
last_name VARCHAR(100),
|
||||||
|
citizen_id VARCHAR(50) UNIQUE,
|
||||||
|
public_key LONGBLOB,
|
||||||
|
has_voted BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
certificate_hash VARCHAR(255),
|
||||||
|
INDEX idx_email (email)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE elections (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
start_date DATETIME NOT NULL,
|
||||||
|
end_date DATETIME NOT NULL,
|
||||||
|
elgamal_p INT,
|
||||||
|
elgamal_g INT,
|
||||||
|
public_key LONGBLOB,
|
||||||
|
is_active BOOLEAN DEFAULT FALSE,
|
||||||
|
results_published BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_active (is_active)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE candidates (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
election_id INT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
`order` INT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_election (election_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE votes (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
voter_id INT NOT NULL,
|
||||||
|
election_id INT NOT NULL,
|
||||||
|
candidate_id INT NOT NULL,
|
||||||
|
encrypted_vote LONGBLOB NOT NULL,
|
||||||
|
zero_knowledge_proof LONGBLOB,
|
||||||
|
ballot_hash VARCHAR(255),
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
FOREIGN KEY (voter_id) REFERENCES voters(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (candidate_id) REFERENCES candidates(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_voter (voter_id),
|
||||||
|
INDEX idx_election (election_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
"""
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.sql', delete=False) as f:
|
||||||
|
f.write(create_tables_sql)
|
||||||
|
temp_file = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = f'docker exec -i evoting_db mariadb -u {DB_USER} -p{DB_PASSWORD} {DB_NAME} < {temp_file}'
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log("✓ Tables recréées", "✅")
|
||||||
|
else:
|
||||||
|
log(f"Erreur: {result.stderr}", "❌")
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
log("PHASE 1: Création de 1700 utilisateurs...", "👥")
|
||||||
|
|
||||||
|
sql_users = "INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, certificate_hash, public_key, created_at) VALUES\n"
|
||||||
|
values = []
|
||||||
|
|
||||||
|
# 1698 utilisateurs normaux
|
||||||
|
for i in range(1, 1699):
|
||||||
|
email = f"voter_{i}@voting.local"
|
||||||
|
first_name = f"User{i}"
|
||||||
|
last_name = f"Voter{i}"
|
||||||
|
citizen_id = f"ID_{i:06d}"
|
||||||
|
values.append(f"('{email}', '{HASHED_PASSWORD}', '{first_name}', '{last_name}', '{citizen_id}', 'cert_{i}', 'pk_{i}', NOW())")
|
||||||
|
|
||||||
|
# 2 utilisateurs spéciaux
|
||||||
|
special_users = [
|
||||||
|
("new_user_e13_157@voting.local", "NewUser157", "Election13_157", "ID_SPEC_157"),
|
||||||
|
("new_user_e13_192@voting.local", "NewUser192", "Election13_192", "ID_SPEC_192"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for email, first_name, last_name, citizen_id in special_users:
|
||||||
|
values.append(f"('{email}', '{HASHED_PASSWORD}', '{first_name}', '{last_name}', '{citizen_id}', 'cert_special', 'pk_special', NOW())")
|
||||||
|
|
||||||
|
sql_users += ",\n".join(values) + ";"
|
||||||
|
|
||||||
|
result = run_sql(sql_users)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log("✓ 1700 utilisateurs créés", "✅")
|
||||||
|
else:
|
||||||
|
log(f"Erreur: {result.stderr}", "❌")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PHASE 2: CRÉER LES 30 ÉLECTIONS (10 passées + 5 actives + 15 futures)
|
||||||
|
# ============================================================================
|
||||||
|
log("PHASE 2: Création des 30 élections...", "🗳️")
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
elections = []
|
||||||
|
|
||||||
|
# 10 élections passées (historique)
|
||||||
|
for i in range(1, 11):
|
||||||
|
start = (now - timedelta(days=365-i*30)).strftime("%Y-%m-%d")
|
||||||
|
end = (now - timedelta(days=365-(i*30)-7)).strftime("%Y-%m-%d")
|
||||||
|
elections.append({
|
||||||
|
"name": f"Historical Election {i}",
|
||||||
|
"description": f"Past election {i}",
|
||||||
|
"start": start,
|
||||||
|
"end": end,
|
||||||
|
"is_active": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# 5 élections actives (en cours)
|
||||||
|
for i in range(1, 6):
|
||||||
|
start = (now - timedelta(days=7-i)).strftime("%Y-%m-%d")
|
||||||
|
end = (now + timedelta(days=30+i)).strftime("%Y-%m-%d")
|
||||||
|
elections.append({
|
||||||
|
"name": f"Active Election {i}",
|
||||||
|
"description": f"Current election {i}",
|
||||||
|
"start": start,
|
||||||
|
"end": end,
|
||||||
|
"is_active": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
# 15 élections futures
|
||||||
|
for i in range(1, 16):
|
||||||
|
start = (now + timedelta(days=30+i*5)).strftime("%Y-%m-%d")
|
||||||
|
end = (now + timedelta(days=37+i*5)).strftime("%Y-%m-%d")
|
||||||
|
elections.append({
|
||||||
|
"name": f"Upcoming Election {i}",
|
||||||
|
"description": f"Future election {i}",
|
||||||
|
"start": start,
|
||||||
|
"end": end,
|
||||||
|
"is_active": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
sql_elections = "INSERT INTO elections (name, description, start_date, end_date, elgamal_p, elgamal_g, public_key, is_active, results_published) VALUES\n"
|
||||||
|
election_values = []
|
||||||
|
|
||||||
|
for idx, election in enumerate(elections, 1):
|
||||||
|
election_values.append(
|
||||||
|
f"('{election['name']}', '{election['description']}', '{election['start']}', '{election['end']}', '23', '5', 'pk_{idx}', {election['is_active']}, 0)"
|
||||||
|
)
|
||||||
|
|
||||||
|
sql_elections += ",\n".join(election_values) + ";"
|
||||||
|
|
||||||
|
result = run_sql(sql_elections)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log(f"✓ 30 élections créées (10 passées + 5 actives + 15 futures)", "✅")
|
||||||
|
else:
|
||||||
|
log(f"Erreur: {result.stderr}", "❌")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PHASE 3: CRÉER LES CANDIDATS
|
||||||
|
# ============================================================================
|
||||||
|
log("PHASE 3: Création des candidats...", "🎭")
|
||||||
|
|
||||||
|
candidate_names = [
|
||||||
|
"Alice Johnson", "Bob Smith", "Carol White", "David Brown",
|
||||||
|
"Emily Davis", "Frank Miller", "Grace Wilson", "Henry Moore"
|
||||||
|
]
|
||||||
|
|
||||||
|
sql_candidates = "INSERT INTO candidates (election_id, name, description, `order`, created_at) VALUES\n"
|
||||||
|
candidate_values = []
|
||||||
|
|
||||||
|
for election_id in range(1, 31):
|
||||||
|
# 4-8 candidats par élection
|
||||||
|
num_candidates = random.randint(4, 8)
|
||||||
|
selected_candidates = random.sample(candidate_names, min(num_candidates, len(candidate_names)))
|
||||||
|
|
||||||
|
for order, name in enumerate(selected_candidates, 1):
|
||||||
|
candidate_values.append(
|
||||||
|
f"({election_id}, '{name}', 'Candidate {name}', {order}, NOW())"
|
||||||
|
)
|
||||||
|
|
||||||
|
sql_candidates += ",\n".join(candidate_values) + ";"
|
||||||
|
|
||||||
|
result = run_sql(sql_candidates)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log(f"✓ Candidats créés pour toutes les élections", "✅")
|
||||||
|
else:
|
||||||
|
log(f"Erreur: {result.stderr}", "❌")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PHASE 4: CRÉER LES VOTES
|
||||||
|
# ============================================================================
|
||||||
|
log("PHASE 4: Création des votes...", "🗳️")
|
||||||
|
|
||||||
|
sql_votes = "INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp) VALUES\n"
|
||||||
|
vote_values = []
|
||||||
|
|
||||||
|
# Pour chaque élection
|
||||||
|
for election_id in range(1, 31):
|
||||||
|
# Récupérer les candidats de cette élection
|
||||||
|
result_candidates = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id};")
|
||||||
|
candidate_ids = []
|
||||||
|
if result_candidates.returncode == 0:
|
||||||
|
lines = result_candidates.stdout.strip().split('\n')[1:]
|
||||||
|
for line in lines:
|
||||||
|
if line.strip():
|
||||||
|
candidate_ids.append(int(line.split()[0]))
|
||||||
|
|
||||||
|
if not candidate_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Déterminer le nombre de votants pour cette élection
|
||||||
|
# Entre 20% et 80% des utilisateurs votent pour cette élection
|
||||||
|
num_voters = random.randint(int(1700 * 0.2), int(1700 * 0.8))
|
||||||
|
|
||||||
|
# Sélectionner les votants aléatoirement
|
||||||
|
voter_ids = random.sample(range(1, 1701), num_voters)
|
||||||
|
|
||||||
|
for voter_id in voter_ids:
|
||||||
|
candidate_id = random.choice(candidate_ids)
|
||||||
|
ballot_hash = f"hash_{voter_id}_{election_id}"
|
||||||
|
vote_values.append(
|
||||||
|
f"({voter_id}, {election_id}, {candidate_id}, 'encrypted_{voter_id}_{election_id}', '{ballot_hash}', NOW())"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insérer tous les votes en batch
|
||||||
|
batch_size = 1000
|
||||||
|
for i in range(0, len(vote_values), batch_size):
|
||||||
|
batch = vote_values[i:i+batch_size]
|
||||||
|
sql_batch = "INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp) VALUES\n"
|
||||||
|
sql_batch += ",\n".join(batch) + ";"
|
||||||
|
|
||||||
|
result = run_sql(sql_batch)
|
||||||
|
if result.returncode != 0:
|
||||||
|
log(f"Erreur batch {i//batch_size}: {result.stderr}", "❌")
|
||||||
|
|
||||||
|
log(f"✓ {len(vote_values)} votes créés", "✅")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PHASE 5: VOTES SPÉCIAUX POUR new_user_e13_192
|
||||||
|
# ============================================================================
|
||||||
|
log("PHASE 5: Configuration des votes pour new_user_e13_192...", "⚙️")
|
||||||
|
|
||||||
|
# Récupérer l'ID de new_user_e13_192
|
||||||
|
result = run_sql("SELECT id FROM voters WHERE email = 'new_user_e13_192@voting.local';")
|
||||||
|
special_user_id = None
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
lines = result.stdout.strip().split('\n')
|
||||||
|
if len(lines) > 1:
|
||||||
|
special_user_id = int(lines[1].split()[0])
|
||||||
|
|
||||||
|
if special_user_id:
|
||||||
|
# Supprimer tous les votes actuels pour cet utilisateur
|
||||||
|
run_sql(f"DELETE FROM votes WHERE voter_id = {special_user_id};")
|
||||||
|
|
||||||
|
special_votes = []
|
||||||
|
|
||||||
|
# 10 votes pour les élections passées (1-10)
|
||||||
|
for election_id in range(1, 11):
|
||||||
|
result_cand = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id} LIMIT 1;")
|
||||||
|
if result_cand.returncode == 0 and result_cand.stdout.strip():
|
||||||
|
lines = result_cand.stdout.strip().split('\n')
|
||||||
|
if len(lines) > 1:
|
||||||
|
candidate_id = int(lines[1].split()[0])
|
||||||
|
ballot_hash = f"hash_{special_user_id}_{election_id}"
|
||||||
|
special_votes.append(
|
||||||
|
f"({special_user_id}, {election_id}, {candidate_id}, 'encrypted_{special_user_id}_{election_id}', '{ballot_hash}', NOW())"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4 votes pour les élections actives (11, 12, 14, 15) - PAS 13
|
||||||
|
for election_id in [11, 12, 14, 15]:
|
||||||
|
result_cand = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id} LIMIT 1;")
|
||||||
|
if result_cand.returncode == 0 and result_cand.stdout.strip():
|
||||||
|
lines = result_cand.stdout.strip().split('\n')
|
||||||
|
if len(lines) > 1:
|
||||||
|
candidate_id = int(lines[1].split()[0])
|
||||||
|
ballot_hash = f"hash_{special_user_id}_{election_id}"
|
||||||
|
special_votes.append(
|
||||||
|
f"({special_user_id}, {election_id}, {candidate_id}, 'encrypted_{special_user_id}_{election_id}', '{ballot_hash}', NOW())"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4 votes pour les élections futures (21, 24, 27, 30)
|
||||||
|
for election_id in [21, 24, 27, 30]:
|
||||||
|
result_cand = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id} LIMIT 1;")
|
||||||
|
if result_cand.returncode == 0 and result_cand.stdout.strip():
|
||||||
|
lines = result_cand.stdout.strip().split('\n')
|
||||||
|
if len(lines) > 1:
|
||||||
|
candidate_id = int(lines[1].split()[0])
|
||||||
|
ballot_hash = f"hash_{special_user_id}_{election_id}"
|
||||||
|
special_votes.append(
|
||||||
|
f"({special_user_id}, {election_id}, {candidate_id}, 'encrypted_{special_user_id}_{election_id}', '{ballot_hash}', NOW())"
|
||||||
|
)
|
||||||
|
|
||||||
|
if special_votes:
|
||||||
|
sql_special = "INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp) VALUES\n"
|
||||||
|
sql_special += ",\n".join(special_votes) + ";"
|
||||||
|
|
||||||
|
result = run_sql(sql_special)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log(f"✓ 18 votes spéciaux créés pour new_user_e13_192 (10 + 4 + 4)", "✅")
|
||||||
|
else:
|
||||||
|
log(f"Erreur: {result.stderr}", "❌")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PHASE 6: STATISTIQUES FINALES
|
||||||
|
# ============================================================================
|
||||||
|
log("PHASE 6: Vérification des données...", "📊")
|
||||||
|
|
||||||
|
result = run_sql("""
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM elections) as total_elections,
|
||||||
|
(SELECT COUNT(*) FROM voters) as total_voters,
|
||||||
|
(SELECT COUNT(*) FROM candidates) as total_candidates,
|
||||||
|
(SELECT COUNT(*) FROM votes) as total_votes;
|
||||||
|
""")
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("✅ BASE DE DONNÉES REPUPLÉE AVEC SUCCÈS!")
|
||||||
|
print("="*60)
|
||||||
|
print(result.stdout)
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Afficher les utilisateurs spéciaux
|
||||||
|
result = run_sql("SELECT id, email, first_name, last_name FROM voters WHERE email LIKE 'new_user_e13_%' ORDER BY id;")
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("👤 UTILISATEURS SPÉCIAUX:")
|
||||||
|
print("="*60)
|
||||||
|
print(result.stdout)
|
||||||
|
print("\n✅ Vous pouvez maintenant vous connecter avec:")
|
||||||
|
print(" Email: new_user_e13_192@voting.local")
|
||||||
|
print(" Mot de passe: epita1234")
|
||||||
|
print(" Votes: 10 historiques + 4 actifs + 4 futurs")
|
||||||
|
print(" Élection sans vote: Active Election 3 (ID 13)")
|
||||||
|
print("="*60)
|
||||||
Loading…
x
Reference in New Issue
Block a user