uhc-portal-components-react 0.1.28 → 0.1.30

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.30
4
+
5
+ - Pin the corrected shared clinical reader so IPS cards prefer each resource's
6
+ clinical event date over the enclosing Composition authoring timestamp.
7
+
8
+ ## 0.1.29
9
+
10
+ - Bind `ClinicalArtifactEditor` to the shared multichannel JSON Schema. The
11
+ component now creates the claims-first changes Bundle through the reusable
12
+ `BundleEditor` helper and can hydrate the same form from authoritative
13
+ `BundleReader` readback or a native FHIR import.
14
+ - Keep one stable draft identifier across retries, expose pending/retryable
15
+ synchronization without reporting local state as saved, and add the same
16
+ retry contract to encounter transfers.
17
+ - Derive form input types from that JSON Schema, so textual FHIR fields such
18
+ as a vaccine lot number are not misclassified by their claim name.
19
+
3
20
  ## 0.1.28
4
21
 
5
22
  - Add a reusable emergency-contact directory with provider-owned create,
package/README.md CHANGED
@@ -71,8 +71,15 @@ The package deliberately exposes modules instead of a cloned application:
71
71
  contacts and delegates create, update, delete and fresh provider readback to
72
72
  the host portal. Browser editing never grants contact access.
73
73
  - `ClinicalArtifactEditor`: renders the same schema-driven clinical form for
74
- human and animal subjects. The host supplies its UHC/Vet schema,
75
- terminology-search adapter and authorized persistence callback. Selecting a
74
+ human and animal subjects. For writes, the host supplies the shared
75
+ `MULTICHANNEL_OPERATION_SCHEMAS` contract and provenance context. The
76
+ component turns validated form values into the canonical claims-first batch
77
+ with `buildMultichannelOperationBundle`; it does not ask each portal to map
78
+ its own claims. Existing claims-first records can be reopened from an
79
+ authoritative Bundle with `BundleReader`, while native FHIR imports use the
80
+ shared FHIR converter before selecting the same schema-owned fields. The host
81
+ also supplies its IPS section descriptor, terminology-search adapter and
82
+ authorized persistence callback. Selecting a
76
83
  terminology candidate preserves source text and adds only the confirmed
77
84
  `system|code` and display. Success requires a fresh provider readback Bundle;
78
85
  local form state is never presented as a persisted write.
@@ -1,15 +1,26 @@
1
1
  import type { ClinicalResourceBundleLike } from 'gdc-common-utils-ts/utils/clinical-resource-view';
2
+ import { type MultichannelOperationSchema, type MultichannelOperationValues } from 'sos-data-utils-ts/multichannel-operation-schemas';
2
3
  export type ClinicalArtifactSchema = Readonly<{
3
4
  resourceType: string;
5
+ sectionCode?: string;
4
6
  requiredClaims: readonly string[];
5
7
  optionalClaims: readonly string[];
8
+ terminologyTextClaims?: readonly string[];
9
+ initialClaims?: Readonly<Record<string, string>>;
6
10
  }>;
7
11
  export type ClinicalArtifactEditorRequest = Readonly<{
8
12
  resourceType: string;
9
13
  sectionCode: string;
10
14
  mode: 'create' | 'update';
11
15
  artifactId?: string;
16
+ ifMatch?: string;
12
17
  initialClaims?: Readonly<Record<string, unknown>>;
18
+ /** Native FHIR R4 resource hydrated through the selected shared schema. */
19
+ initialResource?: Readonly<Record<string, unknown> & {
20
+ resourceType: string;
21
+ }>;
22
+ /** Authoritative claims-first provider Bundle hydrated through BundleReader. */
23
+ initialBundle?: Readonly<Record<string, unknown>>;
13
24
  }>;
14
25
  export type ClinicalArtifactDraft = Readonly<{
15
26
  resourceType: string;
@@ -17,6 +28,26 @@ export type ClinicalArtifactDraft = Readonly<{
17
28
  sectionCode: string;
18
29
  mode: 'create' | 'update';
19
30
  claims: Readonly<Record<string, string>>;
31
+ /** Complete schema-validated operation values when a multichannel schema is supplied. */
32
+ operationValues?: MultichannelOperationValues;
33
+ /** Claims-first batch produced by the shared BundleEditor. */
34
+ changesBundle?: ClinicalArtifactChangesBundle;
35
+ }>;
36
+ export type ClinicalArtifactChangesBundle = Readonly<{
37
+ resourceType: string;
38
+ type: string;
39
+ data?: readonly unknown[];
40
+ entry?: readonly unknown[];
41
+ }>;
42
+ export type ClinicalArtifactAuthoringContext = Readonly<{
43
+ subjectReference: string;
44
+ language: string;
45
+ createdAt: string;
46
+ dataAuthorReference: string;
47
+ attestationState: 'draft' | 'confirmed';
48
+ attesterReference?: string;
49
+ attesterMode?: string;
50
+ attestedAt?: string;
20
51
  }>;
21
52
  export type ClinicalTerminologyCandidate = Readonly<{
22
53
  system: string;
@@ -42,16 +73,32 @@ export type ClinicalArtifactEditorCopy = Readonly<{
42
73
  terminologyLoading: string;
43
74
  terminologyUnavailable: string;
44
75
  readbackMissing: string;
76
+ pending?: string;
77
+ retry?: string;
78
+ }>;
79
+ export type ClinicalArtifactSaveOutcome = Readonly<{
80
+ state: 'confirmed';
81
+ bundle: ClinicalResourceBundleLike;
82
+ } | {
83
+ state: 'pending' | 'retryable';
84
+ message?: string;
85
+ } | {
86
+ state: 'rejected';
87
+ message: string;
45
88
  }>;
46
89
  export type ClinicalArtifactEditorProps = Readonly<{
47
90
  request: ClinicalArtifactEditorRequest;
48
91
  schema: ClinicalArtifactSchema;
92
+ /** Shared JSON Schema 2020-12 contract used by portal, voice, chat and WhatsApp. */
93
+ operationSchema?: MultichannelOperationSchema;
94
+ /** Host-owned subject and provenance; never inferred from the transport sender. */
95
+ authoringContext?: ClinicalArtifactAuthoringContext;
49
96
  copy: ClinicalArtifactEditorCopy;
50
97
  locale?: string;
51
98
  taxonomyId?: string;
52
99
  fieldLabel: (claim: string) => string;
53
100
  searchTerminology?: (input: ClinicalTerminologySearch) => Promise<readonly ClinicalTerminologyCandidate[]>;
54
- saveArtifact: (draft: ClinicalArtifactDraft) => Promise<ClinicalResourceBundleLike | undefined>;
101
+ saveArtifact: (draft: ClinicalArtifactDraft) => Promise<ClinicalResourceBundleLike | ClinicalArtifactSaveOutcome | undefined>;
55
102
  onCancel: () => void;
56
103
  onReadback: (bundle: ClinicalResourceBundleLike) => void;
57
104
  }>;
@@ -60,5 +107,5 @@ export type ClinicalArtifactEditorProps = Readonly<{
60
107
  * persistence and terminology service access. A successful form submission is
61
108
  * published only after the host returns a fresh provider readback Bundle.
62
109
  */
63
- export declare function ClinicalArtifactEditor({ request, schema, copy, locale, taxonomyId, fieldLabel, searchTerminology, saveArtifact, onCancel, onReadback, }: ClinicalArtifactEditorProps): import("react").JSX.Element;
110
+ export declare function ClinicalArtifactEditor({ request, schema, operationSchema, authoringContext, copy, locale, taxonomyId, fieldLabel, searchTerminology, saveArtifact, onCancel, onReadback, }: ClinicalArtifactEditorProps): import("react").JSX.Element;
64
111
  //# sourceMappingURL=ClinicalArtifactEditor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ClinicalArtifactEditor.d.ts","sourceRoot":"","sources":["../src/ClinicalArtifactEditor.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kDAAkD,CAAA;AAElG,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC,CAAC,CAAA;AAEF,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACnD,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAClD,CAAC,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,YAAY,EAAE,MAAM,CAAA;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAA;IACzB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACzC,CAAC,CAAA;AAEF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAC,CAAA;AAEF,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd,CAAC,CAAA;AAEF,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,kBAAkB,EAAE,MAAM,CAAA;IAC1B,sBAAsB,EAAE,MAAM,CAAA;IAC9B,eAAe,EAAE,MAAM,CAAA;CACxB,CAAC,CAAA;AAEF,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IACjD,OAAO,EAAE,6BAA6B,CAAA;IACtC,MAAM,EAAE,sBAAsB,CAAA;IAC9B,IAAI,EAAE,0BAA0B,CAAA;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACrC,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,OAAO,CAAC,SAAS,4BAA4B,EAAE,CAAC,CAAA;IAC1G,YAAY,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC,CAAA;IAC/F,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,UAAU,EAAE,CAAC,MAAM,EAAE,0BAA0B,KAAK,IAAI,CAAA;CACzD,CAAC,CAAA;AAEF;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAa,EACb,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,UAAU,GACX,EAAE,2BAA2B,+BA2G7B"}
1
+ {"version":3,"file":"ClinicalArtifactEditor.d.ts","sourceRoot":"","sources":["../src/ClinicalArtifactEditor.tsx"],"names":[],"mappings":"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"}
@@ -1,29 +1,60 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useMemo, useState } from 'react';
3
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
4
+ import { clinicalArtifactEditorClaimsFromFhirR4, clinicalArtifactFhirR4ToFlat, } from 'fhir-data-utils-ts';
5
+ import { buildMultichannelOperationBundle, MultichannelOperationMode, readMultichannelOperationFromBundle, selectMultichannelOperationClaimValues, } from 'sos-data-utils-ts/multichannel-operation-schemas';
4
6
  /**
5
7
  * Shared human/animal clinical editor. The host owns authorization, transport,
6
8
  * persistence and terminology service access. A successful form submission is
7
9
  * published only after the host returns a fresh provider readback Bundle.
8
10
  */
9
- export function ClinicalArtifactEditor({ request, schema, copy, locale = 'en', taxonomyId, fieldLabel, searchTerminology, saveArtifact, onCancel, onReadback, }) {
11
+ export function ClinicalArtifactEditor({ request, schema, operationSchema, authoringContext, copy, locale = 'en', taxonomyId, fieldLabel, searchTerminology, saveArtifact, onCancel, onReadback, }) {
10
12
  const titleId = useId();
11
13
  const descriptionId = useId();
12
- const acceptedClaims = useMemo(() => [...schema.requiredClaims, ...schema.optionalClaims], [schema.optionalClaims, schema.requiredClaims]);
14
+ const acceptedClaims = useMemo(() => operationSchema
15
+ ? Object.keys(operationSchema.properties.claims?.properties ?? {})
16
+ : [...schema.requiredClaims, ...schema.optionalClaims], [operationSchema, schema.optionalClaims, schema.requiredClaims]);
13
17
  const fields = useMemo(() => acceptedClaims
14
18
  .filter((claim) => !claim.endsWith('.section') && !claim.endsWith('.user-selected'))
15
19
  .filter((claim) => !isHiddenCodingCompanion(request.resourceType, claim, acceptedClaims)), [acceptedClaims, request.resourceType]);
16
- const requiredClaims = useMemo(() => new Set(schema.requiredClaims), [schema.requiredClaims]);
17
- const [claims, setClaims] = useState(() => ({
18
- ...defaultClaims(request.resourceType),
19
- ...Object.fromEntries(Object.entries(request.initialClaims ?? {}).map(([claim, value]) => [claim, String(value ?? '')])),
20
- }));
20
+ const requiredClaims = useMemo(() => new Set(operationSchema?.properties.claims?.required ?? schema.requiredClaims), [operationSchema, schema.requiredClaims]);
21
+ const [claims, setClaims] = useState(() => {
22
+ const operationId = operationSchema?.['x-operation-id'];
23
+ const initialBundleClaims = request.initialBundle && operationId
24
+ ? readMultichannelOperationFromBundle(operationId, request.initialBundle, {
25
+ operation: request.mode === 'update' ? MultichannelOperationMode.Update : MultichannelOperationMode.Create,
26
+ resourceId: request.artifactId,
27
+ ifMatch: request.ifMatch,
28
+ }).claims
29
+ : {};
30
+ const initialResourceClaims = request.initialResource
31
+ ? operationId
32
+ ? selectMultichannelOperationClaimValues(operationId, clinicalArtifactFhirR4ToFlat(request.initialResource))
33
+ : clinicalArtifactEditorClaimsFromFhirR4(request.initialResource, {
34
+ resourceType: request.resourceType,
35
+ sectionCode: schema.sectionCode ?? request.sectionCode,
36
+ requiredClaims: schema.requiredClaims,
37
+ optionalClaims: schema.optionalClaims,
38
+ terminologyTextClaims: schema.terminologyTextClaims ?? [],
39
+ initialClaims: schema.initialClaims ?? defaultClaims(request.resourceType),
40
+ })
41
+ : {};
42
+ return {
43
+ ...(operationSchema ? jsonSchemaDefaultClaims(operationSchema) : defaultClaims(request.resourceType)),
44
+ ...schema.initialClaims,
45
+ ...stringClaims(initialBundleClaims),
46
+ ...stringClaims(initialResourceClaims),
47
+ ...Object.fromEntries(Object.entries(request.initialClaims ?? {}).map(([claim, value]) => [claim, String(value ?? '')])),
48
+ };
49
+ });
21
50
  const [activeTerminologyClaim, setActiveTerminologyClaim] = useState();
22
51
  const [candidates, setCandidates] = useState([]);
23
52
  const [terminologyBusy, setTerminologyBusy] = useState(false);
24
53
  const [terminologyError, setTerminologyError] = useState(false);
25
54
  const [saving, setSaving] = useState(false);
26
55
  const [error, setError] = useState('');
56
+ const [pending, setPending] = useState('');
57
+ const draftId = useRef(request.artifactId ?? crypto.randomUUID());
27
58
  useEffect(() => {
28
59
  if (!activeTerminologyClaim || !searchTerminology)
29
60
  return;
@@ -63,16 +94,29 @@ export function ClinicalArtifactEditor({ request, schema, copy, locale = 'en', t
63
94
  setSaving(true);
64
95
  setError('');
65
96
  try {
66
- const readback = await saveArtifact({
97
+ const result = await saveArtifact({
67
98
  resourceType: request.resourceType,
68
- id: request.artifactId ?? crypto.randomUUID(),
99
+ id: draftId.current,
69
100
  sectionCode: request.sectionCode,
70
101
  mode: request.mode,
71
102
  claims,
103
+ ...(operationSchema ? buildOperationDraft(operationSchema, authoringContext, request, draftId.current, claims) : {}),
72
104
  });
73
- if (!readback)
105
+ if (!result)
74
106
  throw new Error(copy.readbackMissing);
75
- onReadback(readback);
107
+ if (isSaveOutcome(result)) {
108
+ if (result.state === 'confirmed') {
109
+ setPending('');
110
+ onReadback(result.bundle);
111
+ return;
112
+ }
113
+ if (result.state === 'rejected')
114
+ throw new Error(result.message);
115
+ setPending(result.message || copy.pending || copy.readbackMissing);
116
+ return;
117
+ }
118
+ setPending('');
119
+ onReadback(result);
76
120
  }
77
121
  catch (reason) {
78
122
  setError(reason instanceof Error ? reason.message : String(reason));
@@ -93,18 +137,54 @@ export function ClinicalArtifactEditor({ request, schema, copy, locale = 'en', t
93
137
  setActiveTerminologyClaim(claim);
94
138
  };
95
139
  return _jsxs("label", { className: isMultiline(parameter) ? 'uhcp-clinical-editor-wide' : undefined, htmlFor: inputId, children: [_jsxs("span", { children: [label, requiredClaims.has(claim) ? ' *' : ''] }), statusOptions(request.resourceType, parameter) ? _jsx("select", { id: inputId, "aria-label": label, value: value, required: requiredClaims.has(claim), onChange: update, children: statusOptions(request.resourceType, parameter).map((option) => _jsx("option", { value: option, children: humanize(option) }, option)) }) : isMultiline(parameter) ? _jsx("textarea", { id: inputId, "aria-label": label, value: value, required: requiredClaims.has(claim), rows: 3, onChange: update })
96
- : _jsx("input", { id: inputId, "aria-label": label, value: value, required: requiredClaims.has(claim), type: fieldInputType(parameter), onChange: update }), terminologyEnabled && activeTerminologyClaim === claim ? _jsxs("div", { className: "uhcp-terminology-results", children: [terminologyBusy ? _jsx("small", { role: "status", children: copy.terminologyLoading }) : null, terminologyError ? _jsx("small", { role: "alert", children: copy.terminologyUnavailable }) : null, candidates.length ? _jsx("ul", { children: candidates.map((candidate) => _jsx("li", { children: _jsxs("button", { type: "button", onClick: () => {
140
+ : _jsx("input", { id: inputId, "aria-label": label, value: value, required: requiredClaims.has(claim), type: fieldInputType(parameter, operationSchema?.properties.claims?.properties?.[claim]), onChange: update }), terminologyEnabled && activeTerminologyClaim === claim ? _jsxs("div", { className: "uhcp-terminology-results", children: [terminologyBusy ? _jsx("small", { role: "status", children: copy.terminologyLoading }) : null, terminologyError ? _jsx("small", { role: "alert", children: copy.terminologyUnavailable }) : null, candidates.length ? _jsx("ul", { children: candidates.map((candidate) => _jsx("li", { children: _jsxs("button", { type: "button", onClick: () => {
97
141
  setClaims((current) => selectedCodingClaims(request.resourceType, claim, candidate, acceptedClaims, current));
98
142
  setCandidates([]);
99
143
  setActiveTerminologyClaim(undefined);
100
144
  }, children: [candidate.display, _jsxs("small", { children: [candidate.system, " \u00B7 ", candidate.code] })] }) }, `${candidate.system}|${candidate.code}`)) }) : null] }) : null] }, claim);
101
- }), error ? _jsx("p", { className: "uhcp-clinical-editor-error", role: "alert", children: error }) : null, _jsxs("footer", { children: [_jsx("button", { type: "button", onClick: onCancel, children: copy.cancel }), _jsx("button", { type: "submit", disabled: saving, children: saving ? copy.saving : copy.save })] })] })] });
145
+ }), error ? _jsx("p", { className: "uhcp-clinical-editor-error", role: "alert", children: error }) : null, pending ? _jsx("p", { className: "uhcp-clinical-editor-pending", role: "status", children: copy.pending || pending }) : null, _jsxs("footer", { children: [_jsx("button", { type: "button", onClick: onCancel, children: copy.cancel }), _jsx("button", { type: "submit", disabled: saving, children: saving ? copy.saving : pending ? (copy.retry || copy.save) : copy.save })] })] })] });
146
+ }
147
+ function isSaveOutcome(value) {
148
+ return typeof value === 'object' && value !== null && 'state' in value;
149
+ }
150
+ function buildOperationDraft(operationSchema, authoringContext, request, resourceId, claims) {
151
+ if (!authoringContext)
152
+ throw new TypeError('clinical_artifact_authoring_context_required');
153
+ const operationId = operationSchema['x-operation-id'];
154
+ const operation = request.mode === 'update' ? MultichannelOperationMode.Update : MultichannelOperationMode.Create;
155
+ const operationValues = {
156
+ operation,
157
+ resourceId,
158
+ ...authoringContext,
159
+ ...(request.ifMatch ? { ifMatch: request.ifMatch } : {}),
160
+ claims: coerceClaimsFromJsonSchema(operationSchema, claims),
161
+ };
162
+ return Object.freeze({
163
+ operationValues,
164
+ changesBundle: buildMultichannelOperationBundle(operationId, operationValues),
165
+ });
166
+ }
167
+ function coerceClaimsFromJsonSchema(schema, claims) {
168
+ const properties = schema.properties.claims?.properties ?? {};
169
+ return Object.freeze(Object.fromEntries(Object.entries(claims)
170
+ .filter(([claim, value]) => properties[claim] && value !== '')
171
+ .map(([claim, value]) => [claim, properties[claim]?.type === 'number' ? Number(value) : value])));
172
+ }
173
+ function stringClaims(claims) {
174
+ return Object.fromEntries(Object.entries(claims).map(([claim, value]) => [claim, String(value ?? '')]));
175
+ }
176
+ function jsonSchemaDefaultClaims(schema) {
177
+ return Object.fromEntries(Object.entries(schema.properties.claims?.properties ?? {})
178
+ .filter(([, node]) => node.default !== undefined)
179
+ .map(([claim, node]) => [claim, String(node.default)]));
102
180
  }
103
181
  function defaultClaims(resourceType) {
104
182
  if (resourceType === 'Immunization')
105
183
  return { 'Immunization.status': 'completed' };
106
- if (resourceType === 'Observation' || resourceType === 'DiagnosticReport' || resourceType === 'Procedure')
184
+ if (resourceType === 'Observation' || resourceType === 'DiagnosticReport')
107
185
  return { [`${resourceType}.status`]: 'final' };
186
+ if (resourceType === 'Procedure')
187
+ return { [`${resourceType}.status`]: 'completed' };
108
188
  if (resourceType === 'Condition' || resourceType === 'AllergyIntolerance')
109
189
  return { [`${resourceType}.clinical-status`]: 'active' };
110
190
  return {};
@@ -148,7 +228,9 @@ function selectedCodingClaims(resourceType, inputClaim, candidate, acceptedClaim
148
228
  }
149
229
  function isMultiline(parameter) { return /note|description|summary|instruction|outcome|reference-range-text/.test(parameter); }
150
230
  function humanize(value) { return value ? value[0].toUpperCase() + value.slice(1).replaceAll('_', ' ') : value; }
151
- function fieldInputType(parameter) {
231
+ function fieldInputType(parameter, schemaNode) {
232
+ if (schemaNode)
233
+ return schemaNode.type === 'number' || schemaNode.type === 'integer' ? 'number' : 'text';
152
234
  if (/(?:^|-)number$|frequency$|dose-sequence$/.test(parameter))
153
235
  return 'number';
154
236
  // FHIR date/search fields may carry a partial date or a complete dateTime
@@ -60,6 +60,13 @@ export type EncounterTransferConsoleCopy = Readonly<{
60
60
  attachmentReady: string;
61
61
  technicalDetails: string;
62
62
  encounterIdentifier: string;
63
+ transferPending: string;
64
+ retryTransfer: string;
65
+ transferRejected: string;
66
+ }>;
67
+ export type EncounterTransferApplyOutcome = Readonly<{
68
+ state: 'confirmed' | 'pending' | 'retryable' | 'rejected';
69
+ message?: string;
63
70
  }>;
64
71
  export type EncounterTransferConsoleProps = Readonly<{
65
72
  copy: EncounterTransferConsoleCopy;
@@ -69,7 +76,7 @@ export type EncounterTransferConsoleProps = Readonly<{
69
76
  destinationEncounterId: string;
70
77
  contacts: readonly EmergencyContactAction[];
71
78
  demo?: boolean;
72
- onApplyTransfer?: (sourceOutcome: EncounterSourceOutcome) => void;
79
+ onApplyTransfer?: (sourceOutcome: EncounterSourceOutcome) => void | EncounterTransferApplyOutcome | Promise<void | EncounterTransferApplyOutcome>;
73
80
  }>;
74
81
  /**
75
82
  * Shared visual editor for one already-authorized subject transfer.
@@ -1 +1 @@
1
- {"version":3,"file":"EncounterTransferConsole.d.ts","sourceRoot":"","sources":["../src/EncounterTransferConsole.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"EncounterTransferConsole.d.ts","sourceRoot":"","sources":["../src/EncounterTransferConsole.tsx"],"names":[],"mappings":"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"}
@@ -27,6 +27,7 @@ export function EncounterTransferConsole({ copy, patientDisplay, encounters, sou
27
27
  const [attachments, setAttachments] = useState([]);
28
28
  const [showTechnical, setShowTechnical] = useState(false);
29
29
  const [status, setStatus] = useState('');
30
+ const [transferSync, setTransferSync] = useState('idle');
30
31
  const railRef = useRef(null);
31
32
  const source = useMemo(() => encounters.find((item) => item.identifier === sourceEncounterId), [encounters, sourceEncounterId]);
32
33
  const destination = useMemo(() => encounters.find((item) => item.identifier === destinationEncounterId), [encounters, destinationEncounterId]);
@@ -37,11 +38,29 @@ export function EncounterTransferConsole({ copy, patientDisplay, encounters, sou
37
38
  setAttachments(current => current.includes(kind) ? current.filter(item => item !== kind) : [...current, kind]);
38
39
  setStatus(copy.attachmentReady);
39
40
  }
40
- function applyTransfer() {
41
- onApplyTransfer?.(sourceOutcome);
42
- setStatus(copy.transferReady);
41
+ async function applyTransfer() {
42
+ setTransferSync('submitting');
43
+ try {
44
+ const outcome = await onApplyTransfer?.(sourceOutcome);
45
+ if (outcome?.state === 'pending' || outcome?.state === 'retryable') {
46
+ setTransferSync('retryable');
47
+ setStatus(outcome.message || copy.transferPending);
48
+ return;
49
+ }
50
+ if (outcome?.state === 'rejected') {
51
+ setTransferSync('idle');
52
+ setStatus(outcome.message || copy.transferRejected);
53
+ return;
54
+ }
55
+ setTransferSync('idle');
56
+ setStatus(copy.transferReady);
57
+ }
58
+ catch (reason) {
59
+ setTransferSync('retryable');
60
+ setStatus(reason instanceof Error && reason.message ? reason.message : copy.transferPending);
61
+ }
43
62
  }
44
- return _jsxs("section", { className: "uhcp-transfer-console", children: [_jsx("aside", { className: "uhcp-transfer-contacts", children: _jsxs("details", { open: true, children: [_jsx("summary", { children: copy.emergencyActions }), _jsx("div", { className: "uhcp-transfer-contact-list", role: "list", children: contacts.map((item) => _jsxs("button", { type: "button", role: "listitem", "aria-pressed": item.identifier === contact?.identifier, onClick: () => setSelectedContact(item.identifier), children: [_jsx("span", { "aria-hidden": "true", children: item.name.split(/\s+/).map(part => part[0]).slice(0, 2).join('') }), _jsx("strong", { children: item.name }), _jsxs("small", { children: [item.relationship, item.phone ? ` · ${item.phone}` : ''] })] }, item.identifier)) }), _jsxs("div", { className: "uhcp-transfer-contact-actions", children: [_jsxs("button", { type: "button", onClick: () => setStatus(copy.callReady), children: [_jsx(Icon, { name: "phone" }), copy.call] }), _jsxs("button", { type: "button", onClick: () => setStatus(copy.videoReady), children: [_jsx(Icon, { name: "video" }), copy.videoCall] }), _jsxs("button", { type: "button", "aria-pressed": messageOpen, onClick: () => setMessageOpen(value => !value), children: [_jsx(Icon, { name: "message" }), copy.sendMessage] })] }), messageOpen && contact ? _jsxs("form", { className: "uhcp-transfer-composer", onSubmit: (event) => { event.preventDefault(); setStatus(copy.messageReady); }, children: [_jsxs("h3", { children: [copy.sendMessage, ": ", contact.name] }), _jsxs("div", { className: "uhcp-transfer-mode", role: "group", "aria-label": copy.sendMessage, children: [_jsx("button", { type: "button", "aria-pressed": messageMode === 'text', onClick: () => setMessageMode('text'), children: copy.text }), _jsx("button", { type: "button", "aria-pressed": messageMode === 'audio', onClick: () => setMessageMode('audio'), children: copy.audio })] }), messageMode === 'text' ? _jsxs("label", { children: [copy.messageLabel, _jsx("textarea", { rows: 5 })] }) : _jsxs("button", { className: "uhcp-transfer-record", type: "button", children: ["\u25CF ", copy.audio] }), _jsxs("fieldset", { children: [_jsx("legend", { children: copy.attach }), _jsxs("div", { children: [_jsx("button", { type: "button", "aria-pressed": attachments.includes('pdf'), onClick: () => toggleAttachment('pdf'), children: copy.pdf }), _jsx("button", { type: "button", "aria-pressed": attachments.includes('fhir'), onClick: () => toggleAttachment('fhir'), children: copy.fhirBundle }), _jsx("button", { type: "button", "aria-pressed": attachments.includes('calendar'), onClick: () => toggleAttachment('calendar'), children: copy.calendar })] })] }), _jsx("button", { className: "uhcp-transfer-primary", type: "submit", children: copy.send })] }) : null] }) }), _jsxs("div", { className: "uhcp-transfer-main", children: [_jsxs("header", { className: "uhcp-transfer-patient", children: [_jsx("h2", { children: copy.patientSearchTitle }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setStatus(copy.patientReady), children: [_jsx(Icon, { name: "scan" }), copy.scanQr] }), _jsx("span", { "aria-hidden": "true" }), _jsxs("label", { children: [copy.patientId, _jsx("input", { defaultValue: patientDisplay })] }), _jsx("button", { type: "button", onClick: () => setStatus(copy.patientReady), children: copy.search })] })] }), _jsxs("section", { className: "uhcp-transfer-journey", role: "region", "aria-label": copy.journeyTitle, "aria-describedby": helpId, children: [_jsxs("header", { children: [_jsx("h2", { children: copy.journeyTitle }), _jsxs("div", { children: [_jsx("button", { type: "button", "aria-label": "Anterior", onClick: () => railRef.current?.scrollBy({ left: -260, behavior: 'smooth' }), children: _jsx(Icon, { name: "chevron" }) }), _jsx("button", { type: "button", "aria-label": "Siguiente", onClick: () => railRef.current?.scrollBy({ left: 260, behavior: 'smooth' }), children: _jsx(Icon, { name: "chevron" }) })] })] }), _jsxs("div", { className: "uhcp-transfer-rail", ref: railRef, children: [history.map((item) => _jsx(EncounterCard, { encounter: item }, item.identifier)), source ? _jsxs("article", { className: "uhcp-transfer-card uhcp-transfer-source", children: [_jsx("h3", { children: copy.sourceTitle }), _jsx(EncounterCardBody, { encounter: source, identifierLabel: copy.encounterIdentifier }), _jsxs("fieldset", { children: [_jsx("legend", { children: copy.helpTitle }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "source-outcome", checked: sourceOutcome === 'on-hold', onChange: () => setSourceOutcome('on-hold') }), _jsxs("span", { children: [_jsx("strong", { children: copy.returning }), _jsx("small", { children: copy.returningHint })] })] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "source-outcome", checked: sourceOutcome === 'completed', onChange: () => setSourceOutcome('completed') }), _jsxs("span", { children: [_jsx("strong", { children: copy.notReturning }), _jsx("small", { children: copy.notReturningHint })] })] })] })] }) : null, _jsxs("div", { className: "uhcp-transfer-arrow", children: [_jsx("span", { children: copy.transferTo }), _jsx("i", {}), _jsx(Icon, { name: "chevron" })] }), destination ? _jsxs("article", { className: "uhcp-transfer-card uhcp-transfer-destination", children: [_jsx("h3", { children: copy.destinationTitle }), _jsx(EncounterCardBody, { encounter: { ...destination, status: 'in-progress' }, identifierLabel: copy.encounterIdentifier })] }) : null] }), _jsxs("details", { className: "uhcp-transfer-help", open: true, children: [_jsx("summary", { id: helpId, children: copy.helpTitle }), _jsx("p", { children: copy.helpBody })] }), _jsxs("div", { className: "uhcp-transfer-controls", children: [_jsx("button", { type: "button", onClick: () => { setShowTechnical(true); setStatus(copy.reviewReady); }, children: copy.reviewChanges }), _jsx("button", { className: "uhcp-transfer-primary", type: "button", onClick: applyTransfer, children: copy.applyTransfer })] })] }), _jsxs("section", { className: "uhcp-transfer-review", children: [_jsxs("header", { children: [_jsx("h2", { children: copy.resourceSummary }), demo ? _jsx("p", { children: copy.demoWarning }) : null] }), _jsx("ol", { "aria-label": copy.resourceSummary, children: [[copy.sourceUpdate, 'Encounter'], [copy.sourceHistory, 'EncounterHistory'], [copy.destinationUpdate, 'Encounter'], [copy.destinationHistory, 'EncounterHistory'], [copy.indexProjection, 'Communication']].map(([label, resource], index) => _jsxs("li", { children: [_jsx("span", { children: index + 1 }), _jsx("strong", { children: label }), _jsxs("details", { open: showTechnical, children: [_jsx("summary", { children: copy.technicalDetails }), _jsx("code", { children: resource })] })] }, `${resource}-${index}`)) })] }), status ? _jsx("p", { className: "uhcp-transfer-status", role: "status", children: status }) : null] })] });
63
+ return _jsxs("section", { className: "uhcp-transfer-console", children: [_jsx("aside", { className: "uhcp-transfer-contacts", children: _jsxs("details", { open: true, children: [_jsx("summary", { children: copy.emergencyActions }), _jsx("div", { className: "uhcp-transfer-contact-list", role: "list", children: contacts.map((item) => _jsxs("button", { type: "button", role: "listitem", "aria-pressed": item.identifier === contact?.identifier, onClick: () => setSelectedContact(item.identifier), children: [_jsx("span", { "aria-hidden": "true", children: item.name.split(/\s+/).map(part => part[0]).slice(0, 2).join('') }), _jsx("strong", { children: item.name }), _jsxs("small", { children: [item.relationship, item.phone ? ` · ${item.phone}` : ''] })] }, item.identifier)) }), _jsxs("div", { className: "uhcp-transfer-contact-actions", children: [_jsxs("button", { type: "button", onClick: () => setStatus(copy.callReady), children: [_jsx(Icon, { name: "phone" }), copy.call] }), _jsxs("button", { type: "button", onClick: () => setStatus(copy.videoReady), children: [_jsx(Icon, { name: "video" }), copy.videoCall] }), _jsxs("button", { type: "button", "aria-pressed": messageOpen, onClick: () => setMessageOpen(value => !value), children: [_jsx(Icon, { name: "message" }), copy.sendMessage] })] }), messageOpen && contact ? _jsxs("form", { className: "uhcp-transfer-composer", onSubmit: (event) => { event.preventDefault(); setStatus(copy.messageReady); }, children: [_jsxs("h3", { children: [copy.sendMessage, ": ", contact.name] }), _jsxs("div", { className: "uhcp-transfer-mode", role: "group", "aria-label": copy.sendMessage, children: [_jsx("button", { type: "button", "aria-pressed": messageMode === 'text', onClick: () => setMessageMode('text'), children: copy.text }), _jsx("button", { type: "button", "aria-pressed": messageMode === 'audio', onClick: () => setMessageMode('audio'), children: copy.audio })] }), messageMode === 'text' ? _jsxs("label", { children: [copy.messageLabel, _jsx("textarea", { rows: 5 })] }) : _jsxs("button", { className: "uhcp-transfer-record", type: "button", children: ["\u25CF ", copy.audio] }), _jsxs("fieldset", { children: [_jsx("legend", { children: copy.attach }), _jsxs("div", { children: [_jsx("button", { type: "button", "aria-pressed": attachments.includes('pdf'), onClick: () => toggleAttachment('pdf'), children: copy.pdf }), _jsx("button", { type: "button", "aria-pressed": attachments.includes('fhir'), onClick: () => toggleAttachment('fhir'), children: copy.fhirBundle }), _jsx("button", { type: "button", "aria-pressed": attachments.includes('calendar'), onClick: () => toggleAttachment('calendar'), children: copy.calendar })] })] }), _jsx("button", { className: "uhcp-transfer-primary", type: "submit", children: copy.send })] }) : null] }) }), _jsxs("div", { className: "uhcp-transfer-main", children: [_jsxs("header", { className: "uhcp-transfer-patient", children: [_jsx("h2", { children: copy.patientSearchTitle }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setStatus(copy.patientReady), children: [_jsx(Icon, { name: "scan" }), copy.scanQr] }), _jsx("span", { "aria-hidden": "true" }), _jsxs("label", { children: [copy.patientId, _jsx("input", { defaultValue: patientDisplay })] }), _jsx("button", { type: "button", onClick: () => setStatus(copy.patientReady), children: copy.search })] })] }), _jsxs("section", { className: "uhcp-transfer-journey", role: "region", "aria-label": copy.journeyTitle, "aria-describedby": helpId, children: [_jsxs("header", { children: [_jsx("h2", { children: copy.journeyTitle }), _jsxs("div", { children: [_jsx("button", { type: "button", "aria-label": "Anterior", onClick: () => railRef.current?.scrollBy({ left: -260, behavior: 'smooth' }), children: _jsx(Icon, { name: "chevron" }) }), _jsx("button", { type: "button", "aria-label": "Siguiente", onClick: () => railRef.current?.scrollBy({ left: 260, behavior: 'smooth' }), children: _jsx(Icon, { name: "chevron" }) })] })] }), _jsxs("div", { className: "uhcp-transfer-rail", ref: railRef, children: [history.map((item) => _jsx(EncounterCard, { encounter: item }, item.identifier)), source ? _jsxs("article", { className: "uhcp-transfer-card uhcp-transfer-source", children: [_jsx("h3", { children: copy.sourceTitle }), _jsx(EncounterCardBody, { encounter: source, identifierLabel: copy.encounterIdentifier }), _jsxs("fieldset", { children: [_jsx("legend", { children: copy.helpTitle }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "source-outcome", checked: sourceOutcome === 'on-hold', onChange: () => setSourceOutcome('on-hold') }), _jsxs("span", { children: [_jsx("strong", { children: copy.returning }), _jsx("small", { children: copy.returningHint })] })] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "source-outcome", checked: sourceOutcome === 'completed', onChange: () => setSourceOutcome('completed') }), _jsxs("span", { children: [_jsx("strong", { children: copy.notReturning }), _jsx("small", { children: copy.notReturningHint })] })] })] })] }) : null, _jsxs("div", { className: "uhcp-transfer-arrow", children: [_jsx("span", { children: copy.transferTo }), _jsx("i", {}), _jsx(Icon, { name: "chevron" })] }), destination ? _jsxs("article", { className: "uhcp-transfer-card uhcp-transfer-destination", children: [_jsx("h3", { children: copy.destinationTitle }), _jsx(EncounterCardBody, { encounter: { ...destination, status: 'in-progress' }, identifierLabel: copy.encounterIdentifier })] }) : null] }), _jsxs("details", { className: "uhcp-transfer-help", open: true, children: [_jsx("summary", { id: helpId, children: copy.helpTitle }), _jsx("p", { children: copy.helpBody })] }), _jsxs("div", { className: "uhcp-transfer-controls", children: [_jsx("button", { type: "button", onClick: () => { setShowTechnical(true); setStatus(copy.reviewReady); }, children: copy.reviewChanges }), _jsx("button", { className: "uhcp-transfer-primary", type: "button", disabled: transferSync === 'submitting', onClick: () => void applyTransfer(), children: transferSync === 'retryable' ? copy.retryTransfer : copy.applyTransfer })] })] }), _jsxs("section", { className: "uhcp-transfer-review", children: [_jsxs("header", { children: [_jsx("h2", { children: copy.resourceSummary }), demo ? _jsx("p", { children: copy.demoWarning }) : null] }), _jsx("ol", { "aria-label": copy.resourceSummary, children: [[copy.sourceUpdate, 'Encounter'], [copy.sourceHistory, 'EncounterHistory'], [copy.destinationUpdate, 'Encounter'], [copy.destinationHistory, 'EncounterHistory'], [copy.indexProjection, 'Communication']].map(([label, resource], index) => _jsxs("li", { children: [_jsx("span", { children: index + 1 }), _jsx("strong", { children: label }), _jsxs("details", { open: showTechnical, children: [_jsx("summary", { children: copy.technicalDetails }), _jsx("code", { children: resource })] })] }, `${resource}-${index}`)) })] }), status ? _jsx("p", { className: "uhcp-transfer-status", role: "status", children: status }) : null] })] });
45
64
  }
46
65
  function EncounterCard({ encounter }) {
47
66
  return _jsx("article", { className: "uhcp-transfer-card", children: _jsx(EncounterCardBody, { encounter: encounter }) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uhc-portal-components-react",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Brand-neutral React portal shell, account preferences and protected-profile UI for UHC and VetChain portals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -31,8 +31,9 @@
31
31
  "react-dom": ">=18.3.0"
32
32
  },
33
33
  "dependencies": {
34
- "fhir-data-utils-ts": "0.6.10",
35
- "gdc-common-utils-ts": "^2.3.12"
34
+ "fhir-data-utils-ts": "0.6.11",
35
+ "gdc-common-utils-ts": "2.9.27",
36
+ "sos-data-utils-ts": "0.6.3"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@testing-library/react": "^16.3.2",