first commit
This commit is contained in:
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY app.py schema.sql requirements.txt ./
|
||||||
|
COPY templates ./templates
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python", "app.py"]
|
||||||
|
|
||||||
374
app.py
Normal file
374
app.py
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
import os
|
||||||
|
import math
|
||||||
|
import csv
|
||||||
|
import sqlite3
|
||||||
|
import random
|
||||||
|
from datetime import date
|
||||||
|
from io import StringIO
|
||||||
|
from flask import (
|
||||||
|
Flask, g, render_template, redirect, url_for, flash,
|
||||||
|
request, session
|
||||||
|
)
|
||||||
|
from functools import wraps
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
DATABASE = os.path.join(os.path.dirname(__file__), "avent.db")
|
||||||
|
|
||||||
|
# Configuration upload images
|
||||||
|
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), "static", "uploads")
|
||||||
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
|
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = "change-me-super-secret-key-2025"
|
||||||
|
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||||
|
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024 # 5 Mo max
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if 'db' not in g:
|
||||||
|
g.db = sqlite3.connect(DATABASE)
|
||||||
|
g.db.row_factory = sqlite3.Row
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_db(error=None):
|
||||||
|
db = g.pop('db', None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
db = get_db()
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "schema.sql")) as f:
|
||||||
|
db.executescript(f.read())
|
||||||
|
|
||||||
|
cur = db.execute("SELECT COUNT(*) AS c FROM user")
|
||||||
|
if cur.fetchone()['c'] == 0:
|
||||||
|
db.execute("INSERT INTO user (username, password) VALUES (?, ?)", ("admin", "admin"))
|
||||||
|
|
||||||
|
cur = db.execute("SELECT COUNT(*) AS c FROM project")
|
||||||
|
if cur.fetchone()['c'] == 0:
|
||||||
|
project_id = db.execute(
|
||||||
|
"INSERT INTO project (name, description, image_url, total_days) VALUES (?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
"Calendrier de l'Avent 2025",
|
||||||
|
"Le calendrier de l'Avent officiel avec tirage aléatoire et gestion des utilisateurs.",
|
||||||
|
"https://cdn-icons-png.flaticon.com/512/616/616408.png",
|
||||||
|
24
|
||||||
|
)
|
||||||
|
).lastrowid
|
||||||
|
people_list = ["Valentin", "Nicolas", "Victor", "Julie", "Louis", "Alexandre", "David", "Raphaël"]
|
||||||
|
for name in people_list:
|
||||||
|
db.execute("INSERT INTO people (project_id, name, draws, max_draws) VALUES (?, ?, 0, 0)", (project_id, name))
|
||||||
|
recalc_max_draws_for_project(project_id)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def get_user_by_username(username):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("SELECT * FROM user WHERE username = ?", (username,))
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
def check_login(username, password):
|
||||||
|
user = get_user_by_username(username)
|
||||||
|
if user and user["password"] == password:
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def login_required(fn):
|
||||||
|
@wraps(fn)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
def recalc_max_draws_for_project(project_id):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("SELECT total_days FROM project WHERE id = ?", (project_id,))
|
||||||
|
project = cur.fetchone()
|
||||||
|
if not project:
|
||||||
|
return
|
||||||
|
total_days = project["total_days"]
|
||||||
|
cur = db.execute("SELECT COUNT(*) AS c FROM people WHERE project_id = ?", (project_id,))
|
||||||
|
count = cur.fetchone()["c"]
|
||||||
|
if count == 0:
|
||||||
|
return
|
||||||
|
max_draws = math.ceil(total_days / count)
|
||||||
|
db.execute("UPDATE people SET max_draws = ? WHERE project_id = ?", (max_draws, project_id))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def get_project(project_id=None):
|
||||||
|
db = get_db()
|
||||||
|
if project_id:
|
||||||
|
cur = db.execute("SELECT * FROM project WHERE id = ?", (project_id,))
|
||||||
|
return cur.fetchone()
|
||||||
|
cur = db.execute("SELECT * FROM project ORDER BY id ASC")
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
def get_people_stats(project_id):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("SELECT * FROM people WHERE project_id = ? ORDER BY name ASC", (project_id,))
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
def get_draw_for_project_day(project_id, day):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("""
|
||||||
|
SELECT draws.day, people.name, draws.draw_time
|
||||||
|
FROM draws JOIN people ON people.id = draws.person_id
|
||||||
|
WHERE draws.project_id = ? AND draws.day = ?
|
||||||
|
""", (project_id, day))
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
def get_all_draws_for_project(project_id):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("""
|
||||||
|
SELECT draws.day, people.name, draws.draw_time
|
||||||
|
FROM draws JOIN people ON people.id = draws.person_id
|
||||||
|
WHERE draws.project_id = ?
|
||||||
|
ORDER BY draws.day ASC
|
||||||
|
""", (project_id,))
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
def draw_for_project_day(project_id, day):
|
||||||
|
db = get_db()
|
||||||
|
existing = get_draw_for_project_day(project_id, day)
|
||||||
|
if existing:
|
||||||
|
return existing["name"], False
|
||||||
|
|
||||||
|
cur = db.execute("""
|
||||||
|
SELECT id, name, draws, max_draws
|
||||||
|
FROM people WHERE project_id = ? AND draws < max_draws
|
||||||
|
""", (project_id,))
|
||||||
|
candidates = cur.fetchall()
|
||||||
|
if not candidates:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
chosen = random.choice(candidates)
|
||||||
|
db.execute("UPDATE people SET draws = draws + 1 WHERE id = ?", (chosen["id"],))
|
||||||
|
db.execute("INSERT INTO draws (day, person_id, project_id) VALUES (?, ?, ?)",
|
||||||
|
(day, chosen["id"], project_id))
|
||||||
|
db.commit()
|
||||||
|
return chosen["name"], True
|
||||||
|
|
||||||
|
def catchup_draws(project_id):
|
||||||
|
db = get_db()
|
||||||
|
project = get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
today_day = today.day if today.month == 12 else 1
|
||||||
|
if today_day > project["total_days"]:
|
||||||
|
today_day = project["total_days"]
|
||||||
|
|
||||||
|
new_draws = 0
|
||||||
|
for day in range(1, today_day + 1):
|
||||||
|
existing = get_draw_for_project_day(project_id, day)
|
||||||
|
if not existing:
|
||||||
|
name, success = draw_for_project_day(project_id, day)
|
||||||
|
if success:
|
||||||
|
new_draws += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return new_draws
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
@login_required
|
||||||
|
def dashboard():
|
||||||
|
projects = get_project()
|
||||||
|
return render_template("projects.html", projects=projects)
|
||||||
|
|
||||||
|
@app.route("/project/<int:project_id>")
|
||||||
|
@login_required
|
||||||
|
def project_view(project_id):
|
||||||
|
project = get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
flash("Projet non trouvé.")
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
today_day = today.day if today.month == 12 else 1
|
||||||
|
if today_day > project["total_days"]:
|
||||||
|
today_day = project["total_days"]
|
||||||
|
|
||||||
|
today_draw = get_draw_for_project_day(project_id, today_day)
|
||||||
|
all_draws = get_all_draws_for_project(project_id)
|
||||||
|
stats = get_people_stats(project_id)
|
||||||
|
|
||||||
|
return render_template("project_view.html", project=project, today_day=today_day,
|
||||||
|
today_draw=today_draw, all_draws=all_draws, stats=stats)
|
||||||
|
|
||||||
|
@app.route("/project/<int:project_id>/draw-today")
|
||||||
|
@login_required
|
||||||
|
def draw_today(project_id):
|
||||||
|
today = date.today()
|
||||||
|
today_day = today.day if today.month == 12 else 1
|
||||||
|
project = get_project(project_id)
|
||||||
|
if not project or today_day > project["total_days"]:
|
||||||
|
flash("Projet ou jour invalide.")
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
name, is_new = draw_for_project_day(project_id, today_day)
|
||||||
|
if not name:
|
||||||
|
flash("Plus aucune personne disponible pour ce projet.")
|
||||||
|
else:
|
||||||
|
if is_new:
|
||||||
|
flash(f"Tirage du jour {today_day} pour '{project['name']}': {name}")
|
||||||
|
else:
|
||||||
|
flash(f"Le jour {today_day} a déjà été tiré: {name}")
|
||||||
|
return redirect(url_for("project_view", project_id=project_id))
|
||||||
|
|
||||||
|
@app.route("/project/<int:project_id>/catchup")
|
||||||
|
@login_required
|
||||||
|
def catchup_draws_route(project_id):
|
||||||
|
project = get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
flash("Projet non trouvé.")
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
new_draws = catchup_draws(project_id)
|
||||||
|
if new_draws > 0:
|
||||||
|
flash(f"✅ {new_draws} tirages effectués pour rattraper les jours manquants !")
|
||||||
|
else:
|
||||||
|
flash("Tous les jours ont déjà été tirés.")
|
||||||
|
|
||||||
|
return redirect(url_for("project_view", project_id=project_id))
|
||||||
|
|
||||||
|
@app.route("/admin/projects", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def admin_projects():
|
||||||
|
db = get_db()
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get("action")
|
||||||
|
|
||||||
|
if action in ("add", "update"):
|
||||||
|
name = request.form.get("name", "").strip()
|
||||||
|
description = request.form.get("description", "").strip()
|
||||||
|
total_days = int(request.form.get("total_days", 24))
|
||||||
|
|
||||||
|
# Gestion upload image
|
||||||
|
image_url = None
|
||||||
|
if 'image_file' in request.files:
|
||||||
|
file = request.files['image_file']
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
file.save(filepath)
|
||||||
|
image_url = f"/static/uploads/{filename}"
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
if name:
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO project (name, description, image_url, total_days) VALUES (?, ?, ?, ?)",
|
||||||
|
(name, description, image_url, total_days)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
flash("Projet ajouté avec succès.")
|
||||||
|
else: # update
|
||||||
|
project_id = int(request.form.get("project_id"))
|
||||||
|
# Si pas d'image uploadée, conserver l'ancienne
|
||||||
|
if image_url is None:
|
||||||
|
cur = db.execute("SELECT image_url FROM project WHERE id = ?", (project_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
image_url = row["image_url"] if row else None
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
"UPDATE project SET name = ?, description = ?, image_url = ?, total_days = ? WHERE id = ?",
|
||||||
|
(name, description, image_url, total_days, project_id)
|
||||||
|
)
|
||||||
|
recalc_max_draws_for_project(project_id)
|
||||||
|
db.commit()
|
||||||
|
flash("Projet mis à jour avec succès.")
|
||||||
|
|
||||||
|
elif action == "delete":
|
||||||
|
project_id = int(request.form.get("project_id"))
|
||||||
|
db.execute("DELETE FROM project WHERE id = ?", (project_id,))
|
||||||
|
db.commit()
|
||||||
|
flash("Projet supprimé.")
|
||||||
|
|
||||||
|
projects = get_project()
|
||||||
|
return render_template("admin_projects.html", projects=projects)
|
||||||
|
|
||||||
|
@app.route("/admin/project/<int:project_id>/people", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def admin_project_people(project_id):
|
||||||
|
db = get_db()
|
||||||
|
project = get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
flash("Projet non trouvé.")
|
||||||
|
return redirect(url_for('admin_projects'))
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get("action")
|
||||||
|
if action == "add":
|
||||||
|
name = request.form.get("name", "").strip()
|
||||||
|
if name:
|
||||||
|
db.execute("INSERT INTO people (project_id, name, draws, max_draws) VALUES (?, ?, 0, 0)",
|
||||||
|
(project_id, name))
|
||||||
|
recalc_max_draws_for_project(project_id)
|
||||||
|
db.commit()
|
||||||
|
flash("Personne ajoutée.")
|
||||||
|
elif action == "import_csv":
|
||||||
|
if 'csv_file' in request.files:
|
||||||
|
csv_file = request.files['csv_file']
|
||||||
|
if csv_file.filename:
|
||||||
|
content = csv_file.read().decode('utf-8')
|
||||||
|
reader = csv.DictReader(StringIO(content))
|
||||||
|
count = 0
|
||||||
|
for row in reader:
|
||||||
|
name = row.get("name", "").strip()
|
||||||
|
if name:
|
||||||
|
cur = db.execute("SELECT id FROM people WHERE name = ? AND project_id = ?",
|
||||||
|
(name, project_id))
|
||||||
|
if not cur.fetchone():
|
||||||
|
db.execute("INSERT INTO people (project_id, name, draws, max_draws) VALUES (?, ?, 0, 0)",
|
||||||
|
(project_id, name))
|
||||||
|
count += 1
|
||||||
|
recalc_max_draws_for_project(project_id)
|
||||||
|
db.commit()
|
||||||
|
flash(f"{count} personnes importées depuis le CSV.")
|
||||||
|
elif action == "delete":
|
||||||
|
person_id = int(request.form.get("person_id"))
|
||||||
|
db.execute("DELETE FROM people WHERE id = ? AND project_id = ?", (person_id, project_id))
|
||||||
|
recalc_max_draws_for_project(project_id)
|
||||||
|
db.commit()
|
||||||
|
flash("Personne supprimée.")
|
||||||
|
elif action == "reset_draws":
|
||||||
|
db.execute("UPDATE people SET draws = 0 WHERE project_id = ?", (project_id,))
|
||||||
|
db.execute("DELETE FROM draws WHERE project_id = ?", (project_id,))
|
||||||
|
db.commit()
|
||||||
|
flash("Tirages remis à zéro.")
|
||||||
|
|
||||||
|
people = get_people_stats(project_id)
|
||||||
|
return render_template("admin_project_people.html", project=project, people=people)
|
||||||
|
|
||||||
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
if request.method == "POST":
|
||||||
|
username = request.form.get("username")
|
||||||
|
password = request.form.get("password")
|
||||||
|
user = check_login(username, password)
|
||||||
|
if user:
|
||||||
|
session["user_id"] = user["id"]
|
||||||
|
session["username"] = user["username"]
|
||||||
|
flash("Connexion réussie.")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
else:
|
||||||
|
flash("Identifiants invalides.")
|
||||||
|
return render_template("login.html")
|
||||||
|
|
||||||
|
@app.route("/logout")
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
flash("Déconnecté.")
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not os.path.exists(DATABASE):
|
||||||
|
with app.app_context():
|
||||||
|
init_db()
|
||||||
|
app.run(host="0.0.0.0", debug=True)
|
||||||
|
|
||||||
13
compose.yml
Normal file
13
compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
avent_app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- ./templates:/app/templates:ro
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=development
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
|
||||||
19
restart.sh
Executable file
19
restart.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "🛑 Arrêt des containers..."
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
echo "🔨 Reconstruction de l'image..."
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
echo "🚀 Démarrage de l'application..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo "📊 Status des services :"
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
echo "📋 Logs des 20 dernières lignes :"
|
||||||
|
docker-compose logs --tail=20
|
||||||
|
|
||||||
|
echo "✅ App accessible sur http://localhost:5000"
|
||||||
|
echo " Login admin: admin / admin"
|
||||||
|
|
||||||
38
schema.sql
Normal file
38
schema.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
DROP TABLE IF EXISTS draws;
|
||||||
|
DROP TABLE IF EXISTS people;
|
||||||
|
DROP TABLE IF EXISTS project;
|
||||||
|
DROP TABLE IF EXISTS user;
|
||||||
|
|
||||||
|
CREATE TABLE user (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE project (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
image_url TEXT,
|
||||||
|
total_days INTEGER NOT NULL DEFAULT 24
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE people (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
draws INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_draws INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE draws (
|
||||||
|
day INTEGER NOT NULL,
|
||||||
|
person_id INTEGER NOT NULL,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
draw_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (day, project_id),
|
||||||
|
FOREIGN KEY (person_id) REFERENCES people(id),
|
||||||
|
FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
136
templates/admin_people.html
Normal file
136
templates/admin_people.html
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Administration - Calendrier de l'Avent</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">
|
||||||
|
<i class="fas fa-users-cog text-primary me-2"></i>
|
||||||
|
Gestion des participants
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-success me-3">Connecté: {{ session.username }}</span>
|
||||||
|
<a href="{{ url_for('logout') }}" class="btn btn-outline-danger btn-sm">
|
||||||
|
<i class="fas fa-sign-out-alt me-1"></i>Déconnexion
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-calendar me-1"></i>Calendrier
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-info alert-dismissible fade show">
|
||||||
|
{% for msg in messages %}
|
||||||
|
<i class="fas fa-info-circle me-2"></i>{{ msg }}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-primary">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-plus me-2"></i>Ajouter un participant</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="add">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Nom</label>
|
||||||
|
<input type="text" name="name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Max tirages</label>
|
||||||
|
<input type="number" name="max_draws" class="form-control" value="3" min="1" max="10">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-success w-100">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Ajouter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-sync-alt me-2"></i>Réinitialiser</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<form method="post" class="d-inline" onsubmit="return confirm('⚠️ Remettre TOUS les tirages à zéro ? Cette action est irréversible.');">
|
||||||
|
<input type="hidden" name="action" value="reset_draws">
|
||||||
|
<button class="btn btn-warning btn-lg w-100">
|
||||||
|
<i class="fas fa-undo me-2"></i>Reset complet
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<p class="text-muted small mt-2 mb-0">Remet tous les compteurs à 0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-list me-2"></i>Liste des participants ({{ people|length }})
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Tirages</th>
|
||||||
|
<th>Maximum</th>
|
||||||
|
<th>Restant</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in people %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<i class="fas fa-user-circle text-primary me-2"></i>
|
||||||
|
{{ p.name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">{{ p.draws }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ p.max_draws }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if p.draws < p.max_draws %}bg-success{% else %}bg-danger{% endif %}">
|
||||||
|
{{ p.max_draws - p.draws }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" class="d-inline" onsubmit="return confirm('Supprimer {{ p.name }} ?');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="person_id" value="{{ p.id }}">
|
||||||
|
<button class="btn btn-sm btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
141
templates/admin_project_people.html
Normal file
141
templates/admin_project_people.html
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Gestion des utilisateurs - {{ project.name }}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>Utilisateurs pour: <strong>{{ project.name }}</strong></h1>
|
||||||
|
<a href="{{ url_for('admin_projects') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour projets
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-info alert-dismissible fade show mb-4">
|
||||||
|
{% for m in messages %}
|
||||||
|
<div><i class="fas fa-info-circle me-2"></i>{{ m }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-plus me-2"></i>Ajouter un utilisateur</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="add" />
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="text" name="name" placeholder="Nom complet" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-success w-100">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Ajouter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-file-csv me-2"></i>Import CSV</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="action" value="import_csv" />
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="file" name="csv_file" accept=".csv" class="form-control" required />
|
||||||
|
<small class="text-muted d-block mt-1">
|
||||||
|
Format: une colonne <code>name</code><br>
|
||||||
|
Exemple: <code>Marie</code><br>
|
||||||
|
<code>Pierre</code>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-info w-100">
|
||||||
|
<i class="fas fa-upload me-2"></i>Importer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-sync-alt me-2"></i>Réinitialiser</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<form method="post" class="d-inline" onsubmit="return confirm('⚠️ Remettre TOUS les tirages à zéro ?');">
|
||||||
|
<input type="hidden" name="action" value="reset_draws">
|
||||||
|
<button class="btn btn-warning btn-lg w-100">
|
||||||
|
<i class="fas fa-undo me-2"></i>Reset tirages
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Liste des {{ people|length }} participants (Quota: {{ people[0].max_draws if people else 0 }} max)</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Tirages</th>
|
||||||
|
<th>Maximum</th>
|
||||||
|
<th>Restant</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for person in people %}
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-user-circle text-primary me-2"></i>{{ person.name }}</td>
|
||||||
|
<td><span class="badge bg-info">{{ person.draws }}</span></td>
|
||||||
|
<td>{{ person.max_draws }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if person.draws < person.max_draws %}bg-success{% else %}bg-danger{% endif %}">
|
||||||
|
{{ person.max_draws - person.draws }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" class="d-inline" onsubmit="return confirm('Supprimer {{ person.name }} ?');">
|
||||||
|
<input type="hidden" name="action" value="delete" />
|
||||||
|
<input type="hidden" name="person_id" value="{{ person.id }}" />
|
||||||
|
<button class="btn btn-sm btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{{ url_for('project_view', project_id=project.id) }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-calendar me-2"></i>Voir le calendrier
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
118
templates/admin_projects.html
Normal file
118
templates/admin_projects.html
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Administration des projets</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>Gestion des projets</h1>
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Tableau de bord
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-info alert-dismissible fade show">
|
||||||
|
{% for m in messages %}
|
||||||
|
<div><i class="fas fa-info-circle me-2"></i>{{ m }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="card mb-5">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h3 class="mb-0"><i class="fas fa-plus me-2"></i>Ajouter un projet</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" enctype="multipart/form-data" class="row g-3">
|
||||||
|
<input type="hidden" name="action" value="add" />
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input type="text" name="name" class="form-control" placeholder="Nom du projet" required />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<input type="text" name="description" class="form-control" placeholder="Description" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<input type="file" name="image_file" accept="image/*" class="form-control" />
|
||||||
|
<small class="text-muted">PNG, JPG, GIF (max 5 Mo)</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input type="number" name="total_days" class="form-control" value="24" min="1" max="365" required />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button class="btn btn-primary w-100" type="submit">
|
||||||
|
<i class="fas fa-plus"></i> Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Projets existants ({{ projects|length }})</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Illustration</th>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Jours</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for proj in projects %}
|
||||||
|
<tr>
|
||||||
|
<form method="post" enctype="multipart/form-data" class="row gx-2 gy-0 align-items-center">
|
||||||
|
<input type="hidden" name="project_id" value="{{ proj.id }}" />
|
||||||
|
<td>{{ proj.id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if proj.image_url %}
|
||||||
|
<img src="{{ proj.image_url }}" alt="Illustration" style="width:50px; height:50px; object-fit:cover; border-radius:5px;">
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="col-md-3">
|
||||||
|
<input type="text" name="name" value="{{ proj.name }}" class="form-control form-control-sm mb-1" required />
|
||||||
|
</td>
|
||||||
|
<td class="col-md-3">
|
||||||
|
<input type="text" name="description" value="{{ proj.description or '' }}" class="form-control form-control-sm mb-1" placeholder="Description" />
|
||||||
|
<input type="file" name="image_file" accept="image/*" class="form-control form-control-sm" />
|
||||||
|
</td>
|
||||||
|
<td class="col-md-1">
|
||||||
|
<input type="number" name="total_days" value="{{ proj.total_days }}" class="form-control form-control-sm" min="1" max="365" required />
|
||||||
|
</td>
|
||||||
|
<td class="col-md-2">
|
||||||
|
<button name="action" value="update" class="btn btn-sm btn-success me-2" type="submit">
|
||||||
|
<i class="fas fa-save"></i> Sauvegarder
|
||||||
|
</button>
|
||||||
|
<button name="action" value="delete" class="btn btn-sm btn-danger me-2" type="submit"
|
||||||
|
onclick="return confirm('Supprimer ce projet ?');">
|
||||||
|
<i class="fas fa-trash"></i> Supprimer
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('admin_project_people', project_id=proj.id) }}" class="btn btn-sm btn-primary" title="Gérer utilisateurs">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</form>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-link mt-3">Retour au tableau de bord</a>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
348
templates/index.html
Normal file
348
templates/index.html
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>🎄 Calendrier de l'Avent 🎄</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Styles comme dans la version Noël complète */
|
||||||
|
/* (Le contenu CSS complet déjà fourni) */
|
||||||
|
:root {
|
||||||
|
--noel-red: #dc3545;
|
||||||
|
--noel-green: #198754;
|
||||||
|
--noel-gold: #ffc107;
|
||||||
|
--noel-silver: #6c757d;
|
||||||
|
--snow-white: #f8f9fa;
|
||||||
|
--dark-green: #0f5132;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 80%, rgba(120,119,198,.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(255,255,255,.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 40% 40%, rgba(120,219,255,.2) 0%, transparent 50%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snowflake {
|
||||||
|
position: absolute;
|
||||||
|
color: #fff;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: snowfall linear infinite;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes snowfall {
|
||||||
|
to { transform: translateY(100vh) rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.christmas-title {
|
||||||
|
background: linear-gradient(45deg, var(--noel-red), var(--noel-green), var(--noel-gold));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
text-shadow: 0 0 30px rgba(255,255,255,.5);
|
||||||
|
animation: glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
from { filter: drop-shadow(0 0 20px var(--noel-gold)); }
|
||||||
|
to { filter: drop-shadow(0 0 30px var(--noel-red)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(255,255,255,0.95);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 30px 60px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-christmas {
|
||||||
|
background: linear-gradient(45deg, var(--noel-red), var(--noel-green));
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-christmas:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 10px 30px rgba(220,53,69,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-christmas::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-christmas:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-badge {
|
||||||
|
background: linear-gradient(45deg, var(--noel-gold), #ff9800);
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 50px;
|
||||||
|
box-shadow: 0 5px 15px rgba(255,193,7,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-winner {
|
||||||
|
background: linear-gradient(45deg, #28a745, #20c997);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-shadow: 0 0 20px rgba(40,167,69,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table th {
|
||||||
|
background: linear-gradient(45deg, var(--noel-red), var(--noel-green));
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row-full {
|
||||||
|
background: linear-gradient(90deg, var(--noel-gold), #ff9800) !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
right: -20px;
|
||||||
|
font-size: 3rem;
|
||||||
|
animation: bounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
|
||||||
|
40% { transform: translateY(-10px); }
|
||||||
|
60% { transform: translateY(-5px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 15px;
|
||||||
|
border: none;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.person-winner { font-size: 2rem; }
|
||||||
|
.christmas-title { font-size: 2rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="snowflake" style="left:10%; animation-duration: 15s;">❄️</div>
|
||||||
|
<div class="snowflake" style="left:20%; animation-duration: 20s; animation-delay: 1s;">❄️</div>
|
||||||
|
<div class="snowflake" style="left:30%; animation-duration: 18s; animation-delay: 2s;">❄️</div>
|
||||||
|
<div class="snowflake" style="left:60%; animation-duration: 22s;">❄️</div>
|
||||||
|
<div class="snowflake" style="left:80%; animation-duration: 16s; animation-delay: 0s;">❄️</div>
|
||||||
|
<div class="snowflake" style="left:90%; animation-duration: 19s; animation-delay: 3s;">❄️</div>
|
||||||
|
|
||||||
|
<div class="container py-5 main-container">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h1 class="christmas-title display-4 fw-bold mb-3">
|
||||||
|
<i class="fas fa-gift"></i> Calendrier de l'Avent <i class="fas fa-gift"></i>
|
||||||
|
</h1>
|
||||||
|
<p class="lead text-white-50">Qui aura la chance aujourd'hui ? 🎁</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for msg in messages %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="fas fa-star me-2"></i>{{ msg }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center p-5">
|
||||||
|
<i class="fas fa-tree tree-decoration text-success"></i>
|
||||||
|
<h2 class="h3 mb-4">
|
||||||
|
<i class="fas fa-calendar-day text-danger me-2"></i>
|
||||||
|
Jour <span class="day-badge">{{ today_day }}</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{% if today_draw %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="fas fa-crown fa-3x text-warning mb-3"></i>
|
||||||
|
<div class="person-winner">{{ today_draw["name"] }}</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-clock me-1"></i>
|
||||||
|
{{ today_draw["draw_time"] }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>
|
||||||
|
Déjà tiré pour aujourd'hui !
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="mb-4 py-4">
|
||||||
|
<i class="fas fa-question-circle fa-4x text-muted mb-3"></i>
|
||||||
|
<p class="h5 text-muted mb-0">Prêt pour le tirage ?</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('draw_today') }}" class="btn btn-christmas btn-lg">
|
||||||
|
<i class="fas fa-dice me-2"></i>
|
||||||
|
{% if today_draw %}
|
||||||
|
Revoir le tirage
|
||||||
|
{% else %}
|
||||||
|
Tirer pour aujourd'hui !
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="h5 mb-3">
|
||||||
|
<i class="fas fa-chart-bar me-2 text-primary"></i>Statistiques
|
||||||
|
</h3>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>Tirages effectués</span>
|
||||||
|
<span class="fw-bold">{{ all_draws|length }} / {{ max_days }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 8px;">
|
||||||
|
<div class="progress-bar bg-success" style="width: {{ (all_draws|length / max_days * 100)|round(0) }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-transparent border-0">
|
||||||
|
<h3 class="h5 mb-0">
|
||||||
|
<i class="fas fa-history me-2 text-info"></i>Historique
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if all_draws %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for d in all_draws[-10:] %}
|
||||||
|
<div class="list-group-item px-4 py-3">
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h6 class="mb-1">
|
||||||
|
<span class="badge bg-primary rounded-pill me-2">Jour {{ d["day"] }}</span>
|
||||||
|
{{ d["name"] }}
|
||||||
|
</h6>
|
||||||
|
<small class="text-muted">{{ d["draw_time"][:10] }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4 text-muted">
|
||||||
|
<i class="fas fa-calendar-times fa-3x mb-3 opacity-50"></i>
|
||||||
|
<p>Aucun tirage effectué</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-transparent border-0">
|
||||||
|
<h3 class="h5 mb-0">
|
||||||
|
<i class="fas fa-users me-2 text-warning"></i>Statut des participants
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0 stats-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Tirages</th>
|
||||||
|
<th>Max</th>
|
||||||
|
<th>Restant</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in stats %}
|
||||||
|
<tr class="{% if p['draws'] == p['max_draws'] %}stats-row-full{% endif %}">
|
||||||
|
<td><i class="fas fa-user me-2"></i>{{ p["name"] }}</td>
|
||||||
|
<td><span class="badge bg-info">{{ p["draws"] }}</span></td>
|
||||||
|
<td>{{ p["max_draws"] }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if p['draws'] < p['max_draws'] %}bg-success{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ p["max_draws"] - p["draws"] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-5">
|
||||||
|
<p class="text-white-50 mb-0">
|
||||||
|
<i class="fas fa-heart text-danger me-1"></i>
|
||||||
|
Joyeuses fêtes ! 🎄✨
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
51
templates/login.html
Normal file
51
templates/login.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Connexion - Calendrier de l'Avent</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h1 class="h3 mb-3 text-center">
|
||||||
|
<i class="fas fa-sign-in-alt"></i> Connexion
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
{% for msg in messages %}
|
||||||
|
<div>{{ msg }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="post" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Utilisateur</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Mot de passe</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100" type="submit">
|
||||||
|
<i class="fas fa-door-open me-2"></i> Se connecter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center text-muted">
|
||||||
|
<small>Admin par défaut: <strong>admin / admin</strong></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
168
templates/project_view.html
Normal file
168
templates/project_view.html
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ project.name }} - Calendrier de l'Avent</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.person-winner {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: gold;
|
||||||
|
text-shadow: 0 0 10px #ffd700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<img src="{{ project.image_url or url_for('static', filename='default_project.png') }}" alt="Illustration projet" style="max-height:180px; border-radius:12px; margin-bottom:15px;" />
|
||||||
|
<h1 class="display-4 fw-bold mb-1">{{ project.name }}</h1>
|
||||||
|
<p class="lead text-light">{{ project.description or '' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show mb-4">
|
||||||
|
{% for m in messages %}
|
||||||
|
<div><i class="fas fa-star me-2"></i>{{ m }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="card mb-5 text-center p-5">
|
||||||
|
<h2 class="display-5 mb-4">🎄 Jour {{ today_day }} 🎄</h2>
|
||||||
|
{% if today_draw %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="fas fa-crown fa-4x text-warning mb-3"></i>
|
||||||
|
<div class="person-winner">{{ today_draw.name }}</div>
|
||||||
|
<small>{{ today_draw.draw_time }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>Déjà tiré !
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="fas fa-question-circle fa-5x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-white-50">Prêt pour le tirage ?</h3>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<a href="{{ url_for('draw_today', project_id=project.id) }}" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fas fa-dice me-2"></i>Tirer aujourd'hui
|
||||||
|
</a>
|
||||||
|
{% set missing_days = project.total_days - all_draws|length %}
|
||||||
|
{% if missing_days > 0 %}
|
||||||
|
<a href="{{ url_for('catchup_draws_route', project_id=project.id) }}"
|
||||||
|
class="btn btn-warning btn-lg"
|
||||||
|
onclick="return confirm('Rattraper TOUS les {{ missing_days }} jours manquants ?')">
|
||||||
|
<i class="fas fa-magic me-2"></i>Rattraper ({{ missing_days }})
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card text-white bg-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5><i class="fas fa-calendar-check me-2"></i>Tirages effectués</h5>
|
||||||
|
<h2>{{ all_draws|length }} / {{ project.total_days }}</h2>
|
||||||
|
<div class="progress" style="height: 20px;">
|
||||||
|
<div class="progress-bar bg-light" style="width: {{ (all_draws|length / project.total_days * 100)|round(0) }}%">
|
||||||
|
{{ (all_draws|length / project.total_days * 100)|round(0) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-history me-2"></i>Historique récent</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if all_draws %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for d in all_draws[-8:] %}
|
||||||
|
<div class="list-group-item px-3 py-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<h6 class="mb-1">
|
||||||
|
<span class="badge bg-primary rounded-pill me-2">J{{ d.day }}</span>
|
||||||
|
{{ d.name }}
|
||||||
|
</h6>
|
||||||
|
<small>{{ d.draw_time[:10] }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4 text-muted">
|
||||||
|
<i class="fas fa-calendar-times fa-3x mb-3"></i>
|
||||||
|
Aucun tirage
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-users me-2"></i>Participants ({{ stats|length }})</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-bordered table-striped mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Tirages</th>
|
||||||
|
<th>Max tirages</th>
|
||||||
|
<th>Restant</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in stats %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.name }}</td>
|
||||||
|
<td>{{ p.draws }}</td>
|
||||||
|
<td>{{ p.max_draws }}</td>
|
||||||
|
<td>{{ p.max_draws - p.draws }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-5">
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-light btn-lg me-2">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour aux projets
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('admin_project_people', project_id=project.id) }}" class="btn btn-outline-light btn-lg ms-2">
|
||||||
|
<i class="fas fa-cog me-2"></i>Administration
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
63
templates/projects.html
Normal file
63
templates/projects.html
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Tableau de bord</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-5">
|
||||||
|
<h1>Tableau de bord</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-info alert-dismissible fade show mt-4">
|
||||||
|
{% for m in messages %}
|
||||||
|
<div><i class="fas fa-info-circle me-2"></i>{{ m }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<table class="table table-hover mt-3">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Illustration</th>
|
||||||
|
<th>Nom du projet</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Nombre de jours</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in projects %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if p.image_url %}
|
||||||
|
<img src="{{ p.image_url }}" alt="Illustration" style="width:50px; height:50px; object-fit:cover; border-radius:5px;">
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Aucune</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ p.name }}</td>
|
||||||
|
<td>{{ p.description or '—' }}</td>
|
||||||
|
<td>{{ p.total_days }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('project_view', project_id=p.id) }}" class="btn btn-primary btn-sm">Voir calendrier</a>
|
||||||
|
<a href="{{ url_for('admin_project_people', project_id=p.id) }}" class="btn btn-secondary btn-sm">Gérer utilisateurs</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a href="{{ url_for('logout') }}" class="btn btn-link mt-3">Déconnexion</a>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user