clinic-connect-widget 1.1.9 → 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
@@ -49,6 +49,15 @@ You can pre-select a doctor or product context by passing additional attributes.
49
49
 
50
50
  data-product-id="YOUR_PRODUCT_ID"
51
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
+
52
61
  data-locale="vi">
53
62
  </script>
54
63
  ```
@@ -64,14 +73,21 @@ The widget is highly configurable via `data-` attributes on the script tag.
64
73
  | `data-service-operator` | The Service Operator ID (e.g., 'TRUE_DOC'). | **Yes** | - |
65
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` |
66
75
  | `data-locale` | Language code (e.g., 'vi', 'en'). | No | `vi` |
67
- | `data-store-id` | The Store ID associated with the booking system. | No | `null` |
68
- | `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` |
69
78
  | `data-doctor-id` | Pre-select a specific doctor context. | No | `null` |
70
79
  | `data-doctor-name` | Custom name for the doctor (Dynamic). | No | `null` |
71
80
  | `data-doctor-specialty` | Custom specialty for the doctor (Dynamic). | No | `null` |
72
81
  | `data-doctor-avatar` | Custom avatar URL for the doctor (Dynamic). | No | `null` |
73
82
  | `data-product-id` | Pre-select a specific product or service package. | No | `null` |
74
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` |
75
91
 
76
92
  ## Client Hooks (API)
77
93
 
@@ -145,7 +161,7 @@ To verify the final build behaves as expected:
145
161
 
146
162
  ## Project Structure
147
163
 
148
- * `src/api/`: Handles API simulation and fetching widget configuration.
164
+ * `src/api/`: Handles API integration, parsing customer data, calling ActionBooking API, etc.
149
165
  * `src/core/`: Main widget logic (`ClinicWidget` class), lifecycle management, and event binding.
150
166
  * `src/state/`: Centralized state management (Store pattern).
151
167
  * `src/ui/`: Contains HTML Templates (`templates.js`) and CSS Styles (`styles.js`).
@@ -72,7 +72,7 @@ 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",
@@ -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) {
@@ -30259,6 +30289,13 @@ class ClinicWidget {
30259
30289
  const doctorName = currentScript ? currentScript.getAttribute("data-doctor-name") : null;
30260
30290
  const doctorSpecialty = currentScript ? currentScript.getAttribute("data-doctor-specialty") : null;
30261
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;
30262
30299
  const config = {
30263
30300
  widgetId,
30264
30301
  triggerElementId,
@@ -30272,6 +30309,13 @@ class ClinicWidget {
30272
30309
  doctorName,
30273
30310
  doctorSpecialty,
30274
30311
  serviceOperator,
30312
+ appointmentMode,
30313
+ scheduleMode,
30314
+ resourceId,
30315
+ resourceType,
30316
+ subscription,
30317
+ serviceId,
30318
+ orderId,
30275
30319
  scriptElement: currentScript
30276
30320
  };
30277
30321
  function initWidget() {