This commit is contained in:
Louis Mazin 2026-10-05 18:54:10 +02:00
parent 44ba854c7f
commit b890cdd24f
2 changed files with 32 additions and 13 deletions

View File

@ -25,16 +25,25 @@ User = get_user_model()
def planned_minutes_for_period(user_id, start, end):
"""Total planifié sur la période.
"""Total planifié déjà dû sur la période.
Les objectifs hebdomadaires n'imposent aucun jour précis : pour les
statistiques, leur total est réparti uniformément sur les sept jours afin
qu'un jour, un mois et une année restent comparables et additionnables.
Les statistiques ne comptent que les journées strictement antérieures à
aujourd'hui : la journée en cours et les journées futures ne doivent pas
apparaître comme des heures manquantes.
Les objectifs hebdomadaires n'imposent aucun jour précis : leur total reste
réparti uniformément sur les sept jours, mais uniquement pour les journées
déjà passées de la période demandée.
"""
yesterday = timezone.localdate() - timedelta(days=1)
counted_end = min(end, yesterday)
if start > counted_end:
return 0
plan = load_plan(user_id)
total = 0.0
day = start
while day <= end:
while day <= counted_end:
planned = effective_day(user_id, day, plan)
if planned.get('schedule_mode') == 'weekly_hours':
total += float(planned.get('week_target_minutes') or 0) / 7

View File

@ -9,9 +9,8 @@ import {
} from '@/components/ui/dialog';
import { timeApi } from '@/api/time';
import { formatMinutes, personLabel } from '@/components/planning/utils';
import { useAuth } from '@/context/useAuth';
const RACE_DURATION_MS = 20000;
const RACE_DURATION_MS = 8000;
const monthLabel = (value) => {
if (!value) return '';
@ -32,7 +31,6 @@ const dateLabel = (value) => {
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
export default function MonthlyHoursRace() {
const { user } = useAuth();
const [open, setOpen] = useState(false);
const [race, setRace] = useState(null);
const [loading, setLoading] = useState(false);
@ -40,6 +38,7 @@ export default function MonthlyHoursRace() {
const [progress, setProgress] = useState(0);
const [runId, setRunId] = useState(0);
const frameRef = useRef(null);
const replayFrameRef = useRef(null);
const loadRace = useCallback(async () => {
setLoading(true);
@ -60,7 +59,9 @@ export default function MonthlyHoursRace() {
setOpen(nextOpen);
if (!nextOpen) {
if (frameRef.current) cancelAnimationFrame(frameRef.current);
if (replayFrameRef.current) cancelAnimationFrame(replayFrameRef.current);
frameRef.current = null;
replayFrameRef.current = null;
setProgress(0);
}
};
@ -72,9 +73,20 @@ export default function MonthlyHoursRace() {
const replay = () => {
if (frameRef.current) cancelAnimationFrame(frameRef.current);
if (replayFrameRef.current) cancelAnimationFrame(replayFrameRef.current);
frameRef.current = null;
replayFrameRef.current = null;
// On force d'abord un rendu complet à 0 avant de relancer la course.
// Le double requestAnimationFrame garantit qu'au moins une frame vide
// est peinte, ce qui remet aussi correctement la jauge à zéro.
setProgress(0);
setRunId((value) => value + 1);
replayFrameRef.current = requestAnimationFrame(() => {
replayFrameRef.current = requestAnimationFrame(() => {
replayFrameRef.current = null;
setRunId((value) => value + 1);
});
});
};
useEffect(() => {
@ -187,14 +199,13 @@ export default function MonthlyHoursRace() {
<span className="tabular-nums text-muted-foreground">{Math.round(progress * 100)} %</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-muted">
<div className="h-full bg-primary transition-[width] duration-100" style={{ width: `${progress * 100}%` }} />
<div className="h-full bg-primary" style={{ width: `${progress * 100}%` }} />
</div>
</div>
<div className="overflow-x-auto pb-1" aria-label="Course des heures du mois précédent">
<div className="flex min-w-max items-end justify-center gap-1.5 px-2 pt-6 sm:gap-2">
{animatedRows.map((participant) => {
const isMe = String(participant.id) === String(user?.id);
const barHeight = maxFinalMinutes > 0
? clamp((participant.animatedMinutes / maxFinalMinutes) * 100, 0, 100)
: 0;
@ -211,7 +222,7 @@ export default function MonthlyHoursRace() {
<div className="relative flex h-64 w-full items-end justify-center sm:h-72">
<div
className={`w-9 rounded-t-md transition-[height] duration-100 sm:w-10 ${isMe ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' : ''}`}
className="w-9 rounded-t-md sm:w-10"
style={{
height: `${Math.max(barHeight, participant.animatedMinutes > 0 ? 1.5 : 0)}%`,
backgroundColor: participant.color || 'var(--primary)',
@ -224,7 +235,6 @@ export default function MonthlyHoursRace() {
<p className="truncate text-xs font-medium" title={personLabel(participant)}>
{personLabel(participant)}
</p>
{isMe && <p className="text-[10px] text-muted-foreground">toi</p>}
</div>
</div>
);