@viengut/vgq-sdk 0.1.1 → 0.3.0
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/dist/client.d.ts +6 -0
- package/dist/client.js +12 -0
- package/dist/engine.d.ts +3 -0
- package/dist/engine.js +68 -5
- package/dist/session.d.ts +7 -2
- package/dist/session.js +8 -1
- package/dist/types.d.ts +39 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export declare class VgqClient {
|
|
|
30
30
|
/** Tải một Form Version bất biến (Response luôn gắn đúng version đã bắt đầu). */
|
|
31
31
|
getFormVersion(id: string): Promise<FormVersion>;
|
|
32
32
|
createResponse(publicationKey: string, options?: StartResponseOptions): Promise<ResponseDto>;
|
|
33
|
+
/**
|
|
34
|
+
* Bắt đầu Response xem thử trên bản Draft của Form (trang demo cổng bệnh
|
|
35
|
+
* nhân). Server chụp Draft vào shadow snapshot version và XOÁ Response
|
|
36
|
+
* preview cũ của Form — mỗi Form chỉ giữ một bản thử.
|
|
37
|
+
*/
|
|
38
|
+
createPreviewResponse(formId: string, options?: StartResponseOptions): Promise<ResponseDto>;
|
|
33
39
|
getInvitation(id: string): Promise<InvitationDto>;
|
|
34
40
|
/** Bắt đầu (hoặc mở lại) Response duy nhất của một Invitation. */
|
|
35
41
|
startInvitationResponse(invitationId: string): Promise<ResponseDto>;
|
package/dist/client.js
CHANGED
|
@@ -40,6 +40,18 @@ export class VgqClient {
|
|
|
40
40
|
care_context_ref: options.careContextRef ?? null,
|
|
41
41
|
}, headers);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Bắt đầu Response xem thử trên bản Draft của Form (trang demo cổng bệnh
|
|
45
|
+
* nhân). Server chụp Draft vào shadow snapshot version và XOÁ Response
|
|
46
|
+
* preview cũ của Form — mỗi Form chỉ giữ một bản thử.
|
|
47
|
+
*/
|
|
48
|
+
createPreviewResponse(formId, options = {}) {
|
|
49
|
+
return this.request('POST', `/api/forms/${encodeURIComponent(formId)}/preview-responses`, {
|
|
50
|
+
subject_ref: options.subjectRef ?? null,
|
|
51
|
+
respondent_ref: options.respondentRef ?? null,
|
|
52
|
+
care_context_ref: options.careContextRef ?? null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
43
55
|
getInvitation(id) {
|
|
44
56
|
return this.request('GET', `/api/invitations/${encodeURIComponent(id)}`);
|
|
45
57
|
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -11,12 +11,15 @@ export declare function topoOrder(def: FormDefinition): {
|
|
|
11
11
|
export interface ActiveState {
|
|
12
12
|
/** Id các Question đang hoạt động. */
|
|
13
13
|
activeIds: Set<string>;
|
|
14
|
+
/** Id các Category đang hiện (điều kiện của chính nó và mọi tổ tiên đều đúng). */
|
|
15
|
+
activeCategoryIds: Set<string>;
|
|
14
16
|
/** Giá trị hiệu lực theo slug: chỉ gồm giá trị của Question đang hoạt động. */
|
|
15
17
|
effectiveValues: ResponseValues;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* Đánh giá Enable Condition toàn Form theo thứ tự topo. Giá trị của Question
|
|
19
21
|
* không hoạt động bị loại khỏi giá trị hiệu lực nên chuỗi phụ thuộc tắt theo.
|
|
22
|
+
* Question trong Category tắt cũng không hoạt động.
|
|
20
23
|
*/
|
|
21
24
|
export declare function computeActive(def: FormDefinition, values: ResponseValues): ActiveState;
|
|
22
25
|
export declare function isAnswered(value: JsonValue | undefined): boolean;
|
package/dist/engine.js
CHANGED
|
@@ -12,8 +12,27 @@ export function allQuestions(def) {
|
|
|
12
12
|
walk(def.categories);
|
|
13
13
|
return out;
|
|
14
14
|
}
|
|
15
|
-
function conditionRefs(
|
|
16
|
-
return (
|
|
15
|
+
function conditionRefs(cond) {
|
|
16
|
+
return (cond?.any ?? []).flatMap((group) => group.all.map((condition) => condition.question_id));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Phụ thuộc Enable Condition của từng Question: refs của chính nó cộng refs
|
|
20
|
+
* điều kiện của mọi Category tổ tiên (Category tắt kéo Question tắt theo,
|
|
21
|
+
* nên thứ tự topo và phát hiện chu trình phủ luôn điều kiện cấp Category).
|
|
22
|
+
*/
|
|
23
|
+
function dependencyMap(def) {
|
|
24
|
+
const out = new Map();
|
|
25
|
+
const walk = (categories, inherited) => {
|
|
26
|
+
for (const category of categories) {
|
|
27
|
+
const refs = [...inherited, ...conditionRefs(category.enable_condition)];
|
|
28
|
+
for (const q of category.questions) {
|
|
29
|
+
out.set(q.id, [...refs, ...conditionRefs(q.enable_condition)]);
|
|
30
|
+
}
|
|
31
|
+
walk(category.children ?? [], refs);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walk(def.categories, []);
|
|
35
|
+
return out;
|
|
17
36
|
}
|
|
18
37
|
/**
|
|
19
38
|
* Sắp xếp topo theo phụ thuộc Enable Condition (Kahn).
|
|
@@ -22,10 +41,11 @@ function conditionRefs(question) {
|
|
|
22
41
|
export function topoOrder(def) {
|
|
23
42
|
const questions = allQuestions(def);
|
|
24
43
|
const known = new Set(questions.map((q) => q.id));
|
|
44
|
+
const deps = dependencyMap(def);
|
|
25
45
|
const inDegree = new Map();
|
|
26
46
|
const dependents = new Map();
|
|
27
47
|
for (const q of questions) {
|
|
28
|
-
const valid =
|
|
48
|
+
const valid = (deps.get(q.id) ?? []).filter((ref) => known.has(ref));
|
|
29
49
|
inDegree.set(q.id, valid.length);
|
|
30
50
|
for (const ref of valid) {
|
|
31
51
|
const list = dependents.get(ref) ?? [];
|
|
@@ -49,13 +69,35 @@ export function topoOrder(def) {
|
|
|
49
69
|
const stuck = [...known].filter((id) => !done.has(id)).sort();
|
|
50
70
|
return { order, stuck };
|
|
51
71
|
}
|
|
72
|
+
function indexCategories(def) {
|
|
73
|
+
const idx = {
|
|
74
|
+
condition: new Map(),
|
|
75
|
+
parent: new Map(),
|
|
76
|
+
categoryOf: new Map(),
|
|
77
|
+
ordered: [],
|
|
78
|
+
};
|
|
79
|
+
const walk = (categories, parent) => {
|
|
80
|
+
for (const c of categories) {
|
|
81
|
+
idx.condition.set(c.id, c.enable_condition);
|
|
82
|
+
idx.parent.set(c.id, parent);
|
|
83
|
+
idx.ordered.push(c.id);
|
|
84
|
+
for (const q of c.questions)
|
|
85
|
+
idx.categoryOf.set(q.id, c.id);
|
|
86
|
+
walk(c.children ?? [], c.id);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
walk(def.categories, null);
|
|
90
|
+
return idx;
|
|
91
|
+
}
|
|
52
92
|
/**
|
|
53
93
|
* Đánh giá Enable Condition toàn Form theo thứ tự topo. Giá trị của Question
|
|
54
94
|
* không hoạt động bị loại khỏi giá trị hiệu lực nên chuỗi phụ thuộc tắt theo.
|
|
95
|
+
* Question trong Category tắt cũng không hoạt động.
|
|
55
96
|
*/
|
|
56
97
|
export function computeActive(def, values) {
|
|
57
98
|
const questions = allQuestions(def);
|
|
58
99
|
const byId = new Map(questions.map((q) => [q.id, q]));
|
|
100
|
+
const idx = indexCategories(def);
|
|
59
101
|
const { order, stuck } = topoOrder(def);
|
|
60
102
|
const evaluationOrder = stuck.length === 0 ? order : questions.map((q) => q.id);
|
|
61
103
|
const effectiveValues = {};
|
|
@@ -64,16 +106,37 @@ export function computeActive(def, values) {
|
|
|
64
106
|
if (v !== undefined && v !== null)
|
|
65
107
|
effectiveValues[q.slug] = v;
|
|
66
108
|
}
|
|
109
|
+
// Category hiện khi điều kiện của nó và mọi tổ tiên đều đúng. Memo an toàn
|
|
110
|
+
// vì thứ tự topo bảo đảm các Question được tham chiếu đã chốt giá trị
|
|
111
|
+
// trước khi bất kỳ Question nào bên trong Category được đánh giá.
|
|
112
|
+
const catMemo = new Map();
|
|
113
|
+
const categoryActive = (id) => {
|
|
114
|
+
const memoized = catMemo.get(id);
|
|
115
|
+
if (memoized !== undefined)
|
|
116
|
+
return memoized;
|
|
117
|
+
const parent = idx.parent.get(id) ?? null;
|
|
118
|
+
const cond = idx.condition.get(id);
|
|
119
|
+
const active = (parent === null || categoryActive(parent)) &&
|
|
120
|
+
(!cond || evalCondition(cond, effectiveValues, byId));
|
|
121
|
+
catMemo.set(id, active);
|
|
122
|
+
return active;
|
|
123
|
+
};
|
|
67
124
|
const activeIds = new Set();
|
|
68
125
|
for (const id of evaluationOrder) {
|
|
69
126
|
const q = byId.get(id);
|
|
70
|
-
const
|
|
127
|
+
const cat = idx.categoryOf.get(id);
|
|
128
|
+
const active = (cat === undefined || categoryActive(cat)) &&
|
|
129
|
+
(!q.enable_condition ||
|
|
130
|
+
evalCondition(q.enable_condition, effectiveValues, byId));
|
|
71
131
|
if (active)
|
|
72
132
|
activeIds.add(id);
|
|
73
133
|
else
|
|
74
134
|
delete effectiveValues[q.slug];
|
|
75
135
|
}
|
|
76
|
-
|
|
136
|
+
// Category không chứa Question nào chưa được đánh giá trong vòng trên;
|
|
137
|
+
// giá trị hiệu lực đã chốt nên đánh giá nốt tại đây.
|
|
138
|
+
const activeCategoryIds = new Set(idx.ordered.filter(categoryActive));
|
|
139
|
+
return { activeIds, activeCategoryIds, effectiveValues };
|
|
77
140
|
}
|
|
78
141
|
function evalCondition(condition, effective, byId) {
|
|
79
142
|
return condition.any.some((group) => group.all.every((c) => evalSingle(c, effective, byId)));
|
package/dist/session.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { VgqClient } from './client.js';
|
|
|
2
2
|
import type { ActiveState, CompletionResult } from './engine.js';
|
|
3
3
|
import type { FormDefinition, JsonValue, ResponseDto, ResponseValues } from './types.js';
|
|
4
4
|
/** Phần VgqClient mà ResponseSession cần — thu hẹp để test dễ stub. */
|
|
5
|
-
export type SessionClient = Pick<VgqClient, 'getFormVersion' | 'createResponse' | 'startInvitationResponse' | 'getResponse' | 'updateValues' | 'completeResponse'>;
|
|
5
|
+
export type SessionClient = Pick<VgqClient, 'getFormVersion' | 'createResponse' | 'createPreviewResponse' | 'startInvitationResponse' | 'getResponse' | 'updateValues' | 'completeResponse'>;
|
|
6
6
|
export interface SessionOptions {
|
|
7
7
|
/** Thời gian gom thay đổi trước khi đồng bộ lên server (ms). Mặc định 800. */
|
|
8
8
|
debounceMs?: number;
|
|
@@ -14,8 +14,13 @@ export interface SessionOptions {
|
|
|
14
14
|
export interface StartSessionOptions {
|
|
15
15
|
/** Bắt đầu qua Publication key... */
|
|
16
16
|
publicationKey?: string;
|
|
17
|
-
/** ...hoặc qua Invitation đã được tạo sẵn
|
|
17
|
+
/** ...hoặc qua Invitation đã được tạo sẵn... */
|
|
18
18
|
invitationId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* ...hoặc xem thử bản Draft của một Form (chưa phát hành). Server chụp
|
|
21
|
+
* Draft thành snapshot và thay thế Response preview cũ của Form đó.
|
|
22
|
+
*/
|
|
23
|
+
previewFormId?: string;
|
|
19
24
|
subjectRef?: string;
|
|
20
25
|
respondentRef?: string;
|
|
21
26
|
careContextRef?: string;
|
package/dist/session.js
CHANGED
|
@@ -41,8 +41,15 @@ export class ResponseSession {
|
|
|
41
41
|
idempotencyKey: start.idempotencyKey,
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
+
else if (start.previewFormId) {
|
|
45
|
+
response = await client.createPreviewResponse(start.previewFormId, {
|
|
46
|
+
subjectRef: start.subjectRef,
|
|
47
|
+
respondentRef: start.respondentRef,
|
|
48
|
+
careContextRef: start.careContextRef,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
44
51
|
else {
|
|
45
|
-
throw new Error('Cần publicationKey hoặc
|
|
52
|
+
throw new Error('Cần publicationKey, invitationId hoặc previewFormId để bắt đầu Response');
|
|
46
53
|
}
|
|
47
54
|
// Định nghĩa lấy theo đúng Form Version mà Response đã ghim, không phải
|
|
48
55
|
// version mới nhất của Publication (Draft không đổi Form giữa chừng).
|
package/dist/types.d.ts
CHANGED
|
@@ -54,12 +54,39 @@ export interface BodyMapSpot {
|
|
|
54
54
|
/** Metadata lâm sàng tuỳ ý; VGQ không diễn giải. */
|
|
55
55
|
metadata?: JsonValue;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Hướng dẫn bằng audio/video kèm lời dẫn, đặt ở Form, Category hoặc dùng làm
|
|
59
|
+
* audio phát khi chọn Option. Ứng dụng phát media (nếu có) và/hoặc đọc `text`.
|
|
60
|
+
*/
|
|
61
|
+
export interface Guidance {
|
|
62
|
+
/** Media Asset dạng audio hoặc video; vắng mặt nghĩa là chỉ có lời dẫn. */
|
|
63
|
+
media_asset_id?: string;
|
|
64
|
+
text?: string;
|
|
65
|
+
}
|
|
66
|
+
/** Thang đau VAS: các khoảng điểm liên tiếp kèm mô tả để tự đối chiếu. */
|
|
67
|
+
export interface PainScale {
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
ranges: PainRange[];
|
|
70
|
+
}
|
|
71
|
+
export interface PainRange {
|
|
72
|
+
start: number;
|
|
73
|
+
end: number;
|
|
74
|
+
label?: string;
|
|
75
|
+
}
|
|
57
76
|
export interface OptionDef {
|
|
58
77
|
id: string;
|
|
59
78
|
slug: string;
|
|
60
79
|
label: string;
|
|
61
80
|
/** Chỉ có mặt khi Question có data type body_map. */
|
|
62
81
|
body_map?: BodyMapSpot | null;
|
|
82
|
+
/**
|
|
83
|
+
* Option cha trong cùng Question, theo id nội bộ. Body Map nhiều tầng: vùng
|
|
84
|
+
* lớn là Option cha, điểm chi tiết là Option con. Response Value vẫn phẳng.
|
|
85
|
+
*/
|
|
86
|
+
parent_id?: string | null;
|
|
87
|
+
/** Audio phát khi Respondent chọn Option. */
|
|
88
|
+
audio?: Guidance | null;
|
|
89
|
+
pain_scale?: PainScale | null;
|
|
63
90
|
}
|
|
64
91
|
export interface MediaAssetDto {
|
|
65
92
|
id: string;
|
|
@@ -88,11 +115,21 @@ export interface CategoryDef {
|
|
|
88
115
|
label: string;
|
|
89
116
|
children?: CategoryDef[];
|
|
90
117
|
questions: QuestionDef[];
|
|
118
|
+
/** Hướng dẫn mở đầu cho riêng Category này. */
|
|
119
|
+
guidance?: Guidance | null;
|
|
120
|
+
/**
|
|
121
|
+
* Điều kiện hiện cả Category (ví dụ tab bệnh hiện theo lý do đến khám).
|
|
122
|
+
* Category tắt thì mọi Question bên trong — kể cả trong Category con —
|
|
123
|
+
* tắt theo và giá trị của chúng bị loại khỏi official values.
|
|
124
|
+
*/
|
|
125
|
+
enable_condition?: EnableCondition | null;
|
|
91
126
|
}
|
|
92
127
|
export interface FormDefinition {
|
|
93
128
|
title: string;
|
|
94
129
|
form_type: FormType;
|
|
95
130
|
categories: CategoryDef[];
|
|
131
|
+
/** Hướng dẫn mở đầu cho cả Form. */
|
|
132
|
+
guidance?: Guidance | null;
|
|
96
133
|
}
|
|
97
134
|
export interface ValidationIssue {
|
|
98
135
|
path: string;
|
|
@@ -114,6 +151,8 @@ export interface ResponseDto {
|
|
|
114
151
|
updated_at: string;
|
|
115
152
|
completed_at: string | null;
|
|
116
153
|
completed_by: string | null;
|
|
154
|
+
/** Response xem thử từ trang demo cổng bệnh nhân (server < 0.3.0 không trả). */
|
|
155
|
+
is_preview?: boolean;
|
|
117
156
|
}
|
|
118
157
|
export interface PublicationForm {
|
|
119
158
|
publication_key: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viengut/vgq-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Headless TypeScript SDK for the VGQ clinical form platform: load published Forms, manage Response state, evaluate Enable Conditions and validate Response Values without any UI components.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|