From edf52acdc9df49f4d8b88b25e0b83f14c551a29b Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Fri, 11 Sep 2026 15:29:46 +0200 Subject: [PATCH] =?UTF-8?q?Semaine=20parfaite=20:=20c=C3=A9l=C3=A9bration?= =?UTF-8?q?=20kitsch=20quand=20tout=20boucle=20=C3=A0=20+0h00?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La dernière heure saisie qui clôt la semaine à +0h00 pile, sans aucune demi-journée restante, déclenche une fête assumée : fond disco en rotation de teintes, pluie d'une fourgonnée de confettis (emojis et rectangles), trophée bondissant et titre doré défilant. Une seule fois par semaine et par onglet, fermeture au clic ou après 8 s, et animation réduite pour prefers-reduced-motion. Le serveur pose un drapeau data-perfect (solde nul, plus rien à remplir, heures dues non nulles) sur les soldes re-rendus à chaque saisie et rafraîchissement ; le client le détecte et célèbre. 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- README.md | 6 +++ app/calcul.py | 1 + app/main.py | 6 +++ app/templates/_soldes.html | 2 +- app/templates/base.html | 55 ++++++++++++++++++++++++++++ app/templates/semaine.html | 69 +++++++++++++++++++++++++++++++++++ app/tests/test_calcul.py | 1 + app/tests/test_ui_refonte.py | 28 ++++++++++++++ docs/screenshot-parfaite.png | Bin 0 -> 986543 bytes 9 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 docs/screenshot-parfaite.png diff --git a/README.md b/README.md index 09eed3d..f0e65de 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,12 @@ Une **alerte de départ** (bandeau ⏰ + notification navigateur) se déclenche quand l'heure de sortie cible est atteinte — y compris si elle a glissé plus tard dans la journée, l'alerte suit la cible la plus fraîche. +Et quand la dernière heure saisie boucle la semaine à **+0h00 pile**, une célébration +kitsch assumée (pluie de confettis, fond disco, trophée bondissant) récompense la +performance — une seule fois par semaine et par onglet, promis. + +![Semaine parfaite](docs/screenshot-parfaite.png) + --- ## Pages diff --git a/app/calcul.py b/app/calcul.py index 24fa7a2..82d3161 100644 --- a/app/calcul.py +++ b/app/calcul.py @@ -1023,6 +1023,7 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int "jours": jours, "total_travaille": minutes_to_hhmm(total_travaille), "total_du": minutes_to_hhmm(total_du), + "total_du_min": total_du, "solde": minutes_to_hhmm(total_travaille - total_du), "solde_min": total_travaille - total_du, "reste_min": reste_min, diff --git a/app/main.py b/app/main.py index 93f58fe..3423733 100644 --- a/app/main.py +++ b/app/main.py @@ -327,6 +327,12 @@ def _build_soldes_ctx(year, week, conges, h_jour, user_id): contexte = _contexte_semaine_precedente(year, week, dates, user_id) result = compute_week(pointages, conges, h_jour, load_plages(user_id), load_astreintes_effectives(user_id), load_facteurs_astreinte(), load_feries(user_id), contexte) result["heures_jour"] = minutes_to_hhmm(h_jour) + # Semaine parfaite : plus rien à remplir et solde à 0 pile — déclenche la + # célébration côté client (une seule fois par onglet). + result["perfect"] = (result["solde_min"] == 0 and result["slots_restants"] == 0 + and result["total_du_min"] > 0) + if result["perfect"]: + result["perfect_key"] = f"{year}-W{week:02d}" # Panneau « aujourd'hui » : uniquement si le jour courant appartient à la # semaine affichée (re-rendu à chaque enregistrement via _soldes.html). today_str = datetime.today().date().isoformat() diff --git a/app/templates/_soldes.html b/app/templates/_soldes.html index bff0d04..9fc2f03 100644 --- a/app/templates/_soldes.html +++ b/app/templates/_soldes.html @@ -1,4 +1,4 @@ -
+
{% if result.today %} {% set jt = result.today.jour %}
diff --git a/app/templates/base.html b/app/templates/base.html index 6e65e4d..aeb326b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1560,6 +1560,61 @@ .chart-tooltip.visible { opacity: 1; } .chart-tooltip .ct-val { font-weight: 500; } .chart-tooltip .ct-label { color: var(--muted); margin-left: .35em; } + + /* ── Semaine parfaite : célébration kitsch (+0h00 pile, rien à remplir) ── */ + #perfect-week { + position: fixed; inset: 0; z-index: 400; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: .6rem; cursor: pointer; + background: + radial-gradient(circle at 20% 20%, rgba(255,0,128,.45), transparent 40%), + radial-gradient(circle at 80% 30%, rgba(0,201,167,.45), transparent 45%), + radial-gradient(circle at 30% 80%, rgba(255,215,0,.45), transparent 40%), + radial-gradient(circle at 75% 85%, rgba(90,80,255,.45), transparent 45%), + rgba(20, 16, 28, .88); + animation: perfect-disco 1.6s linear infinite; + } + .perfect-trophy { font-size: 4.2rem; line-height: 1; animation: perfect-bounce .7s ease-in-out infinite alternate; } + .perfect-title { + font-size: clamp(1.9rem, 6vw, 3.6rem); font-weight: 800; letter-spacing: .04em; + background: linear-gradient(90deg, #ffd700, #ff5ec4, #7af5ff, #ffd700); + background-size: 300% 100%; + -webkit-background-clip: text; background-clip: text; color: transparent; + animation: perfect-shine 2.2s linear infinite, perfect-pop .8s cubic-bezier(.2,1.6,.4,1) 1; + text-align: center; padding: 0 1rem; + } + .perfect-sub { color: #fff; font-size: 1.05rem; text-align: center; padding: 0 1rem; } + .perfect-sub b { font-family: var(--mono); color: #ffd700; } + .perfect-close { color: rgba(255,255,255,.75); font-size: .85rem; margin-top: 1.2rem; } + .perfect-confetti { + position: absolute; top: -8vh; user-select: none; pointer-events: none; + animation: perfect-fall linear forwards; + } + @keyframes perfect-disco { + 0% { filter: hue-rotate(0deg) saturate(1.2); } + 100% { filter: hue-rotate(360deg) saturate(1.2); } + } + @keyframes perfect-shine { + 0% { background-position: 0% 50%; } + 100% { background-position: 300% 50%; } + } + @keyframes perfect-pop { + 0% { transform: scale(.2) rotate(-6deg); opacity: 0; } + 70% { transform: scale(1.15) rotate(2deg); opacity: 1; } + 100% { transform: scale(1) rotate(0deg); opacity: 1; } + } + @keyframes perfect-bounce { + 0% { transform: translateY(0) scale(1); } + 100% { transform: translateY(-14px) scale(1.12); } + } + @keyframes perfect-fall { + 0% { transform: translateY(0) rotate(0deg); opacity: 1; } + 100% { transform: translateY(118vh) rotate(720deg); opacity: .85; } + } + @media (prefers-reduced-motion: reduce) { + #perfect-week, .perfect-trophy, .perfect-title, .perfect-confetti { animation: none; } + .perfect-confetti { display: none; } + } diff --git a/app/templates/semaine.html b/app/templates/semaine.html index 4f243fb..7f7d8e1 100644 --- a/app/templates/semaine.html +++ b/app/templates/semaine.html @@ -230,6 +230,75 @@ })(); + +