@viengut/vgq-sdk 0.1.0 → 0.2.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/engine.d.ts +3 -0
- package/dist/engine.js +68 -5
- package/dist/types.d.ts +37 -0
- package/package.json +1 -1
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/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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viengut/vgq-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|