feat: History tab on Upload page with one-click Re-export ZIP
Add a second tab to the Upload page that surfaces the persisted batch
archive and lets the user re-download any 837P batch as a ZIP without
re-parsing the original file.
- Backend: /api/batches now carries per-row claimIds (837P only).
835 batches return an empty list, which the UI uses as the signal
to hide the Re-export button on those rows. Avoids an extra
round-trip to /api/batches/{id} per row.
- Frontend: BatchSummary.claimIds added to the list-endpoint type.
- Upload page: page body wrapped in Tabs.Root with a History trigger
that mirrors ?tab= in the URL for deep-link round-trip. The
History tab renders UploadHistory → HistoryTable → HistoryRow with
a one-click Re-export ZIP button per 837P row. The button calls
POST /api/batches/{id}/export-837 with the row's claim ids and
downloads the ZIP via downloadBlob. Falls back to the in-memory
parsedBatches store when the backend returns no rows so the tab
stays useful in sample-data mode.
- Backend tests: claimIds present on 837P rows, empty on 835 rows.
- Frontend tests: 13 tests covering tab switching, URL deep-link,
loading/error/empty states, the 837P-vs-835 button visibility
split, the Re-export happy path, and the failure toast.
This commit is contained in:
@@ -1628,12 +1628,39 @@ def _batch_summary_claim_count(rec: BatchRecord) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _batch_summary_claim_ids(rec: BatchRecord) -> list[str]:
|
||||
"""Return per-claim ids for an 837P batch, or ``[]`` otherwise.
|
||||
|
||||
The Upload page's History tab renders a one-click Re-export ZIP
|
||||
button per row; that button calls
|
||||
``POST /api/batches/{id}/export-837`` with the row's claim ids.
|
||||
Carrying them in the list response avoids an extra round-trip
|
||||
to ``/api/batches/{id}`` for every row. 835 has no re-export
|
||||
endpoint, so the list is empty for those — the UI uses the
|
||||
empty list as the signal to hide the button.
|
||||
"""
|
||||
if rec.kind != "837p":
|
||||
return []
|
||||
return [
|
||||
c.claim_id
|
||||
for c in rec.result.claims # type: ignore[attr-defined]
|
||||
if getattr(c, "claim_id", None)
|
||||
]
|
||||
|
||||
|
||||
@app.get("/api/batches", dependencies=[Depends(matrix_gate)])
|
||||
def list_batches(
|
||||
request: Request,
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
) -> Any:
|
||||
"""Summary of all parsed batches, newest first."""
|
||||
"""Summary of all parsed batches, newest first.
|
||||
|
||||
Each item includes ``claimIds`` (837P only) so the History tab
|
||||
on the Upload page can render a one-click re-export button per
|
||||
row without an extra round-trip to ``/api/batches/{id}``. The
|
||||
list is still capped at ``limit`` claims; see the full result
|
||||
via the by-id endpoint when more is needed.
|
||||
"""
|
||||
records = store.list(limit=limit)
|
||||
items = [
|
||||
{
|
||||
@@ -1642,6 +1669,7 @@ def list_batches(
|
||||
"inputFilename": r.input_filename,
|
||||
"parsedAt": r.parsed_at.isoformat().replace("+00:00", "Z"),
|
||||
"claimCount": _batch_summary_claim_count(r),
|
||||
"claimIds": _batch_summary_claim_ids(r),
|
||||
}
|
||||
for r in records
|
||||
]
|
||||
|
||||
@@ -65,6 +65,42 @@ def test_batches_returns_summary_after_parse(seeded_store):
|
||||
assert body["items"][0]["inputFilename"] == "x.txt"
|
||||
|
||||
|
||||
def test_batches_includes_claim_ids_for_837p(seeded_store):
|
||||
"""The Upload page's History tab renders a Re-export ZIP button per
|
||||
row that fires ``POST /api/batches/{id}/export-837`` with the row's
|
||||
claim ids. Carry those ids in the list response so the UI doesn't
|
||||
need an extra round-trip per row to fetch them."""
|
||||
batches = seeded_store.get("/api/batches", headers=JSON).json()["items"]
|
||||
assert len(batches) == 1
|
||||
item = batches[0]
|
||||
assert item["kind"] == "837p"
|
||||
# claimIds is a non-empty list of the same claim ids that live
|
||||
# inside the parsed result — see `seeded_store` in conftest.py.
|
||||
assert isinstance(item["claimIds"], list)
|
||||
assert len(item["claimIds"]) == 2
|
||||
full = seeded_store.get(f"/api/batches/{item['id']}").json()
|
||||
assert sorted(item["claimIds"]) == sorted(c["claim_id"] for c in full["claims"])
|
||||
|
||||
|
||||
def test_batches_claim_ids_empty_for_835(client: TestClient, tmp_path):
|
||||
"""835 has no re-export endpoint — the field is always ``[]`` so the
|
||||
History tab can use it as the signal to hide the Re-export button."""
|
||||
src = Path(__file__).parent / "fixtures" / "minimal_835.txt"
|
||||
files = {"file": ("minimal_835.txt", src.read_bytes(), "text/plain")}
|
||||
r = client.post(
|
||||
"/api/parse-835",
|
||||
params={"payer": "co_medicaid_835"},
|
||||
files=files,
|
||||
headers=JSON,
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
body = client.get("/api/batches", headers=JSON).json()
|
||||
assert body["total"] == 1
|
||||
item = body["items"][0]
|
||||
assert item["kind"] == "835"
|
||||
assert item["claimIds"] == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# /api/batches/{id}
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user