@ovok/core 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,6 +83,11 @@ CommunicationRequest-backed notifications.
83
83
 
84
84
  ```typescript
85
85
  const me = await client.getCurrentPatientProfile();
86
+ const dashboard = await client.getPatientDashboard({
87
+ latestObservations: { code: ["8867-4", "9279-1"], max: 5 },
88
+ measurementHistory: { _sort: "-date", _count: 50 },
89
+ carePlans: { status: "active" },
90
+ });
86
91
  const latest = await client.getPatientLatestObservations({
87
92
  patient: `Patient/${me.profile.id}`,
88
93
  code: ["8867-4", "9279-1"],
@@ -2,9 +2,16 @@ import { Observation } from "@medplum/fhirtypes";
2
2
  import { BloodGlucoseMeasurement, BloodPressureMeasurement, BodyTemperatureMeasurement, BodyWeightMeasurement, EcgMeasurement, HeartRateMeasurement, HeartRateVariabilityMeasurement, Measurement, MeasurementTypeKey, PulseOximeterMeasurement, RespiratoryRateMeasurement, RestingHeartRateMeasurement, StepCountMeasurement, UrineAnalyzeMeasurement, Vo2MaxMeasurement, WalkingHeartRateAverageMeasurement } from "../../../types";
3
3
  export type SupportedMeasurement = BloodGlucoseMeasurement | BloodPressureMeasurement | BodyTemperatureMeasurement | BodyWeightMeasurement | EcgMeasurement | HeartRateMeasurement | HeartRateVariabilityMeasurement | PulseOximeterMeasurement | RespiratoryRateMeasurement | RestingHeartRateMeasurement | StepCountMeasurement | UrineAnalyzeMeasurement | Vo2MaxMeasurement | WalkingHeartRateAverageMeasurement | Measurement;
4
4
  type SupportedType = Exclude<MeasurementTypeKey, MeasurementTypeKey.symptomQuestionnaire>;
5
+ export type ObservationGrouping = "hasMember" | "time" | ((observation: Observation, index: number, observations: readonly Observation[]) => string);
6
+ export interface ObservationsToMeasurementsOptions {
7
+ /** How related Observation resources should be reconstructed into measurements. */
8
+ groupBy?: ObservationGrouping;
9
+ /** Maximum timestamp difference used by the time-based fallback. */
10
+ timeToleranceMs?: number;
11
+ }
5
12
  export declare const observationCodesForTypes: (types: readonly SupportedType[]) => string;
6
13
  /** Converts FHIR observations back into SDK measurements without patient/app assumptions. */
7
- export declare const observationsToMeasurements: <T extends readonly SupportedType[]>(observations: readonly Observation[], types: T) => Array<Extract<SupportedMeasurement, {
14
+ export declare const observationsToMeasurements: <T extends readonly SupportedType[]>(observations: readonly Observation[], types: T, options?: ObservationsToMeasurementsOptions) => Array<Extract<SupportedMeasurement, {
8
15
  measurementTypeKey: T[number];
9
16
  }>>;
10
17
  export {};
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.observationsToMeasurements = exports.observationCodesForTypes = void 0;
5
5
  const types_1 = require("../../../types");
6
+ const DEFAULT_OBSERVATION_TIME_TOLERANCE_MS = 1000;
6
7
  const codeOf = (observation) => {
7
8
  var _a, _b, _c, _d, _e, _f, _g;
8
9
  return (_d = (_c = (_b = (_a = observation.code) === null || _a === void 0 ? void 0 : _a.coding) === null || _b === void 0 ? void 0 : _b.find((coding) => coding.system === "http://loinc.org")) === null || _c === void 0 ? void 0 : _c.code) !== null && _d !== void 0 ? _d : (_g = (_f = (_e = observation.code) === null || _e === void 0 ? void 0 : _e.coding) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.code;
@@ -50,28 +51,138 @@ const ecgGroupKey = (observation, observations) => {
50
51
  });
51
52
  return root ? groupKey(root) : groupKey(observation);
52
53
  };
53
- const flattenMembers = (observations) => {
54
- const byReference = new Map(observations
55
- .filter((observation) => observation.id)
56
- .map((observation) => [`Observation/${observation.id}`, observation]));
57
- const flattened = [];
58
- const add = (observation) => {
54
+ const referenceKeys = (reference) => {
55
+ const id = reference.replace(/^urn:uuid:/, "").replace(/^Observation\//, "");
56
+ return [reference, id, `Observation/${id}`, `urn:uuid:${id}`];
57
+ };
58
+ const observationGroups = (observations) => {
59
+ const byReference = new Map();
60
+ observations.forEach((observation, index) => {
61
+ if (observation.id) {
62
+ referenceKeys(observation.id).forEach((key) => byReference.set(key, index));
63
+ }
64
+ });
65
+ const parent = observations.map((_, index) => index);
66
+ const find = (index) => {
67
+ if (parent[index] !== index) {
68
+ parent[index] = find(parent[index]);
69
+ }
70
+ return parent[index];
71
+ };
72
+ const union = (left, right) => {
73
+ const leftRoot = find(left);
74
+ const rightRoot = find(right);
75
+ if (leftRoot !== rightRoot) {
76
+ parent[rightRoot] = leftRoot;
77
+ }
78
+ };
79
+ const linked = new Set();
80
+ observations.forEach((observation, index) => {
59
81
  var _a;
60
- flattened.push(observation);
61
82
  (_a = observation.hasMember) === null || _a === void 0 ? void 0 : _a.forEach((reference) => {
62
- const member = reference.reference ? byReference.get(reference.reference) : undefined;
63
- if (member) {
64
- add(member);
83
+ const referenced = reference.reference
84
+ ? referenceKeys(reference.reference)
85
+ .map((key) => byReference.get(key))
86
+ .find((candidate) => candidate !== undefined)
87
+ : undefined;
88
+ if (referenced !== undefined) {
89
+ linked.add(index);
90
+ linked.add(referenced);
91
+ union(index, referenced);
65
92
  }
66
93
  });
94
+ });
95
+ return {
96
+ componentOf: new Map(observations.map((_, index) => [index, find(index)])),
97
+ linked,
67
98
  };
68
- observations.filter((observation) => { var _a; return !((_a = observation.hasMember) === null || _a === void 0 ? void 0 : _a.length); }).forEach(add);
69
- observations.filter((observation) => { var _a; return (_a = observation.hasMember) === null || _a === void 0 ? void 0 : _a.length; }).forEach((observation) => {
70
- if (!flattened.includes(observation)) {
71
- add(observation);
99
+ };
100
+ const addToGroup = (groups, key, observation, index) => {
101
+ const group = groups.get(key);
102
+ if (group) {
103
+ group.observations.push(observation);
104
+ }
105
+ else {
106
+ groups.set(key, { firstIndex: index, observations: [observation] });
107
+ }
108
+ };
109
+ const groupByTime = (observations, timeToleranceMs) => {
110
+ const groups = [];
111
+ observations.forEach((observation, index) => {
112
+ var _a;
113
+ const recordedAt = (_a = recordedAtOf(observation)) === null || _a === void 0 ? void 0 : _a.getTime();
114
+ const group = recordedAt === undefined
115
+ ? undefined
116
+ : groups.find((candidate) => candidate.anchor !== undefined &&
117
+ Math.abs(candidate.anchor - recordedAt) <= timeToleranceMs);
118
+ if (group) {
119
+ group.observations.push(observation);
120
+ return;
72
121
  }
122
+ groups.push({
123
+ firstIndex: index,
124
+ ...(recordedAt !== undefined ? { anchor: recordedAt } : {}),
125
+ observations: [observation],
126
+ });
73
127
  });
74
- return flattened;
128
+ return groups;
129
+ };
130
+ const groupObservations = (observations, options, legacyKey) => {
131
+ var _a, _b;
132
+ const grouping = (_a = options.groupBy) !== null && _a !== void 0 ? _a : "hasMember";
133
+ const tolerance = Math.max(0, (_b = options.timeToleranceMs) !== null && _b !== void 0 ? _b : DEFAULT_OBSERVATION_TIME_TOLERANCE_MS);
134
+ if (grouping === "time") {
135
+ return groupByTime(observations, tolerance);
136
+ }
137
+ if (typeof grouping === "function") {
138
+ const groups = new Map();
139
+ observations.forEach((observation, index) => {
140
+ addToGroup(groups, grouping(observation, index, observations), observation, index);
141
+ });
142
+ return [...groups.values()].sort((left, right) => left.firstIndex - right.firstIndex);
143
+ }
144
+ const { componentOf, linked } = observationGroups(observations);
145
+ const componentSizes = new Map();
146
+ componentOf.forEach((root) => {
147
+ var _a;
148
+ componentSizes.set(root, ((_a = componentSizes.get(root)) !== null && _a !== void 0 ? _a : 0) + 1);
149
+ });
150
+ const groups = new Map();
151
+ const fallback = observations.filter((_, index) => {
152
+ var _a;
153
+ const root = componentOf.get(index);
154
+ return ((_a = componentSizes.get(root)) !== null && _a !== void 0 ? _a : 0) === 1;
155
+ });
156
+ observations.forEach((observation, index) => {
157
+ var _a;
158
+ const root = componentOf.get(index);
159
+ if (((_a = componentSizes.get(root)) !== null && _a !== void 0 ? _a : 0) > 1) {
160
+ addToGroup(groups, `member:${root}`, observation, index);
161
+ }
162
+ });
163
+ if (fallback.length > 0) {
164
+ const fallbackIndexes = new Set(fallback.map((observation) => observations.indexOf(observation)));
165
+ const unlinked = observations.filter((_, index) => fallbackIndexes.has(index) && !linked.has(index));
166
+ const unresolved = observations.filter((_, index) => fallbackIndexes.has(index) && linked.has(index));
167
+ groupByTime(unlinked, tolerance).forEach((group) => {
168
+ const first = group.observations[0];
169
+ const firstIndex = observations.indexOf(first);
170
+ const key = legacyKey
171
+ ? `legacy:${legacyKey(first, firstIndex)}`
172
+ : `time:${firstIndex}`;
173
+ addToGroup(groups, key, first, firstIndex);
174
+ group.observations.slice(1).forEach((observation) => {
175
+ const index = observations.indexOf(observation);
176
+ addToGroup(groups, key, observation, index);
177
+ });
178
+ });
179
+ unresolved.forEach((observation) => {
180
+ var _a;
181
+ const index = observations.indexOf(observation);
182
+ addToGroup(groups, `unresolved:${(_a = observation.id) !== null && _a !== void 0 ? _a : index}`, observation, index);
183
+ });
184
+ }
185
+ return [...groups.values()].sort((left, right) => left.firstIndex - right.firstIndex);
75
186
  };
76
187
  const firstValue = (observations, code) => observations.find((observation) => codeOf(observation) === code)
77
188
  ? valueOf(observations.find((observation) => codeOf(observation) === code))
@@ -82,7 +193,11 @@ const baseMeasurement = (measurementTypeKey, observation) => ({
82
193
  });
83
194
  const inverseGroup = (type, observations) => {
84
195
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
85
- const first = observations[0];
196
+ const first = [...observations].sort((left, right) => {
197
+ var _a, _b, _c, _d;
198
+ return ((_b = (_a = recordedAtOf(left)) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER) -
199
+ ((_d = (_c = recordedAtOf(right)) === null || _c === void 0 ? void 0 : _c.getTime()) !== null && _d !== void 0 ? _d : Number.MAX_SAFE_INTEGER);
200
+ })[0];
86
201
  const measurement = baseMeasurement(type, first);
87
202
  const value = (code) => firstValue(observations, code);
88
203
  switch (type) {
@@ -225,22 +340,16 @@ const codesForType = {
225
340
  const observationCodesForTypes = (types) => [...new Set(types.flatMap((type) => { var _a; return (_a = codesForType[type]) !== null && _a !== void 0 ? _a : []; }))].join(",");
226
341
  exports.observationCodesForTypes = observationCodesForTypes;
227
342
  /** Converts FHIR observations back into SDK measurements without patient/app assumptions. */
228
- const observationsToMeasurements = (observations, types) => {
229
- const source = flattenMembers(observations);
343
+ const observationsToMeasurements = (observations, types, options = {}) => {
230
344
  const output = [];
231
345
  types.forEach((type) => {
232
346
  var _a;
233
347
  const codes = (_a = codesForType[type]) !== null && _a !== void 0 ? _a : [];
234
- const matching = source.filter((observation) => { var _a; return codes.includes((_a = codeOf(observation)) !== null && _a !== void 0 ? _a : ""); });
235
- const groups = new Map();
236
- matching.forEach((observation) => {
237
- var _a, _b, _c;
238
- const key = type === types_1.MeasurementTypeKey.ecg
239
- ? ecgGroupKey(observation, matching)
240
- : (_b = (_a = recordedAtOf(observation)) === null || _a === void 0 ? void 0 : _a.toISOString()) !== null && _b !== void 0 ? _b : "unknown";
241
- groups.set(key, [...((_c = groups.get(key)) !== null && _c !== void 0 ? _c : []), observation]);
242
- });
243
- groups.forEach((group) => output.push(inverseGroup(type, group)));
348
+ const matching = observations.filter((observation) => { var _a; return codes.includes((_a = codeOf(observation)) !== null && _a !== void 0 ? _a : ""); });
349
+ const groups = groupObservations(matching, options, type === types_1.MeasurementTypeKey.ecg
350
+ ? (observation, index) => ecgGroupKey(observation, matching) || `ecg:${index}`
351
+ : undefined);
352
+ groups.forEach((group) => output.push(inverseGroup(type, group.observations)));
244
353
  });
245
354
  return output;
246
355
  };
@@ -24,6 +24,34 @@ const linkObservationMembers = (observations) => {
24
24
  });
25
25
  };
26
26
  exports.linkObservationMembers = linkObservationMembers;
27
+ const createdObservationReference = (response, index) => {
28
+ var _a, _b, _c, _d;
29
+ const entry = (_a = response.entry) === null || _a === void 0 ? void 0 : _a[index];
30
+ const id = ((_b = entry === null || entry === void 0 ? void 0 : entry.resource) === null || _b === void 0 ? void 0 : _b.resourceType) === "Observation"
31
+ ? entry.resource.id
32
+ : undefined;
33
+ if (id) {
34
+ return `Observation/${id}`;
35
+ }
36
+ const location = (_d = (_c = entry === null || entry === void 0 ? void 0 : entry.response) === null || _c === void 0 ? void 0 : _c.location) === null || _d === void 0 ? void 0 : _d.split("?")[0];
37
+ const match = location === null || location === void 0 ? void 0 : location.match(/(?:^|\/)(Observation\/[^/]+)(?:\/_history\/[^/]+)?$/);
38
+ return match === null || match === void 0 ? void 0 : match[1];
39
+ };
40
+ const rewriteObservationReference = (entry, from, to) => {
41
+ var _a;
42
+ const resource = entry.resource;
43
+ if ((resource === null || resource === void 0 ? void 0 : resource.resourceType) !== "Observation" || !((_a = resource.hasMember) === null || _a === void 0 ? void 0 : _a.length)) {
44
+ return entry;
45
+ }
46
+ return {
47
+ ...entry,
48
+ resource: {
49
+ ...resource,
50
+ hasMember: resource.hasMember.map((reference) => reference.reference === from ? { ...reference, reference: to } : reference),
51
+ },
52
+ };
53
+ };
54
+ const rewriteObservationReferences = (entries, from, to) => entries.map((entry) => rewriteObservationReference(entry, from, to));
27
55
  const splitObservationBundleEntries = (bundle) => {
28
56
  var _a;
29
57
  const entries = (_a = bundle.entry) !== null && _a !== void 0 ? _a : [];
@@ -50,26 +78,49 @@ const splitObservationBundleEntries = (bundle) => {
50
78
  };
51
79
  exports.splitObservationBundleEntries = splitObservationBundleEntries;
52
80
  const executeObservationBundle = async (bundle, executeBatch) => {
53
- var _a, _b, _c, _d;
81
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
54
82
  const entries = (_a = bundle.entry) !== null && _a !== void 0 ? _a : [];
55
83
  if (bundle.type === "transaction" &&
56
84
  entries.length > exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES) {
85
+ const rootReference = (_c = (_b = entries[0]) === null || _b === void 0 ? void 0 : _b.fullUrl) !== null && _c !== void 0 ? _c : (((_e = (_d = entries[0]) === null || _d === void 0 ? void 0 : _d.resource) === null || _e === void 0 ? void 0 : _e.resourceType) === "Observation"
86
+ ? `urn:uuid:${(0, offline_1.stableMeasurementId)(entries[0].resource)}`
87
+ : undefined);
57
88
  const transactionResponse = await (0, exports.executeObservationBundle)({ ...bundle, entry: entries.slice(0, 1) }, executeBatch);
58
- const batchResponse = await (0, exports.executeObservationBundle)({ ...bundle, type: "batch", entry: entries.slice(1) }, executeBatch);
89
+ const savedRootReference = rootReference
90
+ ? createdObservationReference(transactionResponse, 0)
91
+ : undefined;
92
+ const batchResponse = await (0, exports.executeObservationBundle)({
93
+ ...bundle,
94
+ type: "batch",
95
+ entry: savedRootReference
96
+ ? rewriteObservationReferences(entries.slice(1), rootReference, savedRootReference)
97
+ : entries.slice(1),
98
+ }, executeBatch);
59
99
  return {
60
100
  resourceType: "Bundle",
61
101
  type: "batch-response",
62
102
  entry: [
63
- ...((_b = transactionResponse.entry) !== null && _b !== void 0 ? _b : []),
64
- ...((_c = batchResponse.entry) !== null && _c !== void 0 ? _c : []),
103
+ ...((_f = transactionResponse.entry) !== null && _f !== void 0 ? _f : []),
104
+ ...((_g = batchResponse.entry) !== null && _g !== void 0 ? _g : []),
65
105
  ],
66
106
  };
67
107
  }
68
108
  const chunks = (0, exports.splitObservationBundleEntries)(bundle);
69
109
  if (chunks) {
70
110
  const responses = [];
71
- for (const entry of chunks) {
72
- responses.push(await (0, exports.executeObservationBundle)({ ...bundle, entry }, executeBatch));
111
+ const rootReference = (_j = (_h = entries[0]) === null || _h === void 0 ? void 0 : _h.fullUrl) !== null && _j !== void 0 ? _j : (((_l = (_k = entries[0]) === null || _k === void 0 ? void 0 : _k.resource) === null || _l === void 0 ? void 0 : _l.resourceType) === "Observation"
112
+ ? `urn:uuid:${(0, offline_1.stableMeasurementId)(entries[0].resource)}`
113
+ : undefined);
114
+ let savedRootReference;
115
+ for (const [index, entry] of chunks.entries()) {
116
+ const chunkEntries = index > 0 && rootReference && savedRootReference
117
+ ? rewriteObservationReferences(entry, rootReference, savedRootReference)
118
+ : entry;
119
+ const response = await (0, exports.executeObservationBundle)({ ...bundle, entry: chunkEntries }, executeBatch);
120
+ if (index === 0 && rootReference) {
121
+ savedRootReference = createdObservationReference(response, 0);
122
+ }
123
+ responses.push(response);
73
124
  }
74
125
  return {
75
126
  resourceType: "Bundle",
@@ -78,7 +129,7 @@ const executeObservationBundle = async (bundle, executeBatch) => {
78
129
  };
79
130
  }
80
131
  const response = await executeBatch(bundle);
81
- if ((_d = response.entry) === null || _d === void 0 ? void 0 : _d.some((entry) => {
132
+ if ((_m = response.entry) === null || _m === void 0 ? void 0 : _m.some((entry) => {
82
133
  var _a, _b;
83
134
  const status = (_b = (_a = entry.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : "";
84
135
  return status.startsWith("4") || status.startsWith("5");
@@ -1,5 +1,5 @@
1
1
  import { OvokClient } from "../ovok-client";
2
- import { OvokCurrentPatientProfile, OvokPatientLatestObservationsQuery, OvokPatientObservationBundle, OvokPatientAppointment, OvokPatientCarePlan, OvokPatientDevice, OvokPatientMeasurement, OvokPatientNotification, OvokPatientQuestionnaire, OvokPatientQuestionnaireResponse, OvokPatientResourceQuery, OvokPatientSchedule, OvokPatientSchedulingQuery, OvokPatientSlot, OvokPatientTelemetryHistory, OvokPatientTelemetryHistoryQuery } from "./types";
2
+ import { OvokCurrentPatientProfile, OvokPatientLatestObservationsQuery, OvokPatientObservationBundle, OvokPatientAppointment, OvokPatientCarePlan, OvokPatientDashboard, OvokPatientDashboardQuery, OvokPatientDevice, OvokPatientMeasurement, OvokPatientNotification, OvokPatientQuestionnaire, OvokPatientQuestionnaireResponse, OvokPatientResourceQuery, OvokPatientSchedule, OvokPatientSchedulingQuery, OvokPatientSlot, OvokPatientTelemetryHistory, OvokPatientTelemetryHistoryQuery } from "./types";
3
3
  export declare function getCurrentPatientProfile(this: OvokClient): Promise<OvokCurrentPatientProfile>;
4
4
  export declare function getPatientLatestObservations(this: OvokClient, params?: OvokPatientLatestObservationsQuery): Promise<OvokPatientObservationBundle>;
5
5
  export declare function getPatientDeviceTelemetry(this: OvokClient, deviceId: string): Promise<OvokPatientObservationBundle>;
@@ -22,3 +22,4 @@ export declare function listPatientDevices(this: OvokClient, params?: OvokPatien
22
22
  export declare function getPatientDevice(this: OvokClient, deviceId: string): Promise<OvokPatientDevice>;
23
23
  export declare function listPatientNotifications(this: OvokClient, params?: OvokPatientResourceQuery): Promise<OvokPatientNotification[]>;
24
24
  export declare function getPatientNotification(this: OvokClient, notificationId: string): Promise<OvokPatientNotification>;
25
+ export declare function getPatientDashboard(this: OvokClient, params?: OvokPatientDashboardQuery): Promise<OvokPatientDashboard>;
@@ -22,6 +22,7 @@ exports.listPatientDevices = listPatientDevices;
22
22
  exports.getPatientDevice = getPatientDevice;
23
23
  exports.listPatientNotifications = listPatientNotifications;
24
24
  exports.getPatientNotification = getPatientNotification;
25
+ exports.getPatientDashboard = getPatientDashboard;
25
26
  const segment = (value) => encodeURIComponent(value);
26
27
  const query = (params) => {
27
28
  const values = new URLSearchParams();
@@ -104,3 +105,23 @@ async function listPatientNotifications(params = {}) {
104
105
  async function getPatientNotification(notificationId) {
105
106
  return this.readResource("CommunicationRequest", notificationId);
106
107
  }
108
+ async function getPatientDashboard(params = {}) {
109
+ var _a, _b, _c, _d, _e, _f;
110
+ const profile = await this.getCurrentPatientProfile();
111
+ const patient = `Patient/${profile.profile.id}`;
112
+ const latestObservations = await this.getPatientLatestObservations({
113
+ ...params.latestObservations,
114
+ patient: (_b = (_a = params.latestObservations) === null || _a === void 0 ? void 0 : _a.patient) !== null && _b !== void 0 ? _b : patient,
115
+ });
116
+ const [measurementHistory, carePlans] = await Promise.all([
117
+ this.listPatientMeasurementHistory({
118
+ ...params.measurementHistory,
119
+ patient: (_d = (_c = params.measurementHistory) === null || _c === void 0 ? void 0 : _c.patient) !== null && _d !== void 0 ? _d : patient,
120
+ }),
121
+ this.listPatientCarePlans({
122
+ ...params.carePlans,
123
+ patient: (_f = (_e = params.carePlans) === null || _e === void 0 ? void 0 : _e.patient) !== null && _f !== void 0 ? _f : patient,
124
+ }),
125
+ ]);
126
+ return { carePlans, latestObservations, measurementHistory, profile };
127
+ }
@@ -70,6 +70,11 @@ export type OvokPatientTelemetryHistoryQuery = {
70
70
  };
71
71
  export type OvokPatientSchedulingQuery = Record<string, string | number | boolean | undefined>;
72
72
  export type OvokPatientResourceQuery = OvokPatientSchedulingQuery;
73
+ export type OvokPatientDashboardQuery = {
74
+ latestObservations?: OvokPatientLatestObservationsQuery;
75
+ measurementHistory?: OvokPatientResourceQuery;
76
+ carePlans?: OvokPatientResourceQuery;
77
+ };
73
78
  export type OvokPatientAppointment = Appointment;
74
79
  export type OvokPatientSchedule = Schedule;
75
80
  export type OvokPatientSlot = Slot;
@@ -79,6 +84,12 @@ export type OvokPatientDevice = Device;
79
84
  export type OvokPatientNotification = CommunicationRequest;
80
85
  export type OvokPatientQuestionnaire = Questionnaire;
81
86
  export type OvokPatientQuestionnaireResponse = QuestionnaireResponse;
87
+ export type OvokPatientDashboard = {
88
+ profile: OvokCurrentPatientProfile;
89
+ latestObservations: OvokPatientObservationBundle;
90
+ measurementHistory: OvokPatientMeasurement[];
91
+ carePlans: OvokPatientCarePlan[];
92
+ };
82
93
  export type OvokPatientObservationBundle = Bundle<Observation>;
83
94
  export type OvokPatientTelemetryHistory = {
84
95
  deviceId: string;
@@ -48,7 +48,7 @@ exports.OVOK_CORE_REQUIREMENTS = {
48
48
  description: "The FHIR surface a server must provide for the @ovok/core client library's own methods to function.",
49
49
  software: {
50
50
  name: "@ovok/core",
51
- version: "0.3.15",
51
+ version: "0.3.17",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
@@ -9,4 +9,4 @@ export { observationsToMeasurements, useEcgRecording, useObservations, useSaveMe
9
9
  export { useAdministeredProjects, useCarehubDeviceTelemetry, useCarehubDevices, useCarehubNotifications, useCarehubPatients, useCapabilityStatement, useOvokMutation, useOvokRequest, useProjectFeatures, useProjectSettings, usePublicApiEndpoints, useRecentCarehubNotifications, } from "./api-hooks";
10
10
  export { useConsentSignature, useContent, useCreateVideoCallAppointment, useCreateVideoCallUserAccess, useDocuments, useVideoCallAccess, useVideoCallAppointments, } from "./experience-hooks";
11
11
  export { useI18nextDocument, useLocales, useLocalizations, useUpdateI18nextDocument, useUpdateLocalization, } from "./platform-hooks";
12
- export { useCurrentPatientProfile, useExtractQuestionnaireObservations, usePatientAppointments, usePatientCarePlans, usePatientDevices, usePatientDeviceTelemetry, usePatientDeviceTelemetryHistory, usePatientMeasurement, usePatientMeasurementHistory, usePatientLatestObservations, usePatientNotifications, usePatientQuestionnaireResponses, usePatientQuestionnaires, usePatientSchedules, usePatientSlots, usePopulateQuestionnaire, useSubmitQuestionnaireResponse, } from "./patient-hooks";
12
+ export { useCurrentPatientProfile, useExtractQuestionnaireObservations, usePatientAppointments, usePatientCarePlans, usePatientDevices, usePatientDeviceTelemetry, usePatientDeviceTelemetryHistory, usePatientMeasurement, usePatientMeasurementHistory, usePatientDashboard, usePatientLatestObservations, usePatientNotifications, usePatientQuestionnaireResponses, usePatientQuestionnaires, usePatientSchedules, usePatientSlots, usePopulateQuestionnaire, useSubmitQuestionnaireResponse, } from "./patient-hooks";
@@ -35,7 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.usePatientMeasurement = exports.usePatientDeviceTelemetryHistory = exports.usePatientDeviceTelemetry = exports.usePatientDevices = exports.usePatientCarePlans = exports.usePatientAppointments = exports.useExtractQuestionnaireObservations = exports.useCurrentPatientProfile = exports.useUpdateLocalization = exports.useUpdateI18nextDocument = exports.useLocalizations = exports.useLocales = exports.useI18nextDocument = exports.useVideoCallAppointments = exports.useVideoCallAccess = exports.useDocuments = exports.useCreateVideoCallUserAccess = exports.useCreateVideoCallAppointment = exports.useContent = exports.useConsentSignature = exports.useRecentCarehubNotifications = exports.usePublicApiEndpoints = exports.useProjectSettings = exports.useProjectFeatures = exports.useOvokRequest = exports.useOvokMutation = exports.useCapabilityStatement = exports.useCarehubPatients = exports.useCarehubNotifications = exports.useCarehubDevices = exports.useCarehubDeviceTelemetry = exports.useAdministeredProjects = exports.useUrineTests = exports.useSaveMeasurement = exports.useObservations = exports.useEcgRecording = exports.observationsToMeasurements = exports.OvokProvider = exports.useClient = exports.useCachedBinaryUrl = exports.reactContext = exports.useSubscription = exports.useSearchResources = exports.useSearchOne = exports.useSearch = exports.useResource = exports.usePrevious = exports.useMedplumProfile = exports.useMedplumNavigate = exports.useMedplumContext = void 0;
38
- exports.useSubmitQuestionnaireResponse = exports.usePopulateQuestionnaire = exports.usePatientSlots = exports.usePatientSchedules = exports.usePatientQuestionnaires = exports.usePatientQuestionnaireResponses = exports.usePatientNotifications = exports.usePatientLatestObservations = exports.usePatientMeasurementHistory = void 0;
38
+ exports.useSubmitQuestionnaireResponse = exports.usePopulateQuestionnaire = exports.usePatientSlots = exports.usePatientSchedules = exports.usePatientQuestionnaires = exports.usePatientQuestionnaireResponses = exports.usePatientNotifications = exports.usePatientLatestObservations = exports.usePatientDashboard = exports.usePatientMeasurementHistory = void 0;
39
39
  /*
40
40
  * The only React-dependent module in the package.
41
41
  *
@@ -114,6 +114,7 @@ Object.defineProperty(exports, "usePatientDeviceTelemetry", { enumerable: true,
114
114
  Object.defineProperty(exports, "usePatientDeviceTelemetryHistory", { enumerable: true, get: function () { return patient_hooks_1.usePatientDeviceTelemetryHistory; } });
115
115
  Object.defineProperty(exports, "usePatientMeasurement", { enumerable: true, get: function () { return patient_hooks_1.usePatientMeasurement; } });
116
116
  Object.defineProperty(exports, "usePatientMeasurementHistory", { enumerable: true, get: function () { return patient_hooks_1.usePatientMeasurementHistory; } });
117
+ Object.defineProperty(exports, "usePatientDashboard", { enumerable: true, get: function () { return patient_hooks_1.usePatientDashboard; } });
117
118
  Object.defineProperty(exports, "usePatientLatestObservations", { enumerable: true, get: function () { return patient_hooks_1.usePatientLatestObservations; } });
118
119
  Object.defineProperty(exports, "usePatientNotifications", { enumerable: true, get: function () { return patient_hooks_1.usePatientNotifications; } });
119
120
  Object.defineProperty(exports, "usePatientQuestionnaireResponses", { enumerable: true, get: function () { return patient_hooks_1.usePatientQuestionnaireResponses; } });
@@ -1,4 +1,5 @@
1
1
  import { Observation } from "@medplum/fhirtypes";
2
+ import type { ObservationsToMeasurementsOptions } from "../client/observation/methods/observationsToMeasurements";
2
3
  import { observationsToMeasurements, SupportedMeasurement } from "../client/observation/methods/observationsToMeasurements";
3
4
  import { GenerateObservationBodyParams } from "../client/observation/types/generate-observation-body/GenerateObservationBodyParams";
4
5
  import { OvokClient } from "../client/ovok-client";
@@ -12,10 +13,12 @@ export interface UseObservationsOptions<T extends readonly SupportedType[]> {
12
13
  types: T;
13
14
  period?: ObservationPeriod;
14
15
  patientId?: string;
16
+ groupBy?: ObservationsToMeasurementsOptions["groupBy"];
17
+ timeToleranceMs?: number;
15
18
  }
16
19
  /** Plain inverse mapping for apps that already fetched their Observation resources. */
17
20
  export { observationsToMeasurements };
18
- export declare const useObservations: <T extends readonly SupportedType[]>({ types, period, patientId, }: UseObservationsOptions<T>) => {
21
+ export declare const useObservations: <T extends readonly SupportedType[]>({ types, period, patientId, groupBy, timeToleranceMs, }: UseObservationsOptions<T>) => {
19
22
  measurements: Array<Extract<SupportedMeasurement, {
20
23
  measurementTypeKey: T[number];
21
24
  }>>;
@@ -41,7 +41,7 @@ const observationsToMeasurements_1 = require("../client/observation/methods/obse
41
41
  Object.defineProperty(exports, "observationsToMeasurements", { enumerable: true, get: function () { return observationsToMeasurements_1.observationsToMeasurements; } });
42
42
  const types_1 = require("../types");
43
43
  const index_1 = require("./index");
44
- const useObservations = ({ types, period, patientId, }) => {
44
+ const useObservations = ({ types, period, patientId, groupBy, timeToleranceMs, }) => {
45
45
  var _a, _b, _c;
46
46
  // A broad search is intentional: one measurement can expand into several
47
47
  // observations, and the inverse mapper performs the type-specific grouping.
@@ -60,7 +60,7 @@ const useObservations = ({ types, period, patientId, }) => {
60
60
  }), [codes, patientId, period === null || period === void 0 ? void 0 : period.end, period === null || period === void 0 ? void 0 : period.start]);
61
61
  const [resources, loading, outcome] = (0, react_hooks_1.useSearchResources)("Observation", query);
62
62
  const observations = resources !== null && resources !== void 0 ? resources : [];
63
- const measurements = React.useMemo(() => (0, observationsToMeasurements_1.observationsToMeasurements)(observations, types), [observations, types]);
63
+ const measurements = React.useMemo(() => (0, observationsToMeasurements_1.observationsToMeasurements)(observations, types, { groupBy, timeToleranceMs }), [groupBy, observations, timeToleranceMs, types]);
64
64
  return {
65
65
  measurements,
66
66
  loading,
@@ -1,8 +1,9 @@
1
1
  import { Bundle, Observation, Parameters, QuestionnaireResponse } from "@medplum/fhirtypes";
2
- import { OvokCurrentPatientProfile, OvokPatientAppointment, OvokPatientCarePlan, OvokPatientDevice, OvokPatientLatestObservationsQuery, OvokPatientMeasurement, OvokPatientNotification, OvokPatientObservationBundle, OvokPatientQuestionnaire, OvokPatientQuestionnaireResponse, OvokPatientResourceQuery, OvokPatientSchedule, OvokPatientSchedulingQuery, OvokPatientSlot, OvokPatientTelemetryHistory, OvokPatientTelemetryHistoryQuery } from "../client/patient/types";
2
+ import { OvokCurrentPatientProfile, OvokPatientAppointment, OvokPatientCarePlan, OvokPatientDashboard, OvokPatientDashboardQuery, OvokPatientDevice, OvokPatientLatestObservationsQuery, OvokPatientMeasurement, OvokPatientNotification, OvokPatientObservationBundle, OvokPatientQuestionnaire, OvokPatientQuestionnaireResponse, OvokPatientResourceQuery, OvokPatientSchedule, OvokPatientSchedulingQuery, OvokPatientSlot, OvokPatientTelemetryHistory, OvokPatientTelemetryHistoryQuery } from "../client/patient/types";
3
3
  import { OvokQuestionnaireOperationResponse } from "../client/questionnaire-response/types";
4
4
  import { OvokMutationState, OvokRequestState } from "./api-hooks";
5
5
  export declare const useCurrentPatientProfile: () => OvokRequestState<OvokCurrentPatientProfile>;
6
+ export declare const usePatientDashboard: (params?: OvokPatientDashboardQuery) => OvokRequestState<OvokPatientDashboard>;
6
7
  export declare const usePatientLatestObservations: (params?: OvokPatientLatestObservationsQuery) => OvokRequestState<OvokPatientObservationBundle>;
7
8
  export declare const usePatientDeviceTelemetry: (deviceId?: string) => OvokRequestState<OvokPatientObservationBundle>;
8
9
  export declare const usePatientDeviceTelemetryHistory: (deviceId: string | undefined, params?: OvokPatientTelemetryHistoryQuery) => OvokRequestState<OvokPatientTelemetryHistory>;
@@ -34,7 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.useExtractQuestionnaireObservations = exports.useSubmitQuestionnaireResponse = exports.usePopulateQuestionnaire = exports.usePatientNotifications = exports.usePatientDevices = exports.usePatientQuestionnaireResponses = exports.usePatientQuestionnaires = exports.usePatientCarePlans = exports.usePatientMeasurement = exports.usePatientMeasurementHistory = exports.usePatientSlots = exports.usePatientSchedules = exports.usePatientAppointments = exports.usePatientDeviceTelemetryHistory = exports.usePatientDeviceTelemetry = exports.usePatientLatestObservations = exports.useCurrentPatientProfile = void 0;
37
+ exports.useExtractQuestionnaireObservations = exports.useSubmitQuestionnaireResponse = exports.usePopulateQuestionnaire = exports.usePatientNotifications = exports.usePatientDevices = exports.usePatientQuestionnaireResponses = exports.usePatientQuestionnaires = exports.usePatientCarePlans = exports.usePatientMeasurement = exports.usePatientMeasurementHistory = exports.usePatientSlots = exports.usePatientSchedules = exports.usePatientAppointments = exports.usePatientDeviceTelemetryHistory = exports.usePatientDeviceTelemetry = exports.usePatientLatestObservations = exports.usePatientDashboard = exports.useCurrentPatientProfile = void 0;
38
38
  const React = __importStar(require("react"));
39
39
  const api_hooks_1 = require("./api-hooks");
40
40
  const index_1 = require("./index");
@@ -44,6 +44,13 @@ const useCurrentPatientProfile = () => {
44
44
  return (0, api_hooks_1.useOvokRequest)(load);
45
45
  };
46
46
  exports.useCurrentPatientProfile = useCurrentPatientProfile;
47
+ const usePatientDashboard = (params = {}) => {
48
+ const client = (0, index_1.useClient)();
49
+ const serialized = JSON.stringify(params);
50
+ const load = React.useCallback(() => client.getPatientDashboard(params), [client, serialized]);
51
+ return (0, api_hooks_1.useOvokRequest)(load);
52
+ };
53
+ exports.usePatientDashboard = usePatientDashboard;
47
54
  const usePatientLatestObservations = (params = {}) => {
48
55
  const client = (0, index_1.useClient)();
49
56
  const serialized = JSON.stringify(params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovok/core",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",