@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 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(question) {
16
- return (question.enable_condition?.any ?? []).flatMap((group) => group.all.map((condition) => condition.question_id));
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 = conditionRefs(q).filter((ref) => known.has(ref));
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 active = !q.enable_condition || evalCondition(q.enable_condition, effectiveValues, byId);
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
- return { activeIds, effectiveValues };
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 invitationId để bắt đầu Response');
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.1.1",
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",