From 93abec428f9b61a71dafac0e72f3a1d0496c37ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:21:42 +0000 Subject: [PATCH] docs(clinical): rewrite condition docs and add symptom/diagnosis flows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../concepts/clinical/condition.mdx | 91 +++++------ .../flows/clinical/record-a-diagnosis.mdx | 82 ++++++++++ .../flows/clinical/record-a-symptom.mdx | 82 ++++++++++ .../references/clinical/condition.mdx | 152 ++++++++++-------- .../concepts/clinical/condition.mdx | 91 +++++------ .../flows/clinical/record-a-diagnosis.mdx | 82 ++++++++++ .../flows/clinical/record-a-symptom.mdx | 82 ++++++++++ .../references/clinical/condition.mdx | 152 ++++++++++-------- versioned_sidebars/version-3.0-sidebars.json | 6 +- versioned_sidebars/version-3.1-sidebars.json | 4 +- 10 files changed, 592 insertions(+), 232 deletions(-) create mode 100644 versioned_docs/version-3.0/flows/clinical/record-a-diagnosis.mdx create mode 100644 versioned_docs/version-3.0/flows/clinical/record-a-symptom.mdx create mode 100644 versioned_docs/version-3.1/flows/clinical/record-a-diagnosis.mdx create mode 100644 versioned_docs/version-3.1/flows/clinical/record-a-symptom.mdx diff --git a/versioned_docs/version-3.0/concepts/clinical/condition.mdx b/versioned_docs/version-3.0/concepts/clinical/condition.mdx index f62b02f..8c5c101 100644 --- a/versioned_docs/version-3.0/concepts/clinical/condition.mdx +++ b/versioned_docs/version-3.0/concepts/clinical/condition.mdx @@ -4,73 +4,64 @@ sidebar_position: 3 # Condition -A **condition** is a clinical problem recorded for a patient — a diagnosis, a chronic illness, or a presenting symptom. It is how a patient's diagnoses and problem list are captured, giving every clinician a shared, durable view of what the patient is being treated for. +## Definition -In the Care product UI, conditions are surfaced as **Symptoms**. +A **[condition](https://build.fhir.org/condition.html)** is a clinical problem that the care team records for a patient. Care uses one condition record for two things that clinicians enter separately: a **Symptom**, which is what the patient reports or the clinician observes, and a **Diagnosis**, which is what the clinician concludes. A condition is a standing statement about the patient. It stays in the record after the encounter ends, until someone changes its status. -## What it represents +## Key Attributes -A condition is a *standing statement* about a patient's health, not a one-time reading. A blood pressure value or a lab result is an [observation](../clinical/observation.mdx) — true at the moment it was taken. A condition is a claim the care team is making and tracking: "this patient has diabetes," "this patient presented with chest pain." That claim persists across visits until someone changes its status, which is why a condition needs two things an observation does not — a statement of **how certain** the team is, and a statement of **how the condition is progressing**. +| Components | What it captures | +| --- | --- | +| Symptom or Diagnosis | The coded clinical finding, selected from the Care terminology list. The clinician cannot type free text here. | +| Status | How the condition progresses over time, from Active to Resolved. | +| Verification | How certain the care team is about the condition. | +| Severity | How severe the condition is: Mild, Moderate, or Severe. | +| Onset Date | The date the condition started. The date cannot be in the future. | +| Notes | Free text that the clinician adds about the condition. | +| Encounter | The encounter in which the clinician records the condition. Care attaches the condition to the patient of that encounter. | +| Recorded by | The user who records the condition, and the date of the record. | -To keep problems comparable across patients and facilities, the condition itself is recorded as a coded clinical finding (drawn from a SNOMED CT vocabulary) rather than free text. Onset, optional severity, the encounter it was noted in, and a free-text note round out the record. +### Symptom and Diagnosis -## Classification +Care keeps symptoms and diagnoses in separate lists, and gives each list its own form. -The **category** answers "what kind of problem is this, and where does it live in the record?" Every condition is one of: +- A **Symptom** is what the patient has. The clinician records a symptom early in the encounter, such as fever or chest pain. +- A **Diagnosis** is what the clinician concludes. The clinician records a diagnosis for the encounter, such as pneumonia. -- **Problem-list item** — an ongoing problem the care team is tracking for this patient -- **Encounter diagnosis** — a diagnosis made or confirmed during a specific visit -- **Chronic condition** — a long-term condition such as diabetes or hypertension, carried across encounters +The two lists never mix. A symptom that you record stays under **Symptoms**, and a diagnosis stays under **Diagnoses**. -Running alongside the category is a separate axis — the **verification status** — that records certainty. A symptom under investigation might be `unconfirmed`, `provisional`, or `differential`; a settled diagnosis is `confirmed`; something logged in error is `refuted` or `entered_in_error`. Care always requires a verification status, so the record never blurs a working hypothesis with an established fact. +### Status -## Lifecycle +Status records the course of the condition. Verification records the certainty of the care team. The two are independent: a condition can be Active and still Provisional. -The **clinical status** tracks where a condition stands over time. It is distinct from verification — that is about how sure the team is; this is about the condition's actual course: +| Status | Description | +| --- | --- | +| Active | The condition is present now, and the care team manages it. | +| Recurrence | The condition returned after a period without symptoms. | +| Relapse | The condition returned after remission. | +| Inactive | The condition is not active now, but nobody resolved it. | +| Remission | The symptoms decreased, but the condition can return. | +| Resolved | The condition cleared. | -```text -active → inactive → remission → resolved - ↑ | - └─ recurrence / relapse ─┘ -``` +### Verification -- **active** — currently present and being managed -- **recurrence** — returned after a symptom-free period -- **relapse** — returned after being in remission -- **inactive** — no longer active, but not formally resolved -- **remission** — symptoms have abated, but the condition may return -- **resolved** — fully cleared -- **unknown** — current state is not known +| Verification | Description | +| --- | --- | +| Unconfirmed | The care team recorded the condition, but has no evidence yet. | +| Provisional | The condition is a working answer while the care team continues to examine the patient. | +| Differential | The condition is one of the possible answers that the care team examines. | +| Confirmed | The care team has enough evidence for the condition. | +| Refuted | The care team examined the condition and rejected it. | +| Entered in Error | A user recorded the condition by mistake. Care keeps the record for audit and hides it from the lists. | -This is not a one-way pipeline. A chronic condition can cycle through active, remission, and recurrence many times over a patient's history. - -## How it connects - -A condition never stands alone — it is always anchored to a patient and the visit where it was noted: - -- **Patient** — the person the condition describes. Care derives this automatically from the encounter, so the condition is always attached to the right record; clients never set it directly. -- **Encounter** — the visit during which the condition was recorded. Every condition is created in the context of an encounter; chronic conditions can later be re-associated with a new encounter as care continues. - -Conditions sit alongside the patient's other clinical records — most closely [allergies and intolerances](../clinical/allergy-intolerance.mdx), which capture a different kind of standing risk, and [observations](../clinical/observation.mdx), which capture point-in-time measurements and findings. - -## Permissions - -A condition has no permission file of its own — as patient clinical data, it is governed by the **patient** and **encounter** clinical-data permissions a user holds in the relevant facility. Creating, updating, and deleting a condition is gated by write access to the encounter's clinical data; reading it requires the patient's clinical-data permission, falling back to the encounter's clinical-data read permission. Chronic conditions are a special case — updating one is gated by the patient's clinical-data permission rather than the encounter's. Conditions can also be captured by submitting a symptom or diagnosis questionnaire. - -| Permission | Description | System Roles | -| --- | --- | --- | -| `can_write_encounter_clinical_data` | Create, update, or delete a condition (the create, update, and destroy paths check write access to the encounter's clinical data; chronic-condition updates are the exception below) | Admin, Doctor, Nurse, Facility Admin | -| `can_view_clinical_data` | Read a patient's conditions, and update a chronic condition (the read path checks the patient's clinical-data permission; chronic-condition updates check this same permission) | Staff, Doctor, Nurse, Admin, Facility Admin | -| `can_read_encounter_clinical_data` | Read conditions via an encounter when patient-level clinical access is absent (the read path falls back to this when an `encounter` query param is supplied) | Admin, Doctor, Nurse, Facility Admin | -| `can_submit_patient_questionnaire` | Submit a patient-subject questionnaire (such as symptom or diagnosis), which can record conditions | Volunteer, Staff, Doctor, Nurse, Admin, Facility Admin, Administrator | -| `can_submit_encounter_questionnaire` | Submit an encounter-linked questionnaire, which can record conditions | Staff, Doctor, Nurse, Admin, Facility Admin | - -Roles are granted to users through facility, organization, or patient memberships, and they cascade down the organization tree — a role held high in the hierarchy applies to the facilities and patients beneath it. +A clinician cannot select **Entered in Error** for a new record. Care sets this verification when the clinician removes a condition that is already saved. ## Related -- Reference: [Condition (technical)](../../references/clinical/condition.mdx) +- Flow: [Record a symptom](../../flows/clinical/record-a-symptom.mdx) +- Flow: [Record a diagnosis](../../flows/clinical/record-a-diagnosis.mdx) - Concept: [Patient](../clinical/patient) - Concept: [Encounter](../clinical/encounter.mdx) - Concept: [Allergy / intolerance](../clinical/allergy-intolerance.mdx) - Concept: [Observation](../clinical/observation.mdx) +- Reference: [Condition](../../references/clinical/condition.mdx) diff --git a/versioned_docs/version-3.0/flows/clinical/record-a-diagnosis.mdx b/versioned_docs/version-3.0/flows/clinical/record-a-diagnosis.mdx new file mode 100644 index 0000000..df7b6e9 --- /dev/null +++ b/versioned_docs/version-3.0/flows/clinical/record-a-diagnosis.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 3 +--- + +# How to record a diagnosis + +## Overview + +This flow describes how to record a diagnosis for a patient during an encounter. A diagnosis is one kind of [condition](../../concepts/clinical/condition.mdx) in Care. + +## Pre-requisites + +- The patient is registered in Care. +- The patient has an encounter that is open. You cannot record a diagnosis in an encounter that is completed, cancelled, discontinued, or marked as entered in error. +- You are a member of the facility that owns the encounter. +- You have the permissions listed below. + +## Permissions + +| Permission | Access | +| --- | --- | +| Update Encounter related clinical data | Lets you add, change, and remove diagnoses in an encounter. | +| Can view clinical data about patients | Lets you see the diagnoses that the care team already recorded. | + +## Steps + +### 1. Open the encounter + +1. Open the facility. +2. Find the patient, and open the encounter. + +### 2. Open the diagnosis form + +1. Select **Encounter Actions**. +2. Select **Add Diagnosis**. + +You can press `d` instead, while you view the encounter. The `d` shortcut opens the same diagnosis form. + +### 3. Select the diagnosis + +1. Select **Add Diagnosis** in the form. +2. Enter the name of the diagnosis in the search box. +3. Select the diagnosis from the list of results. + +Care shows a warning if you select a diagnosis that the list already contains. Change the existing entry instead of adding a second one. + +### 4. Complete the details + +Set the details of the diagnosis. Care fills the values in the Default column for you. + +| Components | What it captures | Default | +| --- | --- | --- | +| Onset Date | The date the diagnosis started. You cannot select a date in the future. | Today | +| Status | The course of the diagnosis, from Active to Resolved. | Active | +| Severity | How severe the diagnosis is: Mild, Moderate, or Severe. | Moderate | +| Verification | How certain you are about the diagnosis. Select **Provisional** or **Differential** while you still examine the patient. | Confirmed | +| Notes | Free text about the diagnosis. | Empty | + +Repeat step 3 and step 4 for each additional diagnosis. + +### 5. Save the diagnoses + +- Select **Submit** to save the diagnoses to the encounter. +- Select **Save as Draft** to keep your entries and finish later. + +## Expected Outcome + +- The diagnoses show in the **Diagnoses** list on the **Overview** tab of the encounter. +- The diagnoses show under **Past Diagnoses** in the clinical history of the patient. +- Care records your name and the date against each diagnosis. + +## Related + +Concepts: + +- [Condition](../../concepts/clinical/condition.mdx) +- [Encounter](../../concepts/clinical/encounter.mdx) +- [Patient](../../concepts/clinical/patient) + +Flows: + +- [How to record a symptom](./record-a-symptom.mdx) diff --git a/versioned_docs/version-3.0/flows/clinical/record-a-symptom.mdx b/versioned_docs/version-3.0/flows/clinical/record-a-symptom.mdx new file mode 100644 index 0000000..741cfe4 --- /dev/null +++ b/versioned_docs/version-3.0/flows/clinical/record-a-symptom.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 2 +--- + +# How to record a symptom + +## Overview + +This flow describes how to record a symptom for a patient during an encounter. A symptom is one kind of [condition](../../concepts/clinical/condition.mdx) in Care. + +## Pre-requisites + +- The patient is registered in Care. +- The patient has an encounter that is open. You cannot record a symptom in an encounter that is completed, cancelled, discontinued, or marked as entered in error. +- You are a member of the facility that owns the encounter. +- You have the permissions listed below. + +## Permissions + +| Permission | Access | +| --- | --- | +| Update Encounter related clinical data | Lets you add, change, and remove symptoms in an encounter. | +| Can view clinical data about patients | Lets you see the symptoms that the care team already recorded. | + +## Steps + +### 1. Open the encounter + +1. Open the facility. +2. Find the patient, and open the encounter. + +### 2. Open the symptom form + +1. Select **Encounter Actions**. +2. Select **Add Symptom**. + +You can press `s` instead, while you view the encounter. The `s` shortcut opens the same symptom form. + +### 3. Select the symptom + +1. Select **Add Symptom** in the form. +2. Enter the name of the symptom in the search box. +3. Select the symptom from the list of results. + +Care shows a warning if you select a symptom that the list already contains. Change the existing entry instead of adding a second one. + +### 4. Complete the details + +Set the details of the symptom. Care fills the values in the Default column for you. + +| Components | What it captures | Default | +| --- | --- | --- | +| Onset Date | The date the symptom started. You cannot select a date in the future. | Today | +| Status | The course of the symptom, from Active to Resolved. | Active | +| Severity | How severe the symptom is: Mild, Moderate, or Severe. | Moderate | +| Verification | How certain you are about the symptom. | Confirmed | +| Notes | Free text about the symptom. | Empty | + +Repeat step 3 and step 4 for each additional symptom. + +### 5. Save the symptoms + +- Select **Submit** to save the symptoms to the encounter. +- Select **Save as Draft** to keep your entries and finish later. + +## Expected Outcome + +- The symptoms show in the **Symptoms** list on the **Overview** tab of the encounter. +- The symptoms show under **Past Symptoms** in the clinical history of the patient. +- Care records your name and the date against each symptom. + +## Related + +Concepts: + +- [Condition](../../concepts/clinical/condition.mdx) +- [Encounter](../../concepts/clinical/encounter.mdx) +- [Patient](../../concepts/clinical/patient) + +Flows: + +- [How to record a diagnosis](./record-a-diagnosis.mdx) diff --git a/versioned_docs/version-3.0/references/clinical/condition.mdx b/versioned_docs/version-3.0/references/clinical/condition.mdx index b8fc14d..a488749 100644 --- a/versioned_docs/version-3.0/references/clinical/condition.mdx +++ b/versioned_docs/version-3.0/references/clinical/condition.mdx @@ -4,63 +4,66 @@ sidebar_position: 3 # Condition -A `Condition` records a clinical problem, diagnosis, or symptom against a patient. It maps to the FHIR `Condition` resource and shows up in the Care product UI as **Symptoms**. +Technical reference for the `Condition` module in Care EMR. For the plain-language view, read the [Condition concept](../../concepts/clinical/condition.mdx). **Source:** - Model: [`care/emr/models/condition.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/models/condition.py) - Resource spec: [`care/emr/resources/condition/spec.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/spec.py) - Value set: [`care/emr/resources/condition/valueset.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/valueset.py) +- Viewsets: [`care/emr/api/viewsets/condition.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/api/viewsets/condition.py) -A `Condition` lives across two layers, and the split matters when you read the code: +`Condition` has two layers, and the split matters when you read the code: -- The Django model (`care/emr/models/condition.py`) is storage. Its coded and timing fields (`code`, `body_site`, `onset`, `abatement`) are opaque `JSONField`s — the model says nothing about their shape. -- The Pydantic resource specs (`care/emr/resources/condition/spec.py`) are the API. They define the enums, the structure inside each JSON field, field validation, the value-set binding for `code`, and the separate read and write schemas. +- The Django model is storage. Its coded and timing fields (`code`, `body_site`, `onset`, `abatement`) are opaque `JSONField`s. The model says nothing about their shape. +- The Pydantic resource specs are the API. They define the enums, the structure inside each JSON field, the validation, the value-set binding for `code`, and the separate read and write schemas. + +One model backs two API surfaces. `SymptomViewSet` serves `symptom`, and `DiagnosisViewSet` serves `diagnosis`. The `category` field separates them. ## Models | Model | Purpose | | --- | --- | -| `Condition` | A clinical condition, problem, diagnosis, or symptom recorded for a patient | +| `Condition` | A clinical problem, symptom, or diagnosis recorded for a patient | -`Condition` extends [`EMRBaseModel`](../foundation/base-model.mdx), the shared Care EMR base that provides `external_id`, `created_date`/`modified_date`, soft-delete via `deleted`, `created_by`/`updated_by`, and `history`/`meta` JSON. +`Condition` extends [`EMRBaseModel`](../foundation/base-model.mdx), which provides `external_id`, `created_date`/`modified_date`, `created_by`/`updated_by`, soft delete through `deleted`, and the `history`/`meta` JSON fields. ## `Condition` fields -### Status & classification +### Status and classification | Field | Type | Notes | | --- | --- | --- | -| `clinical_status` | `CharField(100)`, nullable | Clinical state. Spec binds to [`ClinicalStatusChoices`](#clinicalstatuschoices-values); optional on write. | -| `verification_status` | `CharField(100)`, nullable | Level of certainty. Spec binds to [`VerificationStatusChoices`](#verificationstatuschoices-values); **required** on write (`ConditionSpec`/`ConditionUpdateSpec`). | -| `category` | `CharField(100)`, nullable | Classification. Spec binds to [`CategoryChoices`](#categorychoices-values); **required** on create (`ConditionSpec`). | -| `severity` | `CharField(100)`, nullable | Subjective severity. Spec binds to [`SeverityChoices`](#severitychoices-values); optional on write. | +| `clinical_status` | `CharField(100)`, nullable | Course of the condition. Bound to [`ClinicalStatusChoices`](#clinicalstatuschoices-values) by the specs; optional on write. | +| `verification_status` | `CharField(100)`, nullable | Certainty. Bound to [`VerificationStatusChoices`](#verificationstatuschoices-values); **required** on `ConditionSpec` and `ConditionUpdateSpec`. | +| `category` | `CharField(100)`, nullable | Bound to [`CategoryChoices`](#categorychoices-values); **required** on create. `SymptomViewSet` overwrites it with `problem_list_item` in `perform_create`. | +| `severity` | `CharField(100)`, nullable | Bound to [`SeverityChoices`](#severitychoices-values); optional on write. | ### Coded concepts | Field | Type | Notes | | --- | --- | --- | -| `code` | `JSONField` (`default=dict`, not null/blank) | The condition itself. On write, a single [`Coding`](#coding-shape) bound to the [`CARE_CODITION_CODE_VALUESET`](#code-value-set-binding) value set (SNOMED CT clinical findings). On read, serialized as a plain `Coding`. | -| `body_site` | `JSONField` (`default=dict`, not null/blank) | Anatomical location(s). Exists on the model but no current spec exposes it — never written or read through the standard `Condition` API. | +| `code` | `JSONField` (`default=dict`, not null/blank) | The condition itself. On write, a single [`Coding`](#coding-shape) bound to the [condition code value set](#code-value-set-binding). On read, a plain `Coding`. | +| `body_site` | `JSONField` (`default=dict`, not null/blank) | Anatomical site. No current spec exposes it, so no client reads or writes it. | ### Timing | Field | Type | Notes | | --- | --- | --- | -| `onset` | `JSONField` (`default=dict`) | Choice-of-type onset, shaped by [`ConditionOnSetSpec`](#conditiononsetspec-onset-shape). Defaults to `{}`. | -| `abatement` | `JSONField` (`default=dict`) | When the condition resolved or went into remission. Shaped by [`ConditionAbatementSpec`](#conditionabatementspec-abatement-shape). Defaults to `{}`. | -| `recorded_date` | `DateTimeField`, nullable | When the condition was first recorded. Not exposed by the current specs. | +| `onset` | `JSONField` (`default=dict`) | Shaped by [`ConditionOnSetSpec`](#conditiononsetspec-onset-shape). | +| `abatement` | `JSONField` (`default=dict`) | Shaped by [`ConditionAbatementSpec`](#conditionabatementspec-abatement-shape). | +| `recorded_date` | `DateTimeField`, nullable | Not exposed by the current specs. | -### Context & notes +### Context and notes | Field | Type | Notes | | --- | --- | --- | -| `patient` | `FK → Patient`, `on_delete=CASCADE` | Subject of the condition. Derived server-side from the encounter on create, never read from the client (`__exclude__`). | -| `encounter` | `FK → Encounter`, nullable, `on_delete=CASCADE` | Encounter the condition was recorded in. Set server-side on create (`__exclude__`); the client supplies a UUID in the spec, which is validated to exist. | -| `note` | `TextField`, nullable | Free-text clinical note. | +| `patient` | `FK → Patient`, `on_delete=CASCADE` | Derived server-side from the encounter on create. Listed in `__exclude__`, so no client sets it. | +| `encounter` | `FK → Encounter`, nullable, `on_delete=CASCADE` | Set server-side on create from the UUID in the write spec. Listed in `__exclude__`. | +| `note` | `TextField`, nullable | Free-text note. | ## Enums -Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value stored and serialized is the string in the table. +Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The stored and serialized value is the string in the table. ### `ClinicalStatusChoices` values @@ -74,6 +77,8 @@ Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value | `resolved` | | `unknown` | +The frontend offers every value except `unknown`. + ### `VerificationStatusChoices` values | Value | @@ -87,11 +92,11 @@ Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value ### `CategoryChoices` values -| Value | -| --- | -| `problem_list_item` | -| `encounter_diagnosis` | -| `chronic_condition` | +| Value | Used by | +| --- | --- | +| `problem_list_item` | `SymptomViewSet`, which forces this value on create and filters its queryset by it | +| `encounter_diagnosis` | `DiagnosisViewSet`, the value the diagnosis form sends | +| `chronic_condition` | `DiagnosisViewSet`, for long-term diagnoses | ### `SeverityChoices` values @@ -103,91 +108,108 @@ Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value ## Nested JSON shapes -These spec classes (all extend `EMRResource`) are the real structure behind the model's JSON fields. +These spec classes extend `EMRResource` and define the real structure behind the JSON fields. ### `ConditionOnSetSpec` (`onset` shape) | Field | Type | Default | Notes | | --- | --- | --- | --- | -| `onset_datetime` | `datetime \| None` | `None` | Coerced to timezone-aware (`make_aware`) if naive. **Cannot be in the future** — a value `> care_now()` is rejected. | +| `onset_datetime` | `datetime \| None` | `None` | Made timezone-aware when naive. A value after `care_now()` is rejected. | | `onset_age` | `int \| None` | `None` | Age at onset. | -| `onset_string` | `str \| None` | `None` | Free-text onset description. | +| `onset_string` | `str \| None` | `None` | Free-text onset. | | `note` | `str \| None` | `None` | Note about the onset. | ### `ConditionAbatementSpec` (`abatement` shape) | Field | Type | Default | Notes | | --- | --- | --- | --- | -| `abatement_datetime` | `datetime \| None` | `None` | Unlike `onset_datetime`, no future-date check. | +| `abatement_datetime` | `datetime \| None` | `None` | No future-date check. | | `abatement_age` | `int \| None` | `None` | Age at abatement. | -| `abatement_string` | `str \| None` | `None` | Free-text abatement description. | +| `abatement_string` | `str \| None` | `None` | Free-text abatement. | | `note` | `str \| None` | `None` | Note about the abatement. | ### `Coding` shape -`code` is a single [`Coding`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/common/coding.py), not a `CodeableConcept`. `extra="forbid"`. +`code` is a single [`Coding`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/common/coding.py), not a `CodeableConcept`. | Field | Type | Notes | | --- | --- | --- | -| `system` | `str \| None` | Code system URI (e.g. `http://snomed.info/sct`). | +| `system` | `str \| None` | Code system URI, such as `http://snomed.info/sct`. | | `version` | `str \| None` | Code system version. | | `code` | `str` | **Required.** The code value. | -| `display` | `str \| None` | Human-readable label. | +| `display` | `str \| None` | Label for the code. | ### `code` value-set binding -On write, `code` is typed `ValueSetBoundCoding[CARE_CODITION_CODE_VALUESET.slug]` — a `Coding` checked against the Condition code value set (`care/emr/resources/condition/valueset.py`, slug `system-condition-code`), which covers SNOMED CT concepts that are `is-a` `404684003` (*Clinical finding*). Codes outside it are rejected on `ConditionSpec`, `ConditionUpdateSpec`, and `ChronicConditionUpdateSpec`. The read spec falls back to a plain `Coding` and skips value-set validation, so reads stay cheap. +On write, `code` is typed `ValueSetBoundCoding[CARE_CODITION_CODE_VALUESET.slug]`. The value set has the slug `system-condition-code` and includes the SNOMED CT concepts that are `is-a` `404684003` (Clinical finding). Care rejects codes outside the value set on `ConditionSpec`, `ConditionUpdateSpec`, and `ChronicConditionUpdateSpec`. The read spec uses a plain `Coding` and skips the check, which keeps reads cheap. ## Resource specs (API schema) -Every spec extends `BaseConditionSpec` → `EMRResource` and round-trips through `serialize` (DB → Pydantic) and `de_serialize` (Pydantic → DB). `BaseConditionSpec` sets `__model__ = Condition`, sets `__exclude__ = ["patient", "encounter"]` (both server-maintained, never trusted from the client), and exposes `id: UUID4`. +Every spec extends `BaseConditionSpec` → `EMRResource`. `BaseConditionSpec` sets `__model__ = Condition`, sets `__exclude__ = ["patient", "encounter"]`, and exposes `id: UUID4`. | Spec class | Role | Exposes / behaviour | | --- | --- | --- | | `BaseConditionSpec` | shared base | `id`; excludes `patient` and `encounter` from direct mapping. | -| `ConditionSpec` | write · create | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (value-set bound, **required**), `encounter` (UUID4, **required**), `onset` (`={}`), `abatement` (`={}`), `note?`, `category` (**required**). Validates that the encounter exists; on create sets `obj.encounter` and `obj.patient = encounter.patient`. | -| `ConditionUpdateSpec` | write · update | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (value-set bound, **required**), `onset` (`={}`), `abatement` (`={}`), `note?`. Does **not** accept `encounter`, `category`, or `patient`. | -| `ChronicConditionUpdateSpec` | write · update (chronic) | Extends `ConditionUpdateSpec` and adds `encounter` (UUID4). On deserialize, if `encounter` is set, resolves it (`get_object_or_404`) and assigns `obj.encounter`. | -| `ConditionReadSpec` | read · detail/list | `clinical_status`, `verification_status`, `category`, `criticality`, `severity` (all plain `str`), `code` (plain `Coding`), `encounter` (UUID4), `onset`, `abatement`, `created_by?`, `updated_by?`, `note?`, `created_date`, `modified_date`. | +| `ConditionSpec` | write · create | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (**required**, value-set bound), `encounter` (UUID4, **required**), `onset`, `abatement`, `note?`, `category` (**required**). Validates that the encounter exists; on create sets `obj.encounter` and `obj.patient = encounter.patient`. | +| `ConditionUpdateSpec` | write · update | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (**required**, value-set bound), `onset`, `abatement`, `note?`. Accepts neither `encounter` nor `category`. | +| `ChronicConditionUpdateSpec` | write · update | Extends `ConditionUpdateSpec` and adds `encounter` (UUID4). On deserialize, resolves the encounter with `get_object_or_404` and assigns it. | +| `ConditionReadSpec` | read · list/detail | `clinical_status`, `verification_status`, `category`, `severity` (plain `str`), `code` (plain `Coding`), `encounter` (UUID4), `onset`, `abatement`, `created_by?`, `updated_by?`, `note?`, `created_date`, `modified_date`. | -### Validation & server-side behaviour +### Validation and server-side behaviour -- Encounter is required and verified on create. `ConditionSpec.validate_encounter_exists` rejects unknown encounter UUIDs; `perform_extra_deserialization` (create only) loads the encounter and derives `patient` from it. Clients never set `patient` or `encounter` directly. -- `verification_status` is mandatory on both `ConditionSpec` and `ConditionUpdateSpec`; `category` is mandatory only on create (`ConditionSpec`). -- `code` must belong to the bound value set (SNOMED CT clinical findings) on every write spec. -- `onset_datetime` cannot be in the future and is forced timezone-aware; `abatement_datetime` carries no such constraint. -- `ConditionReadSpec` exposes `criticality`, a string in the read schema with no backing column on the `Condition` model. The standard serializer never populates it, so it comes back unset. -- Read serialization (`perform_extra_serialization`) maps `id = external_id`, replaces `encounter` with its `external_id`, and expands `created_by`/`updated_by` via `serialize_audit_users` (cached `UserSpec`). -- `body_site` and `recorded_date` exist on the model but no current spec reads or writes them — storage-only, outside the standard `Condition` API surface. +- `ConditionSpec.validate_encounter_exists` rejects an unknown encounter UUID. `perform_extra_deserialization` runs on create only, loads the encounter, and derives `patient` from it. +- `verification_status` is mandatory on both write specs. `category` is mandatory on create only. +- `code` must belong to the bound value set on every write spec. +- `onset_datetime` cannot be in the future, and Care makes it timezone-aware. `abatement_datetime` has no such rule. +- `ConditionReadSpec.perform_extra_serialization` maps `id` to `external_id`, replaces `encounter` with its `external_id`, and expands `created_by`/`updated_by`. +- `body_site` and `recorded_date` are storage only. No spec reads or writes them. -## Related models +## Viewsets -`Condition` links to two clinical records: +Both viewsets extend `EMRModelViewSet`, `EncounterBasedAuthorizationBase`, `EMRQuestionnaireResponseMixin`, and the local `ValidateEncounterMixin`. They are registered under the patient-nested router. -```text -patient → FK Patient (CASCADE, server-derived from encounter) -encounter → FK Encounter (CASCADE, nullable; required on create) -``` +| Viewset | Route | Queryset | Notes | +| --- | --- | --- | --- | +| `SymptomViewSet` | `patient//symptom/` | Conditions of the patient with `category = problem_list_item` | `perform_create` forces `category` to `problem_list_item`. Registered as the `symptom` system questionnaire. | +| `DiagnosisViewSet` | `patient//diagnosis/` | Every condition of the patient | Registered as the `diagnosis` system questionnaire. Overrides `authorize_update` for chronic conditions. | + +`ValidateEncounterMixin.validate_data` rejects the request when the encounter belongs to a different patient than the one in the URL. -Deleting a `Patient` or `Encounter` cascades to its `Condition` rows. The column allows a null `encounter`, but the standard create flow always supplies one and derives `patient` from it. +Both viewsets expose the shared `upsert` action, which the frontend uses to send several conditions in one atomic request. -## Methods & save behaviour +### Filters + +`ConditionFilters` applies to both viewsets: `encounter`, `clinical_status`, `exclude_clinical_status`, `verification_status`, `exclude_verification_status`, `severity` (case-insensitive exact), `name` (matches `code__display`), and `category`. The status and category filters accept several comma-separated values. + +### Authorization + +| Action | Check | Permission | +| --- | --- | --- | +| List, retrieve | `authorize_read_encounter` in `get_queryset` | `can_view_clinical_data` on the patient, or `can_read_encounter_clinical_data` on the encounter in the `encounter` query parameter | +| Create, update, destroy | `can_update_encounter_clinical_data` | `can_write_encounter_clinical_data` on the encounter | +| Update a chronic condition | `DiagnosisViewSet.authorize_update` | `can_view_clinical_data` on the patient | + +`can_update_encounter_clinical_data` returns `False` when the encounter status is Completed, Cancelled, Entered in Error, or Discontinued. No user writes a condition to a closed encounter. + +## Related models + +```text +patient → FK Patient (CASCADE, derived from the encounter) +encounter → FK Encounter (CASCADE, nullable in the column, required on create) +``` -- `serialize(obj)` and `de_serialize(obj)` (from `EMRResource`) convert between the `Condition` model and the spec. Field mapping uses the model's non-FK column names; `__exclude__` (`patient`, `encounter`) and `id`/`external_id` are skipped during deserialization. -- `ConditionSpec.perform_extra_deserialization(is_update=False, obj)` — create path: resolves the encounter and sets `obj.patient = obj.encounter.patient`. -- `ChronicConditionUpdateSpec.perform_extra_deserialization` — resolves and assigns `encounter` when provided. -- `ConditionReadSpec.perform_extra_serialization` — sets `id`/`encounter` external ids and serializes audit users. -- `external_id`, audit fields, `meta`/`history`, and soft-delete (`deleted`) are platform-maintained via [`EMRBaseModel`](../foundation/base-model.mdx). +Deletion of a `Patient` or an `Encounter` cascades to its `Condition` rows. ## API integration notes -- Send `code` as a structured `Coding` (`{system, code, display}`) drawn from the SNOMED CT clinical-finding value set, not as free text. Always include `verification_status`; add `category` and `encounter` when creating. -- `onset` and `abatement` are structured choice-of-type objects (`*_datetime`, `*_age`, `*_string`, `note`), not arbitrary JSON. `onset_datetime` must be timezone-aware and not in the future. -- Don't send `patient`, `external_id`, audit fields, or `deleted` from the client — the server owns them. +- Send `code` as a `Coding` from the condition code value set, never as free text. Always send `verification_status`. Send `category` and `encounter` on create. +- `onset` and `abatement` are structured objects, not arbitrary JSON. +- Do not send `patient`, `external_id`, the audit fields, or `deleted`. The server owns them. +- To remove a saved condition from the lists, set `verification_status` to `entered_in_error`. The frontend lists exclude that value. ## Related +- Concept: [Condition](../../concepts/clinical/condition.mdx) - Reference: [Patient](../clinical/patient) - Reference: [Encounter](../clinical/encounter.mdx) - Reference: [Base model](../foundation/base-model.mdx) -- Source: [condition.py](https://github.com/ohcnetwork/care/blob/develop/care/emr/models/condition.py) · [spec.py](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/spec.py) · [valueset.py](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/valueset.py) diff --git a/versioned_docs/version-3.1/concepts/clinical/condition.mdx b/versioned_docs/version-3.1/concepts/clinical/condition.mdx index f62b02f..8c5c101 100644 --- a/versioned_docs/version-3.1/concepts/clinical/condition.mdx +++ b/versioned_docs/version-3.1/concepts/clinical/condition.mdx @@ -4,73 +4,64 @@ sidebar_position: 3 # Condition -A **condition** is a clinical problem recorded for a patient — a diagnosis, a chronic illness, or a presenting symptom. It is how a patient's diagnoses and problem list are captured, giving every clinician a shared, durable view of what the patient is being treated for. +## Definition -In the Care product UI, conditions are surfaced as **Symptoms**. +A **[condition](https://build.fhir.org/condition.html)** is a clinical problem that the care team records for a patient. Care uses one condition record for two things that clinicians enter separately: a **Symptom**, which is what the patient reports or the clinician observes, and a **Diagnosis**, which is what the clinician concludes. A condition is a standing statement about the patient. It stays in the record after the encounter ends, until someone changes its status. -## What it represents +## Key Attributes -A condition is a *standing statement* about a patient's health, not a one-time reading. A blood pressure value or a lab result is an [observation](../clinical/observation.mdx) — true at the moment it was taken. A condition is a claim the care team is making and tracking: "this patient has diabetes," "this patient presented with chest pain." That claim persists across visits until someone changes its status, which is why a condition needs two things an observation does not — a statement of **how certain** the team is, and a statement of **how the condition is progressing**. +| Components | What it captures | +| --- | --- | +| Symptom or Diagnosis | The coded clinical finding, selected from the Care terminology list. The clinician cannot type free text here. | +| Status | How the condition progresses over time, from Active to Resolved. | +| Verification | How certain the care team is about the condition. | +| Severity | How severe the condition is: Mild, Moderate, or Severe. | +| Onset Date | The date the condition started. The date cannot be in the future. | +| Notes | Free text that the clinician adds about the condition. | +| Encounter | The encounter in which the clinician records the condition. Care attaches the condition to the patient of that encounter. | +| Recorded by | The user who records the condition, and the date of the record. | -To keep problems comparable across patients and facilities, the condition itself is recorded as a coded clinical finding (drawn from a SNOMED CT vocabulary) rather than free text. Onset, optional severity, the encounter it was noted in, and a free-text note round out the record. +### Symptom and Diagnosis -## Classification +Care keeps symptoms and diagnoses in separate lists, and gives each list its own form. -The **category** answers "what kind of problem is this, and where does it live in the record?" Every condition is one of: +- A **Symptom** is what the patient has. The clinician records a symptom early in the encounter, such as fever or chest pain. +- A **Diagnosis** is what the clinician concludes. The clinician records a diagnosis for the encounter, such as pneumonia. -- **Problem-list item** — an ongoing problem the care team is tracking for this patient -- **Encounter diagnosis** — a diagnosis made or confirmed during a specific visit -- **Chronic condition** — a long-term condition such as diabetes or hypertension, carried across encounters +The two lists never mix. A symptom that you record stays under **Symptoms**, and a diagnosis stays under **Diagnoses**. -Running alongside the category is a separate axis — the **verification status** — that records certainty. A symptom under investigation might be `unconfirmed`, `provisional`, or `differential`; a settled diagnosis is `confirmed`; something logged in error is `refuted` or `entered_in_error`. Care always requires a verification status, so the record never blurs a working hypothesis with an established fact. +### Status -## Lifecycle +Status records the course of the condition. Verification records the certainty of the care team. The two are independent: a condition can be Active and still Provisional. -The **clinical status** tracks where a condition stands over time. It is distinct from verification — that is about how sure the team is; this is about the condition's actual course: +| Status | Description | +| --- | --- | +| Active | The condition is present now, and the care team manages it. | +| Recurrence | The condition returned after a period without symptoms. | +| Relapse | The condition returned after remission. | +| Inactive | The condition is not active now, but nobody resolved it. | +| Remission | The symptoms decreased, but the condition can return. | +| Resolved | The condition cleared. | -```text -active → inactive → remission → resolved - ↑ | - └─ recurrence / relapse ─┘ -``` +### Verification -- **active** — currently present and being managed -- **recurrence** — returned after a symptom-free period -- **relapse** — returned after being in remission -- **inactive** — no longer active, but not formally resolved -- **remission** — symptoms have abated, but the condition may return -- **resolved** — fully cleared -- **unknown** — current state is not known +| Verification | Description | +| --- | --- | +| Unconfirmed | The care team recorded the condition, but has no evidence yet. | +| Provisional | The condition is a working answer while the care team continues to examine the patient. | +| Differential | The condition is one of the possible answers that the care team examines. | +| Confirmed | The care team has enough evidence for the condition. | +| Refuted | The care team examined the condition and rejected it. | +| Entered in Error | A user recorded the condition by mistake. Care keeps the record for audit and hides it from the lists. | -This is not a one-way pipeline. A chronic condition can cycle through active, remission, and recurrence many times over a patient's history. - -## How it connects - -A condition never stands alone — it is always anchored to a patient and the visit where it was noted: - -- **Patient** — the person the condition describes. Care derives this automatically from the encounter, so the condition is always attached to the right record; clients never set it directly. -- **Encounter** — the visit during which the condition was recorded. Every condition is created in the context of an encounter; chronic conditions can later be re-associated with a new encounter as care continues. - -Conditions sit alongside the patient's other clinical records — most closely [allergies and intolerances](../clinical/allergy-intolerance.mdx), which capture a different kind of standing risk, and [observations](../clinical/observation.mdx), which capture point-in-time measurements and findings. - -## Permissions - -A condition has no permission file of its own — as patient clinical data, it is governed by the **patient** and **encounter** clinical-data permissions a user holds in the relevant facility. Creating, updating, and deleting a condition is gated by write access to the encounter's clinical data; reading it requires the patient's clinical-data permission, falling back to the encounter's clinical-data read permission. Chronic conditions are a special case — updating one is gated by the patient's clinical-data permission rather than the encounter's. Conditions can also be captured by submitting a symptom or diagnosis questionnaire. - -| Permission | Description | System Roles | -| --- | --- | --- | -| `can_write_encounter_clinical_data` | Create, update, or delete a condition (the create, update, and destroy paths check write access to the encounter's clinical data; chronic-condition updates are the exception below) | Admin, Doctor, Nurse, Facility Admin | -| `can_view_clinical_data` | Read a patient's conditions, and update a chronic condition (the read path checks the patient's clinical-data permission; chronic-condition updates check this same permission) | Staff, Doctor, Nurse, Admin, Facility Admin | -| `can_read_encounter_clinical_data` | Read conditions via an encounter when patient-level clinical access is absent (the read path falls back to this when an `encounter` query param is supplied) | Admin, Doctor, Nurse, Facility Admin | -| `can_submit_patient_questionnaire` | Submit a patient-subject questionnaire (such as symptom or diagnosis), which can record conditions | Volunteer, Staff, Doctor, Nurse, Admin, Facility Admin, Administrator | -| `can_submit_encounter_questionnaire` | Submit an encounter-linked questionnaire, which can record conditions | Staff, Doctor, Nurse, Admin, Facility Admin | - -Roles are granted to users through facility, organization, or patient memberships, and they cascade down the organization tree — a role held high in the hierarchy applies to the facilities and patients beneath it. +A clinician cannot select **Entered in Error** for a new record. Care sets this verification when the clinician removes a condition that is already saved. ## Related -- Reference: [Condition (technical)](../../references/clinical/condition.mdx) +- Flow: [Record a symptom](../../flows/clinical/record-a-symptom.mdx) +- Flow: [Record a diagnosis](../../flows/clinical/record-a-diagnosis.mdx) - Concept: [Patient](../clinical/patient) - Concept: [Encounter](../clinical/encounter.mdx) - Concept: [Allergy / intolerance](../clinical/allergy-intolerance.mdx) - Concept: [Observation](../clinical/observation.mdx) +- Reference: [Condition](../../references/clinical/condition.mdx) diff --git a/versioned_docs/version-3.1/flows/clinical/record-a-diagnosis.mdx b/versioned_docs/version-3.1/flows/clinical/record-a-diagnosis.mdx new file mode 100644 index 0000000..df7b6e9 --- /dev/null +++ b/versioned_docs/version-3.1/flows/clinical/record-a-diagnosis.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 3 +--- + +# How to record a diagnosis + +## Overview + +This flow describes how to record a diagnosis for a patient during an encounter. A diagnosis is one kind of [condition](../../concepts/clinical/condition.mdx) in Care. + +## Pre-requisites + +- The patient is registered in Care. +- The patient has an encounter that is open. You cannot record a diagnosis in an encounter that is completed, cancelled, discontinued, or marked as entered in error. +- You are a member of the facility that owns the encounter. +- You have the permissions listed below. + +## Permissions + +| Permission | Access | +| --- | --- | +| Update Encounter related clinical data | Lets you add, change, and remove diagnoses in an encounter. | +| Can view clinical data about patients | Lets you see the diagnoses that the care team already recorded. | + +## Steps + +### 1. Open the encounter + +1. Open the facility. +2. Find the patient, and open the encounter. + +### 2. Open the diagnosis form + +1. Select **Encounter Actions**. +2. Select **Add Diagnosis**. + +You can press `d` instead, while you view the encounter. The `d` shortcut opens the same diagnosis form. + +### 3. Select the diagnosis + +1. Select **Add Diagnosis** in the form. +2. Enter the name of the diagnosis in the search box. +3. Select the diagnosis from the list of results. + +Care shows a warning if you select a diagnosis that the list already contains. Change the existing entry instead of adding a second one. + +### 4. Complete the details + +Set the details of the diagnosis. Care fills the values in the Default column for you. + +| Components | What it captures | Default | +| --- | --- | --- | +| Onset Date | The date the diagnosis started. You cannot select a date in the future. | Today | +| Status | The course of the diagnosis, from Active to Resolved. | Active | +| Severity | How severe the diagnosis is: Mild, Moderate, or Severe. | Moderate | +| Verification | How certain you are about the diagnosis. Select **Provisional** or **Differential** while you still examine the patient. | Confirmed | +| Notes | Free text about the diagnosis. | Empty | + +Repeat step 3 and step 4 for each additional diagnosis. + +### 5. Save the diagnoses + +- Select **Submit** to save the diagnoses to the encounter. +- Select **Save as Draft** to keep your entries and finish later. + +## Expected Outcome + +- The diagnoses show in the **Diagnoses** list on the **Overview** tab of the encounter. +- The diagnoses show under **Past Diagnoses** in the clinical history of the patient. +- Care records your name and the date against each diagnosis. + +## Related + +Concepts: + +- [Condition](../../concepts/clinical/condition.mdx) +- [Encounter](../../concepts/clinical/encounter.mdx) +- [Patient](../../concepts/clinical/patient) + +Flows: + +- [How to record a symptom](./record-a-symptom.mdx) diff --git a/versioned_docs/version-3.1/flows/clinical/record-a-symptom.mdx b/versioned_docs/version-3.1/flows/clinical/record-a-symptom.mdx new file mode 100644 index 0000000..741cfe4 --- /dev/null +++ b/versioned_docs/version-3.1/flows/clinical/record-a-symptom.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 2 +--- + +# How to record a symptom + +## Overview + +This flow describes how to record a symptom for a patient during an encounter. A symptom is one kind of [condition](../../concepts/clinical/condition.mdx) in Care. + +## Pre-requisites + +- The patient is registered in Care. +- The patient has an encounter that is open. You cannot record a symptom in an encounter that is completed, cancelled, discontinued, or marked as entered in error. +- You are a member of the facility that owns the encounter. +- You have the permissions listed below. + +## Permissions + +| Permission | Access | +| --- | --- | +| Update Encounter related clinical data | Lets you add, change, and remove symptoms in an encounter. | +| Can view clinical data about patients | Lets you see the symptoms that the care team already recorded. | + +## Steps + +### 1. Open the encounter + +1. Open the facility. +2. Find the patient, and open the encounter. + +### 2. Open the symptom form + +1. Select **Encounter Actions**. +2. Select **Add Symptom**. + +You can press `s` instead, while you view the encounter. The `s` shortcut opens the same symptom form. + +### 3. Select the symptom + +1. Select **Add Symptom** in the form. +2. Enter the name of the symptom in the search box. +3. Select the symptom from the list of results. + +Care shows a warning if you select a symptom that the list already contains. Change the existing entry instead of adding a second one. + +### 4. Complete the details + +Set the details of the symptom. Care fills the values in the Default column for you. + +| Components | What it captures | Default | +| --- | --- | --- | +| Onset Date | The date the symptom started. You cannot select a date in the future. | Today | +| Status | The course of the symptom, from Active to Resolved. | Active | +| Severity | How severe the symptom is: Mild, Moderate, or Severe. | Moderate | +| Verification | How certain you are about the symptom. | Confirmed | +| Notes | Free text about the symptom. | Empty | + +Repeat step 3 and step 4 for each additional symptom. + +### 5. Save the symptoms + +- Select **Submit** to save the symptoms to the encounter. +- Select **Save as Draft** to keep your entries and finish later. + +## Expected Outcome + +- The symptoms show in the **Symptoms** list on the **Overview** tab of the encounter. +- The symptoms show under **Past Symptoms** in the clinical history of the patient. +- Care records your name and the date against each symptom. + +## Related + +Concepts: + +- [Condition](../../concepts/clinical/condition.mdx) +- [Encounter](../../concepts/clinical/encounter.mdx) +- [Patient](../../concepts/clinical/patient) + +Flows: + +- [How to record a diagnosis](./record-a-diagnosis.mdx) diff --git a/versioned_docs/version-3.1/references/clinical/condition.mdx b/versioned_docs/version-3.1/references/clinical/condition.mdx index b8fc14d..a488749 100644 --- a/versioned_docs/version-3.1/references/clinical/condition.mdx +++ b/versioned_docs/version-3.1/references/clinical/condition.mdx @@ -4,63 +4,66 @@ sidebar_position: 3 # Condition -A `Condition` records a clinical problem, diagnosis, or symptom against a patient. It maps to the FHIR `Condition` resource and shows up in the Care product UI as **Symptoms**. +Technical reference for the `Condition` module in Care EMR. For the plain-language view, read the [Condition concept](../../concepts/clinical/condition.mdx). **Source:** - Model: [`care/emr/models/condition.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/models/condition.py) - Resource spec: [`care/emr/resources/condition/spec.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/spec.py) - Value set: [`care/emr/resources/condition/valueset.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/valueset.py) +- Viewsets: [`care/emr/api/viewsets/condition.py`](https://github.com/ohcnetwork/care/blob/develop/care/emr/api/viewsets/condition.py) -A `Condition` lives across two layers, and the split matters when you read the code: +`Condition` has two layers, and the split matters when you read the code: -- The Django model (`care/emr/models/condition.py`) is storage. Its coded and timing fields (`code`, `body_site`, `onset`, `abatement`) are opaque `JSONField`s — the model says nothing about their shape. -- The Pydantic resource specs (`care/emr/resources/condition/spec.py`) are the API. They define the enums, the structure inside each JSON field, field validation, the value-set binding for `code`, and the separate read and write schemas. +- The Django model is storage. Its coded and timing fields (`code`, `body_site`, `onset`, `abatement`) are opaque `JSONField`s. The model says nothing about their shape. +- The Pydantic resource specs are the API. They define the enums, the structure inside each JSON field, the validation, the value-set binding for `code`, and the separate read and write schemas. + +One model backs two API surfaces. `SymptomViewSet` serves `symptom`, and `DiagnosisViewSet` serves `diagnosis`. The `category` field separates them. ## Models | Model | Purpose | | --- | --- | -| `Condition` | A clinical condition, problem, diagnosis, or symptom recorded for a patient | +| `Condition` | A clinical problem, symptom, or diagnosis recorded for a patient | -`Condition` extends [`EMRBaseModel`](../foundation/base-model.mdx), the shared Care EMR base that provides `external_id`, `created_date`/`modified_date`, soft-delete via `deleted`, `created_by`/`updated_by`, and `history`/`meta` JSON. +`Condition` extends [`EMRBaseModel`](../foundation/base-model.mdx), which provides `external_id`, `created_date`/`modified_date`, `created_by`/`updated_by`, soft delete through `deleted`, and the `history`/`meta` JSON fields. ## `Condition` fields -### Status & classification +### Status and classification | Field | Type | Notes | | --- | --- | --- | -| `clinical_status` | `CharField(100)`, nullable | Clinical state. Spec binds to [`ClinicalStatusChoices`](#clinicalstatuschoices-values); optional on write. | -| `verification_status` | `CharField(100)`, nullable | Level of certainty. Spec binds to [`VerificationStatusChoices`](#verificationstatuschoices-values); **required** on write (`ConditionSpec`/`ConditionUpdateSpec`). | -| `category` | `CharField(100)`, nullable | Classification. Spec binds to [`CategoryChoices`](#categorychoices-values); **required** on create (`ConditionSpec`). | -| `severity` | `CharField(100)`, nullable | Subjective severity. Spec binds to [`SeverityChoices`](#severitychoices-values); optional on write. | +| `clinical_status` | `CharField(100)`, nullable | Course of the condition. Bound to [`ClinicalStatusChoices`](#clinicalstatuschoices-values) by the specs; optional on write. | +| `verification_status` | `CharField(100)`, nullable | Certainty. Bound to [`VerificationStatusChoices`](#verificationstatuschoices-values); **required** on `ConditionSpec` and `ConditionUpdateSpec`. | +| `category` | `CharField(100)`, nullable | Bound to [`CategoryChoices`](#categorychoices-values); **required** on create. `SymptomViewSet` overwrites it with `problem_list_item` in `perform_create`. | +| `severity` | `CharField(100)`, nullable | Bound to [`SeverityChoices`](#severitychoices-values); optional on write. | ### Coded concepts | Field | Type | Notes | | --- | --- | --- | -| `code` | `JSONField` (`default=dict`, not null/blank) | The condition itself. On write, a single [`Coding`](#coding-shape) bound to the [`CARE_CODITION_CODE_VALUESET`](#code-value-set-binding) value set (SNOMED CT clinical findings). On read, serialized as a plain `Coding`. | -| `body_site` | `JSONField` (`default=dict`, not null/blank) | Anatomical location(s). Exists on the model but no current spec exposes it — never written or read through the standard `Condition` API. | +| `code` | `JSONField` (`default=dict`, not null/blank) | The condition itself. On write, a single [`Coding`](#coding-shape) bound to the [condition code value set](#code-value-set-binding). On read, a plain `Coding`. | +| `body_site` | `JSONField` (`default=dict`, not null/blank) | Anatomical site. No current spec exposes it, so no client reads or writes it. | ### Timing | Field | Type | Notes | | --- | --- | --- | -| `onset` | `JSONField` (`default=dict`) | Choice-of-type onset, shaped by [`ConditionOnSetSpec`](#conditiononsetspec-onset-shape). Defaults to `{}`. | -| `abatement` | `JSONField` (`default=dict`) | When the condition resolved or went into remission. Shaped by [`ConditionAbatementSpec`](#conditionabatementspec-abatement-shape). Defaults to `{}`. | -| `recorded_date` | `DateTimeField`, nullable | When the condition was first recorded. Not exposed by the current specs. | +| `onset` | `JSONField` (`default=dict`) | Shaped by [`ConditionOnSetSpec`](#conditiononsetspec-onset-shape). | +| `abatement` | `JSONField` (`default=dict`) | Shaped by [`ConditionAbatementSpec`](#conditionabatementspec-abatement-shape). | +| `recorded_date` | `DateTimeField`, nullable | Not exposed by the current specs. | -### Context & notes +### Context and notes | Field | Type | Notes | | --- | --- | --- | -| `patient` | `FK → Patient`, `on_delete=CASCADE` | Subject of the condition. Derived server-side from the encounter on create, never read from the client (`__exclude__`). | -| `encounter` | `FK → Encounter`, nullable, `on_delete=CASCADE` | Encounter the condition was recorded in. Set server-side on create (`__exclude__`); the client supplies a UUID in the spec, which is validated to exist. | -| `note` | `TextField`, nullable | Free-text clinical note. | +| `patient` | `FK → Patient`, `on_delete=CASCADE` | Derived server-side from the encounter on create. Listed in `__exclude__`, so no client sets it. | +| `encounter` | `FK → Encounter`, nullable, `on_delete=CASCADE` | Set server-side on create from the UUID in the write spec. Listed in `__exclude__`. | +| `note` | `TextField`, nullable | Free-text note. | ## Enums -Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value stored and serialized is the string in the table. +Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The stored and serialized value is the string in the table. ### `ClinicalStatusChoices` values @@ -74,6 +77,8 @@ Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value | `resolved` | | `unknown` | +The frontend offers every value except `unknown`. + ### `VerificationStatusChoices` values | Value | @@ -87,11 +92,11 @@ Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value ### `CategoryChoices` values -| Value | -| --- | -| `problem_list_item` | -| `encounter_diagnosis` | -| `chronic_condition` | +| Value | Used by | +| --- | --- | +| `problem_list_item` | `SymptomViewSet`, which forces this value on create and filters its queryset by it | +| `encounter_diagnosis` | `DiagnosisViewSet`, the value the diagnosis form sends | +| `chronic_condition` | `DiagnosisViewSet`, for long-term diagnoses | ### `SeverityChoices` values @@ -103,91 +108,108 @@ Every enum is a `str, Enum` in `care/emr/resources/condition/spec.py`. The value ## Nested JSON shapes -These spec classes (all extend `EMRResource`) are the real structure behind the model's JSON fields. +These spec classes extend `EMRResource` and define the real structure behind the JSON fields. ### `ConditionOnSetSpec` (`onset` shape) | Field | Type | Default | Notes | | --- | --- | --- | --- | -| `onset_datetime` | `datetime \| None` | `None` | Coerced to timezone-aware (`make_aware`) if naive. **Cannot be in the future** — a value `> care_now()` is rejected. | +| `onset_datetime` | `datetime \| None` | `None` | Made timezone-aware when naive. A value after `care_now()` is rejected. | | `onset_age` | `int \| None` | `None` | Age at onset. | -| `onset_string` | `str \| None` | `None` | Free-text onset description. | +| `onset_string` | `str \| None` | `None` | Free-text onset. | | `note` | `str \| None` | `None` | Note about the onset. | ### `ConditionAbatementSpec` (`abatement` shape) | Field | Type | Default | Notes | | --- | --- | --- | --- | -| `abatement_datetime` | `datetime \| None` | `None` | Unlike `onset_datetime`, no future-date check. | +| `abatement_datetime` | `datetime \| None` | `None` | No future-date check. | | `abatement_age` | `int \| None` | `None` | Age at abatement. | -| `abatement_string` | `str \| None` | `None` | Free-text abatement description. | +| `abatement_string` | `str \| None` | `None` | Free-text abatement. | | `note` | `str \| None` | `None` | Note about the abatement. | ### `Coding` shape -`code` is a single [`Coding`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/common/coding.py), not a `CodeableConcept`. `extra="forbid"`. +`code` is a single [`Coding`](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/common/coding.py), not a `CodeableConcept`. | Field | Type | Notes | | --- | --- | --- | -| `system` | `str \| None` | Code system URI (e.g. `http://snomed.info/sct`). | +| `system` | `str \| None` | Code system URI, such as `http://snomed.info/sct`. | | `version` | `str \| None` | Code system version. | | `code` | `str` | **Required.** The code value. | -| `display` | `str \| None` | Human-readable label. | +| `display` | `str \| None` | Label for the code. | ### `code` value-set binding -On write, `code` is typed `ValueSetBoundCoding[CARE_CODITION_CODE_VALUESET.slug]` — a `Coding` checked against the Condition code value set (`care/emr/resources/condition/valueset.py`, slug `system-condition-code`), which covers SNOMED CT concepts that are `is-a` `404684003` (*Clinical finding*). Codes outside it are rejected on `ConditionSpec`, `ConditionUpdateSpec`, and `ChronicConditionUpdateSpec`. The read spec falls back to a plain `Coding` and skips value-set validation, so reads stay cheap. +On write, `code` is typed `ValueSetBoundCoding[CARE_CODITION_CODE_VALUESET.slug]`. The value set has the slug `system-condition-code` and includes the SNOMED CT concepts that are `is-a` `404684003` (Clinical finding). Care rejects codes outside the value set on `ConditionSpec`, `ConditionUpdateSpec`, and `ChronicConditionUpdateSpec`. The read spec uses a plain `Coding` and skips the check, which keeps reads cheap. ## Resource specs (API schema) -Every spec extends `BaseConditionSpec` → `EMRResource` and round-trips through `serialize` (DB → Pydantic) and `de_serialize` (Pydantic → DB). `BaseConditionSpec` sets `__model__ = Condition`, sets `__exclude__ = ["patient", "encounter"]` (both server-maintained, never trusted from the client), and exposes `id: UUID4`. +Every spec extends `BaseConditionSpec` → `EMRResource`. `BaseConditionSpec` sets `__model__ = Condition`, sets `__exclude__ = ["patient", "encounter"]`, and exposes `id: UUID4`. | Spec class | Role | Exposes / behaviour | | --- | --- | --- | | `BaseConditionSpec` | shared base | `id`; excludes `patient` and `encounter` from direct mapping. | -| `ConditionSpec` | write · create | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (value-set bound, **required**), `encounter` (UUID4, **required**), `onset` (`={}`), `abatement` (`={}`), `note?`, `category` (**required**). Validates that the encounter exists; on create sets `obj.encounter` and `obj.patient = encounter.patient`. | -| `ConditionUpdateSpec` | write · update | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (value-set bound, **required**), `onset` (`={}`), `abatement` (`={}`), `note?`. Does **not** accept `encounter`, `category`, or `patient`. | -| `ChronicConditionUpdateSpec` | write · update (chronic) | Extends `ConditionUpdateSpec` and adds `encounter` (UUID4). On deserialize, if `encounter` is set, resolves it (`get_object_or_404`) and assigns `obj.encounter`. | -| `ConditionReadSpec` | read · detail/list | `clinical_status`, `verification_status`, `category`, `criticality`, `severity` (all plain `str`), `code` (plain `Coding`), `encounter` (UUID4), `onset`, `abatement`, `created_by?`, `updated_by?`, `note?`, `created_date`, `modified_date`. | +| `ConditionSpec` | write · create | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (**required**, value-set bound), `encounter` (UUID4, **required**), `onset`, `abatement`, `note?`, `category` (**required**). Validates that the encounter exists; on create sets `obj.encounter` and `obj.patient = encounter.patient`. | +| `ConditionUpdateSpec` | write · update | `clinical_status?`, `verification_status` (**required**), `severity?`, `code` (**required**, value-set bound), `onset`, `abatement`, `note?`. Accepts neither `encounter` nor `category`. | +| `ChronicConditionUpdateSpec` | write · update | Extends `ConditionUpdateSpec` and adds `encounter` (UUID4). On deserialize, resolves the encounter with `get_object_or_404` and assigns it. | +| `ConditionReadSpec` | read · list/detail | `clinical_status`, `verification_status`, `category`, `severity` (plain `str`), `code` (plain `Coding`), `encounter` (UUID4), `onset`, `abatement`, `created_by?`, `updated_by?`, `note?`, `created_date`, `modified_date`. | -### Validation & server-side behaviour +### Validation and server-side behaviour -- Encounter is required and verified on create. `ConditionSpec.validate_encounter_exists` rejects unknown encounter UUIDs; `perform_extra_deserialization` (create only) loads the encounter and derives `patient` from it. Clients never set `patient` or `encounter` directly. -- `verification_status` is mandatory on both `ConditionSpec` and `ConditionUpdateSpec`; `category` is mandatory only on create (`ConditionSpec`). -- `code` must belong to the bound value set (SNOMED CT clinical findings) on every write spec. -- `onset_datetime` cannot be in the future and is forced timezone-aware; `abatement_datetime` carries no such constraint. -- `ConditionReadSpec` exposes `criticality`, a string in the read schema with no backing column on the `Condition` model. The standard serializer never populates it, so it comes back unset. -- Read serialization (`perform_extra_serialization`) maps `id = external_id`, replaces `encounter` with its `external_id`, and expands `created_by`/`updated_by` via `serialize_audit_users` (cached `UserSpec`). -- `body_site` and `recorded_date` exist on the model but no current spec reads or writes them — storage-only, outside the standard `Condition` API surface. +- `ConditionSpec.validate_encounter_exists` rejects an unknown encounter UUID. `perform_extra_deserialization` runs on create only, loads the encounter, and derives `patient` from it. +- `verification_status` is mandatory on both write specs. `category` is mandatory on create only. +- `code` must belong to the bound value set on every write spec. +- `onset_datetime` cannot be in the future, and Care makes it timezone-aware. `abatement_datetime` has no such rule. +- `ConditionReadSpec.perform_extra_serialization` maps `id` to `external_id`, replaces `encounter` with its `external_id`, and expands `created_by`/`updated_by`. +- `body_site` and `recorded_date` are storage only. No spec reads or writes them. -## Related models +## Viewsets -`Condition` links to two clinical records: +Both viewsets extend `EMRModelViewSet`, `EncounterBasedAuthorizationBase`, `EMRQuestionnaireResponseMixin`, and the local `ValidateEncounterMixin`. They are registered under the patient-nested router. -```text -patient → FK Patient (CASCADE, server-derived from encounter) -encounter → FK Encounter (CASCADE, nullable; required on create) -``` +| Viewset | Route | Queryset | Notes | +| --- | --- | --- | --- | +| `SymptomViewSet` | `patient//symptom/` | Conditions of the patient with `category = problem_list_item` | `perform_create` forces `category` to `problem_list_item`. Registered as the `symptom` system questionnaire. | +| `DiagnosisViewSet` | `patient//diagnosis/` | Every condition of the patient | Registered as the `diagnosis` system questionnaire. Overrides `authorize_update` for chronic conditions. | + +`ValidateEncounterMixin.validate_data` rejects the request when the encounter belongs to a different patient than the one in the URL. -Deleting a `Patient` or `Encounter` cascades to its `Condition` rows. The column allows a null `encounter`, but the standard create flow always supplies one and derives `patient` from it. +Both viewsets expose the shared `upsert` action, which the frontend uses to send several conditions in one atomic request. -## Methods & save behaviour +### Filters + +`ConditionFilters` applies to both viewsets: `encounter`, `clinical_status`, `exclude_clinical_status`, `verification_status`, `exclude_verification_status`, `severity` (case-insensitive exact), `name` (matches `code__display`), and `category`. The status and category filters accept several comma-separated values. + +### Authorization + +| Action | Check | Permission | +| --- | --- | --- | +| List, retrieve | `authorize_read_encounter` in `get_queryset` | `can_view_clinical_data` on the patient, or `can_read_encounter_clinical_data` on the encounter in the `encounter` query parameter | +| Create, update, destroy | `can_update_encounter_clinical_data` | `can_write_encounter_clinical_data` on the encounter | +| Update a chronic condition | `DiagnosisViewSet.authorize_update` | `can_view_clinical_data` on the patient | + +`can_update_encounter_clinical_data` returns `False` when the encounter status is Completed, Cancelled, Entered in Error, or Discontinued. No user writes a condition to a closed encounter. + +## Related models + +```text +patient → FK Patient (CASCADE, derived from the encounter) +encounter → FK Encounter (CASCADE, nullable in the column, required on create) +``` -- `serialize(obj)` and `de_serialize(obj)` (from `EMRResource`) convert between the `Condition` model and the spec. Field mapping uses the model's non-FK column names; `__exclude__` (`patient`, `encounter`) and `id`/`external_id` are skipped during deserialization. -- `ConditionSpec.perform_extra_deserialization(is_update=False, obj)` — create path: resolves the encounter and sets `obj.patient = obj.encounter.patient`. -- `ChronicConditionUpdateSpec.perform_extra_deserialization` — resolves and assigns `encounter` when provided. -- `ConditionReadSpec.perform_extra_serialization` — sets `id`/`encounter` external ids and serializes audit users. -- `external_id`, audit fields, `meta`/`history`, and soft-delete (`deleted`) are platform-maintained via [`EMRBaseModel`](../foundation/base-model.mdx). +Deletion of a `Patient` or an `Encounter` cascades to its `Condition` rows. ## API integration notes -- Send `code` as a structured `Coding` (`{system, code, display}`) drawn from the SNOMED CT clinical-finding value set, not as free text. Always include `verification_status`; add `category` and `encounter` when creating. -- `onset` and `abatement` are structured choice-of-type objects (`*_datetime`, `*_age`, `*_string`, `note`), not arbitrary JSON. `onset_datetime` must be timezone-aware and not in the future. -- Don't send `patient`, `external_id`, audit fields, or `deleted` from the client — the server owns them. +- Send `code` as a `Coding` from the condition code value set, never as free text. Always send `verification_status`. Send `category` and `encounter` on create. +- `onset` and `abatement` are structured objects, not arbitrary JSON. +- Do not send `patient`, `external_id`, the audit fields, or `deleted`. The server owns them. +- To remove a saved condition from the lists, set `verification_status` to `entered_in_error`. The frontend lists exclude that value. ## Related +- Concept: [Condition](../../concepts/clinical/condition.mdx) - Reference: [Patient](../clinical/patient) - Reference: [Encounter](../clinical/encounter.mdx) - Reference: [Base model](../foundation/base-model.mdx) -- Source: [condition.py](https://github.com/ohcnetwork/care/blob/develop/care/emr/models/condition.py) · [spec.py](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/spec.py) · [valueset.py](https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/condition/valueset.py) diff --git a/versioned_sidebars/version-3.0-sidebars.json b/versioned_sidebars/version-3.0-sidebars.json index 5376f67..4186687 100644 --- a/versioned_sidebars/version-3.0-sidebars.json +++ b/versioned_sidebars/version-3.0-sidebars.json @@ -21,7 +21,11 @@ "type": "category", "label": "Clinical", "key": "clinical-flows", - "items": ["flows/clinical/create-patient"] + "items": [ + "flows/clinical/create-patient", + "flows/clinical/record-a-symptom", + "flows/clinical/record-a-diagnosis" + ] } ] }, diff --git a/versioned_sidebars/version-3.1-sidebars.json b/versioned_sidebars/version-3.1-sidebars.json index 4253732..9e7b94d 100644 --- a/versioned_sidebars/version-3.1-sidebars.json +++ b/versioned_sidebars/version-3.1-sidebars.json @@ -37,7 +37,9 @@ "flows/clinical/patient/manage-patient-users", "flows/clinical/patient/view-patient-appointments" ] - } + }, + "flows/clinical/record-a-symptom", + "flows/clinical/record-a-diagnosis" ] } ]