clinic-connect-widget 1.1.8 → 1.1.10

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/README.md CHANGED
@@ -24,6 +24,7 @@ To add the Clinic Connect widget to your website, simply include the compiled sc
24
24
  <script
25
25
  src="https://your-cdn.com/clinic-widget.umd.js"
26
26
  data-widget-id="YOUR_CLINIC_ID"
27
+ data-service-operator="YOUR_OPERATOR_ID"
27
28
  data-locale="vi">
28
29
  </script>
29
30
  ```
@@ -36,6 +37,7 @@ You can pre-select a doctor or product context by passing additional attributes.
36
37
  <script
37
38
  src="https://your-cdn.com/clinic-widget.umd.js"
38
39
  data-widget-id="YOUR_CLINIC_ID"
40
+ data-service-operator="YOUR_OPERATOR_ID"
39
41
  data-store-id="YOUR_STORE_ID"
40
42
 
41
43
  data-org-id="YOUR_ORG_ID"
@@ -47,6 +49,15 @@ You can pre-select a doctor or product context by passing additional attributes.
47
49
 
48
50
  data-product-id="YOUR_PRODUCT_ID"
49
51
  data-url-video="YOUR_VIDEO_URL"
52
+
53
+ data-appointment-mode="ONLINE"
54
+ data-schedule-mode="TELENOW"
55
+ data-resource-id="YOUR_RESOURCE_ID"
56
+ data-resource-type="DOCTOR"
57
+ data-subscription="YOUR_SUBSCRIPTION_ID"
58
+ data-service-id="YOUR_SERVICE_ID"
59
+ data-order-id="YOUR_ORDER_ID"
60
+
50
61
  data-locale="vi">
51
62
  </script>
52
63
  ```
@@ -57,18 +68,26 @@ The widget is highly configurable via `data-` attributes on the script tag.
57
68
 
58
69
  | Attribute | Description | Required | Default |
59
70
  | :--- | :--- | :---: | :---: |
60
- | `src` | Path to the widget JavaScript file. | Yes | - |
71
+ | `src` | Path to the widget JavaScript file. | **Yes** | - |
61
72
  | `data-widget-id` | Unique ID for your clinic's implementation. | **Yes** | - |
73
+ | `data-service-operator` | The Service Operator ID (e.g., 'TRUE_DOC'). | **Yes** | - |
62
74
  | `data-trigger-element-id`| The generic DOM ID of the element where you want to render the inline trigger card (instead of floating). | No | `null` |
63
75
  | `data-locale` | Language code (e.g., 'vi', 'en'). | No | `vi` |
64
- | `data-store-id` | The Store ID associated with the booking system. | No | `null` |
65
- | `data-org-id` | The Organization ID. | No | `null` |
76
+ | `data-store-id` | The Store ID associated with the booking system. | No | `demo-store` |
77
+ | `data-org-id` | The Organization ID. | No | `demo-org` |
66
78
  | `data-doctor-id` | Pre-select a specific doctor context. | No | `null` |
67
79
  | `data-doctor-name` | Custom name for the doctor (Dynamic). | No | `null` |
68
80
  | `data-doctor-specialty` | Custom specialty for the doctor (Dynamic). | No | `null` |
69
81
  | `data-doctor-avatar` | Custom avatar URL for the doctor (Dynamic). | No | `null` |
70
82
  | `data-product-id` | Pre-select a specific product or service package. | No | `null` |
71
83
  | `data-url-video` | URL for the introductory video used in the permission flow. | No | `null` |
84
+ | `data-appointment-mode`| Mode of appointment (e.g., 'ONLINE', 'OFFLINE'). | No | `null` |
85
+ | `data-schedule-mode` | Mode of schedule (e.g., 'SCHEDULED', 'TELENOW'). | No | `null` |
86
+ | `data-resource-id` | ID of the specific resource to book. | No | `null` |
87
+ | `data-resource-type` | Type of the resource (e.g., 'DOCTOR'). | No | `null` |
88
+ | `data-subscription` | Subscription package ID. | No | `null` |
89
+ | `data-service-id` | Specific service ID for the booking. | No | `null` |
90
+ | `data-order-id` | External order ID to link with the booking. | No | `null` |
72
91
 
73
92
  ## Client Hooks (API)
74
93
 
@@ -142,7 +161,7 @@ To verify the final build behaves as expected:
142
161
 
143
162
  ## Project Structure
144
163
 
145
- * `src/api/`: Handles API simulation and fetching widget configuration.
164
+ * `src/api/`: Handles API integration, parsing customer data, calling ActionBooking API, etc.
146
165
  * `src/core/`: Main widget logic (`ClinicWidget` class), lifecycle management, and event binding.
147
166
  * `src/state/`: Centralized state management (Store pattern).
148
167
  * `src/ui/`: Contains HTML Templates (`templates.js`) and CSS Styles (`styles.js`).
@@ -72,12 +72,12 @@ const CONFIG = {
72
72
  LIVEKIT_URL: "wss://livekit.longvan.vn",
73
73
  PARTICIPANTS_API_URL: "https://call.longvan.vn/api/livekit/room/participants",
74
74
  API_BASE_URL: "https://portal.longvan.vn",
75
- BOOKING_API_URL: "https://portal.longvan.vn/dynamic-collection/public/v2/schedule/createBooking",
75
+ BOOKING_API_URL: "https://portal.longvan.vn/dynamic-collection/public/v2/schedule/actionBooking",
76
76
  USER_GRAPHQL: "https://user.longvan.vn/user-gateway/graphql",
77
77
  CRM_GRAPHQL: "https://crm.longvan.vn/authorization-gateway/graphql",
78
78
  PRODUCT_GRAPHQL: "https://product-service.longvan.vn/product-service/graphql",
79
79
  DOCTOR_STATUS_API_URL: "https://portal.longvan.vn/dynamic-collection/public/v2/schedules/telenow",
80
- TELE_NOW_ORG: "TRUE_DOC"
80
+ TELE_NOW_ORG: null
81
81
  };
82
82
  class ApiService {
83
83
  constructor(config) {
@@ -256,20 +256,41 @@ class ApiService {
256
256
  }
257
257
  }
258
258
  async createBooking(bookingData) {
259
- const { partnerId, ownerId, employeeId, startDateExpect, appointmentMode, clinicId } = bookingData;
260
- const baseUrl = this.config.bookingApiUrl || CONFIG.BOOKING_API_URL;
261
- const cleanBaseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
262
- const url2 = `${cleanBaseUrl}/${clinicId}`;
259
+ const url2 = this.config.bookingApiUrl || CONFIG.BOOKING_API_URL;
260
+ const {
261
+ partnerId,
262
+ ownerId,
263
+ employeeId,
264
+ startDateExpect,
265
+ endDateExpect,
266
+ appointmentMode = "ONLINE",
267
+ scheduleMode = "TELENOW",
268
+ resourceId,
269
+ resourceType = "DOCTOR",
270
+ serviceOperatorId,
271
+ note,
272
+ subscription,
273
+ serviceId,
274
+ orderId
275
+ } = bookingData;
263
276
  const payload = {
264
277
  partnerId,
265
278
  ownerId,
266
279
  employeeId,
267
280
  startDateExpect,
268
- endDateExpect: bookingData.endDateExpect,
269
- // Pass through if provided
270
- appointmentMode
281
+ endDateExpect,
282
+ appointmentMode,
283
+ scheduleMode,
284
+ resourceId: resourceId || employeeId,
285
+ // Default to employeeId if not provided
286
+ resourceType,
287
+ serviceOperatorId,
288
+ note
271
289
  };
272
- console.log("[ApiService] Creating background booking:", { url: url2, payload });
290
+ if (subscription) payload.subscription = subscription;
291
+ if (serviceId) payload.serviceId = serviceId;
292
+ if (orderId) payload.orderId = orderId;
293
+ console.log("[ApiService] Creating Booking (ActionBooking):", { url: url2, payload });
273
294
  try {
274
295
  const res = await fetch(url2, {
275
296
  method: "POST",
@@ -280,11 +301,11 @@ class ApiService {
280
301
  body: JSON.stringify(payload)
281
302
  });
282
303
  if (!res.ok) {
283
- console.warn("[ApiService] Background booking request failed:", res.status, res.statusText);
304
+ console.warn("[ApiService] ActionBooking request failed:", res.status, res.statusText);
284
305
  return null;
285
306
  }
286
307
  const json = await res.json();
287
- console.log("[ApiService] Background booking response:", json);
308
+ console.log("[ApiService] ActionBooking response:", json);
288
309
  return json;
289
310
  } catch (e2) {
290
311
  console.error("[ApiService] Error initiating booking:", e2);
@@ -682,7 +703,7 @@ const consultationHtml = `<div class="td-consultation-modal">\r
682
703
  </aside>\r
683
704
  </div>\r
684
705
  `;
685
- const patientFormHtml = '<div class="td-overlay-wrapper">\r\n <div class="td-modal-centered" id="td-patient-form">\r\n <header class="td-header">\r\n <div class="td-header-title">\r\n <div class="td-icon-box">\r\n <span class="material-symbols-outlined">medical_services</span>\r\n </div>\r\n <span>Tư vấn trực tuyến</span>\r\n </div>\r\n <button class="td-close-btn" id="td-form-close">\r\n <span class="material-symbols-outlined">close</span>\r\n </button>\r\n </header>\r\n\r\n <div class="td-content">\r\n <div class="td-form-container">\r\n <div class="td-form-page-header">\r\n <h1 class="td-form-title">Kết nối với Bác sĩ</h1>\r\n <p class="td-form-desc">Vui lòng nhập thông tin để bắt đầu tư vấn trực tuyến.</p>\r\n </div>\r\n\r\n <form class="td-form" id="td-info-form">\r\n <div class="td-input-group">\r\n <label class="td-label">Họ và tên <span style="color: red;">*</span></label>\r\n <input type="text" class="td-input" name="name" placeholder="VD: Nguyễn Văn A" required />\r\n </div>\r\n\r\n <div class="td-input-group">\r\n <label class="td-label">Số điện thoại <span style="color: red;">*</span></label>\r\n <div style="position: relative;">\r\n <input type="tel" class="td-input" name="phone" placeholder="VD: 0912 345 678" required />\r\n <!-- <div\r\n style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--td-primary);">\r\n <span class="material-symbols-outlined" style="font-size: 20px;">smartphone</span>\r\n </div> -->\r\n </div>\r\n </div>\r\n\r\n <!-- <div class="td-input-group">\r\n <label class="td-label">Triệu chứng của bạn?</label>\r\n <div class="td-chips-group">\r\n <button type="button" class="td-chip active">Sốt <span class="material-symbols-outlined"\r\n style="font-size: 16px;">check</span></button>\r\n <button type="button" class="td-chip">Ho / Cảm cúm</button>\r\n <button type="button" class="td-chip">Đau đầu</button>\r\n <button type="button" class="td-chip">Đau bụng</button>\r\n <button type="button" class="td-chip">Đau bụng</button>\r\n </div>\r\n </div> -->\r\n\r\n <!-- <div class="td-input-group">\r\n <label class="td-label">Mô tả thêm <span\r\n style="color: var(--td-text-sub); font-weight: 400;">(Tùy\r\n chọn)</span></label>\r\n <textarea class="td-textarea" name="symptoms"\r\n placeholder="Bạn đang cảm thấy như thế nào?"></textarea>\r\n </div> -->\r\n\r\n <div style="margin-top: 8px;">\r\n <button type="submit" class="td-btn td-btn-primary" id="td-form-submit">\r\n Bắt đầu tư vấn\r\n <span class="material-symbols-outlined" style="margin-left: 8px;">arrow_forward</span>\r\n </button>\r\n </div>\r\n\r\n <div class="td-secure-note">\r\n <span class="material-symbols-outlined" style="font-size: 14px;">lock</span>\r\n Thông tin y tế được bảo mật 100%\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n </div>\r\n</div>';
706
+ const patientFormHtml = '<div class="td-overlay-wrapper">\r\n <div class="td-modal-centered" id="td-patient-form">\r\n <header class="td-header">\r\n <div class="td-header-title">\r\n <div class="td-icon-box">\r\n <span class="material-symbols-outlined">medical_services</span>\r\n </div>\r\n <span>Tư vấn trực tuyến</span>\r\n </div>\r\n <button class="td-close-btn" id="td-form-close">\r\n <span class="material-symbols-outlined">close</span>\r\n </button>\r\n </header>\r\n\r\n <div class="td-content">\r\n <div class="td-form-container">\r\n <div class="td-form-page-header">\r\n <h1 class="td-form-title">Kết nối với Bác sĩ</h1>\r\n <p class="td-form-desc">Vui lòng nhập thông tin để bắt đầu tư vấn trực tuyến.</p>\r\n </div>\r\n\r\n <form class="td-form" id="td-info-form">\r\n <div class="td-input-group">\r\n <label class="td-label">Họ và tên <span style="color: red;">*</span></label>\r\n <input type="text" class="td-input" name="name" placeholder="VD: Nguyễn Văn A" required />\r\n </div>\r\n\r\n <div class="td-input-group">\r\n <label class="td-label">Số điện thoại <span style="color: red;">*</span></label>\r\n <div style="position: relative;">\r\n <input type="tel" class="td-input" name="phone" placeholder="VD: 0912 345 678" required />\r\n <!-- <div\r\n style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--td-primary);">\r\n <span class="material-symbols-outlined" style="font-size: 20px;">smartphone</span>\r\n </div> -->\r\n </div>\r\n </div>\r\n\r\n <!-- <div class="td-input-group">\r\n <label class="td-label">Triệu chứng của bạn?</label>\r\n <div class="td-chips-group">\r\n <button type="button" class="td-chip active">Sốt <span class="material-symbols-outlined"\r\n style="font-size: 16px;">check</span></button>\r\n <button type="button" class="td-chip">Ho / Cảm cúm</button>\r\n <button type="button" class="td-chip">Đau đầu</button>\r\n <button type="button" class="td-chip">Đau bụng</button>\r\n <button type="button" class="td-chip">Đau bụng</button>\r\n </div>\r\n </div> -->\r\n\r\n <div class="td-input-group">\r\n <label class="td-label">Mô tả thêm <span\r\n style="color: var(--td-text-sub); font-weight: 400;">(Tùy\r\n chọn)</span></label>\r\n <textarea class="td-textarea" name="note"\r\n placeholder="Bạn đang cảm thấy như thế nào?"></textarea>\r\n </div>\r\n\r\n <div style="margin-top: 8px;">\r\n <button type="submit" class="td-btn td-btn-primary" id="td-form-submit">\r\n Bắt đầu tư vấn\r\n <span class="material-symbols-outlined" style="margin-left: 8px;">arrow_forward</span>\r\n </button>\r\n </div>\r\n\r\n <div class="td-secure-note">\r\n <span class="material-symbols-outlined" style="font-size: 14px;">lock</span>\r\n Thông tin y tế được bảo mật 100%\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n </div>\r\n</div>';
686
707
  const postConsultationHtml = '<div class="td-overlay-wrapper">\r\n <div class="td-modal-centered" id="td-summary-view" style="max-width: 600px;">\r\n <header class="td-header">\r\n <h2 class="td-header-title" style="font-size: 16px;">Tổng kết tư vấn</h2>\r\n <button class="td-close-btn" id="td-summary-close">\r\n <span class="material-symbols-outlined">close</span>\r\n </button>\r\n </header>\r\n\r\n <div class="td-content">\r\n <div class="td-summary-container">\r\n <!-- Success Header -->\r\n <div class="td-success-header">\r\n <div class="td-success-icon">\r\n <span class="material-symbols-outlined" style="font-size: 32px;">check_circle</span>\r\n </div>\r\n <h3 class="td-summary-title">Tư vấn Hoàn tất</h3>\r\n <p class="td-summary-desc">Cuộc hẹn đã được ghi nhận vào hồ sơ sức khỏe của bạn.</p>\r\n </div>\r\n\r\n <!-- Info Grid -->\r\n <div class="td-info-grid">\r\n <div class="td-info-card">\r\n <div class="td-info-label">MÃ CUỘC HẸN</div>\r\n <div class="td-info-value">{{bookingCode}}</div>\r\n </div>\r\n <div class="td-info-card">\r\n <div class="td-info-label">THỜI GIAN</div>\r\n <div class="td-info-value">{{visitTime}}</div>\r\n </div>\r\n </div>\r\n\r\n <div id="td-prescription-container">\r\n {{prescriptionList}}\r\n </div>\r\n\r\n <!-- Footer Actions Removed as requested -->\r\n <!-- <div class="td-footer-actions"></div> -->\r\n </div>\r\n </div>\r\n </div>\r\n</div>';
687
708
  const inlineTriggerHtml = `<style>\r
688
709
  @keyframes td-pulse-animation {\r
@@ -29286,22 +29307,31 @@ class ClinicWidget {
29286
29307
  const teleNowSlot = state.teleNowSlot || {};
29287
29308
  const formatDate = (val) => {
29288
29309
  if (!val) return null;
29289
- const d = new Date(val);
29310
+ const numVal = !isNaN(Number(val)) && typeof val !== "boolean" && String(val).trim() !== "" ? Number(val) : val;
29311
+ const d = new Date(numVal);
29290
29312
  if (isNaN(d.getTime())) return null;
29291
29313
  const offsetMs = d.getTimezoneOffset() * 60 * 1e3;
29292
29314
  const local = new Date(d.getTime() - offsetMs);
29293
- return local.toISOString().slice(0, 16);
29315
+ return local.toISOString().slice(0, 19);
29294
29316
  };
29295
- const startDateExpect = formatDate(teleNowSlot.startDateExpect) || formatDate(/* @__PURE__ */ new Date());
29317
+ const startDateExpect = formatDate(teleNowSlot.startDateExpect);
29296
29318
  const endDateExpect = formatDate(teleNowSlot.endDateExpect);
29319
+ const doctorId = ((_a = state.doctor) == null ? void 0 : _a.id) || this.config.doctorId;
29297
29320
  const bookingRes = await this.api.createBooking({
29298
29321
  partnerId: orgId,
29299
29322
  ownerId: customerId,
29300
- employeeId: ((_a = state.doctor) == null ? void 0 : _a.id) || this.config.doctorId,
29323
+ employeeId: doctorId,
29301
29324
  startDateExpect,
29302
29325
  endDateExpect,
29303
- appointmentMode: "ONLINE",
29304
- clinicId: orgId
29326
+ appointmentMode: this.config.appointmentMode || "ONLINE",
29327
+ scheduleMode: this.config.scheduleMode || "TELENOW",
29328
+ resourceId: this.config.resourceId || doctorId,
29329
+ resourceType: this.config.resourceType || "DOCTOR",
29330
+ serviceOperatorId: this.config.serviceOperator,
29331
+ subscription: this.config.subscription,
29332
+ serviceId: this.config.serviceId,
29333
+ orderId: this.config.orderId,
29334
+ note: formData.get("note") || ""
29305
29335
  });
29306
29336
  console.log("[Clinic-Connect] [API] Booking Response Received:", bookingRes);
29307
29337
  if (bookingRes && bookingRes.id) {
@@ -29622,7 +29652,11 @@ class ClinicWidget {
29622
29652
  var _a, _b;
29623
29653
  const state = store.getState();
29624
29654
  const doctorId = ((_a = state.doctor) == null ? void 0 : _a.id) || this.config.doctorId;
29625
- const org = ((_b = state.config) == null ? void 0 : _b.teleNowOrg) || CONFIG.TELE_NOW_ORG;
29655
+ const org = this.config.serviceOperator || ((_b = state.config) == null ? void 0 : _b.teleNowOrg) || CONFIG.TELE_NOW_ORG;
29656
+ if (!org) {
29657
+ console.warn("[Clinic-Connect] [Polling] Missing service-operator (teleNowOrg). Status check skipped.");
29658
+ return;
29659
+ }
29626
29660
  if (!doctorId) return;
29627
29661
  try {
29628
29662
  const statusInfo = await this.api.getDoctorStatus(org, doctorId);
@@ -30254,6 +30288,14 @@ class ClinicWidget {
30254
30288
  const doctorAvatar = currentScript ? currentScript.getAttribute("data-doctor-avatar") : null;
30255
30289
  const doctorName = currentScript ? currentScript.getAttribute("data-doctor-name") : null;
30256
30290
  const doctorSpecialty = currentScript ? currentScript.getAttribute("data-doctor-specialty") : null;
30291
+ const serviceOperator = currentScript ? currentScript.getAttribute("data-service-operator") : null;
30292
+ const appointmentMode = currentScript ? currentScript.getAttribute("data-appointment-mode") : null;
30293
+ const scheduleMode = currentScript ? currentScript.getAttribute("data-schedule-mode") : null;
30294
+ const resourceId = currentScript ? currentScript.getAttribute("data-resource-id") : null;
30295
+ const resourceType = currentScript ? currentScript.getAttribute("data-resource-type") : null;
30296
+ const subscription = currentScript ? currentScript.getAttribute("data-subscription") : null;
30297
+ const serviceId = currentScript ? currentScript.getAttribute("data-service-id") : null;
30298
+ const orderId = currentScript ? currentScript.getAttribute("data-order-id") : null;
30257
30299
  const config = {
30258
30300
  widgetId,
30259
30301
  triggerElementId,
@@ -30266,6 +30308,14 @@ class ClinicWidget {
30266
30308
  doctorAvatar,
30267
30309
  doctorName,
30268
30310
  doctorSpecialty,
30311
+ serviceOperator,
30312
+ appointmentMode,
30313
+ scheduleMode,
30314
+ resourceId,
30315
+ resourceType,
30316
+ subscription,
30317
+ serviceId,
30318
+ orderId,
30269
30319
  scriptElement: currentScript
30270
30320
  };
30271
30321
  function initWidget() {