@patientos/website-kit 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.
- package/LICENSE +201 -0
- package/README.md +30 -0
- package/dist/checkout-block-CrO7OxI9.d.ts +121 -0
- package/dist/chunk-G3U6EXJM.js +1240 -0
- package/dist/chunk-JTZ6GYA7.js +30 -0
- package/dist/chunk-LCHW6XF4.js +64 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-Q4URDZAK.js +36 -0
- package/dist/chunk-QDO3SJWR.js +72 -0
- package/dist/chunk-THMT43MV.js +61 -0
- package/dist/chunk-TOPPLOH2.js +12229 -0
- package/dist/chunk-ZOF22TJA.js +19 -0
- package/dist/consult-room.d.ts +35 -0
- package/dist/consult-room.js +31540 -0
- package/dist/contrast.d.ts +63 -0
- package/dist/contrast.js +15 -0
- package/dist/index.d.ts +189 -0
- package/dist/index.js +484 -0
- package/dist/islands-impl.d.ts +68 -0
- package/dist/islands-impl.js +23 -0
- package/dist/islands-registry.d.ts +17 -0
- package/dist/islands-registry.js +14 -0
- package/dist/islands.d.ts +57 -0
- package/dist/islands.js +15 -0
- package/dist/patient-surface-theme.d.ts +22 -0
- package/dist/patient-surface-theme.js +7 -0
- package/dist/portal-account-EhOtLV5u.d.ts +113 -0
- package/dist/portal-account.client-CTget1-3.d.ts +169 -0
- package/dist/portal-booking-client.d.ts +232 -0
- package/dist/portal-booking-client.js +51 -0
- package/dist/script-sources.d.ts +11 -0
- package/dist/script-sources.js +17 -0
- package/dist/show-when.d.ts +70 -0
- package/dist/show-when.js +9 -0
- package/dist/website-kit.css +3580 -0
- package/package.json +120 -0
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
// src/portal-booking.client.tsx
|
|
2
|
+
import * as React4 from "react";
|
|
3
|
+
|
|
4
|
+
// src/portal-shared.client.tsx
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var PORTAL_MAGIC_LINK_URL = "/portal/api/magic-link";
|
|
8
|
+
var PORTAL_SIGN_OUT_URL = "/portal/api/sign-out";
|
|
9
|
+
var PORTAL_REQUEST_INIT = { credentials: "include", cache: "no-store" };
|
|
10
|
+
function portalFetch(fetchImpl) {
|
|
11
|
+
return fetchImpl ?? ((input, init) => fetch(input, init));
|
|
12
|
+
}
|
|
13
|
+
async function readPortalResult(res) {
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
let error = null;
|
|
16
|
+
let body;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = await res.json();
|
|
19
|
+
if (typeof parsed?.error === "string") error = parsed.error;
|
|
20
|
+
body = parsed;
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
return { ok: false, status: res.status, error, body };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return { ok: true, data: await res.json() };
|
|
27
|
+
} catch {
|
|
28
|
+
return { ok: false, status: null, error: null };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function portalGet(url, fetchImpl) {
|
|
32
|
+
try {
|
|
33
|
+
return await readPortalResult(await portalFetch(fetchImpl)(url, PORTAL_REQUEST_INIT));
|
|
34
|
+
} catch {
|
|
35
|
+
return { ok: false, status: null, error: null };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function portalSend(url, method, body, fetchImpl) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await portalFetch(fetchImpl)(url, {
|
|
41
|
+
...PORTAL_REQUEST_INIT,
|
|
42
|
+
method,
|
|
43
|
+
headers: { "content-type": "application/json" },
|
|
44
|
+
body: JSON.stringify(body ?? {})
|
|
45
|
+
});
|
|
46
|
+
return await readPortalResult(res);
|
|
47
|
+
} catch {
|
|
48
|
+
return { ok: false, status: null, error: null };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function portalDelete(url, fetchImpl) {
|
|
52
|
+
try {
|
|
53
|
+
const res = await portalFetch(fetchImpl)(url, { ...PORTAL_REQUEST_INIT, method: "DELETE" });
|
|
54
|
+
return await readPortalResult(res);
|
|
55
|
+
} catch {
|
|
56
|
+
return { ok: false, status: null, error: null };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function hasPortalIdentity(body) {
|
|
60
|
+
return typeof body?.signedIn === "boolean";
|
|
61
|
+
}
|
|
62
|
+
var SIGNED_IN_HINT_KEY = "patientos.portal.signedIn";
|
|
63
|
+
function readPortalSignedInHint() {
|
|
64
|
+
try {
|
|
65
|
+
return globalThis.sessionStorage?.getItem(SIGNED_IN_HINT_KEY) === "1";
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function writePortalSignedInHint(signedIn) {
|
|
71
|
+
try {
|
|
72
|
+
const store = globalThis.sessionStorage;
|
|
73
|
+
if (!store) return;
|
|
74
|
+
if (signedIn) store.setItem(SIGNED_IN_HINT_KEY, "1");
|
|
75
|
+
else store.removeItem(SIGNED_IN_HINT_KEY);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function requestPortalSignInLink(email, redirect, fetchImpl) {
|
|
80
|
+
const res = await portalSend(
|
|
81
|
+
PORTAL_MAGIC_LINK_URL,
|
|
82
|
+
"POST",
|
|
83
|
+
{ email, redirect },
|
|
84
|
+
fetchImpl
|
|
85
|
+
);
|
|
86
|
+
return { ok: res.ok };
|
|
87
|
+
}
|
|
88
|
+
async function signOutOfPortal(fetchImpl) {
|
|
89
|
+
const res = await portalSend(PORTAL_SIGN_OUT_URL, "POST", {}, fetchImpl);
|
|
90
|
+
return { ok: res.ok };
|
|
91
|
+
}
|
|
92
|
+
function currentPagePath() {
|
|
93
|
+
if (typeof window === "undefined") return "/";
|
|
94
|
+
return window.location.pathname || "/";
|
|
95
|
+
}
|
|
96
|
+
var LOCALE = "en-AU";
|
|
97
|
+
function parseInstant(iso) {
|
|
98
|
+
if (!iso) return null;
|
|
99
|
+
const d = new Date(iso);
|
|
100
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
101
|
+
}
|
|
102
|
+
function formatPortalDate(iso, timeZone) {
|
|
103
|
+
const d = parseInstant(iso);
|
|
104
|
+
if (!d) return null;
|
|
105
|
+
const opts = { day: "numeric", month: "short", year: "numeric" };
|
|
106
|
+
try {
|
|
107
|
+
return new Intl.DateTimeFormat(LOCALE, { ...opts, timeZone: timeZone ?? void 0 }).format(d);
|
|
108
|
+
} catch {
|
|
109
|
+
return new Intl.DateTimeFormat(LOCALE, opts).format(d);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function formatPortalDateTime(iso, timeZone) {
|
|
113
|
+
const d = parseInstant(iso);
|
|
114
|
+
if (!d) return null;
|
|
115
|
+
const opts = {
|
|
116
|
+
weekday: "short",
|
|
117
|
+
day: "numeric",
|
|
118
|
+
month: "short",
|
|
119
|
+
year: "numeric",
|
|
120
|
+
hour: "numeric",
|
|
121
|
+
minute: "2-digit"
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
return new Intl.DateTimeFormat(LOCALE, { ...opts, timeZone: timeZone ?? void 0 }).format(d);
|
|
125
|
+
} catch {
|
|
126
|
+
return new Intl.DateTimeFormat(LOCALE, opts).format(d);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function formatPortalTime(iso, timeZone) {
|
|
130
|
+
const d = parseInstant(iso);
|
|
131
|
+
if (!d) return null;
|
|
132
|
+
const opts = { hour: "numeric", minute: "2-digit" };
|
|
133
|
+
try {
|
|
134
|
+
return new Intl.DateTimeFormat(LOCALE, { ...opts, timeZone: timeZone ?? void 0 }).format(d);
|
|
135
|
+
} catch {
|
|
136
|
+
return new Intl.DateTimeFormat(LOCALE, opts).format(d);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function formatPortalMoney(total, currency) {
|
|
140
|
+
if (total === null || total === void 0 || total === "") return null;
|
|
141
|
+
const n = Number(total);
|
|
142
|
+
if (!Number.isFinite(n)) return String(total);
|
|
143
|
+
const code = (currency ?? "AUD").toUpperCase();
|
|
144
|
+
try {
|
|
145
|
+
return new Intl.NumberFormat(LOCALE, { style: "currency", currency: code }).format(n);
|
|
146
|
+
} catch {
|
|
147
|
+
return `${code} ${n.toFixed(2)}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function PortalSignInRamp({
|
|
151
|
+
idSuffix,
|
|
152
|
+
submit,
|
|
153
|
+
onResult,
|
|
154
|
+
intro,
|
|
155
|
+
submitLabel
|
|
156
|
+
}) {
|
|
157
|
+
const [email, setEmail] = React.useState("");
|
|
158
|
+
const [busy, setBusy] = React.useState(false);
|
|
159
|
+
const inputId = `sk-portal-email-${idSuffix}`;
|
|
160
|
+
async function onSubmit(e) {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
if (!email || busy) return;
|
|
163
|
+
setBusy(true);
|
|
164
|
+
const { ok } = await submit(email);
|
|
165
|
+
setBusy(false);
|
|
166
|
+
onResult({ ok, email });
|
|
167
|
+
}
|
|
168
|
+
return /* @__PURE__ */ jsxs("form", { className: "sk-portal-ramp", onSubmit: (e) => void onSubmit(e), children: [
|
|
169
|
+
/* @__PURE__ */ jsx("p", { className: "sk-portal-ramp__intro", children: intro ?? "Enter your email and we'll send you a sign-in link." }),
|
|
170
|
+
/* @__PURE__ */ jsx("label", { className: "sk-portal-label", htmlFor: inputId, children: "Email" }),
|
|
171
|
+
/* @__PURE__ */ jsx(
|
|
172
|
+
"input",
|
|
173
|
+
{
|
|
174
|
+
id: inputId,
|
|
175
|
+
className: "sk-portal-input",
|
|
176
|
+
type: "email",
|
|
177
|
+
name: "email",
|
|
178
|
+
required: true,
|
|
179
|
+
autoComplete: "email",
|
|
180
|
+
value: email,
|
|
181
|
+
onChange: (e) => setEmail(e.target.value)
|
|
182
|
+
}
|
|
183
|
+
),
|
|
184
|
+
/* @__PURE__ */ jsx("button", { className: "sk-portal-button", type: "submit", disabled: busy, children: busy ? "Sending\u2026" : submitLabel ?? "Email me a sign-in link" })
|
|
185
|
+
] });
|
|
186
|
+
}
|
|
187
|
+
function PortalLinkSentNotice({ email }) {
|
|
188
|
+
return /* @__PURE__ */ jsxs("p", { className: "sk-portal-ramp__sent", role: "status", children: [
|
|
189
|
+
"If ",
|
|
190
|
+
email,
|
|
191
|
+
" is on file with us, a sign-in link is on its way. It expires in a few minutes."
|
|
192
|
+
] });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/slot-picker.tsx
|
|
196
|
+
import * as React3 from "react";
|
|
197
|
+
|
|
198
|
+
// src/flow-shell.tsx
|
|
199
|
+
import * as React2 from "react";
|
|
200
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
201
|
+
var FlowRailContext = React2.createContext(null);
|
|
202
|
+
function FlowShell({
|
|
203
|
+
eyebrow,
|
|
204
|
+
heading,
|
|
205
|
+
subheading,
|
|
206
|
+
stepKey,
|
|
207
|
+
stepNumber,
|
|
208
|
+
totalSteps,
|
|
209
|
+
onBack,
|
|
210
|
+
documentTitle,
|
|
211
|
+
footer,
|
|
212
|
+
children,
|
|
213
|
+
className
|
|
214
|
+
}) {
|
|
215
|
+
const rail = React2.useContext(FlowRailContext);
|
|
216
|
+
const headingRef = React2.useRef(null);
|
|
217
|
+
const mounted = React2.useRef(false);
|
|
218
|
+
React2.useEffect(() => {
|
|
219
|
+
if (!mounted.current) {
|
|
220
|
+
mounted.current = true;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
headingRef.current?.focus();
|
|
224
|
+
}, [stepKey]);
|
|
225
|
+
React2.useEffect(() => {
|
|
226
|
+
if (documentTitle && typeof document !== "undefined") document.title = documentTitle;
|
|
227
|
+
}, [documentTitle]);
|
|
228
|
+
const [rootEl, setRootEl] = React2.useState(null);
|
|
229
|
+
useResolvedSurface(rootEl);
|
|
230
|
+
const place = Boolean(eyebrow) || Boolean(stepNumber && totalSteps);
|
|
231
|
+
return /* @__PURE__ */ jsxs2(
|
|
232
|
+
"div",
|
|
233
|
+
{
|
|
234
|
+
ref: setRootEl,
|
|
235
|
+
className: ["sk-flow", rail ? "sk-flow--railed" : null, className].filter(Boolean).join(" "),
|
|
236
|
+
"data-sk-step": stepKey,
|
|
237
|
+
children: [
|
|
238
|
+
/* @__PURE__ */ jsxs2("header", { className: "sk-flow__bar", children: [
|
|
239
|
+
onBack ? /* @__PURE__ */ jsxs2("button", { type: "button", className: "sk-flow__back", onClick: onBack, "data-sk-flow-back": "", children: [
|
|
240
|
+
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: "\u2190" }),
|
|
241
|
+
" Back"
|
|
242
|
+
] }) : place ? (
|
|
243
|
+
// Only earns its 36px when there IS something on the right to push over.
|
|
244
|
+
/* @__PURE__ */ jsx2("span", { className: "sk-flow__back-spacer", "aria-hidden": "true" })
|
|
245
|
+
) : null,
|
|
246
|
+
place ? /* @__PURE__ */ jsxs2("div", { className: "sk-flow__place", children: [
|
|
247
|
+
eyebrow && /* @__PURE__ */ jsx2("p", { className: "sk-flow__eyebrow", children: eyebrow }),
|
|
248
|
+
stepNumber && totalSteps ? /* @__PURE__ */ jsx2(Progress, { current: stepNumber, total: totalSteps }) : null
|
|
249
|
+
] }) : null
|
|
250
|
+
] }),
|
|
251
|
+
/* @__PURE__ */ jsxs2("main", { className: "sk-flow__body", children: [
|
|
252
|
+
(heading || subheading) && /* @__PURE__ */ jsxs2("div", { className: "sk-flow__head", children: [
|
|
253
|
+
heading && // tabIndex -1 makes a non-interactive heading programmatically focusable.
|
|
254
|
+
/* @__PURE__ */ jsx2("h2", { ref: headingRef, tabIndex: -1, className: "sk-flow__heading", children: heading }),
|
|
255
|
+
subheading && /* @__PURE__ */ jsx2("div", { className: "sk-flow__sub", children: subheading })
|
|
256
|
+
] }),
|
|
257
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-flow__status", role: "status", "aria-live": "polite", children: eyebrow ?? "" }),
|
|
258
|
+
children
|
|
259
|
+
] }),
|
|
260
|
+
footer && /* @__PURE__ */ jsx2("footer", { className: "sk-flow__footer", children: footer }),
|
|
261
|
+
rail ? (
|
|
262
|
+
// Named, or a screen reader announces a bare "complementary" landmark and the
|
|
263
|
+
// patient has to enter it to find out whether it is worth reading.
|
|
264
|
+
/* @__PURE__ */ jsx2("aside", { className: "sk-flow__rail", "aria-label": "About this clinic", children: rail })
|
|
265
|
+
) : null
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
function useResolvedSurface(el) {
|
|
271
|
+
React2.useEffect(() => {
|
|
272
|
+
if (!el || typeof window === "undefined") return;
|
|
273
|
+
const apply = () => {
|
|
274
|
+
const ink = window.getComputedStyle(el).color;
|
|
275
|
+
if (ink) el.style.setProperty("--sk-flow-ink", ink);
|
|
276
|
+
el.style.setProperty("--sk-flow-canvas", opaqueBackdrop(el));
|
|
277
|
+
};
|
|
278
|
+
apply();
|
|
279
|
+
const mq = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
280
|
+
mq?.addEventListener?.("change", apply);
|
|
281
|
+
return () => mq?.removeEventListener?.("change", apply);
|
|
282
|
+
}, [el]);
|
|
283
|
+
}
|
|
284
|
+
function opaqueBackdrop(el, read = (e) => window.getComputedStyle(e).backgroundColor) {
|
|
285
|
+
for (let node = el; node; node = node.parentElement) {
|
|
286
|
+
const bg = read(node);
|
|
287
|
+
if (bg && alphaOf(bg) === 1) return bg;
|
|
288
|
+
}
|
|
289
|
+
return "white";
|
|
290
|
+
}
|
|
291
|
+
function alphaOf(bg) {
|
|
292
|
+
if (bg === "transparent") return 0;
|
|
293
|
+
const inner = /^rgba?\(([^)]*)\)$/.exec(bg)?.[1];
|
|
294
|
+
if (inner === void 0) return 1;
|
|
295
|
+
const parts = inner.split(/[,/]/).map((s) => s.trim());
|
|
296
|
+
if (parts.length < 4) return 1;
|
|
297
|
+
const raw = parts[3];
|
|
298
|
+
const n = raw.endsWith("%") ? Number.parseFloat(raw) / 100 : Number.parseFloat(raw);
|
|
299
|
+
return Number.isFinite(n) ? n : 1;
|
|
300
|
+
}
|
|
301
|
+
function Progress({ current, total }) {
|
|
302
|
+
return /* @__PURE__ */ jsx2(
|
|
303
|
+
"div",
|
|
304
|
+
{
|
|
305
|
+
className: "sk-flow__progress",
|
|
306
|
+
role: "progressbar",
|
|
307
|
+
"aria-label": "Progress",
|
|
308
|
+
"aria-valuemin": 1,
|
|
309
|
+
"aria-valuemax": total,
|
|
310
|
+
"aria-valuenow": current,
|
|
311
|
+
"aria-valuetext": `Step ${current} of ${total}`,
|
|
312
|
+
children: Array.from({ length: total }, (_, i) => /* @__PURE__ */ jsx2(
|
|
313
|
+
"span",
|
|
314
|
+
{
|
|
315
|
+
"aria-hidden": "true",
|
|
316
|
+
className: [
|
|
317
|
+
"sk-flow__dot",
|
|
318
|
+
i + 1 === current ? "sk-flow__dot--current" : "",
|
|
319
|
+
i + 1 < current ? "sk-flow__dot--done" : ""
|
|
320
|
+
].filter(Boolean).join(" ")
|
|
321
|
+
},
|
|
322
|
+
i
|
|
323
|
+
))
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
function FlowButton({
|
|
328
|
+
variant = "primary",
|
|
329
|
+
busy,
|
|
330
|
+
children,
|
|
331
|
+
className,
|
|
332
|
+
...rest
|
|
333
|
+
}) {
|
|
334
|
+
return /* @__PURE__ */ jsx2(
|
|
335
|
+
"button",
|
|
336
|
+
{
|
|
337
|
+
...rest,
|
|
338
|
+
className: ["sk-flow__btn", `sk-flow__btn--${variant}`, className].filter(Boolean).join(" "),
|
|
339
|
+
"aria-busy": busy || void 0,
|
|
340
|
+
disabled: rest.disabled || busy,
|
|
341
|
+
children
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
var SummarisedErrorsContext = React2.createContext(false);
|
|
346
|
+
function SummarisedFieldErrors({
|
|
347
|
+
children
|
|
348
|
+
}) {
|
|
349
|
+
return /* @__PURE__ */ jsx2(SummarisedErrorsContext.Provider, { value: true, children });
|
|
350
|
+
}
|
|
351
|
+
function FlowError({
|
|
352
|
+
children,
|
|
353
|
+
id
|
|
354
|
+
}) {
|
|
355
|
+
const summarised = React2.useContext(SummarisedErrorsContext);
|
|
356
|
+
if (!children) return null;
|
|
357
|
+
return /* @__PURE__ */ jsx2("p", { className: "sk-flow__error", id, role: summarised ? void 0 : "alert", children });
|
|
358
|
+
}
|
|
359
|
+
function FlowFieldError({
|
|
360
|
+
id,
|
|
361
|
+
children
|
|
362
|
+
}) {
|
|
363
|
+
if (!children) return null;
|
|
364
|
+
return /* @__PURE__ */ jsx2("p", { className: "sk-flow__error", id, children });
|
|
365
|
+
}
|
|
366
|
+
function describedBy(...ids) {
|
|
367
|
+
return ids.filter(Boolean).join(" ") || void 0;
|
|
368
|
+
}
|
|
369
|
+
function invalidFields(fields, errors) {
|
|
370
|
+
return fields.filter((f) => Boolean(errors[f.key]));
|
|
371
|
+
}
|
|
372
|
+
function FlowErrorSummary({
|
|
373
|
+
fields
|
|
374
|
+
}) {
|
|
375
|
+
if (fields.length === 0) return null;
|
|
376
|
+
const names = fields.map((f) => f.label);
|
|
377
|
+
const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
378
|
+
return /* @__PURE__ */ jsx2("p", { className: "sk-flow__error", role: "alert", children: fields.length === 1 ? `Please check one field: ${list}.` : `Please check ${fields.length} fields: ${list}.` });
|
|
379
|
+
}
|
|
380
|
+
function focusField(id) {
|
|
381
|
+
if (typeof document === "undefined") return false;
|
|
382
|
+
const el = document.getElementById(id);
|
|
383
|
+
if (!el || typeof el.focus !== "function") return false;
|
|
384
|
+
el.focus();
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
function FlowLoading({ message = "One moment\u2026" }) {
|
|
388
|
+
return /* @__PURE__ */ jsx2("p", { className: "sk-flow__loading", role: "status", "aria-live": "polite", children: message });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/public-client.ts
|
|
392
|
+
import {
|
|
393
|
+
createPatientOSClient,
|
|
394
|
+
PatientOSApiError
|
|
395
|
+
} from "@patientos/public-sdk";
|
|
396
|
+
var CLAIM_STORAGE_KEY = "pos:claim-token";
|
|
397
|
+
function session() {
|
|
398
|
+
try {
|
|
399
|
+
return typeof window === "undefined" ? null : window.sessionStorage;
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function readClaim() {
|
|
405
|
+
try {
|
|
406
|
+
return session()?.getItem(CLAIM_STORAGE_KEY) ?? null;
|
|
407
|
+
} catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function writeClaim(token) {
|
|
412
|
+
try {
|
|
413
|
+
const store = session();
|
|
414
|
+
if (!store) return;
|
|
415
|
+
if (token) store.setItem(CLAIM_STORAGE_KEY, token);
|
|
416
|
+
else store.removeItem(CLAIM_STORAGE_KEY);
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
var client = null;
|
|
421
|
+
var clientKey = "";
|
|
422
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
423
|
+
function boundedFetch(input, init) {
|
|
424
|
+
const controller = new AbortController();
|
|
425
|
+
const upstreamSignal = init?.signal;
|
|
426
|
+
const abortFromUpstream = () => controller.abort();
|
|
427
|
+
if (upstreamSignal?.aborted) controller.abort();
|
|
428
|
+
else upstreamSignal?.addEventListener("abort", abortFromUpstream, { once: true });
|
|
429
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
430
|
+
return globalThis.fetch(input, { ...init, signal: controller.signal }).finally(() => {
|
|
431
|
+
clearTimeout(timer);
|
|
432
|
+
upstreamSignal?.removeEventListener("abort", abortFromUpstream);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function patientos(config) {
|
|
436
|
+
const key = `${config.publishableKey}|${config.apiBase}`;
|
|
437
|
+
if (!client || clientKey !== key) {
|
|
438
|
+
client = createPatientOSClient({
|
|
439
|
+
baseUrl: config.apiBase,
|
|
440
|
+
publishableKey: config.publishableKey,
|
|
441
|
+
claimToken: readClaim(),
|
|
442
|
+
fetch: boundedFetch
|
|
443
|
+
});
|
|
444
|
+
clientKey = key;
|
|
445
|
+
}
|
|
446
|
+
return client;
|
|
447
|
+
}
|
|
448
|
+
function persistClaimToken(c) {
|
|
449
|
+
writeClaim(c.getClaimToken());
|
|
450
|
+
}
|
|
451
|
+
function adoptClaimToken(c, token) {
|
|
452
|
+
c.setClaimToken(token);
|
|
453
|
+
writeClaim(token);
|
|
454
|
+
}
|
|
455
|
+
function resetSession() {
|
|
456
|
+
writeClaim(null);
|
|
457
|
+
client = null;
|
|
458
|
+
clientKey = "";
|
|
459
|
+
}
|
|
460
|
+
var flowKey = (requestId) => `pos:flow:${requestId}`;
|
|
461
|
+
function saveFlowRecord(requestId, record) {
|
|
462
|
+
try {
|
|
463
|
+
session()?.setItem(flowKey(requestId), JSON.stringify(record));
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function loadFlowRecord(requestId) {
|
|
468
|
+
try {
|
|
469
|
+
const raw = session()?.getItem(flowKey(requestId));
|
|
470
|
+
return raw ? JSON.parse(raw) : null;
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function once(subscribe) {
|
|
476
|
+
return new Promise((resolve) => {
|
|
477
|
+
const unsubscribe = subscribe((rows) => {
|
|
478
|
+
unsubscribe();
|
|
479
|
+
resolve(rows);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
async function findServiceByKey(c, key) {
|
|
484
|
+
const services = await c.listServices();
|
|
485
|
+
return services.find((s) => s.key === key) ?? null;
|
|
486
|
+
}
|
|
487
|
+
function pickDeliveryOption(options, serviceId) {
|
|
488
|
+
const mine = options.filter((o) => o.serviceId === serviceId);
|
|
489
|
+
if (mine.length === 0) return null;
|
|
490
|
+
return mine.find((o) => o.isDefault) ?? [...mine].sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
491
|
+
}
|
|
492
|
+
async function findDeliveryOption(c, serviceId) {
|
|
493
|
+
return pickDeliveryOption(await once(c.subscribeDeliveryOptions), serviceId);
|
|
494
|
+
}
|
|
495
|
+
function effectiveFee(svc, option) {
|
|
496
|
+
return { amount: option?.feeOverride ?? svc.feeAmount, requiresPayment: svc.requiresPayment };
|
|
497
|
+
}
|
|
498
|
+
function flowIsFree(flow) {
|
|
499
|
+
if (!flow) return null;
|
|
500
|
+
if (typeof flow.requiresPayment === "boolean") return !flow.requiresPayment;
|
|
501
|
+
return flow.feeAmount == null || flow.feeAmount === "";
|
|
502
|
+
}
|
|
503
|
+
function formatFee(amount) {
|
|
504
|
+
if (amount == null || amount === "") return "";
|
|
505
|
+
const n = Number(amount);
|
|
506
|
+
if (Number.isNaN(n)) return "";
|
|
507
|
+
return Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`;
|
|
508
|
+
}
|
|
509
|
+
function feeNote(fee) {
|
|
510
|
+
if (!fee) return "";
|
|
511
|
+
if (!fee.requiresPayment || !fee.amount) return "Bulk billed \u2014 no cost to you";
|
|
512
|
+
const text = formatFee(fee.amount);
|
|
513
|
+
return text ? `${text} \u2014 held on your card, charged only if what you asked for is issued` : "";
|
|
514
|
+
}
|
|
515
|
+
function feeRowNote(fee) {
|
|
516
|
+
if (!fee) return "";
|
|
517
|
+
if (!fee.requiresPayment || !fee.amount) return "Bulk billed \u2014 no cost to you";
|
|
518
|
+
return formatFee(fee.amount);
|
|
519
|
+
}
|
|
520
|
+
function gateToExit(gate) {
|
|
521
|
+
if (!gate) return null;
|
|
522
|
+
if (gate.verdict === "escalate") return { kind: "escalate", message: gate.message };
|
|
523
|
+
return { kind: "block", ruleId: gate.ruleId, message: gate.message };
|
|
524
|
+
}
|
|
525
|
+
function errorMessage(err, fallback) {
|
|
526
|
+
if (err instanceof PatientOSApiError && (err.code === "missing_claim" || err.code === "invalid_claim" || err.code === "claim_mismatch")) {
|
|
527
|
+
return fallback;
|
|
528
|
+
}
|
|
529
|
+
if (isLocalMissingClaim(err)) return fallback;
|
|
530
|
+
if (err instanceof PatientOSApiError) return err.message || fallback;
|
|
531
|
+
if (err instanceof Error && err.message) return err.message;
|
|
532
|
+
return fallback;
|
|
533
|
+
}
|
|
534
|
+
function errorCode(err) {
|
|
535
|
+
return err instanceof PatientOSApiError ? err.code : null;
|
|
536
|
+
}
|
|
537
|
+
function isSessionExpired(err) {
|
|
538
|
+
const code = errorCode(err);
|
|
539
|
+
return code === "missing_claim" || code === "invalid_claim" || isLocalMissingClaim(err);
|
|
540
|
+
}
|
|
541
|
+
function isLocalMissingClaim(err) {
|
|
542
|
+
return err instanceof Error && err.message.startsWith("No active session \u2014");
|
|
543
|
+
}
|
|
544
|
+
function isClaimRejected(err) {
|
|
545
|
+
return isSessionExpired(err) || errorCode(err) === "claim_mismatch";
|
|
546
|
+
}
|
|
547
|
+
function observeSessionExpiry(c, onExpired) {
|
|
548
|
+
return new Proxy(c, {
|
|
549
|
+
get(target, property, receiver) {
|
|
550
|
+
const value = Reflect.get(target, property, receiver);
|
|
551
|
+
if (typeof value !== "function") return value;
|
|
552
|
+
return (...args) => {
|
|
553
|
+
try {
|
|
554
|
+
const result = value.apply(target, args);
|
|
555
|
+
if (result && typeof result.then === "function") {
|
|
556
|
+
return Promise.resolve(result).catch((err) => {
|
|
557
|
+
if (isClaimRejected(err)) onExpired();
|
|
558
|
+
throw err;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
} catch (err) {
|
|
563
|
+
if (isClaimRejected(err)) onExpired();
|
|
564
|
+
throw err;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/slot-picker.tsx
|
|
572
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
573
|
+
var SLOT_TAKEN_CODES = ["slot_taken", "slot_unavailable", "slot_full", "hold_conflict"];
|
|
574
|
+
function isSlotTakenError(err) {
|
|
575
|
+
const code = errorCode(err);
|
|
576
|
+
return code !== null && SLOT_TAKEN_CODES.includes(code);
|
|
577
|
+
}
|
|
578
|
+
function groupByTime(slots) {
|
|
579
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
580
|
+
for (const s of slots) {
|
|
581
|
+
const existing = byStart.get(s.start);
|
|
582
|
+
if (existing) existing.count += 1;
|
|
583
|
+
else byStart.set(s.start, { start: s.start, practitionerId: s.practitionerId, count: 1 });
|
|
584
|
+
}
|
|
585
|
+
return [...byStart.values()].sort((a, b) => a.start.localeCompare(b.start));
|
|
586
|
+
}
|
|
587
|
+
function dayKeyOf(iso, tz) {
|
|
588
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
589
|
+
timeZone: tz,
|
|
590
|
+
year: "numeric",
|
|
591
|
+
month: "2-digit",
|
|
592
|
+
day: "2-digit"
|
|
593
|
+
}).format(new Date(iso));
|
|
594
|
+
}
|
|
595
|
+
function formatSlotTime(iso, tz) {
|
|
596
|
+
return new Intl.DateTimeFormat("en-AU", {
|
|
597
|
+
timeZone: tz,
|
|
598
|
+
hour: "2-digit",
|
|
599
|
+
minute: "2-digit",
|
|
600
|
+
hour12: false
|
|
601
|
+
}).format(new Date(iso));
|
|
602
|
+
}
|
|
603
|
+
function zoneAbbr(iso, tz) {
|
|
604
|
+
const abbr = new Intl.DateTimeFormat("en-AU", { timeZone: tz, timeZoneName: "short" }).formatToParts(new Date(iso)).find((p) => p.type === "timeZoneName")?.value;
|
|
605
|
+
return abbr && !/^(GMT|UTC)/.test(abbr) ? abbr : null;
|
|
606
|
+
}
|
|
607
|
+
function zoneLabel(iso, tz) {
|
|
608
|
+
const city = (tz.split("/").pop() ?? tz).replace(/_/g, " ");
|
|
609
|
+
const abbr = zoneAbbr(iso, tz);
|
|
610
|
+
return abbr ? `${city} time (${abbr})` : `${city} time`;
|
|
611
|
+
}
|
|
612
|
+
function groupByDay(slots, tz) {
|
|
613
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const slot of slots) {
|
|
615
|
+
const key = dayKeyOf(slot.start, tz);
|
|
616
|
+
const existing = byDay.get(key);
|
|
617
|
+
if (existing) {
|
|
618
|
+
existing.slots.push(slot);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
byDay.set(key, {
|
|
622
|
+
key,
|
|
623
|
+
tabLabel: new Intl.DateTimeFormat("en-AU", {
|
|
624
|
+
timeZone: tz,
|
|
625
|
+
weekday: "short",
|
|
626
|
+
day: "numeric"
|
|
627
|
+
}).format(new Date(slot.start)),
|
|
628
|
+
heading: new Intl.DateTimeFormat("en-AU", {
|
|
629
|
+
timeZone: tz,
|
|
630
|
+
weekday: "long",
|
|
631
|
+
day: "numeric",
|
|
632
|
+
month: "long"
|
|
633
|
+
}).format(new Date(slot.start)),
|
|
634
|
+
zone: zoneLabel(slot.start, tz),
|
|
635
|
+
slots: [slot]
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return [...byDay.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
639
|
+
}
|
|
640
|
+
function heldStarts(holds, now = Date.now()) {
|
|
641
|
+
const out = /* @__PURE__ */ new Set();
|
|
642
|
+
for (const h of holds) {
|
|
643
|
+
if (Date.parse(h.expiresAt) <= now) continue;
|
|
644
|
+
out.add(h.slotStart);
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
function SlotBoard({
|
|
649
|
+
slots,
|
|
650
|
+
tz,
|
|
651
|
+
held,
|
|
652
|
+
selected,
|
|
653
|
+
onSelect,
|
|
654
|
+
mode,
|
|
655
|
+
className
|
|
656
|
+
}) {
|
|
657
|
+
const [dayKey, setDayKey] = React3.useState(null);
|
|
658
|
+
const days = React3.useMemo(() => groupByDay(slots, tz), [slots, tz]);
|
|
659
|
+
const activeDay = days.find((d) => d.key === dayKey) ?? days[0] ?? null;
|
|
660
|
+
if (!activeDay) return /* @__PURE__ */ jsx3(Fragment, {});
|
|
661
|
+
return /* @__PURE__ */ jsxs3("div", { className: ["sk-slots", className].filter(Boolean).join(" "), "data-sk-slot-mode": mode, children: [
|
|
662
|
+
/* @__PURE__ */ jsx3("div", { className: "sk-slots__days", role: "group", "aria-label": "Choose a day", children: days.map((day) => {
|
|
663
|
+
const isActive = day.key === activeDay.key;
|
|
664
|
+
return /* @__PURE__ */ jsx3(
|
|
665
|
+
"button",
|
|
666
|
+
{
|
|
667
|
+
type: "button",
|
|
668
|
+
className: ["sk-slots__day", isActive ? "sk-slots__day--active" : ""].filter(Boolean).join(" "),
|
|
669
|
+
"aria-pressed": isActive,
|
|
670
|
+
onClick: () => {
|
|
671
|
+
setDayKey(day.key);
|
|
672
|
+
onSelect(null);
|
|
673
|
+
},
|
|
674
|
+
children: day.tabLabel
|
|
675
|
+
},
|
|
676
|
+
day.key
|
|
677
|
+
);
|
|
678
|
+
}) }),
|
|
679
|
+
/* @__PURE__ */ jsxs3("p", { className: "sk-slots__heading", children: [
|
|
680
|
+
activeDay.heading,
|
|
681
|
+
" ",
|
|
682
|
+
/* @__PURE__ */ jsxs3("span", { className: "sk-slots__heading-tz", children: [
|
|
683
|
+
"\xB7 ",
|
|
684
|
+
activeDay.zone
|
|
685
|
+
] })
|
|
686
|
+
] }),
|
|
687
|
+
/* @__PURE__ */ jsx3(
|
|
688
|
+
"div",
|
|
689
|
+
{
|
|
690
|
+
className: "sk-slots__grid",
|
|
691
|
+
role: "group",
|
|
692
|
+
"aria-label": `Times on ${activeDay.heading}, ${activeDay.zone}`,
|
|
693
|
+
children: activeDay.slots.map((slot) => {
|
|
694
|
+
const isSelected = selected?.start === slot.start;
|
|
695
|
+
const isHeld = (held?.has(slot.start) ?? false) && slot.count === 1;
|
|
696
|
+
return /* @__PURE__ */ jsxs3(
|
|
697
|
+
"button",
|
|
698
|
+
{
|
|
699
|
+
type: "button",
|
|
700
|
+
className: [
|
|
701
|
+
"sk-slots__time",
|
|
702
|
+
isSelected ? "sk-slots__time--selected" : "",
|
|
703
|
+
isHeld ? "sk-slots__time--held" : ""
|
|
704
|
+
].filter(Boolean).join(" "),
|
|
705
|
+
"aria-pressed": isSelected,
|
|
706
|
+
onClick: () => onSelect(slot),
|
|
707
|
+
children: [
|
|
708
|
+
/* @__PURE__ */ jsx3("span", { className: "sk-slots__time-label", children: formatSlotTime(slot.start, tz) }),
|
|
709
|
+
isHeld && /* @__PURE__ */ jsx3("span", { className: "sk-slots__time-note", children: "Being booked" }),
|
|
710
|
+
!isHeld && slot.count === 1 && /* @__PURE__ */ jsx3("span", { className: "sk-slots__time-note", children: "1 left" })
|
|
711
|
+
]
|
|
712
|
+
},
|
|
713
|
+
slot.start
|
|
714
|
+
);
|
|
715
|
+
})
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
] });
|
|
719
|
+
}
|
|
720
|
+
function SlotPicker({
|
|
721
|
+
client: client2,
|
|
722
|
+
appointmentTypeId,
|
|
723
|
+
mode,
|
|
724
|
+
onConfirm,
|
|
725
|
+
confirmLabel
|
|
726
|
+
}) {
|
|
727
|
+
const [slots, setSlots] = React3.useState(null);
|
|
728
|
+
const [tz, setTz] = React3.useState("Australia/Sydney");
|
|
729
|
+
const [held, setHeld] = React3.useState(() => /* @__PURE__ */ new Set());
|
|
730
|
+
const [loadError, setLoadError] = React3.useState(null);
|
|
731
|
+
const [bookError, setBookError] = React3.useState(null);
|
|
732
|
+
const [selected, setSelected] = React3.useState(null);
|
|
733
|
+
const [booking, setBooking] = React3.useState(false);
|
|
734
|
+
const aliveRef = React3.useRef(true);
|
|
735
|
+
const load = React3.useCallback(async () => {
|
|
736
|
+
setLoadError(null);
|
|
737
|
+
try {
|
|
738
|
+
const availability = await client2.getAvailability({ appointmentTypeId });
|
|
739
|
+
if (!aliveRef.current) return;
|
|
740
|
+
const grouped = groupByTime(availability.slots);
|
|
741
|
+
setTz(availability.timezone);
|
|
742
|
+
setSlots(grouped);
|
|
743
|
+
setSelected((prev) => prev && grouped.some((s) => s.start === prev.start) ? prev : null);
|
|
744
|
+
} catch (e) {
|
|
745
|
+
if (!aliveRef.current) return;
|
|
746
|
+
setLoadError(errorMessage(e, "We could not load available times."));
|
|
747
|
+
}
|
|
748
|
+
}, [client2, appointmentTypeId]);
|
|
749
|
+
React3.useEffect(() => {
|
|
750
|
+
aliveRef.current = true;
|
|
751
|
+
void load();
|
|
752
|
+
return () => {
|
|
753
|
+
aliveRef.current = false;
|
|
754
|
+
};
|
|
755
|
+
}, [load]);
|
|
756
|
+
React3.useEffect(() => {
|
|
757
|
+
let stop = null;
|
|
758
|
+
try {
|
|
759
|
+
stop = client2.subscribeHolds((rows) => {
|
|
760
|
+
if (aliveRef.current) setHeld(heldStarts(rows));
|
|
761
|
+
});
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
return () => stop?.();
|
|
765
|
+
}, [client2]);
|
|
766
|
+
async function handleConfirm() {
|
|
767
|
+
if (!selected || booking) return;
|
|
768
|
+
setBooking(true);
|
|
769
|
+
setBookError(null);
|
|
770
|
+
try {
|
|
771
|
+
await onConfirm(selected, tz);
|
|
772
|
+
} catch (e) {
|
|
773
|
+
if (isSlotTakenError(e)) {
|
|
774
|
+
setBookError("That time just filled up. Please choose another.");
|
|
775
|
+
setSelected(null);
|
|
776
|
+
await load();
|
|
777
|
+
} else {
|
|
778
|
+
setBookError(errorMessage(e, "We could not book that time. Please try again."));
|
|
779
|
+
}
|
|
780
|
+
} finally {
|
|
781
|
+
if (aliveRef.current) setBooking(false);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (loadError) {
|
|
785
|
+
return /* @__PURE__ */ jsxs3("div", { className: "sk-slots", children: [
|
|
786
|
+
/* @__PURE__ */ jsx3(FlowError, { children: loadError }),
|
|
787
|
+
/* @__PURE__ */ jsx3(FlowButton, { type: "button", variant: "secondary", onClick: () => void load(), children: "Try again" })
|
|
788
|
+
] });
|
|
789
|
+
}
|
|
790
|
+
if (slots === null) return /* @__PURE__ */ jsx3(FlowLoading, { message: "Loading available times\u2026" });
|
|
791
|
+
if (slots.length === 0) {
|
|
792
|
+
return /* @__PURE__ */ jsxs3("div", { className: "sk-slots sk-slots--empty", children: [
|
|
793
|
+
/* @__PURE__ */ jsx3("p", { className: "sk-slots__empty-title", children: "No times available right now" }),
|
|
794
|
+
/* @__PURE__ */ jsx3("p", { className: "sk-slots__empty-body", children: "Every time is either booked or outside the booking window." }),
|
|
795
|
+
/* @__PURE__ */ jsx3("div", { className: "sk-slots__empty-actions", children: /* @__PURE__ */ jsx3(FlowButton, { type: "button", variant: "secondary", onClick: () => void load(), children: "Check for times again" }) })
|
|
796
|
+
] });
|
|
797
|
+
}
|
|
798
|
+
const timeText = selected ? formatSlotTime(selected.start, tz) : "";
|
|
799
|
+
const label = selected ? confirmLabel?.(timeText) ?? (mode === "request" ? `Book ${timeText}` : `Confirm ${timeText}`) : "Select a time";
|
|
800
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
801
|
+
/* @__PURE__ */ jsx3(SlotBoard, { slots, tz, held, selected, onSelect: setSelected, mode }),
|
|
802
|
+
/* @__PURE__ */ jsx3(FlowError, { children: bookError }),
|
|
803
|
+
/* @__PURE__ */ jsx3("div", { className: "sk-slots__confirm", children: /* @__PURE__ */ jsx3(
|
|
804
|
+
FlowButton,
|
|
805
|
+
{
|
|
806
|
+
type: "button",
|
|
807
|
+
variant: "primary",
|
|
808
|
+
busy: booking,
|
|
809
|
+
disabled: !selected,
|
|
810
|
+
onClick: () => void handleConfirm(),
|
|
811
|
+
children: booking ? "Booking\u2026" : label
|
|
812
|
+
}
|
|
813
|
+
) })
|
|
814
|
+
] });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/portal-booking.client.tsx
|
|
818
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
819
|
+
function rescheduleContextTypeId(ctx) {
|
|
820
|
+
return ctx.appointmentTypeId ?? ctx.appointment?.appointmentType?.id ?? null;
|
|
821
|
+
}
|
|
822
|
+
var BOOKING_TYPES_URL = "/portal/api/booking/types";
|
|
823
|
+
var AVAILABILITY_URL = "/portal/api/booking/availability";
|
|
824
|
+
var HOLDS_URL = "/portal/api/booking/holds";
|
|
825
|
+
var BOOK_URL = "/portal/api/booking/book";
|
|
826
|
+
var APPOINTMENTS_URL = "/portal/api/appointments";
|
|
827
|
+
var PORTAL_BOOKING_TYPES_URL = BOOKING_TYPES_URL;
|
|
828
|
+
function portalRescheduleContextUrl(id) {
|
|
829
|
+
return `${APPOINTMENTS_URL}/${encodeURIComponent(id)}/reschedule-context`;
|
|
830
|
+
}
|
|
831
|
+
var SLOT_TAKEN_COPY = "That time just filled up. Please choose another.";
|
|
832
|
+
var BOOK_FAILED_COPY = "We couldn't book that time just now. Please try again.";
|
|
833
|
+
var AVAILABILITY_FAILED_COPY = "We could not load available times.";
|
|
834
|
+
var RESCHEDULE_CUTOFF_COPY = "It's too close to your appointment to change it online. Please call the clinic and we'll sort it out.";
|
|
835
|
+
var RESCHEDULE_FAILED_COPY = "We couldn't move that appointment just now. Please try again.";
|
|
836
|
+
function readPortalBookingTypes(body) {
|
|
837
|
+
const b = body ?? {};
|
|
838
|
+
return { types: Array.isArray(b.types) ? b.types : [] };
|
|
839
|
+
}
|
|
840
|
+
function readPortalAvailability(body) {
|
|
841
|
+
const b = body ?? {};
|
|
842
|
+
const slots = Array.isArray(b.slots) ? b.slots.filter((s) => typeof s?.start === "string") : [];
|
|
843
|
+
const timezone = typeof b.timezone === "string" && b.timezone ? b.timezone : slots.find((s) => typeof s.timezone === "string" && s.timezone)?.timezone ?? "Australia/Sydney";
|
|
844
|
+
return { slots, timezone };
|
|
845
|
+
}
|
|
846
|
+
function portalTimeSlots(slots) {
|
|
847
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
848
|
+
for (const s of slots) {
|
|
849
|
+
const existing = byStart.get(s.start);
|
|
850
|
+
if (existing) existing.count += 1;
|
|
851
|
+
else byStart.set(s.start, { start: s.start, practitionerId: s.practitionerId ?? "", count: 1 });
|
|
852
|
+
}
|
|
853
|
+
return [...byStart.values()].sort((a, b) => a.start.localeCompare(b.start));
|
|
854
|
+
}
|
|
855
|
+
function portalSlotAt(slots, start) {
|
|
856
|
+
if (!start) return null;
|
|
857
|
+
return slots.find((s) => s.start === start) ?? null;
|
|
858
|
+
}
|
|
859
|
+
function portalAvailabilityWindow(now = Date.now(), days = 14) {
|
|
860
|
+
return {
|
|
861
|
+
from: toYmd(now),
|
|
862
|
+
to: toYmd(now + days * 24 * 60 * 60 * 1e3)
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function toYmd(ms) {
|
|
866
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
867
|
+
}
|
|
868
|
+
function portalAvailabilityUrl(typeId, window2 = portalAvailabilityWindow()) {
|
|
869
|
+
const q = new URLSearchParams({ typeId, from: window2.from, to: window2.to });
|
|
870
|
+
return `${AVAILABILITY_URL}?${q.toString()}`;
|
|
871
|
+
}
|
|
872
|
+
async function loadPortalBookingTypes(fetchImpl) {
|
|
873
|
+
const res = await portalGet(BOOKING_TYPES_URL, fetchImpl);
|
|
874
|
+
return res.ok ? { ok: true, data: readPortalBookingTypes(res.data) } : res;
|
|
875
|
+
}
|
|
876
|
+
async function loadPortalAvailability(typeId, fetchImpl, window2) {
|
|
877
|
+
const res = await portalGet(portalAvailabilityUrl(typeId, window2), fetchImpl);
|
|
878
|
+
return res.ok ? { ok: true, data: readPortalAvailability(res.data) } : res;
|
|
879
|
+
}
|
|
880
|
+
async function createPortalHold(input, fetchImpl) {
|
|
881
|
+
const res = await portalSend(
|
|
882
|
+
HOLDS_URL,
|
|
883
|
+
"POST",
|
|
884
|
+
{
|
|
885
|
+
// Wire names are the server's (PAT-789 canonical contract): `slotStart`, with
|
|
886
|
+
// `typeId` accepted as the documented alias of `appointmentTypeId`.
|
|
887
|
+
typeId: input.typeId,
|
|
888
|
+
slotStart: input.start,
|
|
889
|
+
practitionerId: input.practitionerId || void 0
|
|
890
|
+
},
|
|
891
|
+
fetchImpl
|
|
892
|
+
);
|
|
893
|
+
if (!res.ok || typeof res.data?.holdId !== "string" || res.data.holdId === "") return null;
|
|
894
|
+
return { holdId: res.data.holdId, expiresAt: res.data.expiresAt ?? null };
|
|
895
|
+
}
|
|
896
|
+
async function releasePortalHold(holdId, fetchImpl) {
|
|
897
|
+
const res = await portalDelete(
|
|
898
|
+
`${HOLDS_URL}/${encodeURIComponent(holdId)}`,
|
|
899
|
+
fetchImpl
|
|
900
|
+
);
|
|
901
|
+
return { ok: res.ok };
|
|
902
|
+
}
|
|
903
|
+
async function bookPortalAppointment(input, fetchImpl) {
|
|
904
|
+
const res = await portalSend(
|
|
905
|
+
BOOK_URL,
|
|
906
|
+
"POST",
|
|
907
|
+
{
|
|
908
|
+
typeId: input.typeId,
|
|
909
|
+
slotStart: input.start,
|
|
910
|
+
practitionerId: input.practitionerId || void 0,
|
|
911
|
+
holdId: input.holdId || void 0
|
|
912
|
+
},
|
|
913
|
+
fetchImpl
|
|
914
|
+
);
|
|
915
|
+
if (!res.ok) {
|
|
916
|
+
return {
|
|
917
|
+
ok: false,
|
|
918
|
+
slotTaken: res.status === 409,
|
|
919
|
+
error: res.status === 409 ? SLOT_TAKEN_COPY : BOOK_FAILED_COPY
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
ok: true,
|
|
924
|
+
appointmentId: typeof res.data?.appointmentId === "string" ? res.data.appointmentId : null
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
async function loadPortalRescheduleContext(id, fetchImpl) {
|
|
928
|
+
const res = await portalGet(portalRescheduleContextUrl(id), fetchImpl);
|
|
929
|
+
if (!res.ok) {
|
|
930
|
+
const cutoff = res.status === 409;
|
|
931
|
+
return { ok: false, cutoff, error: cutoff ? RESCHEDULE_CUTOFF_COPY : RESCHEDULE_FAILED_COPY };
|
|
932
|
+
}
|
|
933
|
+
return { ok: true, context: res.data ?? {} };
|
|
934
|
+
}
|
|
935
|
+
async function reschedulePortalAppointmentAndReload(id, input, fetchImpl) {
|
|
936
|
+
const res = await portalSend(
|
|
937
|
+
`${APPOINTMENTS_URL}/${encodeURIComponent(id)}/reschedule`,
|
|
938
|
+
"POST",
|
|
939
|
+
{ slotStart: input.start, practitionerId: input.practitionerId || void 0 },
|
|
940
|
+
fetchImpl
|
|
941
|
+
);
|
|
942
|
+
if (!res.ok) {
|
|
943
|
+
const cutoff = res.status === 409 && res.error === "cutoff";
|
|
944
|
+
const slotTaken = res.status === 409 && !cutoff;
|
|
945
|
+
return {
|
|
946
|
+
ok: false,
|
|
947
|
+
cutoff,
|
|
948
|
+
slotTaken,
|
|
949
|
+
error: cutoff ? RESCHEDULE_CUTOFF_COPY : slotTaken ? SLOT_TAKEN_COPY : RESCHEDULE_FAILED_COPY
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const next = await portalGet(APPOINTMENTS_URL, fetchImpl);
|
|
953
|
+
const body = (next.ok ? next.data : null) ?? { upcoming: [], past: [] };
|
|
954
|
+
return {
|
|
955
|
+
ok: true,
|
|
956
|
+
data: {
|
|
957
|
+
upcoming: Array.isArray(body.upcoming) ? body.upcoming : [],
|
|
958
|
+
past: Array.isArray(body.past) ? body.past : []
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function PortalSlotSummary({
|
|
963
|
+
start,
|
|
964
|
+
slot,
|
|
965
|
+
tz,
|
|
966
|
+
typeLabel
|
|
967
|
+
}) {
|
|
968
|
+
const meta = [slot?.practitionerName, slot?.locationName].filter(Boolean).join(" \xB7 ");
|
|
969
|
+
return /* @__PURE__ */ jsxs4("dl", { className: "sk-portal-account__fields", children: [
|
|
970
|
+
typeLabel ? /* @__PURE__ */ jsxs4("div", { className: "sk-portal-account__field", children: [
|
|
971
|
+
/* @__PURE__ */ jsx4("dt", { className: "sk-portal-account__field-label", children: "Appointment" }),
|
|
972
|
+
/* @__PURE__ */ jsx4("dd", { className: "sk-portal-account__field-value", children: typeLabel })
|
|
973
|
+
] }) : null,
|
|
974
|
+
/* @__PURE__ */ jsxs4("div", { className: "sk-portal-account__field", children: [
|
|
975
|
+
/* @__PURE__ */ jsx4("dt", { className: "sk-portal-account__field-label", children: "Time" }),
|
|
976
|
+
/* @__PURE__ */ jsx4("dd", { className: "sk-portal-account__field-value", children: formatPortalDateTime(start, tz) ?? formatSlotTime(start, tz) })
|
|
977
|
+
] }),
|
|
978
|
+
meta ? /* @__PURE__ */ jsxs4("div", { className: "sk-portal-account__field", children: [
|
|
979
|
+
/* @__PURE__ */ jsx4("dt", { className: "sk-portal-account__field-label", children: "With" }),
|
|
980
|
+
/* @__PURE__ */ jsx4("dd", { className: "sk-portal-account__field-value", children: meta })
|
|
981
|
+
] }) : null
|
|
982
|
+
] });
|
|
983
|
+
}
|
|
984
|
+
function ErrorNote({ message }) {
|
|
985
|
+
if (!message) return null;
|
|
986
|
+
return /* @__PURE__ */ jsx4("p", { className: "sk-portal-error", role: "alert", children: message });
|
|
987
|
+
}
|
|
988
|
+
function NoTimes() {
|
|
989
|
+
return /* @__PURE__ */ jsxs4("div", { className: "sk-portal-account__note-card", children: [
|
|
990
|
+
/* @__PURE__ */ jsx4("p", { className: "sk-portal-account__confirm-title", children: "No times available right now" }),
|
|
991
|
+
/* @__PURE__ */ jsx4("p", { className: "sk-portal-account__empty", children: "Every time is either booked or outside the booking window. Please try again shortly, or call the clinic." })
|
|
992
|
+
] });
|
|
993
|
+
}
|
|
994
|
+
function PortalRescheduleInline({
|
|
995
|
+
appointment,
|
|
996
|
+
fetchImpl,
|
|
997
|
+
onRescheduled,
|
|
998
|
+
onClose,
|
|
999
|
+
initialUi
|
|
1000
|
+
}) {
|
|
1001
|
+
const [context, setContext] = React4.useState(
|
|
1002
|
+
initialUi?.context ?? null
|
|
1003
|
+
);
|
|
1004
|
+
const [slots, setSlots] = React4.useState(
|
|
1005
|
+
initialUi?.slots ?? null
|
|
1006
|
+
);
|
|
1007
|
+
const [tz, setTz] = React4.useState(initialUi?.tz ?? appointment.timezone ?? "Australia/Sydney");
|
|
1008
|
+
const [selected, setSelected] = React4.useState(initialUi?.selected ?? null);
|
|
1009
|
+
const [confirming, setConfirming] = React4.useState(initialUi?.confirming ?? false);
|
|
1010
|
+
const [error, setError] = React4.useState(initialUi?.error ?? null);
|
|
1011
|
+
const [blocked, setBlocked] = React4.useState(initialUi?.blocked ?? null);
|
|
1012
|
+
const [busy, setBusy] = React4.useState(false);
|
|
1013
|
+
const seeded = initialUi !== void 0;
|
|
1014
|
+
const timeSlots = React4.useMemo(() => slots ? portalTimeSlots(slots) : [], [slots]);
|
|
1015
|
+
React4.useEffect(() => {
|
|
1016
|
+
if (seeded) return;
|
|
1017
|
+
let live = true;
|
|
1018
|
+
void (async () => {
|
|
1019
|
+
const res = await loadPortalRescheduleContext(appointment.id, fetchImpl);
|
|
1020
|
+
if (!live) return;
|
|
1021
|
+
if (!res.ok) {
|
|
1022
|
+
setBlocked(res.error);
|
|
1023
|
+
setSlots([]);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
setContext(res.context);
|
|
1027
|
+
const inline = res.context.slots;
|
|
1028
|
+
if (Array.isArray(inline)) {
|
|
1029
|
+
const read = readPortalAvailability({ slots: inline, timezone: res.context.timezone });
|
|
1030
|
+
setSlots(read.slots);
|
|
1031
|
+
setTz(read.timezone);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const typeId = rescheduleContextTypeId(res.context);
|
|
1035
|
+
if (!typeId) {
|
|
1036
|
+
setSlots([]);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const avail = await loadPortalAvailability(typeId, fetchImpl);
|
|
1040
|
+
if (!live) return;
|
|
1041
|
+
if (!avail.ok) {
|
|
1042
|
+
setSlots([]);
|
|
1043
|
+
setError(AVAILABILITY_FAILED_COPY);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
setSlots(avail.data.slots);
|
|
1047
|
+
setTz(avail.data.timezone);
|
|
1048
|
+
})();
|
|
1049
|
+
return () => {
|
|
1050
|
+
live = false;
|
|
1051
|
+
};
|
|
1052
|
+
}, [appointment.id, fetchImpl, seeded]);
|
|
1053
|
+
async function refetch() {
|
|
1054
|
+
const typeId = context ? rescheduleContextTypeId(context) : null;
|
|
1055
|
+
if (!typeId) return;
|
|
1056
|
+
const avail = await loadPortalAvailability(typeId, fetchImpl);
|
|
1057
|
+
if (!avail.ok) return;
|
|
1058
|
+
setSlots(avail.data.slots);
|
|
1059
|
+
setTz(avail.data.timezone);
|
|
1060
|
+
}
|
|
1061
|
+
async function move() {
|
|
1062
|
+
if (!selected || busy) return;
|
|
1063
|
+
setBusy(true);
|
|
1064
|
+
setError(null);
|
|
1065
|
+
const res = await reschedulePortalAppointmentAndReload(
|
|
1066
|
+
appointment.id,
|
|
1067
|
+
{ start: selected.start, practitionerId: selected.practitionerId },
|
|
1068
|
+
fetchImpl
|
|
1069
|
+
);
|
|
1070
|
+
setBusy(false);
|
|
1071
|
+
if (res.ok) {
|
|
1072
|
+
onRescheduled(res.data);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (res.cutoff) {
|
|
1076
|
+
setBlocked(res.error);
|
|
1077
|
+
setConfirming(false);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
setError(res.error);
|
|
1081
|
+
if (res.slotTaken) {
|
|
1082
|
+
setSelected(null);
|
|
1083
|
+
setConfirming(false);
|
|
1084
|
+
await refetch();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const typeLabel = context?.typeLabel ?? appointment.title ?? null;
|
|
1088
|
+
return /* @__PURE__ */ jsxs4("div", { className: "sk-portal-account__confirm sk-portal-account__confirm--neutral", children: [
|
|
1089
|
+
/* @__PURE__ */ jsx4("p", { className: "sk-portal-account__confirm-title", children: "Move this appointment" }),
|
|
1090
|
+
/* @__PURE__ */ jsxs4("p", { className: "sk-portal-account__item-meta", children: [
|
|
1091
|
+
"Currently ",
|
|
1092
|
+
formatPortalDateTime(appointment.start, appointment.timezone) ?? "unscheduled"
|
|
1093
|
+
] }),
|
|
1094
|
+
blocked ? /* @__PURE__ */ jsx4("p", { className: "sk-portal-account__note", role: "note", children: blocked }) : slots === null ? /* @__PURE__ */ jsx4("p", { className: "sk-portal-account__placeholder", role: "status", children: "Loading available times\u2026" }) : confirming && selected ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1095
|
+
/* @__PURE__ */ jsx4(
|
|
1096
|
+
PortalSlotSummary,
|
|
1097
|
+
{
|
|
1098
|
+
start: selected.start,
|
|
1099
|
+
slot: portalSlotAt(slots, selected.start),
|
|
1100
|
+
tz,
|
|
1101
|
+
typeLabel
|
|
1102
|
+
}
|
|
1103
|
+
),
|
|
1104
|
+
/* @__PURE__ */ jsx4(ErrorNote, { message: error }),
|
|
1105
|
+
/* @__PURE__ */ jsxs4("div", { className: "sk-portal-account__confirm-actions", children: [
|
|
1106
|
+
/* @__PURE__ */ jsx4(
|
|
1107
|
+
"button",
|
|
1108
|
+
{
|
|
1109
|
+
type: "button",
|
|
1110
|
+
className: "sk-portal-button",
|
|
1111
|
+
onClick: () => void move(),
|
|
1112
|
+
disabled: busy,
|
|
1113
|
+
children: busy ? "Moving\u2026" : "Move appointment"
|
|
1114
|
+
}
|
|
1115
|
+
),
|
|
1116
|
+
/* @__PURE__ */ jsx4(
|
|
1117
|
+
"button",
|
|
1118
|
+
{
|
|
1119
|
+
type: "button",
|
|
1120
|
+
className: "sk-portal-button sk-portal-button--ghost",
|
|
1121
|
+
onClick: () => setConfirming(false),
|
|
1122
|
+
disabled: busy,
|
|
1123
|
+
children: "Choose another time"
|
|
1124
|
+
}
|
|
1125
|
+
)
|
|
1126
|
+
] })
|
|
1127
|
+
] }) : timeSlots.length === 0 ? /* @__PURE__ */ jsx4(NoTimes, {}) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1128
|
+
/* @__PURE__ */ jsx4(ErrorNote, { message: error }),
|
|
1129
|
+
/* @__PURE__ */ jsx4(
|
|
1130
|
+
SlotBoard,
|
|
1131
|
+
{
|
|
1132
|
+
slots: timeSlots,
|
|
1133
|
+
tz,
|
|
1134
|
+
selected,
|
|
1135
|
+
onSelect: setSelected,
|
|
1136
|
+
mode: "portal-reschedule"
|
|
1137
|
+
}
|
|
1138
|
+
),
|
|
1139
|
+
/* @__PURE__ */ jsx4("div", { className: "sk-portal-account__confirm-actions", children: /* @__PURE__ */ jsx4(
|
|
1140
|
+
"button",
|
|
1141
|
+
{
|
|
1142
|
+
type: "button",
|
|
1143
|
+
className: "sk-portal-button",
|
|
1144
|
+
onClick: () => setConfirming(true),
|
|
1145
|
+
disabled: !selected,
|
|
1146
|
+
children: selected ? `Continue with ${formatSlotTime(selected.start, tz)}` : "Select a time"
|
|
1147
|
+
}
|
|
1148
|
+
) })
|
|
1149
|
+
] }),
|
|
1150
|
+
/* @__PURE__ */ jsx4("div", { className: "sk-portal-account__confirm-actions", children: /* @__PURE__ */ jsx4(
|
|
1151
|
+
"button",
|
|
1152
|
+
{
|
|
1153
|
+
type: "button",
|
|
1154
|
+
className: "sk-portal-button sk-portal-button--ghost",
|
|
1155
|
+
onClick: onClose,
|
|
1156
|
+
disabled: busy,
|
|
1157
|
+
children: "Keep the current time"
|
|
1158
|
+
}
|
|
1159
|
+
) })
|
|
1160
|
+
] });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export {
|
|
1164
|
+
FlowRailContext,
|
|
1165
|
+
FlowShell,
|
|
1166
|
+
FlowButton,
|
|
1167
|
+
SummarisedFieldErrors,
|
|
1168
|
+
FlowError,
|
|
1169
|
+
FlowFieldError,
|
|
1170
|
+
describedBy,
|
|
1171
|
+
invalidFields,
|
|
1172
|
+
FlowErrorSummary,
|
|
1173
|
+
focusField,
|
|
1174
|
+
FlowLoading,
|
|
1175
|
+
PatientOSApiError,
|
|
1176
|
+
patientos,
|
|
1177
|
+
persistClaimToken,
|
|
1178
|
+
adoptClaimToken,
|
|
1179
|
+
resetSession,
|
|
1180
|
+
saveFlowRecord,
|
|
1181
|
+
loadFlowRecord,
|
|
1182
|
+
once,
|
|
1183
|
+
findServiceByKey,
|
|
1184
|
+
pickDeliveryOption,
|
|
1185
|
+
findDeliveryOption,
|
|
1186
|
+
effectiveFee,
|
|
1187
|
+
flowIsFree,
|
|
1188
|
+
formatFee,
|
|
1189
|
+
feeNote,
|
|
1190
|
+
feeRowNote,
|
|
1191
|
+
gateToExit,
|
|
1192
|
+
errorMessage,
|
|
1193
|
+
errorCode,
|
|
1194
|
+
isSessionExpired,
|
|
1195
|
+
isClaimRejected,
|
|
1196
|
+
observeSessionExpiry,
|
|
1197
|
+
isSlotTakenError,
|
|
1198
|
+
formatSlotTime,
|
|
1199
|
+
zoneAbbr,
|
|
1200
|
+
SlotPicker,
|
|
1201
|
+
PORTAL_REQUEST_INIT,
|
|
1202
|
+
portalGet,
|
|
1203
|
+
portalSend,
|
|
1204
|
+
portalDelete,
|
|
1205
|
+
hasPortalIdentity,
|
|
1206
|
+
readPortalSignedInHint,
|
|
1207
|
+
writePortalSignedInHint,
|
|
1208
|
+
requestPortalSignInLink,
|
|
1209
|
+
signOutOfPortal,
|
|
1210
|
+
currentPagePath,
|
|
1211
|
+
formatPortalDate,
|
|
1212
|
+
formatPortalDateTime,
|
|
1213
|
+
formatPortalTime,
|
|
1214
|
+
formatPortalMoney,
|
|
1215
|
+
PortalSignInRamp,
|
|
1216
|
+
PortalLinkSentNotice,
|
|
1217
|
+
rescheduleContextTypeId,
|
|
1218
|
+
PORTAL_BOOKING_TYPES_URL,
|
|
1219
|
+
portalRescheduleContextUrl,
|
|
1220
|
+
SLOT_TAKEN_COPY,
|
|
1221
|
+
BOOK_FAILED_COPY,
|
|
1222
|
+
AVAILABILITY_FAILED_COPY,
|
|
1223
|
+
RESCHEDULE_CUTOFF_COPY,
|
|
1224
|
+
RESCHEDULE_FAILED_COPY,
|
|
1225
|
+
readPortalBookingTypes,
|
|
1226
|
+
readPortalAvailability,
|
|
1227
|
+
portalTimeSlots,
|
|
1228
|
+
portalSlotAt,
|
|
1229
|
+
portalAvailabilityWindow,
|
|
1230
|
+
portalAvailabilityUrl,
|
|
1231
|
+
loadPortalBookingTypes,
|
|
1232
|
+
loadPortalAvailability,
|
|
1233
|
+
createPortalHold,
|
|
1234
|
+
releasePortalHold,
|
|
1235
|
+
bookPortalAppointment,
|
|
1236
|
+
loadPortalRescheduleContext,
|
|
1237
|
+
reschedulePortalAppointmentAndReload,
|
|
1238
|
+
PortalSlotSummary,
|
|
1239
|
+
PortalRescheduleInline
|
|
1240
|
+
};
|