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:
|
||||
@echo "E-Voting System - Post-Quantum Cryptography"
|
||||
@echo ""
|
||||
@echo " make up Démarrer (docker-compose up -d)"
|
||||
@echo " make down Arrêter (docker-compose down)"
|
||||
@echo " make logs Voir les logs"
|
||||
@echo " make test Tester (pytest)"
|
||||
@echo "🚀 MAIN COMMAND"
|
||||
@echo " make build 🔨 Build frontend + deploy (RECOMMANDÉ)"
|
||||
@echo ""
|
||||
@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:
|
||||
docker-compose up -d
|
||||
cd build && docker-compose up -d 2>/dev/null || docker-compose up -d
|
||||
|
||||
down:
|
||||
docker-compose down
|
||||
cd build && docker-compose down 2>/dev/null || docker-compose down
|
||||
|
||||
logs:
|
||||
docker-compose logs -f backend
|
||||
# Restauration des données de test uniquement
|
||||
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:
|
||||
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.get("/active", response_model=schemas.ElectionResponse)
|
||||
def get_active_election(db: Session = Depends(get_db)):
|
||||
"""Récupérer l'élection active en cours"""
|
||||
@router.get("/active", response_model=list[schemas.ElectionResponse])
|
||||
def get_active_elections(db: Session = Depends(get_db)):
|
||||
"""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:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="No active election"
|
||||
)
|
||||
|
||||
return election
|
||||
return active
|
||||
|
||||
|
||||
@router.get("/completed")
|
||||
@ -68,6 +69,21 @@ def get_active_election_results(db: Session = Depends(get_db)):
|
||||
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)
|
||||
def get_election(election_id: int, db: Session = Depends(get_db)):
|
||||
"""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.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(
|
||||
vote_bulletin: schemas.VoteBulletin,
|
||||
current_voter: Voter = Depends(get_current_voter),
|
||||
@ -141,13 +218,21 @@ def get_voter_history(
|
||||
).first()
|
||||
|
||||
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({
|
||||
"vote_id": vote.id,
|
||||
"election_id": election.id,
|
||||
"election_name": election.name,
|
||||
"candidate_name": candidate.name if candidate else "Unknown",
|
||||
"vote_date": vote.timestamp,
|
||||
"election_status": "completed" if election.end_date < datetime.utcnow() else "active"
|
||||
"election_status": status
|
||||
})
|
||||
|
||||
return history
|
||||
|
||||
@ -9,7 +9,7 @@ from typing import Optional, List
|
||||
|
||||
class VoterRegister(BaseModel):
|
||||
"""Enregistrement d'un électeur"""
|
||||
email: EmailStr
|
||||
email: str
|
||||
password: str = Field(..., min_length=8)
|
||||
first_name: str
|
||||
last_name: str
|
||||
@ -18,7 +18,7 @@ class VoterRegister(BaseModel):
|
||||
|
||||
class VoterLogin(BaseModel):
|
||||
"""Authentification"""
|
||||
email: EmailStr
|
||||
email: 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):
|
||||
python -m backend.scripts.seed_db
|
||||
La base de données n'est plus peuplée automatiquement.
|
||||
Seul l'utilisateur paul.roost@epita.fr existe en base.
|
||||
|
||||
Ce script supprime/crée les tables et insère des électeurs, élections,
|
||||
candidats et votes d'exemple selon la demande de l'utilisateur.
|
||||
Pour ajouter des données:
|
||||
- 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():
|
||||
reset_db()
|
||||
seed()
|
||||
print("ℹ️ Ce script a été supprimé.")
|
||||
print("La base de données est maintenant gérée manuellement.")
|
||||
|
||||
|
||||
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 VotingPage from './pages/VotingPage';
|
||||
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';
|
||||
|
||||
function App() {
|
||||
@ -77,6 +81,7 @@ function App() {
|
||||
<LoginPage onLogin={handleLogin} />
|
||||
}
|
||||
/>
|
||||
{/* Dashboard Routes */}
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
@ -85,42 +90,56 @@ function App() {
|
||||
<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
|
||||
path="/dashboard/historique"
|
||||
element={
|
||||
voter ?
|
||||
<DashboardPage voter={voter} /> :
|
||||
<HistoriquePage voter={voter} /> :
|
||||
<Navigate to="/login" replace />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/vote/:id"
|
||||
path="/dashboard/actifs"
|
||||
element={
|
||||
voter ?
|
||||
<VotingPage /> :
|
||||
<ActiveVotesPage voter={voter} /> :
|
||||
<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
|
||||
path="/archives"
|
||||
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
|
||||
path="/profile"
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
import { Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
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 = () => {
|
||||
if (vote.status === 'actif') {
|
||||
return (
|
||||
@ -119,20 +121,50 @@ export default function VoteCard({ vote, onVote, userVote = null, showResult = f
|
||||
VOTER MAINTENANT
|
||||
</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 && (
|
||||
<button className="btn btn-success btn-lg" disabled>
|
||||
<CheckCircle size={20} />
|
||||
DÉJÀ VOTÉ
|
||||
</button>
|
||||
)}
|
||||
{vote.status === 'ferme' && (
|
||||
<a href={`/vote/${vote.id}`} className="btn btn-ghost">
|
||||
{(vote.status === 'ferme' || vote.status === 'fermé') && (
|
||||
<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
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
{vote.status === 'futur' && (
|
||||
<button className="btn btn-secondary">
|
||||
M'alerter
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => {
|
||||
if (onShowDetails) {
|
||||
onShowDetails(vote.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Voir les Détails
|
||||
</button>
|
||||
)}
|
||||
</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}
|
||||
vote={vote}
|
||||
showResult={true}
|
||||
context="archives"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { BarChart3, Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import VoteCard from '../components/VoteCard';
|
||||
import ElectionDetailsModal from '../components/ElectionDetailsModal';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import './DashboardPage.css';
|
||||
|
||||
@ -9,7 +10,7 @@ export default function DashboardPage({ voter }) {
|
||||
const [votes, setVotes] = useState([]);
|
||||
const [userVotes, setUserVotes] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState('all'); // all, actifs, futurs, historique
|
||||
const [selectedElectionId, setSelectedElectionId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchVotes();
|
||||
@ -18,17 +19,24 @@ export default function DashboardPage({ voter }) {
|
||||
const fetchVotes = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('http://localhost:8000/elections/', {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
const [activeRes, upcomingRes, completedRes] = await Promise.all([
|
||||
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();
|
||||
setVotes(data);
|
||||
const allVotes = [
|
||||
...((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/votes/my-votes', {
|
||||
const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
|
||||
@ -43,66 +51,50 @@ export default function DashboardPage({ voter }) {
|
||||
}
|
||||
};
|
||||
|
||||
const getActiveVotes = () => votes.filter(v => v.status === 'actif');
|
||||
const getFutureVotes = () => votes.filter(v => v.status === 'futur');
|
||||
const getHistoryVotes = () => votes.filter(v => v.status === 'ferme');
|
||||
|
||||
const activeVotes = getActiveVotes();
|
||||
const futureVotes = getFutureVotes();
|
||||
const historyVotes = getHistoryVotes();
|
||||
const activeVotes = votes.filter(v => v.status === 'actif');
|
||||
const futureVotes = votes.filter(v => v.status === 'futur');
|
||||
const historyVotes = votes.filter(v => v.status === 'ferme');
|
||||
|
||||
if (loading) return <LoadingSpinner fullscreen />;
|
||||
|
||||
return (
|
||||
<div className="dashboard-page">
|
||||
<div className="container">
|
||||
{/* Welcome Section */}
|
||||
<div className="dashboard-header">
|
||||
<div>
|
||||
<h1>Bienvenue, {voter?.nom}! 👋</h1>
|
||||
<p>Voici votre tableau de bord personnel</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Section */}
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon active">
|
||||
<AlertCircle size={24} />
|
||||
</div>
|
||||
<div className="stat-icon active"><AlertCircle size={24} /></div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{activeVotes.length}</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 className="stat-card">
|
||||
<div className="stat-icon future">
|
||||
<Clock size={24} />
|
||||
</div>
|
||||
<div className="stat-icon future"><Clock size={24} /></div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{futureVotes.length}</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 className="stat-card">
|
||||
<div className="stat-icon completed">
|
||||
<CheckCircle size={24} />
|
||||
</div>
|
||||
<div className="stat-icon completed"><CheckCircle size={24} /></div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{historyVotes.length}</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 className="stat-card">
|
||||
<div className="stat-icon total">
|
||||
<BarChart3 size={24} />
|
||||
</div>
|
||||
<div className="stat-icon total"><BarChart3 size={24} /></div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{userVotes.length}</div>
|
||||
<div className="stat-label">Votes Effectués</div>
|
||||
@ -111,39 +103,10 @@ export default function DashboardPage({ voter }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="dashboard-tabs">
|
||||
<button
|
||||
className={`tab ${filter === 'all' ? 'active' : ''}`}
|
||||
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>
|
||||
{activeVotes.length > 0 && (
|
||||
<div className="votes-section">
|
||||
<h2>⚡ Votes Actifs</h2>
|
||||
<p className="section-subtitle">Votes en cours - Participez maintenant!</p>
|
||||
<div className="votes-grid">
|
||||
{activeVotes.slice(0, 2).map(vote => (
|
||||
<VoteCard
|
||||
@ -156,57 +119,75 @@ export default function DashboardPage({ voter }) {
|
||||
</div>
|
||||
{activeVotes.length > 2 && (
|
||||
<Link to="/dashboard/actifs" className="btn btn-secondary">
|
||||
Voir tous les votes actifs
|
||||
Voir tous les votes actifs ({activeVotes.length})
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Votes Display */}
|
||||
{futureVotes.length > 0 && (
|
||||
<div className="votes-section">
|
||||
<h2>
|
||||
{filter === 'all' && 'Tous les votes'}
|
||||
{filter === 'actifs' && 'Votes Actifs'}
|
||||
{filter === 'futurs' && 'Votes à Venir'}
|
||||
{filter === 'historique' && 'Mon Historique'}
|
||||
</h2>
|
||||
|
||||
{(() => {
|
||||
let displayVotes = votes;
|
||||
if (filter === 'actifs') displayVotes = activeVotes;
|
||||
if (filter === 'futurs') displayVotes = futureVotes;
|
||||
if (filter === 'historique') displayVotes = historyVotes;
|
||||
|
||||
if (displayVotes.length === 0) {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">📭</div>
|
||||
<h3>Aucun vote</h3>
|
||||
<p>
|
||||
{filter === 'all' && 'Il n\'y a pas encore de votes disponibles.'}
|
||||
{filter === 'actifs' && 'Aucun vote actif pour le moment.'}
|
||||
{filter === 'futurs' && 'Aucun vote à venir.'}
|
||||
{filter === 'historique' && 'Vous n\'avez pas encore voté.'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<h2>🔮 Votes à Venir</h2>
|
||||
<p className="section-subtitle">Élections qui démarreront bientôt</p>
|
||||
<div className="votes-grid">
|
||||
{displayVotes.map(vote => (
|
||||
{futureVotes.slice(0, 2).map(vote => (
|
||||
<VoteCard
|
||||
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 && (
|
||||
<div className="votes-section">
|
||||
<h2>📋 Mon Historique</h2>
|
||||
<p className="section-subtitle">Vos 2 derniers votes</p>
|
||||
<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={filter === 'historique' || vote.status === 'ferme'}
|
||||
onVote={(id) => window.location.href = `/vote/${id}`}
|
||||
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>
|
||||
)}
|
||||
|
||||
{activeVotes.length === 0 && futureVotes.length === 0 && historyVotes.length === 0 && (
|
||||
<div className="votes-section">
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">📭</div>
|
||||
<h3>Aucun vote disponible</h3>
|
||||
<p>Il n'y a pas encore de votes disponibles.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ElectionDetailsModal
|
||||
electionId={selectedElectionId}
|
||||
isOpen={!!selectedElectionId}
|
||||
onClose={() => setSelectedElectionId(null)}
|
||||
voter={voter}
|
||||
type="futur"
|
||||
/>
|
||||
</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 { 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 Modal from '../components/Modal';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
@ -9,7 +9,10 @@ import './VotingPage.css';
|
||||
export default function VotingPage() {
|
||||
const { id } = useParams();
|
||||
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 [selectedOption, setSelectedOption] = useState('');
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
@ -19,20 +22,56 @@ export default function VotingPage() {
|
||||
const [voting, setVoting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchVote();
|
||||
fetchElectionData();
|
||||
}, [id]);
|
||||
|
||||
const fetchVote = async () => {
|
||||
const fetchElectionData = async () => {
|
||||
try {
|
||||
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}` },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Vote non trouvé');
|
||||
if (!electionResponse.ok) throw new Error('Élection non trouvée');
|
||||
|
||||
const data = await response.json();
|
||||
setVote(data);
|
||||
const electionData = await electionResponse.json();
|
||||
setElection(electionData);
|
||||
|
||||
// Fetch candidates for this election
|
||||
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) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@ -51,14 +90,14 @@ export default function VotingPage() {
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('http://localhost:8000/votes/submit', {
|
||||
const response = await fetch('http://localhost:8000/votes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
election_id: vote.id,
|
||||
election_id: election.id,
|
||||
choix: selectedOption,
|
||||
}),
|
||||
});
|
||||
@ -79,11 +118,11 @@ export default function VotingPage() {
|
||||
|
||||
if (loading) return <LoadingSpinner fullscreen />;
|
||||
|
||||
if (!vote) {
|
||||
if (!election) {
|
||||
return (
|
||||
<div className="voting-page">
|
||||
<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">
|
||||
<ArrowLeft size={20} />
|
||||
Retour au dashboard
|
||||
@ -102,7 +141,7 @@ export default function VotingPage() {
|
||||
<h1>Merci!</h1>
|
||||
<p>Votre vote a été enregistré avec succès.</p>
|
||||
<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 className="success-note">
|
||||
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 (
|
||||
<div className="voting-page">
|
||||
<div className="container">
|
||||
@ -126,117 +169,74 @@ export default function VotingPage() {
|
||||
</button>
|
||||
|
||||
<div className="vote-container">
|
||||
{/* Left Column - Vote Info */}
|
||||
<div className="vote-info-section">
|
||||
<div className="vote-header">
|
||||
<h1>{vote.titre}</h1>
|
||||
<div className="vote-meta">
|
||||
<span className="meta-item">
|
||||
<strong>Statut:</strong> {vote.status === 'actif' ? '🟢 OUVERT' : '🔴 TERMINÉ'}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<strong>Participants:</strong> {vote.total_votes || 0} votes
|
||||
</span>
|
||||
</div>
|
||||
{/* Vote Header */}
|
||||
<div className="vote-header-section">
|
||||
<h1>{election.name}</h1>
|
||||
<p className="vote-description">{election.description}</p>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className="vote-status">
|
||||
{isHistorique && <span className="badge badge-closed">📊 Terminé - Résultats disponibles</span>}
|
||||
{isActif && <span className="badge badge-active">🟢 Élection en cours</span>}
|
||||
{isFutur && <span className="badge badge-future">⏰ À venir</span>}
|
||||
</div>
|
||||
|
||||
<div className="vote-description">
|
||||
<h2>Description</h2>
|
||||
<p>{vote.description}</p>
|
||||
|
||||
{vote.context && (
|
||||
<div className="vote-context">
|
||||
<h3>Contexte</h3>
|
||||
<p>{vote.context}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
{/* 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>
|
||||
|
||||
{/* Right Column - Voting Section */}
|
||||
<div className="vote-voting-section">
|
||||
<div className="voting-card">
|
||||
<h2>Voter</h2>
|
||||
|
||||
{error && (
|
||||
<Alert type="error" message={error} onClose={() => setError('')} />
|
||||
)}
|
||||
|
||||
{vote.status !== 'actif' && (
|
||||
<Alert
|
||||
type="warning"
|
||||
title="Vote fermé"
|
||||
message="Ce vote n'est plus actif. Vous ne pouvez plus voter."
|
||||
/>
|
||||
)}
|
||||
|
||||
{vote.status === 'actif' && (
|
||||
{/* Candidates Section */}
|
||||
<div className="candidates-section">
|
||||
{candidates.length > 0 ? (
|
||||
<>
|
||||
<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>
|
||||
))}
|
||||
<h2>{isHistorique ? '📊 Résultats' : 'Candidats'}</h2>
|
||||
<div className="candidates-list">
|
||||
{candidates.map((candidate) => (
|
||||
<div key={candidate.id} className="candidate-card">
|
||||
<div className="candidate-info">
|
||||
<h3>{candidate.name}</h3>
|
||||
{candidate.description && <p>{candidate.description}</p>}
|
||||
</div>
|
||||
|
||||
{isActif && !userVote && (
|
||||
<button
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={() => setShowConfirmModal(true)}
|
||||
disabled={!selectedOption || voting}
|
||||
className={`btn btn-primary ${selectedOption === candidate.name ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedOption(candidate.name);
|
||||
setShowConfirmModal(true);
|
||||
}}
|
||||
>
|
||||
<CheckCircle size={20} />
|
||||
{voting ? 'Envoi en cours...' : 'Soumettre mon vote'}
|
||||
Voter
|
||||
</button>
|
||||
)}
|
||||
{isHistorique && results && (
|
||||
<div className="candidate-result">
|
||||
<span className="result-percent">
|
||||
{results[candidate.name] || 0}%
|
||||
</span>
|
||||
</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>
|
||||
</>
|
||||
) : (
|
||||
<Alert type="info" message="Aucun candidat pour cette élection" />
|
||||
)}
|
||||
</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>
|
||||
|
||||
|
||||
@ -4,4 +4,5 @@ export { default as RegisterPage } from './RegisterPage';
|
||||
export { default as DashboardPage } from './DashboardPage';
|
||||
export { default as VotingPage } from './VotingPage';
|
||||
export { default as ArchivesPage } from './ArchivesPage';
|
||||
export { default as ElectionDetailsPage } from './ElectionDetailsPage';
|
||||
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