feat: add recap page

This commit is contained in:
2026-07-17 17:19:26 +02:00
parent 0cfe4c7108
commit 63a5a69c67
6 changed files with 281 additions and 58 deletions
+63 -1
View File
@@ -2,7 +2,7 @@ from datetime import date as date_type
from datetime import datetime, timedelta
from typing import Optional
from django.db.models import Avg, Max, Min
from django.db.models import Avg, Count, Max, Min
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.utils import timezone
@@ -81,6 +81,17 @@ class DayOut(Schema):
note: Optional[DailyNoteOut]
class RecapDayOut(Schema):
"""One line of the recap: did every dose go out that day, and what was the pulse."""
date: date_type
taken: int
expected: int
complete: bool
bpm: Optional[int]
note: str
class DoseLogIn(Schema):
scheduled_dose_id: int
date: Optional[date_type] = None
@@ -147,6 +158,51 @@ def build_day(day: date_type) -> dict:
}
def build_recap(days: int) -> list[dict]:
"""One line per day, newest first: dose completeness, pulse and note.
"Complete" is measured against the doses that are active now, not the plan
as it stood on that day — the point is spotting gaps, not auditing history.
"""
today = timezone.localdate()
since = today - timedelta(days=days - 1)
expected = ScheduledDose.objects.filter(
is_active=True, medication__is_active=True
).count()
taken_by_day = {
row["date"]: row["taken"]
for row in DoseLog.objects.filter(
date__gte=since,
taken=True,
scheduled_dose__is_active=True,
scheduled_dose__medication__is_active=True,
)
.values("date")
.annotate(taken=Count("id"))
}
pulse_by_day = {
reading.date: reading.bpm
for reading in PulseReading.objects.filter(date__gte=since)
}
note_by_day = {
note.date: note.body for note in DailyNote.objects.filter(date__gte=since)
}
return [
{
"date": day,
"taken": taken_by_day.get(day, 0),
"expected": expected,
"complete": expected > 0 and taken_by_day.get(day, 0) == expected,
"bpm": pulse_by_day.get(day),
"note": note_by_day.get(day, ""),
}
for day in (today - timedelta(days=offset) for offset in range(days))
]
def pulse_trend(readings: list[PulseReading]) -> Optional[float]:
"""Least-squares slope in bpm/day: ~0 means stable, positive means climbing."""
if len(readings) < 2:
@@ -202,6 +258,12 @@ def get_day(request: HttpRequest, date: Optional[date_type] = None):
return build_day(date or timezone.localdate())
@router.get("/recap", response=list[RecapDayOut])
def get_recap(request: HttpRequest, days: int = 14):
"""Day-by-day recap, newest first: were all doses given, pulse and note."""
return build_recap(days)
@router.post("/doses", response=DayOut)
def log_dose(request: HttpRequest, payload: DoseLogIn):
"""Tick a dose off (or untick it). Idempotent per (dose, day)."""