Overview
Task Management
Create, assign, and track care coordination tasks with priority levels and due dates
Command Center
Enterprise operations dashboard for risk monitoring and population health
Care Gaps
Automated detection of quality opportunities and preventive care needs
Risk Stratification
Patient risk scoring based on conditions, medications, and clinical indicators
Task Management
OmniEHR implements FHIR Task resources for care coordination workflows.Task Data Model
Task Schema
const taskSchema = new mongoose.Schema({
resourceType: {
type: String,
default: "Task"
},
status: {
type: String,
enum: [
"draft", "requested", "received", "accepted", "rejected",
"ready", "cancelled", "in-progress", "on-hold", "failed",
"completed", "entered-in-error"
],
default: "requested"
},
intent: {
type: String,
default: "order"
},
priority: {
type: String,
enum: ["routine", "urgent", "asap", "stat"],
default: "routine"
},
codeText: String,
description: {
type: String,
required: true
},
for: {
reference: {
type: mongoose.Schema.Types.ObjectId,
ref: "Patient",
required: true
}
},
ownerUserId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
},
ownerName: String,
authoredOn: Date,
dueDate: Date,
note: String,
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
}, {
timestamps: true
});
// Indexes for efficient querying
taskSchema.index({ "for.reference": 1, dueDate: 1 });
taskSchema.index({ ownerUserId: 1, status: 1, dueDate: 1 });
Task Categories
Task Categories
const TASK_CATEGORY_OPTIONS = [
"Care coordination",
"Medication reconciliation",
"Lab follow-up",
"Preventive screening",
"Discharge outreach",
"Referral management"
];
Task Priority Levels
| Priority | Description | Use Case |
|---|---|---|
routine | Standard priority | Regular follow-ups, routine care |
urgent | Elevated priority | Time-sensitive coordination |
asap | As soon as possible | Critical but not emergency |
stat | Immediate attention | Emergency clinical needs |
Creating Tasks
const onCreateTask = async (event) => {
event.preventDefault();
const ownerId = user.role === "practitioner" ? user.id : taskForm.ownerId;
const owner = practitionerMap.get(ownerId);
const dueIso = taskForm.dueDate
? new Date(`${taskForm.dueDate}T23:59:59`).toISOString()
: undefined;
await fhirApi.createTask(token, {
resourceType: "Task",
status: "requested",
intent: "order",
priority: taskForm.priority,
code: taskForm.category ? { text: taskForm.category } : undefined,
description: taskForm.description.trim(),
for: {
reference: `Patient/${patientId}`
},
owner: ownerId ? {
reference: `Practitioner/${ownerId}`,
display: owner?.fullName
} : undefined,
authoredOn: new Date().toISOString(),
executionPeriod: dueIso ? { end: dueIso } : undefined,
note: taskForm.note ? [{ text: taskForm.note.trim() }] : undefined
});
await load();
};
Task Status Workflow
Task Status Update
const onUpdateTaskStatus = async (task, nextStatus) => {
await submitClinicalResource({
request: () =>
fhirApi.updateTask(token, task.id, {
...task,
status: nextStatus
}),
onSuccess: () => {}
});
};
// Render status dropdown
<select
value={task.status}
onChange={(event) => onUpdateTaskStatus(task, event.target.value)}
disabled={loading}
>
<option value="requested">requested</option>
<option value="accepted">accepted</option>
<option value="in-progress">in-progress</option>
<option value="on-hold">on-hold</option>
<option value="completed">completed</option>
<option value="cancelled">cancelled</option>
</select>
Overdue Task Detection
Task Utilities
const CLOSED_TASK_STATUSES = new Set([
"completed",
"cancelled",
"rejected",
"failed",
"entered-in-error"
]);
export const isTaskOpen = (task) =>
!CLOSED_TASK_STATUSES.has(normalize(task?.status));
export const getTaskDueDate = (task) => {
return (
task?.executionPeriod?.end ||
task?.dueDate ||
task?.restriction?.period?.end ||
task?.authoredOn ||
null
);
};
export const isTaskOverdue = (task, now = new Date()) => {
if (!isTaskOpen(task)) {
return false;
}
const dueDate = toDate(getTaskDueDate(task));
return Boolean(dueDate && dueDate.getTime() < now.getTime());
};
Command Center
The Command Center provides enterprise-level population health monitoring and care coordination.Dashboard Statistics
Command Center Stats
const dashboardStats = useMemo(() => {
const now = new Date();
const next24h = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const openTasks = tasks.filter(isTaskOpen);
const overdueTasks = openTasks.filter((task) => isTaskOverdue(task, now));
const highRiskPatients = patientRows.filter(
(row) => row.profile.tier === "high"
);
const totalGaps = patientRows.reduce(
(sum, row) => sum + row.profile.careGaps.length,
0
);
const appointmentsNext24h = appointments.filter((appointment) => {
const start = new Date(appointment.start);
if (Number.isNaN(start.getTime())) {
return false;
}
return start >= now && start <= next24h;
});
return {
highRiskCount: highRiskPatients.length,
openTaskCount: openTasks.length,
overdueTaskCount: overdueTasks.length,
totalGapCount: totalGaps,
next24hAppointments: appointmentsNext24h.length,
noShowRate: calculateNoShowRate(appointments, 90)
};
}, [appointments, patientRows, tasks]);
Loading Command Center Data
Data Loading
const loadData = async () => {
const [
patientBundle,
conditionBundle,
allergyBundle,
medicationBundle,
observationBundle,
encounterBundle,
appointmentBundle,
taskBundle,
practitionerResponse
] = await Promise.all([
fhirApi.listPatients(token),
fhirApi.listConditions(token),
fhirApi.listAllergies(token),
fhirApi.listMedicationRequests(token),
fhirApi.listObservations(token),
fhirApi.listEncounters(token),
fhirApi.listAppointments(token),
fhirApi.listTasks(token),
adminApi.listPractitioners(token)
]);
setPatients(bundleToResources(patientBundle));
setConditions(bundleToResources(conditionBundle));
setAllergies(bundleToResources(allergyBundle));
setMedications(bundleToResources(medicationBundle));
setObservations(bundleToResources(observationBundle));
setEncounters(bundleToResources(encounterBundle));
setAppointments(bundleToResources(appointmentBundle));
setTasks(bundleToResources(taskBundle));
setPractitioners(practitionerResponse.data || []);
};
Care Coordination Worklist
Patient Worklist
const patientRows = useMemo(() => {
const now = new Date();
return patients
.map((patient) => {
const patientId = patient.id;
const patientConditions = conditionsByPatient.get(patientId) || [];
const patientAllergies = allergiesByPatient.get(patientId) || [];
const patientMedications = medicationsByPatient.get(patientId) || [];
const patientObservations = observationsByPatient.get(patientId) || [];
const patientEncounters = encountersByPatient.get(patientId) || [];
const patientAppointments = appointmentsByPatient.get(patientId) || [];
const patientTasks = tasksByPatient.get(patientId) || [];
const profile = buildPatientRiskProfile({
conditions: patientConditions,
allergies: patientAllergies,
medications: patientMedications,
observations: patientObservations,
encounters: patientEncounters,
appointments: patientAppointments,
tasks: patientTasks
});
const openTasks = patientTasks.filter(isTaskOpen);
const overdueTasks = openTasks.filter((task) => isTaskOverdue(task, now));
const nextAppointment = [...patientAppointments]
.filter((appointment) => {
const start = new Date(appointment.start);
return !Number.isNaN(start.getTime()) && start >= now;
})
.sort((a, b) => sortByDateAsc(a.start, b.start))[0];
const lastEncounter = [...patientEncounters].sort(
(a, b) =>
new Date(b.period?.start || 0).getTime() -
new Date(a.period?.start || 0).getTime()
)[0];
return {
patient,
profile,
openTaskCount: openTasks.length,
overdueTaskCount: overdueTasks.length,
nextAppointment,
lastEncounter
};
})
.sort((left, right) => {
// Sort by risk score, then by overdue tasks
if (right.profile.score !== left.profile.score) {
return right.profile.score - left.profile.score;
}
return right.overdueTaskCount - left.overdueTaskCount;
});
}, [
allergiesByPatient,
appointmentsByPatient,
conditionsByPatient,
encountersByPatient,
medicationsByPatient,
observationsByPatient,
patients,
tasksByPatient
]);
Population Filters
Filtering Options
const filteredPatientRows = useMemo(() => {
return patientRows.filter((row) => {
if (riskFilter !== "all" && row.profile.tier !== riskFilter) {
return false;
}
if (ownerFilter === "all") {
return true;
}
const patientTasks = tasksByPatient.get(row.patient.id) || [];
const openTasks = patientTasks.filter(isTaskOpen);
if (ownerFilter === "unassigned") {
return openTasks.some((task) => !ownerReference(task));
}
return openTasks.some(
(task) => ownerReference(task) === `Practitioner/${ownerFilter}`
);
});
}, [ownerFilter, patientRows, riskFilter, tasksByPatient]);
// Render filters
<article className="card form-grid two-columns">
<h2>Population filters</h2>
<label>
Risk tier
<select value={riskFilter} onChange={(e) => setRiskFilter(e.target.value)}>
<option value="all">All tiers</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</label>
<label>
Task owner
<select value={ownerFilter} onChange={(e) => setOwnerFilter(e.target.value)}>
<option value="all">All owners</option>
<option value="unassigned">Unassigned</option>
{practitioners.map((practitioner) => (
<option key={practitioner.id} value={practitioner.id}>
{practitioner.fullName}
</option>
))}
</select>
</label>
</article>
Service Line Demand
Service Mix Analysis
export const calculateServiceMix = (appointments) => {
const counts = appointments.reduce((map, appointment) => {
const label =
appointment?.serviceCategory?.[0]?.text ||
appointment?.serviceCategory?.[0]?.coding?.[0]?.display ||
"Unspecified";
map.set(label, (map.get(label) || 0) + 1);
return map;
}, new Map());
return Array.from(counts.entries())
.map(([service, count]) => ({ service, count }))
.sort((a, b) => b.count - a.count);
};
const serviceMix = useMemo(() => {
const rows = calculateServiceMix(appointments);
const total = rows.reduce((sum, row) => sum + row.count, 0);
return rows.slice(0, 8).map((row) => ({
...row,
share: total ? Math.round((row.count / total) * 1000) / 10 : 0
}));
}, [appointments]);
Risk Stratification
OmniEHR automatically calculates patient risk scores based on clinical data.Risk Profile Builder
buildPatientRiskProfile()
export const buildPatientRiskProfile = ({
conditions = [],
allergies = [],
medications = [],
observations = [],
encounters = [],
appointments = [],
tasks = []
}) => {
const activeConditions = conditions.filter((condition) =>
ACTIVE_CONDITION_STATUSES.has(statusOfCondition(condition))
);
const activeMedications = medications.filter(
(medication) => ![
"stopped", "completed", "cancelled", "entered-in-error"
].includes(normalize(medication.status))
);
const severeAllergies = allergies.filter((allergy) => {
const reactionSeverity = normalize(allergy?.reaction?.[0]?.severity);
return normalize(allergy.criticality) === "high" ||
reactionSeverity === "severe";
});
const careGaps = [];
const safetyAlerts = [];
// Build care gaps and safety alerts...
// (See Care Gaps section for full implementation)
const openTasks = tasks.filter(isTaskOpen);
const overdueTasks = openTasks.filter((task) => isTaskOverdue(task));
// Calculate risk score
let score = 0;
score += safetyAlerts.filter((alert) => alert.severity === "high").length * 4;
score += safetyAlerts.filter((alert) => alert.severity === "medium").length * 2;
score += careGaps.filter((gap) => gap.severity === "high").length * 3;
score += careGaps.filter((gap) => gap.severity === "medium").length * 2;
score += careGaps.filter((gap) => gap.severity === "low").length;
if (overdueTasks.length > 0) {
score += 2;
}
if (openTasks.length >= 5) {
score += 1;
}
return {
score,
tier: classifyRiskTier(score),
careGaps,
safetyAlerts,
latestVitals: {
systolic: systolicValue,
diastolic: diastolicValue,
a1c: a1cValue
},
activeConditionCount: activeConditions.length,
activeMedicationCount: activeMedications.length,
openTaskCount: openTasks.length,
overdueTaskCount: overdueTasks.length
};
};
Risk Tier Classification
Risk Tiers
export const classifyRiskTier = (score) => {
if (score >= 8) {
return "high";
}
if (score >= 4) {
return "medium";
}
return "low";
};
Risk Scoring: High risk (8+), Medium risk (4-7), Low risk (0-3). Scores are calculated from safety alerts, care gaps, and task burden.
Care Gaps Detection
OmniEHR automatically identifies quality improvement opportunities and preventive care needs.Care Gap Rules
HbA1c Follow-up Overdue
HbA1c Follow-up Overdue
const hasDiabetes = activeConditions.some((condition) => {
const code = normalize(codingOf(condition).code);
const label = textOf(condition);
return code === "44054006" || label.includes("diabetes");
});
if (hasDiabetes && !hasRecentObservation(observations, A1C_CODES, 180)) {
careGaps.push({
severity: "high",
title: "HbA1c follow-up overdue",
detail: "No HbA1c result found in the last 180 days for an active diabetes condition."
});
}
Blood Pressure Follow-up Needed
Blood Pressure Follow-up Needed
if ((hasHypertension ||
(systolicValue || 0) >= 140 ||
(diastolicValue || 0) >= 90) &&
!hasRecentObservation(observations, SYSTOLIC_BP_CODES, 30)) {
careGaps.push({
severity: "medium",
title: "Blood pressure follow-up needed",
detail: "No recent blood-pressure observation found in the last 30 days."
});
}
Continuity-of-Care Visit Due
Continuity-of-Care Visit Due
const latestEncounter = latestByDate(
encounters,
(encounter) => encounter.period?.start
);
if (!latestEncounter ||
daysBetween(toDate(latestEncounter.period?.start)) > 180) {
careGaps.push({
severity: "medium",
title: "Continuity-of-care visit due",
detail: "No encounter documented in the last 180 days."
});
}
Medication Reconciliation Due
Medication Reconciliation Due
const hasMedicationReviewVisit = appointments.some((appointment) => {
const description = appointmentDescription(appointment);
const start = toDate(appointment.start);
return (
Boolean(start) &&
daysBetween(start) <= 120 &&
(description.includes("medication review") ||
description.includes("follow-up"))
);
});
if (activeMedications.length >= 5 && !hasMedicationReviewVisit) {
careGaps.push({
severity: "medium",
title: "Medication reconciliation due",
detail: "Polypharmacy profile without a recent medication-review follow-up."
});
}
No Upcoming Follow-up Appointment
No Upcoming Follow-up Appointment
if (!hasUpcomingAppointmentWithin(appointments, 60)) {
careGaps.push({
severity: "low",
title: "No upcoming follow-up appointment",
detail: "No booked appointment found in the next 60 days."
});
}
Safety Alerts
Clinical safety alerts are generated based on real-time patient data.Safety Alert Rules
Severe Allergy Profile
Severe Allergy Profile
if (severeAllergies.length > 0) {
safetyAlerts.push({
severity: "high",
title: "Severe allergy profile",
detail: `${severeAllergies.length} severe/high-criticality allergy record(s) require active review.`
});
}
Allergy-Medication Conflict
Allergy-Medication Conflict
const medicationConflicts = activeMedications.filter((medication) => {
const medicationLabel = normalize(
medication?.medicationCodeableConcept?.coding?.[0]?.display ||
medication?.medicationCodeableConcept?.coding?.[0]?.code
);
if (!medicationLabel) {
return false;
}
return allergies.some((allergy) => {
if (!normalize((allergy.category || []).join(" "))
.includes("medication")) {
return false;
}
const allergyLabel = textOf(allergy);
if (!allergyLabel) {
return false;
}
return medicationLabel.includes(allergyLabel) ||
allergyLabel.includes(medicationLabel);
});
});
if (medicationConflicts.length > 0) {
safetyAlerts.push({
severity: "high",
title: "Potential allergy-medication conflict",
detail: `${medicationConflicts.length} active medication(s) may conflict with allergy records.`
});
}
Hypertensive Crisis Threshold
Hypertensive Crisis Threshold
if ((systolicValue || 0) >= 180 || (diastolicValue || 0) >= 120) {
safetyAlerts.push({
severity: "high",
title: "Hypertensive crisis threshold",
detail: `Latest BP ${systolicValue || "-"} / ${diastolicValue || "-"} mmHg is above crisis threshold.`
});
}
Polypharmacy Review Recommended
Polypharmacy Review Recommended
if (activeMedications.length >= 8) {
safetyAlerts.push({
severity: "medium",
title: "Polypharmacy review recommended",
detail: `${activeMedications.length} active medications detected.`
});
}
Clinical Decision Support
Patient detail pages display real-time clinical decision support:Clinical Decision Support Display
<article className="card form-grid two-columns">
<h2>Clinical decision support</h2>
<div className="metric-card">
<h3>Risk tier</h3>
<p className="metric-value">
<span className={`risk-chip risk-chip-${riskProfile.tier}`}>
{riskProfile.tier} ({riskProfile.score})
</span>
</p>
<p className="muted-text">
{riskProfile.openTaskCount} open tasks,
{riskProfile.overdueTaskCount} overdue.
</p>
</div>
<div className="metric-card">
<h3>Latest vitals</h3>
<p className="muted-text">
BP: {riskProfile.latestVitals.systolic || "-"} /
{riskProfile.latestVitals.diastolic || "-"} mmHg
</p>
<p className="muted-text">
HbA1c: {riskProfile.latestVitals.a1c || "-"}
</p>
</div>
<div>
<h3>Safety alerts</h3>
{riskProfile.safetyAlerts.length === 0 ? (
<p className="muted-text">No active alerts.</p>
) : (
<ul className="plain-list">
{riskProfile.safetyAlerts.map((alert) => (
<li key={`${alert.title}-${alert.detail}`}>
<span className={`risk-chip risk-chip-${alert.severity}`}>
{alert.severity}
</span>
{" "}
{alert.title}
</li>
))}
</ul>
)}
</div>
<div>
<h3>Open care gaps</h3>
{riskProfile.careGaps.length === 0 ? (
<p className="muted-text">No open care gaps.</p>
) : (
<ul className="plain-list">
{riskProfile.careGaps.map((gap) => (
<li key={`${gap.title}-${gap.detail}`}>
<span className={`risk-chip risk-chip-${gap.severity}`}>
{gap.severity}
</span>
{" "}
{gap.title}
</li>
))}
</ul>
)}
</div>
</article>
Task Inbox
The Command Center includes a comprehensive task inbox with overdue detection:Task Inbox
const taskRows = useMemo(() => {
return [...tasks].sort((left, right) => {
const overdueLeft = isTaskOverdue(left) ? 1 : 0;
const overdueRight = isTaskOverdue(right) ? 1 : 0;
if (overdueLeft !== overdueRight) {
return overdueRight - overdueLeft;
}
return sortByDateAsc(getTaskDueDate(left), getTaskDueDate(right));
});
}, [tasks]);
// Render task table
<table>
<thead>
<tr>
<th>Task</th>
<th>Patient</th>
<th>Owner</th>
<th>Priority</th>
<th>Due</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{taskRows.map((task) => {
const dueDate = getTaskDueDate(task);
const isOverdue = isTaskOverdue(task);
return (
<tr key={task.id}>
<td>
<p>{task.description || "-"}</p>
{task.code?.text ? (
<p className="muted-text">{task.code.text}</p>
) : null}
</td>
<td>
{patient ? (
<Link to={`/patients/${patient.id}`} className="inline-link">
{patientFullName(patient)}
</Link>
) : "-"}
</td>
<td>{owner}</td>
<td>
<span className={`priority-chip priority-chip-${task.priority}`}>
{task.priority || "routine"}
</span>
</td>
<td>
{formatDateTime(dueDate)}
{isOverdue ? (
<p className="status-text-overdue">Overdue</p>
) : null}
</td>
<td>
<select
value={task.status}
onChange={(event) => onUpdateTaskStatus(task, event.target.value)}
>
{TASK_STATUS_OPTIONS.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</td>
</tr>
);
})}
</tbody>
</table>
Role-Based Task Access
Practitioners can only access tasks assigned to them:Task Owner Permission
const ensureTaskOwnerPermission = (requestingUser, ownerUserId) => {
if (requestingUser.role !== "practitioner") {
return;
}
if (!ownerUserId || String(ownerUserId) !== String(requestingUser.sub)) {
throw new ApiError(403, "Practitioners can only assign or update tasks under their own worklist");
}
};
// Backend query filtering
router.get(
"/Task",
authorize(...readRoles),
asyncHandler(async (req, res) => {
const filter = {};
if (req.user.role === "practitioner") {
filter.ownerUserId = req.user.sub;
}
const records = await Task.find(filter)
.sort({ dueDate: 1, authoredOn: -1 })
.limit(300);
const resources = records.map(taskDocToResource);
res.json(
toSearchsetBundle({
resourceType: "Task",
resources,
total: resources.length,
baseUrl: baseUrl(req),
searchId: randomUUID()
})
);
})
);
API Reference
Task Endpoints
| Method | Endpoint | Description | Required Role |
|---|---|---|---|
| POST | /api/fhir/Task | Create task | admin, practitioner |
| GET | /api/fhir/Task | List tasks | admin, practitioner, auditor |
| GET | /api/fhir/Task/:id | Get task by ID | admin, practitioner, auditor |
| PUT | /api/fhir/Task/:id | Update task | admin, practitioner |
Query Parameters
- By Patient
- By Owner
- By Status
GET /api/fhir/Task?for=Patient/507f1f77bcf86cd799439011
GET /api/fhir/Task?owner=Practitioner/507f1f77bcf86cd799439020
GET /api/fhir/Task?status=requested
GET /api/fhir/Task?status=in-progress
Next Steps
Patient Management
Learn about patient registry and demographics
Scheduling
Explore appointment scheduling system
FHIR Resources
View all supported FHIR resources
Audit Logs
Learn about HIPAA-compliant audit logging