Classification Management¶
Overview¶
Classification Management defines the medical threshold ranges used to categorise clinical readings into actionable severity levels. Four classification systems exist:
- BGL (Blood Glucose Level) Ranges — Hypo / Normal / Hyper bands with severity 1–5.
- Ketone Thresholds — Ketone level bands with action descriptions.
- Blood Pressure Ranges — Systolic/diastolic bands with OR/AND logic, category, and severity.
- Pulse Rate Ranges — BPM bands with category and severity.
All four systems support a two-tier scope model: global defaults that apply to all Care Recipients, and per-CR overrides that replace the global defaults for a specific individual. Classification ranges drive automated alert thresholds, protocol triggers, and push notifications throughout the application.
Scope Model¶
For a given CR read:
1. Query ranges WHERE care_recipient_id = CR_id
2. If any per-CR ranges exist → use ONLY those
3. Else → use global defaults (WHERE care_recipient_id IS NULL)
This means a per-CR override replaces the entire global set, not just individual rows. If a per-CR range set is defined, the global defaults are ignored entirely.
1. BGL Classification Ranges¶
Backend — BglClassificationController¶
File: server/src/Vitara.Api/Controllers/BglClassificationController.cs
Base route: /api/bgl-classification
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /ranges |
AllRoles |
Returns ranges (filtered by optional careRecipientId query param) |
| GET | /ranges/{id} |
AllRoles |
Returns a single range |
| POST | /ranges |
AdminOrHigher |
Creates a new range |
| PUT | /ranges/{id} |
AdminOrHigher |
Updates a range |
| DELETE | /ranges/{id} |
AdminOrHigher |
Deletes a range |
| GET | /ketone-thresholds |
AllRoles |
Returns ketone thresholds |
| POST | /ketone-thresholds |
AdminOrHigher |
Creates a ketone threshold |
| PUT | /ketone-thresholds/{id} |
AdminOrHigher |
Updates a ketone threshold |
| DELETE | /ketone-thresholds/{id} |
AdminOrHigher |
Deletes a ketone threshold |
Model — BglClassificationRange¶
File: server/src/Vitara.Api/Models/BglClassificationRange.cs
| Field | Type | Notes |
|---|---|---|
Id |
int |
PK |
Label |
string |
e.g., "Severe Hypo" |
MinValue |
decimal? |
Null = no lower bound |
MaxValue |
decimal? |
Null = no upper bound |
BglStateId |
int |
1=Normal, 2=Hypo, 3=Hyper |
SeverityId |
int |
1 (mildest) – 5 (most severe) |
CareRecipientId |
int? |
Null = global default; set = per-CR override |
CreatedAt |
DateTime |
|
UpdatedAt |
DateTime |
Service — BglClassificationService¶
File: server/src/Vitara.Api/Services/BglClassificationService.cs
ClassifyReadingAsync(decimal bglValue, int? careRecipientId)¶
- Queries ranges for
careRecipientId(if provided). - If no per-CR ranges found, falls back to global defaults.
- Iterates all ranges; a range matches if
bglValue >= MinValue (or MinValue is null) AND bglValue < MaxValue (or MaxValue is null). - Returns the matching
BglClassificationRange(or null if no match).
GetRangesAsync(int? careRecipientId)¶
Returns the effective range set for the given CR (per-CR if available, else global).
DTO — CreateBglRangeDto¶
| Field | Type | Required | Notes |
|---|---|---|---|
label |
string |
Yes | |
minValue |
decimal? |
No | |
maxValue |
decimal? |
No | |
bglStateId |
int |
Yes | 1, 2, or 3 |
severityId |
int |
Yes | 1–5 |
careRecipientId |
int? |
No |
2. Ketone Classification Thresholds¶
Ketone thresholds are a sub-resource under BglClassificationController.
Model — KetoneThreshold¶
File: server/src/Vitara.Api/Models/KetoneThreshold.cs
| Field | Type | Notes |
|---|---|---|
Id |
int |
PK |
Label |
string |
e.g., "High Ketones" |
MaxValue |
decimal |
Upper bound for this band |
ActionDescription |
string |
Clinical action guidance text |
CareRecipientId |
int? |
Scope: null = global |
SortOrder |
int |
Display ordering |
Ketone thresholds are evaluated as ascending bands: the first threshold where ketoneValue < MaxValue is the matching band.
Usage¶
The IncidentComponent uses ketone thresholds when type = 'DKA' or when the form's ketone field is populated. The matched threshold's ActionDescription is displayed in the protocol step panel.
3. Blood Pressure Classification Ranges¶
Backend — BpClassificationRangeController¶
File: server/src/Vitara.Api/Controllers/BpClassificationRangeController.cs
Base route: /api/bp-classification
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | / |
AllRoles |
Returns ranges (filtered by optional careRecipientId) |
| GET | /{id} |
AllRoles |
Single range |
| POST | / |
AdminOrHigher |
Create range |
| PUT | /{id} |
AdminOrHigher |
Update range |
| DELETE | /{id} |
AdminOrHigher |
Delete range (seed rows protected) |
Model — BpClassificationRange¶
File: server/src/Vitara.Api/Models/BpClassificationRange.cs
| Field | Type | Notes |
|---|---|---|
Id |
int |
PK |
Label |
string |
e.g., "Stage 2 Hypertension" |
MinSystolic |
int? |
Lower systolic bound (inclusive) |
MaxSystolic |
int? |
Upper systolic bound (exclusive) |
MinDiastolic |
int? |
Lower diastolic bound (inclusive) |
MaxDiastolic |
int? |
Upper diastolic bound (exclusive) |
IsOrLogic |
bool |
If true: match if systolic OR diastolic matches; if false: both must match |
Category |
string |
Hypotension, Normal, Elevated, Stage1, Stage2, Crisis |
SeverityId |
int |
1–5 |
SortOrder |
int |
Determines priority when multiple ranges match |
CareRecipientId |
int? |
Null = global seed |
OR vs AND Logic¶
Most AHA (American Heart Association) BP categories use OR logic — a reading qualifies if either systolic OR diastolic falls in the range. For example:
Stage 1 Hypertension: SBP 130–139 OR DBP 80–89.
A single BP session reading is matched to the range with the highest SortOrder among all matching ranges. This ensures the most severe matching category wins.
Matching Algorithm (BpClassificationService.ClassifyAsync)¶
For each range (ordered by SortOrder descending):
systolicMatch = (minSystolic == null OR sbp >= minSystolic) AND (maxSystolic == null OR sbp < maxSystolic)
diastolicMatch = (minDiastolic == null OR dbp >= minDiastolic) AND (maxDiastolic == null OR dbp < maxDiastolic)
if IsOrLogic:
match = systolicMatch OR diastolicMatch
else:
match = systolicMatch AND diastolicMatch
if match → return this range (first match wins due to descending sort)
Global Seed Data (6 Ranges)¶
The seeder creates 6 global default ranges at startup if none exist:
| Label | SBP Range | DBP Range | OR? | Category | Severity |
|---|---|---|---|---|---|
| Normal | < 120 | < 80 | AND | Normal | 1 |
| Elevated | 120–129 | < 80 | AND | Elevated | 2 |
| Stage 1 Hypertension | 130–139 | 80–89 | OR | Stage1 | 3 |
| Stage 2 Hypertension | ≥ 140 | ≥ 90 | OR | Stage2 | 4 |
| Hypertensive Crisis | > 180 | > 120 | OR | Crisis | 5 |
| Hypotension | < 90 | < 60 | OR | Hypotension | 2 |
The GLOBAL_RANGE_COUNT = 6 constant is used to determine which rows are seed rows; seed rows cannot be deleted via the API.
4. Pulse Rate Classification Ranges¶
Backend — PulseRateClassificationRangeController¶
File: server/src/Vitara.Api/Controllers/PulseRateClassificationRangeController.cs
Base route: /api/pulse-rate-classification
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | / |
AllRoles |
Returns ranges |
| POST | / |
AdminOrHigher |
Create range |
| PUT | /{id} |
AdminOrHigher |
Update range |
| DELETE | /{id} |
AdminOrHigher |
Delete (seed rows protected) |
Model — PulseRateClassificationRange¶
File: server/src/Vitara.Api/Models/PulseRateClassificationRange.cs
| Field | Type | Notes |
|---|---|---|
Id |
int |
PK |
Label |
string |
e.g., "Tachycardia" |
MinBpm |
int? |
Lower bound (inclusive) |
MaxBpm |
int? |
Upper bound (exclusive) |
Category |
string |
VeryLow, Low, Normal, High |
SeverityId |
int |
1–5 |
SortOrder |
int |
Matching priority |
CareRecipientId |
int? |
Null = global |
Global Seed Data (4 Ranges)¶
| Label | BPM Range | Category | Severity |
|---|---|---|---|
| Bradycardia (Severe) | < 40 | VeryLow | 5 |
| Bradycardia | 40–59 | Low | 3 |
| Normal | 60–100 | Normal | 1 |
| Tachycardia | > 100 | High | 3 |
The GLOBAL_RANGE_COUNT = 4 constant guards seed rows from deletion.
PulseRateClassificationService.ClassifyAsync¶
Same simple range scan: finds the first range (sorted by SortOrder desc) where bpm >= MinBpm AND bpm < MaxBpm. Returns the matching PulseRateClassificationRange.
Frontend Components¶
Shared Scope Selector Pattern¶
All four management components use the same care-recipient autocomplete input:
- Typing in the field calls GET /carerecipient and filters results.
- Selecting a CR sets the scope to per-CR; showing only that CR's ranges (or global ranges if none).
- Clearing the selector resets to global defaults view.
BglClassificationManagementComponent¶
- CRUD table with search.
- BGL State dropdown: Normal / Hypo / Hyper.
- Min/Max fields: optional (null = unbounded).
- Severity 1–5 selector.
KetoneClassificationManagementComponent¶
- CRUD table.
- Fields: Label, MaxValue, ActionDescription, SortOrder.
- ActionDescription is a textarea (rich guidance text).
BpClassificationManagementComponent¶
- CRUD table with OR/AND logic toggle.
- Category dropdown: Hypotension / Normal / Elevated / Stage1 / Stage2 / Crisis.
- Delete disabled on the 6 global seed rows (detected by SortOrder or a
IsSeededflag).
PulseRateClassificationManagementComponent¶
- CRUD table.
- Category dropdown: VeryLow / Low / Normal / High.
- Delete disabled on the 4 global seed rows.
Classification in Clinical Features¶
| Feature | Uses |
|---|---|
| BGL Assessment | BglClassificationService — classifies each step reading |
| Diabetes Incident | BglClassificationService — classifies BGL on create |
| BP Session | BpClassificationService + PulseRateClassificationService |
| BP Incident | BpClassificationService |
| Supply projection | Not classification-based |
| Dashboard cards | hasHbpConditions() checks conditions, not BP ranges |
The classification result (label, severityId, category) is stored on the read record at creation time. Changing classification ranges does not retroactively update historical records.