nice
This commit is contained in:
parent
44ba854c7f
commit
b890cdd24f
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user