@mylikita/booking-widget 0.1.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.
@@ -0,0 +1,679 @@
1
+ /*! @mylikita/booking-widget v0.1.0 | MIT */
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_THEME: () => DEFAULT_THEME,
24
+ STATUS_COPY: () => STATUS_COPY,
25
+ TERMINAL_STATUSES: () => TERMINAL_STATUSES,
26
+ createBooking: () => createBooking,
27
+ createBookingWidget: () => createBookingWidget,
28
+ fetchProviders: () => fetchProviders,
29
+ fetchStatus: () => fetchStatus,
30
+ newExternalRef: () => newExternalRef,
31
+ pollStatus: () => pollStatus,
32
+ resolveTheme: () => resolveTheme,
33
+ statusCopy: () => statusCopy
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/client.js
38
+ async function createBooking({ relayUrl, websiteKey, payload, signal }) {
39
+ const res = await fetch(`${trimUrl(relayUrl)}/v1/bookings`, {
40
+ method: "POST",
41
+ headers: {
42
+ "Content-Type": "application/json",
43
+ Authorization: `Bearer ${websiteKey}`
44
+ },
45
+ body: JSON.stringify(payload),
46
+ signal
47
+ });
48
+ const body = await readJson(res);
49
+ if (res.status === 409) {
50
+ return { ok: true, duplicate: true, booking_ref: body?.booking_ref, error: body?.message };
51
+ }
52
+ if (!res.ok) {
53
+ throw apiError(res.status, body, "create");
54
+ }
55
+ return { ok: true, duplicate: false, booking_ref: body?.booking_ref, status: body?.status || "pending_confirmation" };
56
+ }
57
+ async function fetchProviders({ relayUrl, websiteKey, signal }) {
58
+ const res = await fetch(`${trimUrl(relayUrl)}/v1/providers`, {
59
+ headers: { Authorization: `Bearer ${websiteKey}` },
60
+ signal
61
+ });
62
+ const body = await readJson(res);
63
+ if (!res.ok) throw apiError(res.status, body, "providers");
64
+ const list = Array.isArray(body?.providers) ? body.providers : [];
65
+ return list.map((p) => ({
66
+ external_id: p.external_id,
67
+ name: p.name || p.external_id,
68
+ specialty: p.specialty || null,
69
+ module: p.module || "general"
70
+ }));
71
+ }
72
+ async function fetchStatus({ relayUrl, websiteKey, bookingRef, signal }) {
73
+ const res = await fetch(`${trimUrl(relayUrl)}/v1/bookings/${encodeURIComponent(bookingRef)}`, {
74
+ headers: { Authorization: `Bearer ${websiteKey}` },
75
+ signal
76
+ });
77
+ const body = await readJson(res);
78
+ if (!res.ok) throw apiError(res.status, body, "status");
79
+ return { booking_ref: body?.booking_ref, status: body?.status || "pending_confirmation", appt_ref: body?.appt_ref || null };
80
+ }
81
+ async function pollStatus(client, { intervalMs = 5e3, maxTries = 12, signal } = {}) {
82
+ for (let i = 0; i < maxTries; i++) {
83
+ if (signal?.aborted) return { status: "aborted", resolved: false, data: null };
84
+ const data = await client();
85
+ if (data.status !== "pending_confirmation") {
86
+ return { status: data.status, resolved: true, data };
87
+ }
88
+ if (i < maxTries - 1) await sleep(intervalMs, signal);
89
+ }
90
+ return { status: "pending_confirmation", resolved: false, data: null };
91
+ }
92
+ function newExternalRef() {
93
+ return `BK-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
94
+ }
95
+ function trimUrl(url) {
96
+ return String(url || "").replace(/\/+$/, "");
97
+ }
98
+ function apiError(status, body, phase) {
99
+ const err = new Error(body?.message || body?.error || `Relay ${phase} failed (HTTP ${status})`);
100
+ err.code = body?.error || "http_error";
101
+ err.status = status;
102
+ return err;
103
+ }
104
+ async function readJson(res) {
105
+ try {
106
+ return await res.json();
107
+ } catch (_) {
108
+ return null;
109
+ }
110
+ }
111
+ function sleep(ms, signal) {
112
+ return new Promise((resolve) => {
113
+ if (signal?.aborted) return resolve();
114
+ const onAbort = () => {
115
+ clearTimeout(t);
116
+ resolve();
117
+ };
118
+ const t = setTimeout(() => {
119
+ signal?.removeEventListener("abort", onAbort);
120
+ resolve();
121
+ }, ms);
122
+ signal?.addEventListener("abort", onAbort, { once: true });
123
+ });
124
+ }
125
+
126
+ // src/state.js
127
+ var STATUS_COPY = {
128
+ pending_confirmation: {
129
+ title: "Request received",
130
+ message: "We've received your booking request \u2014 we'll confirm shortly.",
131
+ kind: "info"
132
+ },
133
+ confirmed: {
134
+ title: "Appointment confirmed",
135
+ message: "Your appointment is confirmed. See you at the clinic!",
136
+ kind: "success"
137
+ },
138
+ cancelled: {
139
+ title: "Appointment cancelled",
140
+ message: "This appointment was cancelled. Please contact the clinic if this was unexpected.",
141
+ kind: "danger"
142
+ },
143
+ rescheduled: {
144
+ title: "Appointment rescheduled",
145
+ message: "This appointment was moved \u2014 the new time was sent to you.",
146
+ kind: "info"
147
+ },
148
+ no_show: {
149
+ title: "Missed appointment",
150
+ message: "This appointment was marked as missed.",
151
+ kind: "danger"
152
+ },
153
+ expired: {
154
+ title: "Request expired",
155
+ message: "This booking request expired \u2014 please call the clinic to book.",
156
+ kind: "danger"
157
+ },
158
+ // Widget-internal: the poll itself failed (network, rotated key, etc.).
159
+ poll_error: {
160
+ title: "Something went wrong",
161
+ message: "We could not check your booking right now. Please try again shortly.",
162
+ kind: "danger"
163
+ }
164
+ };
165
+ function statusCopy(status) {
166
+ return STATUS_COPY[status] || STATUS_COPY.pending_confirmation;
167
+ }
168
+ var TERMINAL_STATUSES = ["confirmed", "cancelled", "rescheduled", "no_show", "expired"];
169
+
170
+ // src/theme.js
171
+ var DEFAULT_THEME = {
172
+ primary: "#0d6efd",
173
+ primaryDark: "#0b5ed7",
174
+ primaryText: "#ffffff",
175
+ bg: "#ffffff",
176
+ text: "#1e293b",
177
+ muted: "#64748b",
178
+ border: "#e2e8f0",
179
+ danger: "#dc3545",
180
+ success: "#15803d",
181
+ radius: 10,
182
+ font: "system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif"
183
+ };
184
+ function resolveTheme(theme = {}) {
185
+ const t = { ...DEFAULT_THEME, ...theme || {} };
186
+ return {
187
+ "--mlw-primary": t.primary,
188
+ "--mlw-primary-dark": t.primaryDark,
189
+ "--mlw-primary-text": t.primaryText,
190
+ "--mlw-bg": t.bg,
191
+ "--mlw-text": t.text,
192
+ "--mlw-muted": t.muted,
193
+ "--mlw-border": t.border,
194
+ "--mlw-danger": t.danger,
195
+ "--mlw-success": t.success,
196
+ "--mlw-radius": `${t.radius}px`,
197
+ "--mlw-font": t.font
198
+ };
199
+ }
200
+
201
+ // src/styles.js
202
+ var STYLES = `
203
+ .mylikita-widget {
204
+ --mlw-primary: #0d6efd;
205
+ --mlw-primary-dark: #0b5ed7;
206
+ --mlw-primary-text: #ffffff;
207
+ --mlw-bg: #ffffff;
208
+ --mlw-text: #1e293b;
209
+ --mlw-muted: #64748b;
210
+ --mlw-border: #e2e8f0;
211
+ --mlw-danger: #dc3545;
212
+ --mlw-success: #15803d;
213
+ --mlw-radius: 10px;
214
+ --mlw-font: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
215
+ font-family: var(--mlw-font);
216
+ color: var(--mlw-text);
217
+ background: var(--mlw-bg);
218
+ border: 1px solid var(--mlw-border);
219
+ border-radius: calc(var(--mlw-radius) + 2px);
220
+ padding: 22px;
221
+ max-width: 480px;
222
+ box-sizing: border-box;
223
+ line-height: 1.5;
224
+ }
225
+ .mylikita-widget *,
226
+ .mylikita-widget *::before,
227
+ .mylikita-widget *::after { box-sizing: border-box; }
228
+
229
+ .mylikita-widget__title {
230
+ font-size: 18px;
231
+ font-weight: 700;
232
+ margin: 0 0 4px;
233
+ color: var(--mlw-text);
234
+ }
235
+ .mylikita-widget__subtitle {
236
+ font-size: 13px;
237
+ color: var(--mlw-muted);
238
+ margin: 0 0 16px;
239
+ }
240
+
241
+ .mylikita-widget__field { margin-bottom: 12px; }
242
+ .mylikita-widget__label {
243
+ display: block;
244
+ font-size: 12px;
245
+ font-weight: 600;
246
+ color: var(--mlw-text);
247
+ margin-bottom: 5px;
248
+ }
249
+ .mylikita-widget__label .req { color: var(--mlw-danger); }
250
+ .mylikita-widget__input,
251
+ .mylikita-widget__select,
252
+ .mylikita-widget__textarea {
253
+ width: 100%;
254
+ font: inherit;
255
+ font-size: 14px;
256
+ color: var(--mlw-text);
257
+ background: var(--mlw-bg);
258
+ border: 1px solid var(--mlw-border);
259
+ border-radius: var(--mlw-radius);
260
+ padding: 9px 11px;
261
+ outline: none;
262
+ transition: border-color .15s ease, box-shadow .15s ease;
263
+ }
264
+ .mylikita-widget__input:focus,
265
+ .mylikita-widget__select:focus,
266
+ .mylikita-widget__textarea:focus {
267
+ border-color: var(--mlw-primary);
268
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--mlw-primary) 22%, transparent);
269
+ }
270
+ .mylikita-widget__row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
271
+ @media (max-width: 420px) { .mylikita-widget__row { grid-template-columns: 1fr; } }
272
+
273
+ .mylikita-widget__error {
274
+ display: none;
275
+ font-size: 13px;
276
+ color: var(--mlw-danger);
277
+ background: color-mix(in srgb, var(--mlw-danger) 8%, transparent);
278
+ border: 1px solid color-mix(in srgb, var(--mlw-danger) 35%, transparent);
279
+ border-radius: var(--mlw-radius);
280
+ padding: 9px 12px;
281
+ margin-bottom: 12px;
282
+ }
283
+ .mylikita-widget__error.visible { display: block; }
284
+
285
+ .mylikita-widget__submit {
286
+ width: 100%;
287
+ font: inherit;
288
+ font-size: 14px;
289
+ font-weight: 600;
290
+ color: var(--mlw-primary-text);
291
+ background: var(--mlw-primary);
292
+ border: none;
293
+ border-radius: var(--mlw-radius);
294
+ padding: 11px 16px;
295
+ cursor: pointer;
296
+ transition: background .15s ease, transform .05s ease;
297
+ }
298
+ .mylikita-widget__submit:hover { background: var(--mlw-primary-dark); }
299
+ .mylikita-widget__submit:active { transform: translateY(1px); }
300
+ .mylikita-widget__submit:disabled { opacity: .6; cursor: wait; }
301
+
302
+ .mylikita-widget__hint { font-size: 12px; color: var(--mlw-muted); margin: 8px 0 0; }
303
+
304
+ /* status view */
305
+ .mylikita-widget__status { text-align: center; padding: 8px 4px; }
306
+ .mylikita-widget__status-icon {
307
+ width: 46px; height: 46px;
308
+ border-radius: 50%;
309
+ display: inline-flex; align-items: center; justify-content: center;
310
+ font-size: 22px; margin-bottom: 10px;
311
+ }
312
+ .mylikita-widget__status-icon.info { background: color-mix(in srgb, var(--mlw-primary) 12%, transparent); }
313
+ .mylikita-widget__status-icon.success { background: color-mix(in srgb, var(--mlw-success) 14%, transparent); }
314
+ .mylikita-widget__status-icon.danger { background: color-mix(in srgb, var(--mlw-danger) 12%, transparent); }
315
+ .mylikita-widget__status-title { font-size: 16px; font-weight: 700; margin: 0 0 4px; }
316
+ .mylikita-widget__status-message { font-size: 13px; color: var(--mlw-muted); margin: 0 0 14px; }
317
+ .mylikita-widget__status-ref { font-size: 12px; color: var(--mlw-muted); margin: 0 0 14px; }
318
+
319
+ .mylikita-widget__spinner {
320
+ width: 18px; height: 18px;
321
+ display: inline-block;
322
+ border: 2px solid color-mix(in srgb, var(--mlw-primary-text) 40%, transparent);
323
+ border-top-color: var(--mlw-primary-text);
324
+ border-radius: 50%;
325
+ animation: mylikita-widget-spin .7s linear infinite;
326
+ vertical-align: -3px;
327
+ margin-right: 7px;
328
+ }
329
+ @keyframes mylikita-widget-spin { to { transform: rotate(360deg); } }
330
+
331
+ .mylikita-widget__link-btn {
332
+ background: none;
333
+ border: 1px solid var(--mlw-border);
334
+ border-radius: var(--mlw-radius);
335
+ color: var(--mlw-text);
336
+ font: inherit;
337
+ font-size: 13px;
338
+ padding: 8px 14px;
339
+ cursor: pointer;
340
+ }
341
+ .mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
342
+ `;
343
+
344
+ // src/widget.js
345
+ var DEFAULT_TEXT = {
346
+ title: "Book an appointment",
347
+ subtitle: "Request a slot and we will confirm shortly.",
348
+ name: "Full name",
349
+ phone: "Phone number",
350
+ email: "Email address",
351
+ provider: "Preferred doctor (optional)",
352
+ noPreference: "No preference",
353
+ service: "Service (optional)",
354
+ datetime: "Preferred date & time",
355
+ visitType: "Appointment type",
356
+ visitPhysical: "In person",
357
+ visitTelemedicine: "Video call",
358
+ visitHome: "Home visit",
359
+ notes: "Notes (optional)",
360
+ submit: "Request appointment",
361
+ submitting: "Submitting\u2026",
362
+ bookAnother: "Book another appointment",
363
+ requiredPhoneOrEmail: "Please provide a phone number or an email address.",
364
+ requiredName: "Please enter your name.",
365
+ requiredDatetime: "Please choose a date and time.",
366
+ networkError: "Could not reach the booking service. Please try again.",
367
+ rateLimited: "Too many requests \u2014 please wait a moment and try again."
368
+ };
369
+ var STYLE_ID = "mylikita-widget-styles";
370
+ function createBookingWidget(element, options = {}) {
371
+ if (!element) throw new Error("createBookingWidget: a container element is required");
372
+ const opts = {
373
+ relayUrl: options.relayUrl,
374
+ websiteKey: options.websiteKey,
375
+ facilityId: options.facilityId,
376
+ providers: options.providers || [],
377
+ services: options.services || [],
378
+ // Phase C2/C3: fetch the facility's mapped provider list from the relay
379
+ // (GET /v1/providers) on mount and populate the doctor dropdown. The
380
+ // static `providers` option, when given, always wins and skips the fetch.
381
+ loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
382
+ pollIntervalMs: options.pollIntervalMs ?? 5e3,
383
+ maxTries: options.maxTries ?? 12,
384
+ text: { ...DEFAULT_TEXT, ...options.text || {} },
385
+ theme: options.theme || {},
386
+ onStatus: typeof options.onStatus === "function" ? options.onStatus : null,
387
+ onError: typeof options.onError === "function" ? options.onError : null,
388
+ onBooking: typeof options.onBooking === "function" ? options.onBooking : null,
389
+ externalRef: typeof options.externalRef === "function" ? options.externalRef : newExternalRef
390
+ };
391
+ if (!opts.relayUrl || !opts.websiteKey || !opts.facilityId) {
392
+ throw new Error("createBookingWidget: relayUrl, websiteKey and facilityId are required");
393
+ }
394
+ injectStyles();
395
+ const t = opts.text;
396
+ const root = element;
397
+ root.classList.add("mylikita-widget");
398
+ applyTheme(root, opts.theme);
399
+ let alive = true;
400
+ let submitting = false;
401
+ let pollCtrl = null;
402
+ const instanceId = Math.random().toString(36).slice(2, 8);
403
+ const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
404
+ const form = el("form", { className: "mylikita-widget__form" });
405
+ const title = el("h3", { className: "mylikita-widget__title", text: t.title });
406
+ const subtitle = el("p", { className: "mylikita-widget__subtitle", text: t.subtitle });
407
+ const errorBox = el("div", { className: "mylikita-widget__error", attrs: { role: "alert" } });
408
+ const name = fieldText("name", t.name, { required: true });
409
+ const phoneEmail = el("div", { className: "mylikita-widget__row" });
410
+ const phone = fieldText("phone", t.phone, { type: "tel", inputmode: "tel" });
411
+ const email = fieldText("email", t.email, { type: "email" });
412
+ phoneEmail.append(phone.wrap, email.wrap);
413
+ const provider = fieldSelect("provider", t.provider, [
414
+ { value: "", label: t.noPreference },
415
+ ...opts.providers.map((p) => ({ value: p.external_id, label: p.label || p.name || p.external_id }))
416
+ ]);
417
+ function setProviderList(list) {
418
+ const current = provider.input.value;
419
+ provider.input.replaceChildren();
420
+ const noPref = document.createElement("option");
421
+ noPref.value = "";
422
+ noPref.textContent = t.noPreference;
423
+ provider.input.append(noPref);
424
+ for (const p of list || []) {
425
+ const opt = document.createElement("option");
426
+ opt.value = p.external_id;
427
+ opt.textContent = p.label || p.name || p.external_id;
428
+ provider.input.append(opt);
429
+ }
430
+ if (current) provider.input.value = current;
431
+ }
432
+ const service = opts.services.length ? fieldSelect("service", t.service, [
433
+ { value: "", label: "\u2014" },
434
+ ...opts.services.map((s) => ({ value: s, label: s }))
435
+ ]) : fieldText("service", t.service);
436
+ const datetime = fieldText("datetime", t.datetime, { type: "datetime-local", required: true });
437
+ datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
438
+ const visitType = fieldSelect("visitType", t.visitType, [
439
+ { value: "physical", label: t.visitPhysical },
440
+ { value: "telemedicine", label: t.visitTelemedicine },
441
+ { value: "home_visit", label: t.visitHome }
442
+ ]);
443
+ const notes = fieldText("notes", t.notes, { type: "textarea", maxlength: 500 });
444
+ const submitBtn = el("button", { className: "mylikita-widget__submit", type: "submit", text: t.submit });
445
+ const submitRow = el("div");
446
+ submitRow.append(submitBtn);
447
+ const hint = el("p", { className: "mylikita-widget__hint" });
448
+ form.append(errorBox, name.wrap, phoneEmail, provider.wrap, service.wrap, datetime.wrap, visitType.wrap, notes.wrap, submitRow, hint);
449
+ const statusView = el("div", { className: "mylikita-widget__status", attrs: { "aria-live": "polite" }, hidden: true });
450
+ form.addEventListener("submit", (e) => {
451
+ e.preventDefault();
452
+ if (submitting) return;
453
+ submit();
454
+ });
455
+ async function submit() {
456
+ submitting = true;
457
+ setError(null);
458
+ submitBtn.disabled = true;
459
+ submitBtn.textContent = t.submitting;
460
+ const payload = {
461
+ facility_id: opts.facilityId,
462
+ patient_name: name.input.value.trim(),
463
+ patient_phone: phone.input.value.trim(),
464
+ patient_email: email.input.value.trim(),
465
+ provider_external_id: provider.input.value || void 0,
466
+ service_name: service.input.value.trim() || void 0,
467
+ appt_datetime: datetime.input.value,
468
+ visit_type: visitType.input.value || "physical",
469
+ duration_mins: opts.durationMins || void 0,
470
+ notes: notes.input.value.trim() || void 0
471
+ };
472
+ if (!payload.patient_name) return fail(t.requiredName);
473
+ if (!payload.patient_phone && !payload.patient_email) return fail(t.requiredPhoneOrEmail);
474
+ if (!payload.appt_datetime || Number.isNaN(Date.parse(payload.appt_datetime))) return fail(t.requiredDatetime);
475
+ let external_ref = readStoredRef(refKey);
476
+ if (!external_ref) {
477
+ external_ref = opts.externalRef();
478
+ try {
479
+ sessionStorage.setItem(refKey, external_ref);
480
+ } catch (_) {
481
+ }
482
+ }
483
+ payload.external_ref = external_ref;
484
+ pollCtrl = new AbortController();
485
+ let bookingRef = null;
486
+ try {
487
+ const created = await createBooking({ relayUrl: opts.relayUrl, websiteKey: opts.websiteKey, payload, signal: pollCtrl.signal });
488
+ bookingRef = created.booking_ref;
489
+ if (created.duplicate) {
490
+ hint.textContent = "";
491
+ showStatus("pending_confirmation", bookingRef, "We found an existing booking request for this slot \u2014 checking it\u2026");
492
+ } else {
493
+ if (opts.onBooking) safeCall(opts.onBooking, created, payload);
494
+ showStatus("pending_confirmation", bookingRef, null);
495
+ }
496
+ startPoll(bookingRef);
497
+ } catch (err) {
498
+ if (!alive || err?.name === "AbortError") return;
499
+ const friendly = err.status === 429 ? t.rateLimited : err.message || t.networkError;
500
+ if (opts.onError) safeCall(opts.onError, err);
501
+ fail(friendly);
502
+ }
503
+ }
504
+ async function startPoll(bookingRef) {
505
+ let result;
506
+ try {
507
+ result = await pollStatus(
508
+ () => fetchStatus({ relayUrl: opts.relayUrl, websiteKey: opts.websiteKey, bookingRef, signal: pollCtrl.signal }),
509
+ { intervalMs: opts.pollIntervalMs, maxTries: opts.maxTries, signal: pollCtrl.signal }
510
+ );
511
+ } catch (err) {
512
+ if (!alive || err?.name === "AbortError") return;
513
+ if (opts.onError) safeCall(opts.onError, err);
514
+ renderStatus("poll_error", bookingRef, err.message || t.networkError);
515
+ submitting = false;
516
+ return;
517
+ }
518
+ if (!alive || result.status === "aborted") return;
519
+ if (opts.onStatus && result.data) safeCall(opts.onStatus, result.data);
520
+ if (result.resolved) {
521
+ try {
522
+ sessionStorage.removeItem(refKey);
523
+ } catch (_) {
524
+ }
525
+ renderStatus(result.status, bookingRef);
526
+ } else {
527
+ renderStatus("pending_confirmation", bookingRef);
528
+ }
529
+ submitting = false;
530
+ }
531
+ function showStatus(status, bookingRef, messageOverride) {
532
+ form.hidden = true;
533
+ title.hidden = true;
534
+ subtitle.hidden = true;
535
+ statusView.hidden = false;
536
+ renderStatus(status, bookingRef, messageOverride);
537
+ }
538
+ function renderStatus(status, bookingRef, messageOverride) {
539
+ const c = statusCopy(status);
540
+ const icon = el("div", { className: `mylikita-widget__status-icon ${c.kind}` });
541
+ icon.textContent = iconGlyph(c.kind);
542
+ const st = el("p", { className: "mylikita-widget__status-title", text: c.title });
543
+ const msg = el("p", { className: "mylikita-widget__status-message", text: messageOverride || c.message });
544
+ const ref = el("p", { className: "mylikita-widget__status-ref", text: bookingRef ? `Booking ref: ${bookingRef}` : "" });
545
+ const again = el("button", { className: "mylikita-widget__link-btn", type: "button", text: t.bookAnother });
546
+ again.addEventListener("click", () => reset());
547
+ statusView.replaceChildren(icon, st, msg, ref, again);
548
+ }
549
+ function fail(message) {
550
+ submitting = false;
551
+ submitBtn.disabled = false;
552
+ submitBtn.textContent = t.submit;
553
+ setError(message);
554
+ }
555
+ function setError(message) {
556
+ errorBox.textContent = message || "";
557
+ errorBox.classList.toggle("visible", Boolean(message));
558
+ }
559
+ function reset() {
560
+ try {
561
+ sessionStorage.removeItem(`mylikita_ref_${opts.facilityId}`);
562
+ } catch (_) {
563
+ }
564
+ form.reset();
565
+ setError(null);
566
+ statusView.replaceChildren();
567
+ statusView.hidden = true;
568
+ form.hidden = false;
569
+ title.hidden = false;
570
+ subtitle.hidden = false;
571
+ hint.textContent = "";
572
+ submitting = false;
573
+ submitBtn.disabled = false;
574
+ submitBtn.textContent = t.submit;
575
+ datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
576
+ }
577
+ root.replaceChildren(title, subtitle, form, statusView);
578
+ let destroyProvidersFetch = null;
579
+ if (opts.loadProviders) {
580
+ const provCtrl = new AbortController();
581
+ (async () => {
582
+ try {
583
+ const list = await fetchProviders({
584
+ relayUrl: opts.relayUrl,
585
+ websiteKey: opts.websiteKey,
586
+ signal: provCtrl.signal
587
+ });
588
+ if (alive && !provCtrl.signal.aborted) setProviderList(list);
589
+ } catch (err) {
590
+ if (!alive || err?.name === "AbortError") return;
591
+ if (opts.onError) safeCall(opts.onError, err);
592
+ }
593
+ })();
594
+ destroyProvidersFetch = () => provCtrl.abort();
595
+ }
596
+ return {
597
+ destroy() {
598
+ alive = false;
599
+ if (pollCtrl) pollCtrl.abort();
600
+ if (destroyProvidersFetch) destroyProvidersFetch();
601
+ root.replaceChildren();
602
+ root.classList.remove("mylikita-widget");
603
+ },
604
+ reset,
605
+ getForm() {
606
+ return { name: name.input.value, phone: phone.input.value, email: email.input.value };
607
+ }
608
+ };
609
+ }
610
+ function fieldText(name, label, { type = "text", required = false, maxlength, inputmode } = {}) {
611
+ const wrap = el("div", { className: "mylikita-widget__field" });
612
+ const lab = el("label", { className: "mylikita-widget__label", attrs: { for: `mlw-${name}` } });
613
+ lab.append(document.createTextNode(label));
614
+ if (required) lab.append(el("span", { className: "req", text: " *" }));
615
+ let input;
616
+ if (type === "textarea") {
617
+ input = el("textarea", { className: "mylikita-widget__textarea", attrs: { id: `mlw-${name}`, rows: 3, maxlength: maxlength || "" } });
618
+ } else {
619
+ input = el("input", { className: "mylikita-widget__input", attrs: { id: `mlw-${name}`, type, inputmode: inputmode || "" } });
620
+ }
621
+ if (required) input.setAttribute("required", "");
622
+ wrap.append(lab, input);
623
+ return { wrap, input };
624
+ }
625
+ function fieldSelect(name, label, options) {
626
+ const wrap = el("div", { className: "mylikita-widget__field" });
627
+ const lab = el("label", { className: "mylikita-widget__label", attrs: { for: `mlw-${name}` } });
628
+ lab.textContent = label;
629
+ const select = el("select", { className: "mylikita-widget__select", attrs: { id: `mlw-${name}` } });
630
+ for (const o of options) {
631
+ const opt = el("option", { text: o.label });
632
+ opt.value = o.value;
633
+ select.append(opt);
634
+ }
635
+ wrap.append(lab, select);
636
+ return { wrap, input: select };
637
+ }
638
+ function el(tag, { className, text, attrs = {} } = {}) {
639
+ const node = document.createElement(tag);
640
+ if (className) node.className = className;
641
+ if (text !== void 0) node.textContent = text;
642
+ for (const [k, v] of Object.entries(attrs)) {
643
+ if (v === void 0 || v === "") continue;
644
+ node.setAttribute(k, v);
645
+ }
646
+ return node;
647
+ }
648
+ function iconGlyph(kind) {
649
+ return kind === "success" ? "\u2713" : kind === "danger" ? "!" : "\u2026";
650
+ }
651
+ function readStoredRef(key) {
652
+ try {
653
+ return sessionStorage.getItem(key) || null;
654
+ } catch (_) {
655
+ return null;
656
+ }
657
+ }
658
+ function safeCall(fn, ...args) {
659
+ try {
660
+ fn(...args);
661
+ } catch (_) {
662
+ }
663
+ }
664
+ function toLocalInputValue(date) {
665
+ const pad = (n) => String(n).padStart(2, "0");
666
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
667
+ }
668
+ function injectStyles() {
669
+ if (document.getElementById(STYLE_ID)) return;
670
+ const style = document.createElement("style");
671
+ style.id = STYLE_ID;
672
+ style.textContent = STYLES;
673
+ document.head.appendChild(style);
674
+ }
675
+ function applyTheme(rootNode, theme) {
676
+ for (const [k, v] of Object.entries(resolveTheme(theme))) {
677
+ rootNode.style.setProperty(k, v);
678
+ }
679
+ }