Blood Infection Register Lock Rules
Three lock states for the blood-infection register, LIS data flow, and view-vs-table write whitelist.
Page: /blood-bank/blood-infection-register
Module: BloodInfectionRegisterModule.tsx
Why locks exist
Q/D (Qualified / Disqualified) verdicts must not be recorded against a bag with no screening data. The lock prevents staff from clicking through Q/D checkboxes on an empty row and creating a verdict the lab never produced.
Three lock states
| State | Condition | Lock icon | Q/D inputs |
|---|---|---|---|
| Locked (default) | No screening data, not manually unlocked | gray closed | read-only |
| Auto-unlocked | At least one panel result present | green open | editable |
| Manually unlocked | User clicked the lock to override | orange open | editable |
Auto-unlock signal
hasScreeningData(row) returns true if any of these are filled:
nat_result,hiv_result,hbv_result,hcv_result,syphilis_result,antibody_screen— individual TTI panel resultslis_received_at— set when LIS feed delivers resultssource === 'lis'— provenance flag for LIS-imported rows
A value counts as “filled” if it’s not null, not empty string, and not 'not_done'.
Data flow
/blood-bank/blood-intake
→ LIS import or manual entry
→ blood_lis_results.{nat_result | hiv_result | ...}
→ blood_infection_register view picks up the result
→ row auto-unlocks on next render
When the lab tech enters or imports the first panel result for a bag at intake, the corresponding row in the infection register auto-unlocks. They can then qualify/disqualify per panel without an explicit unlock click.
Manual unlock — when to use
For bags where:
- Results were recorded outside the system and need to be back-entered
- A correction is needed on a row whose
*_resultcolumns were cleared - You’re testing the screening UI before LIS is wired up
Manual unlock state persists in localStorage (medos.bloodInfectionRegister.unlockedBags.v1).
Common pitfalls
- Don’t auto-unlock just because a row is a split child (PRC/FFP/etc.). A child bag with no screening data should stay locked — same rule applies to parents and children.
- Don’t rely on
nat_qualified/sero_qualified/antibody_qualifiedto detect “has data.” Those are the outputs of this page, not the inputs. Use the raw*_resultcolumns (andlis_received_at) which come from upstream lab/intake. - Don’t skip the
.maybeSingle()discipline inupsertBloodInfectionScreening. The view’sidis theblood_separation_entries.idwhen no LIS row exists yet —UPDATE blood_lis_results WHERE id = Xreturns 0 rows in that case, and.single()throws “Cannot coerce the result to a single JSON object.” - Don’t spread the row directly into the UPDATE/INSERT payload. The
BloodInfectionScreeningRowshape mirrors the view, which joinsblood_separation_entries(bag_type / bag_status / volume / etc.) onto the lab results. Writing those view-only fields to the underlyingblood_lis_resultstable raises PGRST204 (“Could not find the ‘X’ column in the schema cache”). The service uses an explicit column whitelist (BLOOD_LIS_RESULTS_COLUMNS) — keep new lab-result fields in sync with that list, and never bypass it.
Related
- Service:
upsertBloodInfectionScreening— writes go toblood_lis_results, never the view. Uses theBLOOD_LIS_RESULTS_COLUMNSwhitelist +pickLisResultsPayloadhelper. - Callers (must all funnel through the service, never write directly):
BloodInfectionRegisterModule.tsx— Q/D edits + bulk applyBloodIntakeSpreadsheet.tsx— LIS import + manual screening entryBloodWorkflowDialogs.tsx— pre-crossmatch screening dialog
- Visual grouping rules for sibling component bags (PRC + FFP + …): same module, search for
parentBagKeyandparentBagGrouping.