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 @@ })(); + +