feat(repo): expand lab results workflows across backend and frontend

This commit is contained in:
Piotr Oleszczyk 2026-03-05 12:46:49 +01:00
parent f1b104909d
commit 0a4ccefe28
19 changed files with 1330 additions and 170 deletions

View file

@ -3,8 +3,9 @@ from datetime import datetime
from typing import Optional
from uuid import UUID, uuid4
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query
from pydantic import field_validator
from sqlalchemy import Integer, cast, func, or_
from sqlmodel import Session, SQLModel, col, select
from db import get_session
@ -120,6 +121,13 @@ class LabResultUpdate(SQLModel):
notes: Optional[str] = None
class LabResultListResponse(SQLModel):
items: list[LabResult]
total: int
limit: int
offset: int
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
@ -251,27 +259,86 @@ def delete_usage(usage_id: UUID, session: Session = Depends(get_session)):
# ---------------------------------------------------------------------------
@router.get("/lab-results", response_model=list[LabResult])
@router.get("/lab-results", response_model=LabResultListResponse)
def list_lab_results(
q: Optional[str] = None,
test_code: Optional[str] = None,
flag: Optional[ResultFlag] = None,
lab: Optional[str] = None,
flags: list[ResultFlag] = Query(default_factory=list),
from_date: Optional[datetime] = None,
to_date: Optional[datetime] = None,
latest_only: bool = False,
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
session: Session = Depends(get_session),
):
stmt = select(LabResult)
filters = []
if q is not None and q.strip():
query = f"%{q.strip()}%"
filters.append(
or_(
col(LabResult.test_code).ilike(query),
col(LabResult.test_name_original).ilike(query),
)
)
if test_code is not None:
stmt = stmt.where(LabResult.test_code == test_code)
filters.append(LabResult.test_code == test_code)
if flag is not None:
stmt = stmt.where(LabResult.flag == flag)
if lab is not None:
stmt = stmt.where(LabResult.lab == lab)
filters.append(LabResult.flag == flag)
if flags:
filters.append(col(LabResult.flag).in_(flags))
if from_date is not None:
stmt = stmt.where(LabResult.collected_at >= from_date)
filters.append(LabResult.collected_at >= from_date)
if to_date is not None:
stmt = stmt.where(LabResult.collected_at <= to_date)
return session.exec(stmt).all()
filters.append(LabResult.collected_at <= to_date)
if latest_only:
ranked_stmt = select(
col(LabResult.record_id).label("record_id"),
func.row_number()
.over(
partition_by=LabResult.test_code,
order_by=(
col(LabResult.collected_at).desc(),
col(LabResult.created_at).desc(),
col(LabResult.record_id).desc(),
),
)
.label("rank"),
)
if filters:
ranked_stmt = ranked_stmt.where(*filters)
ranked_subquery = ranked_stmt.subquery()
latest_ids = select(ranked_subquery.c.record_id).where(
ranked_subquery.c.rank == 1
)
stmt = select(LabResult).where(col(LabResult.record_id).in_(latest_ids))
count_stmt = select(func.count()).select_from(
select(LabResult.record_id)
.where(col(LabResult.record_id).in_(latest_ids))
.subquery()
)
else:
stmt = select(LabResult)
count_stmt = select(func.count()).select_from(LabResult)
if filters:
stmt = stmt.where(*filters)
count_stmt = count_stmt.where(*filters)
test_code_numeric = cast(
func.replace(col(LabResult.test_code), "-", ""),
Integer,
)
stmt = stmt.order_by(
col(LabResult.collected_at).desc(),
test_code_numeric.asc(),
col(LabResult.record_id).asc(),
)
total = session.exec(count_stmt).one()
items = list(session.exec(stmt.offset(offset).limit(limit)).all())
return LabResultListResponse(items=items, total=total, limit=limit, offset=offset)
@router.post("/lab-results", response_model=LabResult, status_code=201)

View file

@ -224,7 +224,11 @@ def test_create_lab_result_invalid_flag(client):
def test_list_lab_results_empty(client):
r = client.get("/health/lab-results")
assert r.status_code == 200
assert r.json() == []
data = r.json()
assert data["items"] == []
assert data["total"] == 0
assert data["limit"] == 50
assert data["offset"] == 0
def test_list_filter_test_code(client):
@ -232,9 +236,9 @@ def test_list_filter_test_code(client):
client.post("/health/lab-results", json={**LAB_RESULT_DATA, "test_code": "2951-2"})
r = client.get("/health/lab-results?test_code=718-7")
assert r.status_code == 200
data = r.json()
assert len(data) == 1
assert data[0]["test_code"] == "718-7"
items = r.json()["items"]
assert len(items) == 1
assert items[0]["test_code"] == "718-7"
def test_list_filter_flag(client):
@ -242,9 +246,9 @@ def test_list_filter_flag(client):
client.post("/health/lab-results", json={**LAB_RESULT_DATA, "flag": "H"})
r = client.get("/health/lab-results?flag=H")
assert r.status_code == 200
data = r.json()
assert len(data) == 1
assert data[0]["flag"] == "H"
items = r.json()["items"]
assert len(items) == 1
assert items[0]["flag"] == "H"
def test_list_filter_date_range(client):
@ -258,8 +262,139 @@ def test_list_filter_date_range(client):
)
r = client.get("/health/lab-results?from_date=2026-05-01T00:00:00")
assert r.status_code == 200
assert len(r.json()["items"]) == 1
def test_list_lab_results_search_and_pagination(client):
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"test_code": "718-7",
"test_name_original": "Hemoglobin",
},
)
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"test_code": "4548-4",
"test_name_original": "Hemoglobin A1c",
},
)
client.post(
"/health/lab-results",
json={**LAB_RESULT_DATA, "test_code": "2951-2", "test_name_original": "Sodium"},
)
r = client.get("/health/lab-results?q=hemo&limit=1&offset=1")
assert r.status_code == 200
data = r.json()
assert len(data) == 1
assert data["total"] == 2
assert data["limit"] == 1
assert data["offset"] == 1
assert len(data["items"]) == 1
assert "Hemoglobin" in data["items"][0]["test_name_original"]
def test_list_lab_results_sorted_newest_first(client):
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"collected_at": "2026-01-01T00:00:00",
"test_code": "1111-1",
},
)
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"collected_at": "2026-06-01T00:00:00",
"test_code": "2222-2",
},
)
r = client.get("/health/lab-results")
assert r.status_code == 200
items = r.json()["items"]
assert items[0]["collected_at"] == "2026-06-01T00:00:00"
def test_list_lab_results_test_code_sorted_numerically_for_same_date(client):
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"collected_at": "2026-06-01T00:00:00",
"test_code": "1111-1",
},
)
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"collected_at": "2026-06-01T00:00:00",
"test_code": "99-9",
},
)
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"collected_at": "2026-06-01T00:00:00",
"test_code": "718-7",
},
)
r = client.get("/health/lab-results")
assert r.status_code == 200
items = r.json()["items"]
assert [items[0]["test_code"], items[1]["test_code"], items[2]["test_code"]] == [
"99-9",
"718-7",
"1111-1",
]
def test_list_lab_results_latest_only_returns_one_per_test_code(client):
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"test_code": "1742-6",
"test_name_original": "ALT",
"collected_at": "2026-01-01T00:00:00",
"value_num": 30,
},
)
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"test_code": "1742-6",
"test_name_original": "ALT",
"collected_at": "2026-02-01T00:00:00",
"value_num": 60,
},
)
client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"test_code": "2093-3",
"test_name_original": "Cholesterol total",
"collected_at": "2026-01-10T00:00:00",
"value_num": 200,
},
)
r = client.get("/health/lab-results?latest_only=true")
assert r.status_code == 200
data = r.json()
assert data["total"] == 2
assert len(data["items"]) == 2
alt = next(item for item in data["items"] if item["test_code"] == "1742-6")
assert alt["collected_at"] == "2026-02-01T00:00:00"
def test_get_lab_result(client):
@ -285,6 +420,34 @@ def test_update_lab_result(client):
assert r2.json()["notes"] == "Recheck in 3 months"
def test_update_lab_result_can_clear_and_switch_value_type(client):
r = client.post(
"/health/lab-results",
json={
**LAB_RESULT_DATA,
"value_num": 12.5,
"unit_original": "mg/dl",
"flag": "H",
},
)
rid = r.json()["record_id"]
r2 = client.patch(
f"/health/lab-results/{rid}",
json={
"value_num": None,
"value_text": "not detected",
"flag": None,
"unit_original": None,
},
)
assert r2.status_code == 200
data = r2.json()
assert data["value_num"] is None
assert data["value_text"] == "not detected"
assert data["flag"] is None
assert data["unit_original"] is None
def test_delete_lab_result(client):
r = client.post("/health/lab-results", json=LAB_RESULT_DATA)
rid = r.json()["record_id"]