uhc-portal-components-react 0.1.28 → 0.1.31
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 +22 -0
- package/README.md +9 -2
- package/dist/ClinicalArtifactEditor.d.ts +49 -2
- package/dist/ClinicalArtifactEditor.d.ts.map +1 -1
- package/dist/ClinicalArtifactEditor.js +98 -16
- package/dist/ConsentPermissionDirectory.d.ts +60 -0
- package/dist/ConsentPermissionDirectory.d.ts.map +1 -0
- package/dist/ConsentPermissionDirectory.js +52 -0
- package/dist/EncounterTransferConsole.d.ts +8 -1
- package/dist/EncounterTransferConsole.d.ts.map +1 -1
- package/dist/EncounterTransferConsole.js +23 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/styles.css +1 -0
- package/package.json +4 -3
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,
|
|
@@ -130,6 +147,11 @@
|
|
|
130
147
|
|
|
131
148
|
## Unreleased
|
|
132
149
|
|
|
150
|
+
- Add a reusable, accessible Consent permission directory/editor for
|
|
151
|
+
provider-backed create, edit, suspend/reactivate and delete flows. The host
|
|
152
|
+
owns the schema-backed Bundle mutation and must return authoritative
|
|
153
|
+
readback; the component performs no optimistic authorization mutation.
|
|
154
|
+
|
|
133
155
|
- Add the repository-wide mandatory red-green-refactor contributor contract
|
|
134
156
|
and mark the existing component suites with an explicit TDD contract header.
|
|
135
157
|
- Correct the reuse contract: the package currently provides presentation
|
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.
|
|
75
|
-
|
|
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;
|
|
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(() =>
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
97
|
+
const result = await saveArtifact({
|
|
67
98
|
resourceType: request.resourceType,
|
|
68
|
-
id:
|
|
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 (!
|
|
105
|
+
if (!result)
|
|
74
106
|
throw new Error(copy.readbackMissing);
|
|
75
|
-
|
|
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'
|
|
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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ConsentDecision, type ConsentStatus } from 'gdc-common-utils-ts/models/consent-rule';
|
|
2
|
+
export type ConsentPermissionOption = Readonly<{
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}>;
|
|
6
|
+
export type ConsentPermissionContact = Readonly<{
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type ConsentPermissionRecord = Readonly<{
|
|
11
|
+
id: string;
|
|
12
|
+
contactReference: string;
|
|
13
|
+
contactName: string;
|
|
14
|
+
decision: ConsentDecision;
|
|
15
|
+
status: ConsentStatus;
|
|
16
|
+
purposes: readonly string[];
|
|
17
|
+
roles: readonly string[];
|
|
18
|
+
targets: readonly string[];
|
|
19
|
+
}>;
|
|
20
|
+
export type ConsentPermissionDraft = Readonly<Omit<ConsentPermissionRecord, 'id' | 'contactName'> & {
|
|
21
|
+
id?: string;
|
|
22
|
+
contactName?: string;
|
|
23
|
+
}>;
|
|
24
|
+
export type ConsentPermissionDirectoryCopy = Readonly<{
|
|
25
|
+
title: string;
|
|
26
|
+
empty: string;
|
|
27
|
+
add: string;
|
|
28
|
+
edit: string;
|
|
29
|
+
remove: string;
|
|
30
|
+
suspend: string;
|
|
31
|
+
activate: string;
|
|
32
|
+
save: string;
|
|
33
|
+
cancel: string;
|
|
34
|
+
contact: string;
|
|
35
|
+
decision: string;
|
|
36
|
+
permit: string;
|
|
37
|
+
deny: string;
|
|
38
|
+
status: string;
|
|
39
|
+
active: string;
|
|
40
|
+
inactive: string;
|
|
41
|
+
purposes: string;
|
|
42
|
+
roles: string;
|
|
43
|
+
targets: string;
|
|
44
|
+
saving: string;
|
|
45
|
+
loadError: string;
|
|
46
|
+
required: string;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function ConsentPermissionDirectory({ records, contacts, purposeOptions, roleOptions, targetOptions, copy, editable, onSave, onStatusChange, onDelete }: Readonly<{
|
|
49
|
+
records: readonly ConsentPermissionRecord[];
|
|
50
|
+
contacts: readonly ConsentPermissionContact[];
|
|
51
|
+
purposeOptions: readonly ConsentPermissionOption[];
|
|
52
|
+
roleOptions: readonly ConsentPermissionOption[];
|
|
53
|
+
targetOptions: readonly ConsentPermissionOption[];
|
|
54
|
+
copy: ConsentPermissionDirectoryCopy;
|
|
55
|
+
editable?: boolean;
|
|
56
|
+
onSave?: (draft: ConsentPermissionDraft) => Promise<void> | void;
|
|
57
|
+
onStatusChange?: (id: string, status: ConsentStatus) => Promise<void> | void;
|
|
58
|
+
onDelete?: (id: string) => Promise<void> | void;
|
|
59
|
+
}>): import("react").JSX.Element;
|
|
60
|
+
//# sourceMappingURL=ConsentPermissionDirectory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsentPermissionDirectory.d.ts","sourceRoot":"","sources":["../src/ConsentPermissionDirectory.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { ConsentDecisions, ConsentStatuses } from 'gdc-common-utils-ts/models/consent-rule';
|
|
4
|
+
export function ConsentPermissionDirectory({ records, contacts, purposeOptions, roleOptions, targetOptions, copy, editable = false, onSave, onStatusChange, onDelete }) {
|
|
5
|
+
const [editing, setEditing] = useState(null);
|
|
6
|
+
const [saving, setSaving] = useState(false);
|
|
7
|
+
const [error, setError] = useState('');
|
|
8
|
+
const labels = useMemo(() => new Map([...purposeOptions, ...roleOptions, ...targetOptions].map(({ value, label }) => [value, label])), [purposeOptions, roleOptions, targetOptions]);
|
|
9
|
+
async function save() {
|
|
10
|
+
if (!editing || !editing.contactReference || !editing.purposes.length || !editing.roles.length || !editing.targets.length) {
|
|
11
|
+
setError(copy.required);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
setSaving(true);
|
|
15
|
+
setError('');
|
|
16
|
+
try {
|
|
17
|
+
await onSave?.(editing);
|
|
18
|
+
setEditing(null);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
setError(copy.loadError);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setSaving(false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function changeStatus(record) {
|
|
28
|
+
setError('');
|
|
29
|
+
try {
|
|
30
|
+
await onStatusChange?.(record.id, record.status === ConsentStatuses.Active ? ConsentStatuses.Inactive : ConsentStatuses.Active);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
setError(copy.loadError);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function remove(record) {
|
|
37
|
+
setError('');
|
|
38
|
+
try {
|
|
39
|
+
await onDelete?.(record.id);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
setError(copy.loadError);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return _jsxs("section", { className: "uhcp-consent-directory", "aria-labelledby": "uhcp-consent-directory-title", children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("h2", { id: "uhcp-consent-directory-title", children: copy.title }), _jsx("p", { children: records.length ? `${records.length}` : copy.empty })] }), editable ? _jsx("button", { type: "button", onClick: () => setEditing(emptyDraft()), children: copy.add }) : null] }), error ? _jsx("p", { role: "alert", children: error }) : null, _jsx("div", { className: "uhcp-consent-list", children: records.map((record) => _jsxs("article", { children: [_jsxs("div", { children: [_jsx("strong", { children: record.contactName }), _jsx("span", { children: record.decision === ConsentDecisions.Permit ? copy.permit : copy.deny }), _jsx("span", { children: record.status === ConsentStatuses.Active ? copy.active : copy.inactive })] }), _jsx("p", { children: record.purposes.map((value) => labels.get(value) ?? value).join(', ') }), _jsx("p", { children: record.roles.map((value) => labels.get(value) ?? value).join(', ') }), _jsx("p", { children: record.targets.map((value) => labels.get(value) ?? value).join(', ') }), editable ? _jsxs("div", { className: "uhcp-consent-actions", children: [_jsx("button", { type: "button", "aria-label": `${copy.edit} ${record.contactName}`, onClick: () => setEditing(record), children: copy.edit }), _jsx("button", { type: "button", "aria-label": `${record.status === ConsentStatuses.Active ? copy.suspend : copy.activate} ${record.contactName}`, onClick: () => void changeStatus(record), children: record.status === ConsentStatuses.Active ? copy.suspend : copy.activate }), _jsx("button", { type: "button", "aria-label": `${copy.remove} ${record.contactName}`, onClick: () => void remove(record), children: copy.remove })] }) : null] }, record.id)) }), editing ? _jsxs("form", { className: "uhcp-consent-editor", onSubmit: (event) => { event.preventDefault(); void save(); }, children: [_jsxs("label", { children: [copy.contact, _jsxs("select", { value: editing.contactReference, onChange: (event) => setEditing({ ...editing, contactReference: event.target.value, contactName: contacts.find(({ id }) => id === event.target.value)?.name }), children: [_jsx("option", { value: "" }), contacts.map((contact) => _jsx("option", { value: contact.id, children: contact.name }, contact.id))] })] }), _jsxs("label", { children: [copy.decision, _jsxs("select", { value: editing.decision, onChange: (event) => setEditing({ ...editing, decision: event.target.value }), children: [_jsx("option", { value: ConsentDecisions.Permit, children: copy.permit }), _jsx("option", { value: ConsentDecisions.Deny, children: copy.deny })] })] }), _jsxs("label", { children: [copy.status, _jsxs("select", { value: editing.status, onChange: (event) => setEditing({ ...editing, status: event.target.value }), children: [_jsx("option", { value: ConsentStatuses.Active, children: copy.active }), _jsx("option", { value: ConsentStatuses.Inactive, children: copy.inactive })] })] }), _jsx(OptionGroup, { legend: copy.purposes, options: purposeOptions, selected: editing.purposes, onChange: (purposes) => setEditing({ ...editing, purposes }) }), _jsx(OptionGroup, { legend: copy.roles, options: roleOptions, selected: editing.roles, onChange: (roles) => setEditing({ ...editing, roles }) }), _jsx(OptionGroup, { legend: copy.targets, options: targetOptions, selected: editing.targets, onChange: (targets) => setEditing({ ...editing, targets }) }), _jsxs("div", { children: [_jsx("button", { type: "submit", disabled: saving, children: saving ? copy.saving : copy.save }), _jsx("button", { type: "button", onClick: () => setEditing(null), children: copy.cancel })] })] }) : null] });
|
|
46
|
+
}
|
|
47
|
+
function OptionGroup({ legend, options, selected, onChange }) {
|
|
48
|
+
return _jsxs("fieldset", { children: [_jsx("legend", { children: legend }), options.map((option) => _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: selected.includes(option.value), onChange: (event) => onChange(event.target.checked ? [...selected, option.value] : selected.filter((value) => value !== option.value)) }), option.label] }, option.value))] });
|
|
49
|
+
}
|
|
50
|
+
function emptyDraft() {
|
|
51
|
+
return Object.freeze({ contactReference: '', decision: ConsentDecisions.Permit, status: ConsentStatuses.Active, purposes: Object.freeze([]), roles: Object.freeze([]), targets: Object.freeze([]) });
|
|
52
|
+
}
|
|
@@ -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":"AAEA,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,GAAG,WAAW,CAAA;AACzF,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,WAAW,CAAA;AAE5D,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,uBAAuB,CAAA;CAChC,CAAC,CAAA;AAEF,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAC,CAAA;AAEF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;IAC7E,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;IACnE,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;IACxF,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;IAChE,gBAAgB,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;IAC9E,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IAClI,eAAe,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;IACpJ,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;IACrG,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;IACnI,mBAAmB,EAAE,MAAM,CAAA;
|
|
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
|
-
|
|
42
|
-
|
|
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/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from './SubjectAccessRequestPanel.js';
|
|
|
18
18
|
export * from './SoschainWorkspace.js';
|
|
19
19
|
export * from './EncounterTransferConsole.js';
|
|
20
20
|
export * from './EmergencyContactDirectory.js';
|
|
21
|
+
export * from './ConsentPermissionDirectory.js';
|
|
21
22
|
export * from './OrganizationWorkforceAdministration.js';
|
|
22
23
|
export * from './ProtectedIndexEmergencyData.js';
|
|
23
24
|
export * from './PortalSettingsPanel.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,wCAAwC,CAAA;AACtD,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0CAA0C,CAAA;AACxD,cAAc,kCAAkC,CAAA;AAChD,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,oCAAoC,CAAA;AAClD,cAAc,sCAAsC,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,wCAAwC,CAAA;AACtD,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,0CAA0C,CAAA;AACxD,cAAc,kCAAkC,CAAA;AAChD,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,oCAAoC,CAAA;AAClD,cAAc,sCAAsC,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export * from './SubjectAccessRequestPanel.js';
|
|
|
18
18
|
export * from './SoschainWorkspace.js';
|
|
19
19
|
export * from './EncounterTransferConsole.js';
|
|
20
20
|
export * from './EmergencyContactDirectory.js';
|
|
21
|
+
export * from './ConsentPermissionDirectory.js';
|
|
21
22
|
export * from './OrganizationWorkforceAdministration.js';
|
|
22
23
|
export * from './ProtectedIndexEmergencyData.js';
|
|
23
24
|
export * from './PortalSettingsPanel.js';
|
package/dist/styles.css
CHANGED
|
@@ -638,4 +638,5 @@ html[data-theme='dark'] .uhcp-brand-artwork-dark { display: block !important; }
|
|
|
638
638
|
@media (max-width: 620px) { .uhcp-emergency-copy > header { display: grid; } .uhcp-emergency-copy-legend { width: 100%; } }
|
|
639
639
|
.uhcp-settings{max-width:72rem;margin:0 auto;padding:2rem 1rem 4rem;display:grid;gap:1rem}.uhcp-settings>header,.uhcp-settings>section{background:var(--uhcp-surface,#fff);border:1px solid var(--uhcp-border,#dbe4ea);border-radius:1rem;padding:1.25rem}.uhcp-settings h1,.uhcp-settings h2{margin-top:0}.uhcp-settings fieldset{border:0;padding:0;margin:1rem 0;display:flex;gap:.5rem;flex-wrap:wrap}.uhcp-settings legend{width:100%;font-weight:700;margin-bottom:.5rem}.uhcp-settings button,.uhcp-settings-links a{border:1px solid var(--uhcp-border,#c9d7df);border-radius:.7rem;background:transparent;color:inherit;padding:.65rem .85rem;text-decoration:none}.uhcp-settings button[aria-pressed=true]{background:var(--uhcp-accent,#dceff7);border-color:var(--uhcp-accent-strong,#2489a7)}.uhcp-settings-links{display:flex;gap:.6rem;flex-wrap:wrap}
|
|
640
640
|
.uhcp-legal-document{color:var(--uhcp-text);line-height:1.7}.uhcp-legal-document h2{font-size:clamp(1.6rem,4vw,2.35rem);line-height:1.15;margin:0 0 1.25rem}.uhcp-legal-document h3{font-size:1.2rem;line-height:1.3;margin:2rem 0 .65rem}.uhcp-legal-document p{margin:0 0 1rem}.uhcp-legal-document ul{margin:0 0 1rem;padding-left:1.4rem}.uhcp-legal-document code{background:var(--uhcp-panel-muted);border-radius:.3rem;padding:.08rem .3rem}
|
|
641
|
+
.uhcp-consent-directory{background:var(--uhcp-panel);border:1px solid var(--uhcp-border);border-radius:1.5rem;color:var(--uhcp-text);display:grid;gap:1rem;padding:1.25rem}.uhcp-consent-directory>header{align-items:flex-start;display:flex;gap:1rem;justify-content:space-between}.uhcp-consent-directory h2,.uhcp-consent-directory p{margin:0}.uhcp-consent-directory>header p,.uhcp-consent-list article>p{color:var(--uhcp-muted)}.uhcp-consent-directory button,.uhcp-consent-directory select{background:var(--uhcp-panel);border:1px solid var(--uhcp-border);border-radius:.7rem;color:inherit;font:inherit;padding:.65rem .85rem}.uhcp-consent-list{display:grid;gap:.75rem}.uhcp-consent-list article{background:var(--uhcp-panel-muted);border:1px solid var(--uhcp-border);border-radius:1rem;display:grid;gap:.55rem;padding:1rem}.uhcp-consent-list article>div:first-child{align-items:center;display:flex;flex-wrap:wrap;gap:.55rem}.uhcp-consent-list article span{background:color-mix(in srgb,var(--uhcp-turquoise) 13%,transparent);border-radius:999px;font-size:.8rem;padding:.25rem .5rem}.uhcp-consent-actions{display:flex;flex-wrap:wrap;gap:.5rem}.uhcp-consent-editor{border-top:1px solid var(--uhcp-border);display:grid;gap:1rem;padding-top:1rem}.uhcp-consent-editor>label{display:grid;gap:.35rem}.uhcp-consent-editor fieldset{border:1px solid var(--uhcp-border);border-radius:1rem;display:flex;flex-wrap:wrap;gap:.75rem;margin:0;padding:1rem}.uhcp-consent-editor legend{font-weight:800;padding:0 .35rem}.uhcp-consent-editor fieldset label{align-items:center;display:flex;gap:.35rem}.uhcp-consent-editor>div{display:flex;gap:.6rem}@media(max-width:620px){.uhcp-consent-directory>header{display:grid}.uhcp-consent-editor fieldset{display:grid}}
|
|
641
642
|
.uhcp-loading-skeleton{background:var(--uhcp-panel);border:1px solid var(--uhcp-border);border-radius:2rem;display:grid;gap:1rem;margin:0 auto;max-width:1160px;overflow:hidden;padding:clamp(1.25rem,4vw,2.5rem);position:relative}.uhcp-loading-skeleton::after{animation:uhcp-loading-sweep 1.45s ease-in-out infinite;background:linear-gradient(90deg,transparent,color-mix(in srgb,#fff 42%,transparent),transparent);content:"";inset:0;position:absolute;transform:translateX(-100%)}.uhcp-loading-label{height:1px;overflow:hidden;position:absolute;width:1px;clip-path:inset(50%)}.uhcp-loading-heading,.uhcp-loading-copy,.uhcp-loading-row span{animation:uhcp-loading-pulse 1.45s ease-in-out infinite;background:color-mix(in srgb,var(--uhcp-muted) 17%,var(--uhcp-panel-muted));border-radius:999px}.uhcp-loading-heading{height:2.25rem;max-width:23rem;width:62%}.uhcp-loading-copy{height:1rem;max-width:38rem;width:86%}.uhcp-loading-grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));margin-top:.5rem}.uhcp-loading-row{background:var(--uhcp-panel-muted);border:1px solid var(--uhcp-border);border-radius:1.35rem;display:grid;gap:.8rem;padding:1.25rem}.uhcp-loading-row span{display:block;height:.8rem}.uhcp-loading-row span:nth-child(1){width:35%}.uhcp-loading-row span:nth-child(2){height:1.2rem;width:72%}.uhcp-loading-row span:nth-child(3){width:90%}.uhcp-loading-skeleton-compact{border-radius:1.25rem;padding:1.25rem}.uhcp-loading-skeleton-compact .uhcp-loading-heading{height:1.5rem}@keyframes uhcp-loading-pulse{0%,100%{opacity:.55}50%{opacity:1}}@keyframes uhcp-loading-sweep{to{transform:translateX(100%)}}@media(prefers-reduced-motion:reduce){.uhcp-loading-skeleton::after,.uhcp-loading-heading,.uhcp-loading-copy,.uhcp-loading-row span{animation:none}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uhc-portal-components-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
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.
|
|
35
|
-
"gdc-common-utils-ts": "
|
|
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",
|