@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,22 @@
|
|
|
1
|
+
/** The four theme values an island will honour. Anything absent falls back to kit defaults. */
|
|
2
|
+
interface PatientSurfaceTheme {
|
|
3
|
+
/** Brand colour — the island's primary. */
|
|
4
|
+
primary?: string;
|
|
5
|
+
/** Secondary emphasis colour. */
|
|
6
|
+
accent?: string;
|
|
7
|
+
/** Corner radius, a plain CSS length. */
|
|
8
|
+
radius?: string;
|
|
9
|
+
/** Font family stack. */
|
|
10
|
+
fontFamily?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The clinic-supplied half of an island's theme, validated.
|
|
14
|
+
*
|
|
15
|
+
* Reads `primary` (falling back to the `brandColor` key the brand row uses), `accent`,
|
|
16
|
+
* `radius` and `fontFamily` off a loose token bag. Every value is checked; failures are
|
|
17
|
+
* dropped silently — a broken token should degrade to the kit default, never fail a build
|
|
18
|
+
* or a render. `null`/absent tokens yield `{}`.
|
|
19
|
+
*/
|
|
20
|
+
declare function patientSurfaceTheme(themeTokens: Record<string, unknown> | null): PatientSurfaceTheme;
|
|
21
|
+
|
|
22
|
+
export { type PatientSurfaceTheme, patientSurfaceTheme };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Which portal surface this panel shows. */
|
|
4
|
+
type PortalSurface = 'appointments' | 'documents' | 'orders' | 'profile';
|
|
5
|
+
/** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
|
|
6
|
+
type PortalPanelProps = {
|
|
7
|
+
/** The surface to render. Required — a panel is always ONE surface. */
|
|
8
|
+
surface: PortalSurface;
|
|
9
|
+
/**
|
|
10
|
+
* Where a signed-out visitor is sent to sign in. Omitted ⇒ the app's reserved
|
|
11
|
+
* `/portal` path on this same origin.
|
|
12
|
+
*/
|
|
13
|
+
portalUrl?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Marker-only props: the island config plus presentation the page controls. */
|
|
16
|
+
type PortalPanelMarkerProps = PortalPanelProps & {
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* BUILD-TIME marker. Emits the mount point plus a static no-JS fallback.
|
|
21
|
+
*
|
|
22
|
+
* The fallback deliberately shows NO patient data (there is none at build time — the
|
|
23
|
+
* page is ONE static artifact served to every visitor) and only a sign-in link. It is
|
|
24
|
+
* fully VISIBLE, not a hidden skeleton: this HTML is what a crawler indexes, what a
|
|
25
|
+
* screen reader reads before the island mounts, and what a patient with JS blocked
|
|
26
|
+
* gets permanently. A `visibility:hidden` / `opacity:0` placeholder would make the
|
|
27
|
+
* no-JS experience a blank box.
|
|
28
|
+
*/
|
|
29
|
+
declare function PortalPanel({ surface, portalUrl, className, }: PortalPanelMarkerProps): React.ReactElement;
|
|
30
|
+
|
|
31
|
+
/** A service the launcher can offer — what the build snapshot carries, nothing more. */
|
|
32
|
+
type LauncherService = {
|
|
33
|
+
/** The clinic-stable `service.key`; what the funnel starts from. */
|
|
34
|
+
key: string;
|
|
35
|
+
label: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The build-time wiring an island is handed (the snapshot's `publicApi` block). */
|
|
39
|
+
interface PublicApiConfig {
|
|
40
|
+
publishableKey: string;
|
|
41
|
+
/** '' ⇒ same-origin (the one-origin case). */
|
|
42
|
+
apiBase: string;
|
|
43
|
+
turnstileSiteKey?: string;
|
|
44
|
+
portalUrl?: string;
|
|
45
|
+
portalOrigin?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Which account surface this island renders.
|
|
50
|
+
*
|
|
51
|
+
* A SUPERSET of `<PortalPanel/>`'s four: the teaser can only summarise things that
|
|
52
|
+
* already exist, so it has no `book` and no `addresses` — there is nothing to tease. The
|
|
53
|
+
* four shared names stay identical so a clinic page can pair a teaser and a full surface
|
|
54
|
+
* without a second vocabulary (PAT-783), and the account island adds the two DOING
|
|
55
|
+
* surfaces on top (PAT-789).
|
|
56
|
+
*/
|
|
57
|
+
type PortalAccountSurface = PortalSurface | 'book' | 'addresses';
|
|
58
|
+
/** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
|
|
59
|
+
type PortalAccountProps = {
|
|
60
|
+
/** The surface to render. Required — one island is always ONE surface. */
|
|
61
|
+
surface: PortalAccountSurface;
|
|
62
|
+
/**
|
|
63
|
+
* The portal's base path on this origin, used for the two things this island still
|
|
64
|
+
* does NOT inline (joining a telehealth consult, the signed-out/unreachable fallback).
|
|
65
|
+
* Omitted ⇒ the app's reserved `/portal`.
|
|
66
|
+
*/
|
|
67
|
+
portalUrl?: string;
|
|
68
|
+
/**
|
|
69
|
+
* The SITE page carrying `<PortalAccount surface="book"/>`.
|
|
70
|
+
*
|
|
71
|
+
* Booking is inline now (PAT-789), so "Book an appointment" must land on the clinic's
|
|
72
|
+
* OWN page rather than bouncing to `/portal/book` — the bounce is the seam one-origin
|
|
73
|
+
* exists to remove. Omitted ⇒ `/account/book`, where the scaffolded account pages live.
|
|
74
|
+
*/
|
|
75
|
+
bookHref?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The SITE page carrying `<PortalAccount surface="appointments"/>` — where a completed
|
|
78
|
+
* booking sends the patient. Omitted ⇒ `bookHref` minus its last segment, which is the
|
|
79
|
+
* right answer for the scaffolded `/account/book` → `/account` pairing.
|
|
80
|
+
*
|
|
81
|
+
* NO CONSUMER since PAT-899: the account area stopped completing bookings when `book`
|
|
82
|
+
* became a launcher, and the funnel owns where a finished request lands. Kept so pages
|
|
83
|
+
* already passing it keep building; drop it once no site does.
|
|
84
|
+
*/
|
|
85
|
+
appointmentsHref?: string;
|
|
86
|
+
/**
|
|
87
|
+
* The SITE page carrying `<CertificateFunnel/>` — where the `book` surface sends a
|
|
88
|
+
* patient who picks a service (PAT-899). Omitted ⇒ `/certificates`.
|
|
89
|
+
*/
|
|
90
|
+
funnelHref?: string;
|
|
91
|
+
/** The clinic's services, for the `book` launcher. */
|
|
92
|
+
services?: LauncherService[];
|
|
93
|
+
/** How the launcher reaches the public API to price those services. */
|
|
94
|
+
publicApi?: PublicApiConfig;
|
|
95
|
+
/** Shown when the clinic has published no services at all. */
|
|
96
|
+
clinicPhone?: string;
|
|
97
|
+
};
|
|
98
|
+
/** Marker-only props: the island config plus presentation the page controls. */
|
|
99
|
+
type PortalAccountMarkerProps = PortalAccountProps & {
|
|
100
|
+
className?: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* BUILD-TIME marker. Emits the mount point plus a static no-JS fallback.
|
|
104
|
+
*
|
|
105
|
+
* The fallback is REAL, VISIBLE content — a sign-in card with a link into the portal —
|
|
106
|
+
* not a hidden skeleton. This HTML is what a crawler indexes, what a screen reader reads
|
|
107
|
+
* before the island mounts, and what a patient with JavaScript blocked gets PERMANENTLY.
|
|
108
|
+
* It shows no patient data because there is none at build time: the page is ONE static
|
|
109
|
+
* artifact served to every visitor.
|
|
110
|
+
*/
|
|
111
|
+
declare function PortalAccount({ surface, portalUrl, bookHref, appointmentsHref, funnelHref, className, }: PortalAccountMarkerProps): React.ReactElement;
|
|
112
|
+
|
|
113
|
+
export { PortalAccount as P, type PortalAccountMarkerProps as a, type PortalAccountProps as b, type PortalAccountSurface as c, PortalPanel as d, type PortalPanelMarkerProps as e, type PortalPanelProps as f, type PortalSurface as g };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { b as PortalAccountProps } from './portal-account-EhOtLV5u.js';
|
|
3
|
+
|
|
4
|
+
/** The injectable fetch. Present so every behaviour here is provable without a DOM. */
|
|
5
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
/**
|
|
7
|
+
* The outcome of one portal call. A VALUE, never a throw.
|
|
8
|
+
*
|
|
9
|
+
* `status` is the HTTP status when the server answered and refused, and `null` when the
|
|
10
|
+
* transport failed or the body was not JSON — the caller needs that distinction because
|
|
11
|
+
* a 404 means "the clinic turned this off" (a quiet note) while a transport failure means
|
|
12
|
+
* "we cannot answer here" (link out).
|
|
13
|
+
*/
|
|
14
|
+
type PortalResult<T> = {
|
|
15
|
+
ok: true;
|
|
16
|
+
data: T;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `body` is the parsed error payload when there was one — typed `unknown`, NOT `T`:
|
|
20
|
+
* a refusal is not the success shape, and a caller must narrow it before trusting a
|
|
21
|
+
* field (see `hasPortalIdentity`). Keeping it off `T` also keeps a failed result
|
|
22
|
+
* assignable across surface types, which the callers that pass one through rely on.
|
|
23
|
+
*/
|
|
24
|
+
| {
|
|
25
|
+
ok: false;
|
|
26
|
+
status: number | null;
|
|
27
|
+
error: string | null;
|
|
28
|
+
body?: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type PortalAddress = {
|
|
32
|
+
id: string;
|
|
33
|
+
line1?: string | null;
|
|
34
|
+
line2?: string | null;
|
|
35
|
+
suburb?: string | null;
|
|
36
|
+
state?: string | null;
|
|
37
|
+
postcode?: string | null;
|
|
38
|
+
/** Exactly one row is the primary. The SERVER owns that invariant, not this island. */
|
|
39
|
+
isPrimary?: boolean;
|
|
40
|
+
label?: string | null;
|
|
41
|
+
};
|
|
42
|
+
type PortalAddressesBody = {
|
|
43
|
+
items: PortalAddress[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type PortalAppointment = {
|
|
47
|
+
id: string;
|
|
48
|
+
title?: string | null;
|
|
49
|
+
status?: string | null;
|
|
50
|
+
start?: string | null;
|
|
51
|
+
end?: string | null;
|
|
52
|
+
timezone?: string | null;
|
|
53
|
+
locationName?: string | null;
|
|
54
|
+
practitionerName?: string | null;
|
|
55
|
+
telehealth?: boolean;
|
|
56
|
+
canCancel?: boolean;
|
|
57
|
+
canReschedule?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The server's own answer to "can this patient join the video room right now?" — the
|
|
60
|
+
* join window is open AND the clinic has telehealth configured. When true the button
|
|
61
|
+
* shows regardless of `joinOpensAt` (PAT-792).
|
|
62
|
+
*/
|
|
63
|
+
canJoin?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* When the join window OPENS, ISO. Present on a telehealth row whose window is still
|
|
66
|
+
* ahead, so the page can flip to the button on the LOCAL clock rather than making the
|
|
67
|
+
* patient reload at the exact moment they least want to. The mint re-checks anyway.
|
|
68
|
+
*/
|
|
69
|
+
joinOpensAt?: string | null;
|
|
70
|
+
};
|
|
71
|
+
type PortalAppointmentsBody = {
|
|
72
|
+
upcoming: PortalAppointment[];
|
|
73
|
+
past: PortalAppointment[];
|
|
74
|
+
};
|
|
75
|
+
type PortalDocument = {
|
|
76
|
+
id: string;
|
|
77
|
+
title?: string | null;
|
|
78
|
+
status?: string | null;
|
|
79
|
+
date?: string | null;
|
|
80
|
+
};
|
|
81
|
+
type PortalDocumentsBody = {
|
|
82
|
+
items: PortalDocument[];
|
|
83
|
+
};
|
|
84
|
+
type PortalOrder = {
|
|
85
|
+
id: string;
|
|
86
|
+
title?: string | null;
|
|
87
|
+
status?: string | null;
|
|
88
|
+
paymentStatus?: string | null;
|
|
89
|
+
total?: string | null;
|
|
90
|
+
currency?: string | null;
|
|
91
|
+
placedAt?: string | null;
|
|
92
|
+
};
|
|
93
|
+
type PortalOrdersBody = {
|
|
94
|
+
items: PortalOrder[];
|
|
95
|
+
};
|
|
96
|
+
type PortalProfile = {
|
|
97
|
+
givenName?: string | null;
|
|
98
|
+
familyName?: string | null;
|
|
99
|
+
email?: string | null;
|
|
100
|
+
phone?: string | null;
|
|
101
|
+
dateOfBirth?: string | null;
|
|
102
|
+
};
|
|
103
|
+
/** The body of whichever surface this island is showing. */
|
|
104
|
+
type PortalSurfaceBody = PortalAppointmentsBody | PortalDocumentsBody | PortalOrdersBody | PortalProfile | PortalAddressesBody;
|
|
105
|
+
/**
|
|
106
|
+
* Everything the island can be showing. A discriminated union derived once by
|
|
107
|
+
* `loadPortalAccount`, so the render is a pure function of state — and so the whole
|
|
108
|
+
* boot (including every failure mode) is testable without a DOM.
|
|
109
|
+
*/
|
|
110
|
+
type PortalAccountState =
|
|
111
|
+
/**
|
|
112
|
+
* Booting. `hinted` means a previous page in this TAB was signed in (PAT-797) — the
|
|
113
|
+
* account tabs are four separate documents, so without it every tab click flashes
|
|
114
|
+
* "Checking your sign-in…" for an answer that has not changed. Hinted, we show the
|
|
115
|
+
* shape of the surface instead and let the response fill it in. ADVISORY ONLY: it
|
|
116
|
+
* never gates what the patient is shown, it only picks the placeholder.
|
|
117
|
+
*/
|
|
118
|
+
{
|
|
119
|
+
kind: 'loading';
|
|
120
|
+
hinted?: boolean;
|
|
121
|
+
}
|
|
122
|
+
/** No patient session on this host — show the inline magic-link ramp. */
|
|
123
|
+
| {
|
|
124
|
+
kind: 'signed-out';
|
|
125
|
+
}
|
|
126
|
+
/** A link was requested; we never say whether the address matched an account. */
|
|
127
|
+
| {
|
|
128
|
+
kind: 'link-sent';
|
|
129
|
+
email: string;
|
|
130
|
+
} | {
|
|
131
|
+
kind: 'signed-in';
|
|
132
|
+
givenName: string | null;
|
|
133
|
+
data: PortalSurfaceBody;
|
|
134
|
+
}
|
|
135
|
+
/** Signed in, but the clinic has this module off. A quiet note, NOT an error. */
|
|
136
|
+
| {
|
|
137
|
+
kind: 'not-available';
|
|
138
|
+
givenName: string | null;
|
|
139
|
+
}
|
|
140
|
+
/** We cannot answer here. Degrades to a plain link out. */
|
|
141
|
+
| {
|
|
142
|
+
kind: 'link-out';
|
|
143
|
+
};
|
|
144
|
+
/** How a fetched URL is opened. Injected so the open is assertable without a DOM. */
|
|
145
|
+
type OpenUrl = (url: string) => void;
|
|
146
|
+
type PortalAccountClientProps = PortalAccountProps & {
|
|
147
|
+
/** Test seam: the initial state, so the view is renderable without effects. */
|
|
148
|
+
initialState?: PortalAccountState;
|
|
149
|
+
/** Test seam: the fetch every call goes through. Defaults to the global one. */
|
|
150
|
+
fetchImpl?: FetchLike;
|
|
151
|
+
/** Test seam: how a minted document/receipt URL is opened. */
|
|
152
|
+
openUrl?: OpenUrl;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* The island entry point, which forks on ONE surface.
|
|
156
|
+
*
|
|
157
|
+
* `book` is a LAUNCHER now (PAT-899), and a launcher is not an account view: it shows the
|
|
158
|
+
* clinic's public service list, identically to everyone. Routing it around the session
|
|
159
|
+
* machinery is not an optimisation — it is the behaviour. Sending it through the shared
|
|
160
|
+
* path would make it fetch a surface it does not read, and would put a sign-in ramp in
|
|
161
|
+
* front of a patient who is ready to book, when the funnel it links to is itself an
|
|
162
|
+
* anonymous flow that upgrades on session.
|
|
163
|
+
*
|
|
164
|
+
* A wrapper rather than an early return inside the view, so the branch cannot be mistaken
|
|
165
|
+
* for a conditional hook.
|
|
166
|
+
*/
|
|
167
|
+
declare function PortalAccountClient(props: PortalAccountClientProps): React.ReactElement;
|
|
168
|
+
|
|
169
|
+
export { type FetchLike as F, PortalAccountClient as P, type PortalAppointment as a, type PortalAppointmentsBody as b, type PortalResult as c };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { a as PortalAppointment, F as FetchLike, b as PortalAppointmentsBody, c as PortalResult } from './portal-account.client-CTget1-3.js';
|
|
3
|
+
import './portal-account-EhOtLV5u.js';
|
|
4
|
+
|
|
5
|
+
/** One bookable instant, collapsing the practitioners free at the same time. */
|
|
6
|
+
interface TimeSlot {
|
|
7
|
+
start: string;
|
|
8
|
+
practitionerId: string;
|
|
9
|
+
/** How many practitioners are free — drives the "1 left" nudge. */
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PortalBookingType = {
|
|
14
|
+
id: string;
|
|
15
|
+
key?: string | null;
|
|
16
|
+
label?: string | null;
|
|
17
|
+
durationMinutes?: number | null;
|
|
18
|
+
description?: string | null;
|
|
19
|
+
modality?: string | null;
|
|
20
|
+
};
|
|
21
|
+
type PortalBookingTypesBody = {
|
|
22
|
+
types: PortalBookingType[];
|
|
23
|
+
};
|
|
24
|
+
type PortalAvailabilitySlot = {
|
|
25
|
+
start: string;
|
|
26
|
+
end?: string | null;
|
|
27
|
+
practitionerId?: string | null;
|
|
28
|
+
practitionerName?: string | null;
|
|
29
|
+
locationName?: string | null;
|
|
30
|
+
timezone?: string | null;
|
|
31
|
+
};
|
|
32
|
+
type PortalAvailabilityBody = {
|
|
33
|
+
slots: PortalAvailabilitySlot[];
|
|
34
|
+
timezone: string;
|
|
35
|
+
};
|
|
36
|
+
/** The advisory reservation. `expiresAt` is shown to the patient, never enforced here. */
|
|
37
|
+
type PortalHold = {
|
|
38
|
+
holdId: string;
|
|
39
|
+
expiresAt?: string | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* What the server hands back to drive a reschedule.
|
|
43
|
+
*
|
|
44
|
+
* Read LOOSELY on purpose: it may carry the slots itself (one round trip) or only the
|
|
45
|
+
* appointment type to fetch availability for (two). Both are supported, so the server
|
|
46
|
+
* half can settle on either without breaking a published site — which is a real risk
|
|
47
|
+
* here, because a clinic's site is a static artifact built at a moment in time.
|
|
48
|
+
*/
|
|
49
|
+
type PortalRescheduleContext = {
|
|
50
|
+
appointment?: (PortalAppointment & {
|
|
51
|
+
appointmentType?: {
|
|
52
|
+
id?: string | null;
|
|
53
|
+
label?: string | null;
|
|
54
|
+
} | null;
|
|
55
|
+
}) | null;
|
|
56
|
+
appointmentTypeId?: string | null;
|
|
57
|
+
typeLabel?: string | null;
|
|
58
|
+
slots?: PortalAvailabilitySlot[];
|
|
59
|
+
timezone?: string | null;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The appointment type to fetch availability for. The server (PAT-789 canonical
|
|
63
|
+
* contract) nests it as `appointment.appointmentType.id`; the flat `appointmentTypeId`
|
|
64
|
+
* stays supported so an older artifact keeps working if the shape ever flattens.
|
|
65
|
+
*/
|
|
66
|
+
declare function rescheduleContextTypeId(ctx: PortalRescheduleContext): string | null;
|
|
67
|
+
declare const PORTAL_BOOKING_TYPES_URL = "/portal/api/booking/types";
|
|
68
|
+
/** The reschedule context for one appointment. */
|
|
69
|
+
declare function portalRescheduleContextUrl(id: string): string;
|
|
70
|
+
declare const SLOT_TAKEN_COPY = "That time just filled up. Please choose another.";
|
|
71
|
+
declare const BOOK_FAILED_COPY = "We couldn't book that time just now. Please try again.";
|
|
72
|
+
declare const AVAILABILITY_FAILED_COPY = "We could not load available times.";
|
|
73
|
+
declare const RESCHEDULE_CUTOFF_COPY = "It's too close to your appointment to change it online. Please call the clinic and we'll sort it out.";
|
|
74
|
+
declare const RESCHEDULE_FAILED_COPY = "We couldn't move that appointment just now. Please try again.";
|
|
75
|
+
/** Normalise a `{types:[…]}` body — a missing or wrong-typed list is an EMPTY list. */
|
|
76
|
+
declare function readPortalBookingTypes(body: unknown): PortalBookingTypesBody;
|
|
77
|
+
/**
|
|
78
|
+
* Normalise an availability body.
|
|
79
|
+
*
|
|
80
|
+
* The timezone is the CLINIC's and travels with the answer: a patient booking a Perth
|
|
81
|
+
* clinic from Sydney must see Perth wall-clock times or they turn up two hours out. It is
|
|
82
|
+
* read from the body, then from the first slot, then falls back — never from the browser.
|
|
83
|
+
*/
|
|
84
|
+
declare function readPortalAvailability(body: unknown): PortalAvailabilityBody;
|
|
85
|
+
/**
|
|
86
|
+
* Collapse raw slots into the board's `TimeSlot`s.
|
|
87
|
+
*
|
|
88
|
+
* Several practitioners free at the same minute is ONE choice for a patient with a count
|
|
89
|
+
* behind it (which drives the "1 left" nudge) — the same collapsing the anonymous funnel
|
|
90
|
+
* does, so the two grids read identically.
|
|
91
|
+
*/
|
|
92
|
+
declare function portalTimeSlots(slots: readonly PortalAvailabilitySlot[]): TimeSlot[];
|
|
93
|
+
/** The first raw slot at an instant — where the practitioner/location labels come from. */
|
|
94
|
+
declare function portalSlotAt(slots: readonly PortalAvailabilitySlot[], start?: string | null): PortalAvailabilitySlot | null;
|
|
95
|
+
/**
|
|
96
|
+
* The availability window: from today, out a fortnight.
|
|
97
|
+
*
|
|
98
|
+
* `YYYY-MM-DD`, NOT an ISO instant. The edge validates `from`/`to` through the canonical
|
|
99
|
+
* `availabilityQuery` schema, whose dates are strictly date-only — an instant is a 400
|
|
100
|
+
* `invalid_query`, so the surface could never load a single time (PAT-796). The anonymous
|
|
101
|
+
* widget has always sent date-only here; this is the same window, spelled the same way.
|
|
102
|
+
*
|
|
103
|
+
* Losing the time component costs nothing: the availability core drops slots starting
|
|
104
|
+
* before `now + leadTimeMinutes`, so a same-day window never offers a time that has
|
|
105
|
+
* already passed. The day keys are UTC, matching the public path exactly.
|
|
106
|
+
*
|
|
107
|
+
* Pure + exported so the range is assertable without freezing the clock in a component
|
|
108
|
+
* test. Fourteen days matches the public booking window the anonymous funnel uses.
|
|
109
|
+
*/
|
|
110
|
+
declare function portalAvailabilityWindow(now?: number, days?: number): {
|
|
111
|
+
from: string;
|
|
112
|
+
to: string;
|
|
113
|
+
};
|
|
114
|
+
declare function portalAvailabilityUrl(typeId: string, window?: {
|
|
115
|
+
from: string;
|
|
116
|
+
to: string;
|
|
117
|
+
}): string;
|
|
118
|
+
declare function loadPortalBookingTypes(fetchImpl?: FetchLike): Promise<PortalResult<PortalBookingTypesBody>>;
|
|
119
|
+
declare function loadPortalAvailability(typeId: string, fetchImpl?: FetchLike, window?: {
|
|
120
|
+
from: string;
|
|
121
|
+
to: string;
|
|
122
|
+
}): Promise<PortalResult<PortalAvailabilityBody>>;
|
|
123
|
+
/**
|
|
124
|
+
* Take an advisory hold on a slot.
|
|
125
|
+
*
|
|
126
|
+
* Returns `null` rather than an error on failure: a hold that could not be taken is a
|
|
127
|
+
* missing courtesy, not a reason to stop a patient booking. The caller carries on.
|
|
128
|
+
*/
|
|
129
|
+
declare function createPortalHold(input: {
|
|
130
|
+
typeId: string;
|
|
131
|
+
start: string;
|
|
132
|
+
practitionerId?: string | null;
|
|
133
|
+
}, fetchImpl?: FetchLike): Promise<PortalHold | null>;
|
|
134
|
+
/** Give a hold back when the patient steps away from the confirm screen. Best-effort. */
|
|
135
|
+
declare function releasePortalHold(holdId: string, fetchImpl?: FetchLike): Promise<{
|
|
136
|
+
ok: boolean;
|
|
137
|
+
}>;
|
|
138
|
+
type PortalBookInput = {
|
|
139
|
+
typeId: string;
|
|
140
|
+
start: string;
|
|
141
|
+
practitionerId?: string | null;
|
|
142
|
+
holdId?: string | null;
|
|
143
|
+
};
|
|
144
|
+
/** The outcome a patient is shown. `slotTaken` is the one failure with a next step. */
|
|
145
|
+
type PortalBookOutcome = {
|
|
146
|
+
ok: true;
|
|
147
|
+
appointmentId: string | null;
|
|
148
|
+
} | {
|
|
149
|
+
ok: false;
|
|
150
|
+
slotTaken: boolean;
|
|
151
|
+
error: string;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Book. The AUTHORITATIVE call — the hold above only reduced the odds of losing the race.
|
|
155
|
+
*
|
|
156
|
+
* A 409 means someone else got there first, which is not the patient's mistake: it is
|
|
157
|
+
* reported as "choose another" and the caller refetches, never as an error they must
|
|
158
|
+
* interpret.
|
|
159
|
+
*/
|
|
160
|
+
declare function bookPortalAppointment(input: PortalBookInput, fetchImpl?: FetchLike): Promise<PortalBookOutcome>;
|
|
161
|
+
/** What a reschedule context read can turn into. `cutoff` is a real, expected answer. */
|
|
162
|
+
type PortalRescheduleContextOutcome = {
|
|
163
|
+
ok: true;
|
|
164
|
+
context: PortalRescheduleContext;
|
|
165
|
+
} | {
|
|
166
|
+
ok: false;
|
|
167
|
+
cutoff: boolean;
|
|
168
|
+
error: string;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Read the reschedule context.
|
|
172
|
+
*
|
|
173
|
+
* A 409 is the CUTOFF: the appointment is too close to move online. That is clinic policy
|
|
174
|
+
* working, not a failure — it gets its own copy telling the patient to call, because
|
|
175
|
+
* "something went wrong, try again" would have them retrying forever.
|
|
176
|
+
*/
|
|
177
|
+
declare function loadPortalRescheduleContext(id: string, fetchImpl?: FetchLike): Promise<PortalRescheduleContextOutcome>;
|
|
178
|
+
type PortalRescheduleOutcome = {
|
|
179
|
+
ok: true;
|
|
180
|
+
data: PortalAppointmentsBody;
|
|
181
|
+
} | {
|
|
182
|
+
ok: false;
|
|
183
|
+
cutoff: boolean;
|
|
184
|
+
slotTaken: boolean;
|
|
185
|
+
error: string;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Move the appointment, then RE-READ the list.
|
|
189
|
+
*
|
|
190
|
+
* Server-confirmed, never optimistic — the same rule as cancelling. A move the server
|
|
191
|
+
* refused that the UI had already redrawn would send a patient to the clinic on the wrong
|
|
192
|
+
* day, which is the worst outcome this island can produce.
|
|
193
|
+
*/
|
|
194
|
+
declare function reschedulePortalAppointmentAndReload(id: string, input: {
|
|
195
|
+
start: string;
|
|
196
|
+
practitionerId?: string | null;
|
|
197
|
+
}, fetchImpl?: FetchLike): Promise<PortalRescheduleOutcome>;
|
|
198
|
+
/** One line describing a chosen time: the day + time, then who and where. */
|
|
199
|
+
declare function PortalSlotSummary({ start, slot, tz, typeLabel, }: {
|
|
200
|
+
start: string;
|
|
201
|
+
slot: PortalAvailabilitySlot | null;
|
|
202
|
+
tz: string;
|
|
203
|
+
typeLabel?: string | null;
|
|
204
|
+
}): React.ReactElement;
|
|
205
|
+
interface PortalRescheduleInlineProps {
|
|
206
|
+
appointment: PortalAppointment;
|
|
207
|
+
fetchImpl?: FetchLike;
|
|
208
|
+
/** The re-read list, after a successful move. */
|
|
209
|
+
onRescheduled: (data: PortalAppointmentsBody) => void;
|
|
210
|
+
onClose: () => void;
|
|
211
|
+
/** Test seam: the interaction state a static render should start from. */
|
|
212
|
+
initialUi?: {
|
|
213
|
+
context?: PortalRescheduleContext | null;
|
|
214
|
+
slots?: PortalAvailabilitySlot[] | null;
|
|
215
|
+
tz?: string;
|
|
216
|
+
selected?: TimeSlot | null;
|
|
217
|
+
confirming?: boolean;
|
|
218
|
+
error?: string | null;
|
|
219
|
+
/** A blocking answer with no picker behind it — the cutoff. */
|
|
220
|
+
blocked?: string | null;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Rescheduling, expanded IN PLACE under the appointment it moves.
|
|
225
|
+
*
|
|
226
|
+
* In place rather than a route or a dialog: the patient is standing on the clinic's own
|
|
227
|
+
* page, a modal over a layout we do not control is a shape we cannot make safe, and the
|
|
228
|
+
* row above is the context that makes "move it to when?" a sensible question.
|
|
229
|
+
*/
|
|
230
|
+
declare function PortalRescheduleInline({ appointment, fetchImpl, onRescheduled, onClose, initialUi, }: PortalRescheduleInlineProps): React.ReactElement;
|
|
231
|
+
|
|
232
|
+
export { AVAILABILITY_FAILED_COPY, BOOK_FAILED_COPY, PORTAL_BOOKING_TYPES_URL, type PortalAvailabilityBody, type PortalAvailabilitySlot, type PortalBookInput, type PortalBookOutcome, type PortalBookingType, type PortalBookingTypesBody, type PortalHold, type PortalRescheduleContext, type PortalRescheduleContextOutcome, PortalRescheduleInline, type PortalRescheduleInlineProps, type PortalRescheduleOutcome, PortalSlotSummary, RESCHEDULE_CUTOFF_COPY, RESCHEDULE_FAILED_COPY, SLOT_TAKEN_COPY, bookPortalAppointment, createPortalHold, loadPortalAvailability, loadPortalBookingTypes, loadPortalRescheduleContext, portalAvailabilityUrl, portalAvailabilityWindow, portalRescheduleContextUrl, portalSlotAt, portalTimeSlots, readPortalAvailability, readPortalBookingTypes, releasePortalHold, rescheduleContextTypeId, reschedulePortalAppointmentAndReload };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AVAILABILITY_FAILED_COPY,
|
|
3
|
+
BOOK_FAILED_COPY,
|
|
4
|
+
PORTAL_BOOKING_TYPES_URL,
|
|
5
|
+
PortalRescheduleInline,
|
|
6
|
+
PortalSlotSummary,
|
|
7
|
+
RESCHEDULE_CUTOFF_COPY,
|
|
8
|
+
RESCHEDULE_FAILED_COPY,
|
|
9
|
+
SLOT_TAKEN_COPY,
|
|
10
|
+
bookPortalAppointment,
|
|
11
|
+
createPortalHold,
|
|
12
|
+
loadPortalAvailability,
|
|
13
|
+
loadPortalBookingTypes,
|
|
14
|
+
loadPortalRescheduleContext,
|
|
15
|
+
portalAvailabilityUrl,
|
|
16
|
+
portalAvailabilityWindow,
|
|
17
|
+
portalRescheduleContextUrl,
|
|
18
|
+
portalSlotAt,
|
|
19
|
+
portalTimeSlots,
|
|
20
|
+
readPortalAvailability,
|
|
21
|
+
readPortalBookingTypes,
|
|
22
|
+
releasePortalHold,
|
|
23
|
+
rescheduleContextTypeId,
|
|
24
|
+
reschedulePortalAppointmentAndReload
|
|
25
|
+
} from "./chunk-G3U6EXJM.js";
|
|
26
|
+
import "./chunk-MLKGABMK.js";
|
|
27
|
+
export {
|
|
28
|
+
AVAILABILITY_FAILED_COPY,
|
|
29
|
+
BOOK_FAILED_COPY,
|
|
30
|
+
PORTAL_BOOKING_TYPES_URL,
|
|
31
|
+
PortalRescheduleInline,
|
|
32
|
+
PortalSlotSummary,
|
|
33
|
+
RESCHEDULE_CUTOFF_COPY,
|
|
34
|
+
RESCHEDULE_FAILED_COPY,
|
|
35
|
+
SLOT_TAKEN_COPY,
|
|
36
|
+
bookPortalAppointment,
|
|
37
|
+
createPortalHold,
|
|
38
|
+
loadPortalAvailability,
|
|
39
|
+
loadPortalBookingTypes,
|
|
40
|
+
loadPortalRescheduleContext,
|
|
41
|
+
portalAvailabilityUrl,
|
|
42
|
+
portalAvailabilityWindow,
|
|
43
|
+
portalRescheduleContextUrl,
|
|
44
|
+
portalSlotAt,
|
|
45
|
+
portalTimeSlots,
|
|
46
|
+
readPortalAvailability,
|
|
47
|
+
readPortalBookingTypes,
|
|
48
|
+
releasePortalHold,
|
|
49
|
+
rescheduleContextTypeId,
|
|
50
|
+
reschedulePortalAppointmentAndReload
|
|
51
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Kit-authored chunk served beside a published islands site's `main.js`. */
|
|
2
|
+
declare const CONSULT_CHUNK_PATH = "assets/portal-consult.js";
|
|
3
|
+
/** Third-party script origins permitted on published islands sites. */
|
|
4
|
+
declare const TURNSTILE_SCRIPT_ORIGIN = "https://challenges.cloudflare.com";
|
|
5
|
+
declare const BPOINT_SCRIPT_ORIGIN = "https://www.bpoint.com.au";
|
|
6
|
+
declare const THIRD_PARTY_SCRIPT_ORIGINS: readonly ["https://challenges.cloudflare.com", "https://www.bpoint.com.au"];
|
|
7
|
+
/** Provider entry scripts loaded lazily by kit-owned islands. */
|
|
8
|
+
declare const TURNSTILE_SCRIPT_URL = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
9
|
+
declare const BPOINT_SCRIPT_URL = "https://www.bpoint.com.au/rest/clientscripts/api.js";
|
|
10
|
+
|
|
11
|
+
export { BPOINT_SCRIPT_ORIGIN, BPOINT_SCRIPT_URL, CONSULT_CHUNK_PATH, THIRD_PARTY_SCRIPT_ORIGINS, TURNSTILE_SCRIPT_ORIGIN, TURNSTILE_SCRIPT_URL };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BPOINT_SCRIPT_ORIGIN,
|
|
3
|
+
BPOINT_SCRIPT_URL,
|
|
4
|
+
CONSULT_CHUNK_PATH,
|
|
5
|
+
THIRD_PARTY_SCRIPT_ORIGINS,
|
|
6
|
+
TURNSTILE_SCRIPT_ORIGIN,
|
|
7
|
+
TURNSTILE_SCRIPT_URL
|
|
8
|
+
} from "./chunk-ZOF22TJA.js";
|
|
9
|
+
import "./chunk-MLKGABMK.js";
|
|
10
|
+
export {
|
|
11
|
+
BPOINT_SCRIPT_ORIGIN,
|
|
12
|
+
BPOINT_SCRIPT_URL,
|
|
13
|
+
CONSULT_CHUNK_PATH,
|
|
14
|
+
THIRD_PARTY_SCRIPT_ORIGINS,
|
|
15
|
+
TURNSTILE_SCRIPT_ORIGIN,
|
|
16
|
+
TURNSTILE_SCRIPT_URL
|
|
17
|
+
};
|