@medalsocial/booking 0.0.0-placeholder.0 → 0.2.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/CHANGELOG.md +27 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +220 -1
- package/dist/core/age.d.mts +34 -0
- package/dist/core/age.mjs +95 -0
- package/dist/core/age.mjs.map +1 -0
- package/dist/core/attempt-store.d.mts +111 -0
- package/dist/core/attempt-store.mjs +159 -0
- package/dist/core/attempt-store.mjs.map +1 -0
- package/dist/core/categories.d.mts +27 -0
- package/dist/core/categories.mjs +42 -0
- package/dist/core/categories.mjs.map +1 -0
- package/dist/core/clock.d.mts +33 -0
- package/dist/core/clock.mjs +251 -0
- package/dist/core/clock.mjs.map +1 -0
- package/dist/core/config.d.mts +178 -0
- package/dist/core/config.mjs +394 -0
- package/dist/core/config.mjs.map +1 -0
- package/dist/core/consent.d.mts +14 -0
- package/dist/core/consent.mjs +17 -0
- package/dist/core/consent.mjs.map +1 -0
- package/dist/core/deep-link.d.mts +40 -0
- package/dist/core/deep-link.mjs +160 -0
- package/dist/core/deep-link.mjs.map +1 -0
- package/dist/core/display-name.d.mts +18 -0
- package/dist/core/display-name.mjs +30 -0
- package/dist/core/display-name.mjs.map +1 -0
- package/dist/core/draft-store.d.mts +89 -0
- package/dist/core/draft-store.mjs +131 -0
- package/dist/core/draft-store.mjs.map +1 -0
- package/dist/core/dto.d.mts +46 -0
- package/dist/core/dto.mjs +157 -0
- package/dist/core/dto.mjs.map +1 -0
- package/dist/core/ics.d.mts +63 -0
- package/dist/core/ics.mjs +130 -0
- package/dist/core/ics.mjs.map +1 -0
- package/dist/core/index.d.mts +35 -0
- package/dist/core/index.mjs +32 -0
- package/dist/core/labels.d.mts +88 -0
- package/dist/core/labels.mjs +93 -0
- package/dist/core/labels.mjs.map +1 -0
- package/dist/core/machine.d.mts +645 -0
- package/dist/core/machine.mjs +988 -0
- package/dist/core/machine.mjs.map +1 -0
- package/dist/core/money.d.mts +12 -0
- package/dist/core/money.mjs +51 -0
- package/dist/core/money.mjs.map +1 -0
- package/dist/core/next-available.d.mts +20 -0
- package/dist/core/next-available.mjs +36 -0
- package/dist/core/next-available.mjs.map +1 -0
- package/dist/core/next-free.d.mts +25 -0
- package/dist/core/next-free.mjs +44 -0
- package/dist/core/next-free.mjs.map +1 -0
- package/dist/core/party-slots.d.mts +79 -0
- package/dist/core/party-slots.mjs +229 -0
- package/dist/core/party-slots.mjs.map +1 -0
- package/dist/core/paths.d.mts +11 -0
- package/dist/core/paths.mjs +56 -0
- package/dist/core/paths.mjs.map +1 -0
- package/dist/core/phone.d.mts +17 -0
- package/dist/core/phone.mjs +58 -0
- package/dist/core/phone.mjs.map +1 -0
- package/dist/core/portal/cookies.d.mts +29 -0
- package/dist/core/portal/dto.d.mts +140 -0
- package/dist/core/portal/dto.mjs +142 -0
- package/dist/core/portal/dto.mjs.map +1 -0
- package/dist/core/portal/family.d.mts +39 -0
- package/dist/core/portal/family.mjs +39 -0
- package/dist/core/portal/family.mjs.map +1 -0
- package/dist/core/portal/login-input.d.mts +29 -0
- package/dist/core/portal/login-input.mjs +29 -0
- package/dist/core/portal/login-input.mjs.map +1 -0
- package/dist/core/portal/next-path.d.mts +12 -0
- package/dist/core/portal/next-path.mjs +98 -0
- package/dist/core/portal/next-path.mjs.map +1 -0
- package/dist/core/portal/return-path.d.mts +7 -0
- package/dist/core/portal/return-path.mjs +49 -0
- package/dist/core/portal/return-path.mjs.map +1 -0
- package/dist/core/portal/session-cookie.d.mts +27 -0
- package/dist/core/portal/session-cookie.mjs +30 -0
- package/dist/core/portal/session-cookie.mjs.map +1 -0
- package/dist/core/portal/vipps-link.d.mts +56 -0
- package/dist/core/portal/vipps-link.mjs +136 -0
- package/dist/core/portal/vipps-link.mjs.map +1 -0
- package/dist/core/portal/vipps-return.d.mts +39 -0
- package/dist/core/portal/vipps-return.mjs +69 -0
- package/dist/core/portal/vipps-return.mjs.map +1 -0
- package/dist/core/rebook-store.d.mts +28 -0
- package/dist/core/rebook-store.mjs +60 -0
- package/dist/core/rebook-store.mjs.map +1 -0
- package/dist/core/restore-gate.d.mts +40 -0
- package/dist/core/restore-gate.mjs +96 -0
- package/dist/core/restore-gate.mjs.map +1 -0
- package/dist/core/stores.d.mts +8 -0
- package/dist/core/stores.mjs +23 -0
- package/dist/core/stores.mjs.map +1 -0
- package/dist/core/types.d.mts +245 -0
- package/dist/core/visit.d.mts +26 -0
- package/dist/core/visit.mjs +40 -0
- package/dist/core/visit.mjs.map +1 -0
- package/dist/core/wire.d.mts +37 -0
- package/dist/next/cache/memory.d.mts +14 -0
- package/dist/next/cache/memory.mjs +51 -0
- package/dist/next/cache/memory.mjs.map +1 -0
- package/dist/next/cache/next-data.d.mts +6 -0
- package/dist/next/cache/next-data.mjs +35 -0
- package/dist/next/cache/next-data.mjs.map +1 -0
- package/dist/next/cache/noop.d.mts +6 -0
- package/dist/next/cache/noop.mjs +15 -0
- package/dist/next/cache/noop.mjs.map +1 -0
- package/dist/next/cache/workers.d.mts +29 -0
- package/dist/next/cache/workers.mjs +74 -0
- package/dist/next/cache/workers.mjs.map +1 -0
- package/dist/next/catalogue.d.mts +38 -0
- package/dist/next/catalogue.mjs +215 -0
- package/dist/next/catalogue.mjs.map +1 -0
- package/dist/next/handler.d.mts +11 -0
- package/dist/next/handler.mjs +150 -0
- package/dist/next/handler.mjs.map +1 -0
- package/dist/next/index.d.mts +17 -0
- package/dist/next/index.mjs +14 -0
- package/dist/next/loaders.d.mts +79 -0
- package/dist/next/loaders.mjs +195 -0
- package/dist/next/loaders.mjs.map +1 -0
- package/dist/next/manage-dto.d.mts +6 -0
- package/dist/next/manage-dto.mjs +28 -0
- package/dist/next/manage-dto.mjs.map +1 -0
- package/dist/next/medal.d.mts +105 -0
- package/dist/next/medal.mjs +155 -0
- package/dist/next/medal.mjs.map +1 -0
- package/dist/next/options.d.mts +158 -0
- package/dist/next/options.mjs +65 -0
- package/dist/next/options.mjs.map +1 -0
- package/dist/next/portal/guardian.d.mts +42 -0
- package/dist/next/portal/guardian.mjs +76 -0
- package/dist/next/portal/guardian.mjs.map +1 -0
- package/dist/next/portal/medal-portal.d.mts +161 -0
- package/dist/next/portal/medal-portal.mjs +825 -0
- package/dist/next/portal/medal-portal.mjs.map +1 -0
- package/dist/next/portal/session.d.mts +38 -0
- package/dist/next/portal/session.mjs +40 -0
- package/dist/next/portal/session.mjs.map +1 -0
- package/dist/next/portal/vipps-flash.d.mts +14 -0
- package/dist/next/portal/vipps-flash.mjs +41 -0
- package/dist/next/portal/vipps-flash.mjs.map +1 -0
- package/dist/next/portal.d.mts +205 -0
- package/dist/next/portal.mjs +469 -0
- package/dist/next/portal.mjs.map +1 -0
- package/dist/next/request.d.mts +51 -0
- package/dist/next/request.mjs +123 -0
- package/dist/next/request.mjs.map +1 -0
- package/dist/next/routes/catalogue-routes.mjs +344 -0
- package/dist/next/routes/catalogue-routes.mjs.map +1 -0
- package/dist/next/routes/create.mjs +460 -0
- package/dist/next/routes/create.mjs.map +1 -0
- package/dist/next/routes/manage.mjs +191 -0
- package/dist/next/routes/manage.mjs.map +1 -0
- package/dist/next/routes/portal-routes.mjs +312 -0
- package/dist/next/routes/portal-routes.mjs.map +1 -0
- package/dist/next/routes/shared.mjs +162 -0
- package/dist/next/routes/shared.mjs.map +1 -0
- package/dist/next/routes/vipps-return.mjs +166 -0
- package/dist/next/routes/vipps-return.mjs.map +1 -0
- package/dist/next/runtime.d.mts +49 -0
- package/dist/next/runtime.mjs +94 -0
- package/dist/next/runtime.mjs.map +1 -0
- package/dist/next/seed.d.mts +71 -0
- package/dist/next/seed.mjs +495 -0
- package/dist/next/seed.mjs.map +1 -0
- package/dist/next/server.d.mts +26 -0
- package/dist/next/server.mjs +63 -0
- package/dist/next/server.mjs.map +1 -0
- package/dist/react/BookingLink.d.mts +61 -0
- package/dist/react/BookingLink.mjs +102 -0
- package/dist/react/BookingLink.mjs.map +1 -0
- package/dist/react/BookingWizard.d.mts +49 -0
- package/dist/react/BookingWizard.mjs +598 -0
- package/dist/react/BookingWizard.mjs.map +1 -0
- package/dist/react/LoginPageQuery.d.mts +37 -0
- package/dist/react/LoginPageQuery.mjs +106 -0
- package/dist/react/LoginPageQuery.mjs.map +1 -0
- package/dist/react/LoginSheet.d.mts +49 -0
- package/dist/react/LoginSheet.mjs +98 -0
- package/dist/react/LoginSheet.mjs.map +1 -0
- package/dist/react/ManageBooking.d.mts +50 -0
- package/dist/react/ManageBooking.mjs +197 -0
- package/dist/react/ManageBooking.mjs.map +1 -0
- package/dist/react/PortalDashboard.d.mts +10 -0
- package/dist/react/Provider.d.mts +82 -0
- package/dist/react/Provider.mjs +132 -0
- package/dist/react/Provider.mjs.map +1 -0
- package/dist/react/actions.d.mts +133 -0
- package/dist/react/actions.mjs +90 -0
- package/dist/react/actions.mjs.map +1 -0
- package/dist/react/index.d.mts +28 -0
- package/dist/react/index.mjs +20 -0
- package/dist/react/kit.d.mts +54 -0
- package/dist/react/kit.mjs +127 -0
- package/dist/react/kit.mjs.map +1 -0
- package/dist/react/labels/index.d.mts +23 -0
- package/dist/react/labels/index.mjs +44 -0
- package/dist/react/labels/index.mjs.map +1 -0
- package/dist/react/labels/login.d.mts +10 -0
- package/dist/react/labels/login.mjs +13 -0
- package/dist/react/labels/login.mjs.map +1 -0
- package/dist/react/labels/manage.d.mts +15 -0
- package/dist/react/labels/manage.mjs +19 -0
- package/dist/react/labels/manage.mjs.map +1 -0
- package/dist/react/labels/portal.d.mts +25 -0
- package/dist/react/labels/portal.mjs +41 -0
- package/dist/react/labels/portal.mjs.map +1 -0
- package/dist/react/labels/screens.d.mts +11 -0
- package/dist/react/labels/screens.mjs +721 -0
- package/dist/react/labels/screens.mjs.map +1 -0
- package/dist/react/labels/wizard.d.mts +76 -0
- package/dist/react/labels/wizard.mjs +97 -0
- package/dist/react/labels/wizard.mjs.map +1 -0
- package/dist/react/link/index.d.mts +5 -0
- package/dist/react/link/index.mjs +5 -0
- package/dist/react/portal/SessionTouch.d.mts +19 -0
- package/dist/react/portal/SessionTouch.mjs +40 -0
- package/dist/react/portal/SessionTouch.mjs.map +1 -0
- package/dist/react/portal/age-prompt.d.mts +10 -0
- package/dist/react/portal/age-prompt.mjs +21 -0
- package/dist/react/portal/age-prompt.mjs.map +1 -0
- package/dist/react/portal/book-again.d.mts +18 -0
- package/dist/react/portal/book-again.mjs +21 -0
- package/dist/react/portal/book-again.mjs.map +1 -0
- package/dist/react/portal/child-summary.d.mts +17 -0
- package/dist/react/portal/child-summary.mjs +45 -0
- package/dist/react/portal/child-summary.mjs.map +1 -0
- package/dist/react/portal/dashboard.d.mts +74 -0
- package/dist/react/portal/dashboard.mjs +204 -0
- package/dist/react/portal/dashboard.mjs.map +1 -0
- package/dist/react/portal/leave.d.mts +17 -0
- package/dist/react/portal/leave.mjs +21 -0
- package/dist/react/portal/leave.mjs.map +1 -0
- package/dist/react/portal/sections.d.mts +12 -0
- package/dist/react/portal/sections.mjs +48 -0
- package/dist/react/portal/sections.mjs.map +1 -0
- package/dist/react/portal/tabs.d.mts +23 -0
- package/dist/react/portal/tabs.mjs +36 -0
- package/dist/react/portal/tabs.mjs.map +1 -0
- package/dist/react/portal/vipps-flash.d.mts +11 -0
- package/dist/react/portal/vipps-flash.mjs +18 -0
- package/dist/react/portal/vipps-flash.mjs.map +1 -0
- package/dist/react/portal/wired.mjs +202 -0
- package/dist/react/portal/wired.mjs.map +1 -0
- package/dist/react/screen-labels.mjs +14 -0
- package/dist/react/screen-labels.mjs.map +1 -0
- package/dist/react/shared.d.mts +13 -0
- package/dist/react/shared.mjs +7 -0
- package/dist/react/use-next-free.d.mts +19 -0
- package/dist/react/use-next-free.mjs +56 -0
- package/dist/react/use-next-free.mjs.map +1 -0
- package/dist/react/useBooking.d.mts +179 -0
- package/dist/react/useBooking.mjs +1331 -0
- package/dist/react/useBooking.mjs.map +1 -0
- package/dist/react/wizard/adapters.mjs +189 -0
- package/dist/react/wizard/adapters.mjs.map +1 -0
- package/dist/react/wizard/index.d.mts +11 -0
- package/dist/react/wizard/index.mjs +6 -0
- package/dist/react/wizard-error.mjs +29 -0
- package/dist/react/wizard-error.mjs.map +1 -0
- package/package.json +126 -5
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { fill, labelText } from "../core/labels.mjs";
|
|
3
|
+
import { useBookingKit } from "./Provider.mjs";
|
|
4
|
+
import { screenLabels } from "./screen-labels.mjs";
|
|
5
|
+
import { ManageScreen } from "@medalsocial/meda/booking";
|
|
6
|
+
import { useCallback, useMemo, useState } from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
//#region src/react/ManageBooking.tsx
|
|
9
|
+
const DAY_MS = 864e5;
|
|
10
|
+
/**
|
|
11
|
+
* How far either side of the appointment to ask for openings: centred on the
|
|
12
|
+
* booking rather than on today, because somebody moving an appointment three
|
|
13
|
+
* weeks out wants the days around it. Always covers the appointment's own day.
|
|
14
|
+
*/
|
|
15
|
+
const LOOK_BACK_MS = 2 * DAY_MS;
|
|
16
|
+
const LOOK_AHEAD_MS = 7 * DAY_MS;
|
|
17
|
+
/** The engine's English stand-in for a deleted catalogue row; never printed. */
|
|
18
|
+
const UNKNOWN_SERVICE = "Unknown service";
|
|
19
|
+
const CODES = [
|
|
20
|
+
"windowPassed",
|
|
21
|
+
"slotTaken",
|
|
22
|
+
"notFound",
|
|
23
|
+
"conflict",
|
|
24
|
+
"invalidInput"
|
|
25
|
+
];
|
|
26
|
+
/** Anything the route did not name is an outage as far as this page is concerned. */
|
|
27
|
+
function asErrorCode(code) {
|
|
28
|
+
return CODES.includes(code) ? code : "unavailable";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `<ManageBooking>` — the page at the end of the «manage your booking» link.
|
|
32
|
+
*
|
|
33
|
+
* Renders meda's `ManageScreen` and does what it leaves to the caller: loads
|
|
34
|
+
* the openings around the appointment from `${paths.api}/availability`, posts
|
|
35
|
+
* cancel and reschedule to `actionPath`, maps the route's error codes, puts the
|
|
36
|
+
* freshly minted manage link in the address bar after a move, and builds the
|
|
37
|
+
* calendar entry (UID = the booking the entry was created under).
|
|
38
|
+
*
|
|
39
|
+
* The change windows are the engine's: `canCancel` / `canReschedule` are
|
|
40
|
+
* relayed, never recomputed.
|
|
41
|
+
*/
|
|
42
|
+
function ManageBooking({ booking, phone, address, service = null, actionPath, selfManagePath = null, bookHref, siteUrl, ...overrides }) {
|
|
43
|
+
const { kit, classNames, components } = useBookingKit(overrides);
|
|
44
|
+
const { config, labels } = kit;
|
|
45
|
+
const { contact, paths } = config;
|
|
46
|
+
const tel = phone === void 0 ? contact.phone : phone;
|
|
47
|
+
const where = address === void 0 ? contact.address : address;
|
|
48
|
+
const post = actionPath ?? `${paths.api}/manage`;
|
|
49
|
+
const [slots, setSlots] = useState(null);
|
|
50
|
+
const [failed, setFailed] = useState(false);
|
|
51
|
+
const { serviceId, resourceId, startTs } = booking;
|
|
52
|
+
/**
|
|
53
|
+
* The openings, locked to the booked service and stylist — which is what lets
|
|
54
|
+
* the confirm name the stylist without a second lookup.
|
|
55
|
+
*/
|
|
56
|
+
const loadSlots = useCallback(async () => {
|
|
57
|
+
if (serviceId === null) {
|
|
58
|
+
setFailed(true);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
setFailed(false);
|
|
62
|
+
const query = new URLSearchParams({
|
|
63
|
+
service_id: serviceId,
|
|
64
|
+
from_ts: String(Math.max(Date.now(), startTs - LOOK_BACK_MS)),
|
|
65
|
+
to_ts: String(startTs + LOOK_AHEAD_MS),
|
|
66
|
+
...resourceId ? { resource_id: resourceId } : {}
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(`${paths.api}/availability?${query}`);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
setFailed(true);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const body = await response.json();
|
|
75
|
+
setSlots([...body.slots ?? []]);
|
|
76
|
+
} catch {
|
|
77
|
+
setFailed(true);
|
|
78
|
+
}
|
|
79
|
+
}, [
|
|
80
|
+
serviceId,
|
|
81
|
+
resourceId,
|
|
82
|
+
startTs,
|
|
83
|
+
paths.api
|
|
84
|
+
]);
|
|
85
|
+
/** One POST. The screen holds the buttons down for the round trip. */
|
|
86
|
+
const mutate = useCallback(async (body) => {
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(post, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify(body)
|
|
92
|
+
});
|
|
93
|
+
const payload = await response.json().catch(() => null);
|
|
94
|
+
if (!response.ok) return {
|
|
95
|
+
ok: false,
|
|
96
|
+
code: asErrorCode(payload?.error)
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
manageToken: payload?.manageToken
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
code: "unavailable"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}, [post]);
|
|
109
|
+
const onCancel = useCallback(async (reason) => {
|
|
110
|
+
const result = await mutate({
|
|
111
|
+
action: "cancel",
|
|
112
|
+
...reason ? { reason } : {}
|
|
113
|
+
});
|
|
114
|
+
return result.ok ? { ok: true } : result;
|
|
115
|
+
}, [mutate]);
|
|
116
|
+
const onReschedule = useCallback(async (slot) => {
|
|
117
|
+
const result = await mutate({
|
|
118
|
+
action: "reschedule",
|
|
119
|
+
startTs: slot.startTs
|
|
120
|
+
});
|
|
121
|
+
if (!result.ok) return result;
|
|
122
|
+
const manageHref = result.manageToken ? kit.paths.managePath(result.manageToken) : null;
|
|
123
|
+
if (manageHref !== null) window.history.replaceState(null, "", manageHref);
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
manageHref
|
|
127
|
+
};
|
|
128
|
+
}, [mutate, kit.paths]);
|
|
129
|
+
const pricing = useMemo(() => service && {
|
|
130
|
+
priceAt: (ts) => kit.wizard.totalPriceOre([{ service }], ts),
|
|
131
|
+
weekendSurchargePct: service.weekendSurchargePct
|
|
132
|
+
}, [service, kit.wizard]);
|
|
133
|
+
/**
|
|
134
|
+
* «Add to calendar». REQUEST / SEQUENCE:1 when this booking is itself a move,
|
|
135
|
+
* under the UID the entry was created with (`rescheduledFromId`); PUBLISH / 0
|
|
136
|
+
* otherwise. The way back in travels with the entry: a REQUEST without it
|
|
137
|
+
* would delete the manage link from the customer's calendar.
|
|
138
|
+
*/
|
|
139
|
+
const icsHref = useMemo(() => {
|
|
140
|
+
const serviceLabel = booking.serviceName === UNKNOWN_SERVICE ? labelText(labels["manage.serviceFallback"]) : booking.serviceName;
|
|
141
|
+
const name = booking.bookedForName;
|
|
142
|
+
const moved = booking.rescheduledFromId !== null;
|
|
143
|
+
const manageUrl = selfManagePath && (siteUrl ? new URL(selfManagePath, siteUrl).toString() : selfManagePath);
|
|
144
|
+
return kit.ics.icsDataUrl(kit.ics.buildIcs({
|
|
145
|
+
uid: booking.rescheduledFromId ?? booking.bookingId,
|
|
146
|
+
startTs: booking.startTs,
|
|
147
|
+
endTs: booking.endTs,
|
|
148
|
+
summary: fill(labels[name ? "manage.ics.titleFor" : "manage.ics.title"], {
|
|
149
|
+
service: serviceLabel,
|
|
150
|
+
name: name ?? "",
|
|
151
|
+
business: contact.name
|
|
152
|
+
}),
|
|
153
|
+
description: [fill(labels["manage.ics.price"], { price: kit.money.formatMinor(booking.amountOre) }), ...manageUrl ? [fill(labels["manage.ics.manage"], { url: manageUrl })] : []].join("\n"),
|
|
154
|
+
...where ? { location: where } : {},
|
|
155
|
+
method: moved ? "REQUEST" : "PUBLISH",
|
|
156
|
+
sequence: moved ? 1 : 0
|
|
157
|
+
}));
|
|
158
|
+
}, [
|
|
159
|
+
booking,
|
|
160
|
+
labels,
|
|
161
|
+
selfManagePath,
|
|
162
|
+
siteUrl,
|
|
163
|
+
where,
|
|
164
|
+
contact.name,
|
|
165
|
+
kit
|
|
166
|
+
]);
|
|
167
|
+
return /* @__PURE__ */ jsx(ManageScreen, {
|
|
168
|
+
labels: screenLabels(labels),
|
|
169
|
+
format: kit.format,
|
|
170
|
+
dayparts: kit.dayparts,
|
|
171
|
+
booking,
|
|
172
|
+
phone: tel,
|
|
173
|
+
address: where,
|
|
174
|
+
bookingHref: bookHref ?? paths.booking,
|
|
175
|
+
portalHref: paths.portal,
|
|
176
|
+
icsHref,
|
|
177
|
+
icsFileName: labelText(labels["manage.ics.fileName"]),
|
|
178
|
+
pricing,
|
|
179
|
+
reschedule: {
|
|
180
|
+
slots,
|
|
181
|
+
failed
|
|
182
|
+
},
|
|
183
|
+
onRequestSlots: loadSlots,
|
|
184
|
+
onCancel,
|
|
185
|
+
onReschedule,
|
|
186
|
+
classNames: classNames.manage,
|
|
187
|
+
timeClassNames: classNames.time,
|
|
188
|
+
components: {
|
|
189
|
+
DayChip: components.DayChip,
|
|
190
|
+
TimeChip: components.TimeChip
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { ManageBooking };
|
|
196
|
+
|
|
197
|
+
//# sourceMappingURL=ManageBooking.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ManageBooking.mjs","names":[],"sources":["../../src/react/ManageBooking.tsx"],"sourcesContent":["import {\n type ManageErrorCode,\n type ManagePricing,\n type ManageResult,\n ManageScreen,\n} from '@medalsocial/meda/booking';\nimport { useCallback, useMemo, useState } from 'react';\nimport { fill, labelText } from '../core/labels';\nimport type { BookingManageDto, BookingServiceDto, BookingSlotDto } from '../core/types';\nimport { type BookingOverrides, useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\n\nconst DAY_MS = 86_400_000;\n\n/**\n * How far either side of the appointment to ask for openings: centred on the\n * booking rather than on today, because somebody moving an appointment three\n * weeks out wants the days around it. Always covers the appointment's own day.\n */\nconst LOOK_BACK_MS = 2 * DAY_MS;\nconst LOOK_AHEAD_MS = 7 * DAY_MS;\n\n/** The engine's English stand-in for a deleted catalogue row; never printed. */\nconst UNKNOWN_SERVICE = 'Unknown service';\n\nconst CODES: readonly string[] = [\n 'windowPassed',\n 'slotTaken',\n 'notFound',\n 'conflict',\n 'invalidInput',\n];\n\n/** Anything the route did not name is an outage as far as this page is concerned. */\nfunction asErrorCode(code: unknown): ManageErrorCode {\n return CODES.includes(code as string) ? (code as ManageErrorCode) : 'unavailable';\n}\n\nexport interface ManageBookingProps extends BookingOverrides {\n booking: BookingManageDto;\n /**\n * The business's number. Absent = `config.contact.phone`; `null` renders\n * every phone sentence unlinked (a `tel:` that dials nothing is worse than\n * plain text, and worst here, where the phone is the last way to change it).\n */\n phone?: string | null;\n /** The address under the booking and in the calendar entry. Absent = `config.contact.address`. */\n address?: string | null;\n /** The catalogue entry for the booked service, for the weekend note. `null` = no note. */\n service?: BookingServiceDto | null;\n /**\n * Where the two mutations POST: `${config.paths.api}/manage/<token>`, built by\n * the server that holds the token. A path, not the token, so no prop, state\n * or error payload ever names the credential. The default matches no route\n * and so fails loudly rather than posting a tokenless action.\n */\n actionPath?: string;\n /**\n * This page's own manage path (`paths.managePath(token)`), for the calendar\n * entry's «change or cancel» line. `null` = no such line.\n */\n selfManagePath?: string | null;\n /** Where «book a new time» goes. Absent = `config.paths.booking`. */\n bookHref?: string;\n /**\n * The site's origin, to make the calendar entry's manage link absolute. Absent\n * = the link stays root-relative, so server and client render the same.\n */\n siteUrl?: string;\n}\n\n/**\n * `<ManageBooking>` — the page at the end of the «manage your booking» link.\n *\n * Renders meda's `ManageScreen` and does what it leaves to the caller: loads\n * the openings around the appointment from `${paths.api}/availability`, posts\n * cancel and reschedule to `actionPath`, maps the route's error codes, puts the\n * freshly minted manage link in the address bar after a move, and builds the\n * calendar entry (UID = the booking the entry was created under).\n *\n * The change windows are the engine's: `canCancel` / `canReschedule` are\n * relayed, never recomputed.\n */\nexport function ManageBooking({\n booking,\n phone,\n address,\n service = null,\n actionPath,\n selfManagePath = null,\n bookHref,\n siteUrl,\n ...overrides\n}: ManageBookingProps) {\n const { kit, classNames, components } = useBookingKit(overrides);\n const { config, labels } = kit;\n const { contact, paths } = config;\n const tel = phone === undefined ? contact.phone : phone;\n const where = address === undefined ? contact.address : address;\n const post = actionPath ?? `${paths.api}/manage`;\n\n const [slots, setSlots] = useState<BookingSlotDto[] | null>(null);\n const [failed, setFailed] = useState(false);\n\n const { serviceId, resourceId, startTs } = booking;\n /**\n * The openings, locked to the booked service and stylist — which is what lets\n * the confirm name the stylist without a second lookup.\n */\n const loadSlots = useCallback(async () => {\n if (serviceId === null) {\n setFailed(true);\n return;\n }\n setFailed(false);\n const query = new URLSearchParams({\n service_id: serviceId,\n from_ts: String(Math.max(Date.now(), startTs - LOOK_BACK_MS)),\n to_ts: String(startTs + LOOK_AHEAD_MS),\n ...(resourceId ? { resource_id: resourceId } : {}),\n });\n try {\n const response = await fetch(`${paths.api}/availability?${query}`);\n if (!response.ok) {\n setFailed(true);\n return;\n }\n const body = (await response.json()) as { slots?: BookingSlotDto[] };\n // Always a fresh array: the screen hides the one it held when a move lost\n // its slot until a NEW one arrives, and a cached body could hand back the same.\n setSlots([...(body.slots ?? [])]);\n } catch {\n setFailed(true);\n }\n }, [serviceId, resourceId, startTs, paths.api]);\n\n /** One POST. The screen holds the buttons down for the round trip. */\n const mutate = useCallback(\n async (body: Record<string, unknown>): Promise<ManageResult & { manageToken?: string }> => {\n try {\n const response = await fetch(post, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n const payload = (await response.json().catch(() => null)) as {\n error?: string;\n manageToken?: string;\n } | null;\n if (!response.ok) return { ok: false, code: asErrorCode(payload?.error) };\n return { ok: true, manageToken: payload?.manageToken };\n } catch {\n return { ok: false, code: 'unavailable' };\n }\n },\n [post]\n );\n\n const onCancel = useCallback(\n async (reason: string | null): Promise<ManageResult> => {\n const result = await mutate({ action: 'cancel', ...(reason ? { reason } : {}) });\n return result.ok ? { ok: true } : result;\n },\n [mutate]\n );\n\n const onReschedule = useCallback(\n async (slot: BookingSlotDto): Promise<ManageResult> => {\n const result = await mutate({ action: 'reschedule', startTs: slot.startTs });\n if (!result.ok) return result;\n // The token the visitor arrived with belongs to the row the move\n // cancelled; the new one is minted once and omitted from an idempotent\n // replay. No token means no link rather than a broken one.\n const manageHref = result.manageToken ? kit.paths.managePath(result.manageToken) : null;\n // The ADDRESS BAR too: a refresh would otherwise reload the dead token,\n // and this screen holds the only copy of the new one. `replaceState`, so\n // nothing unmounts and Back does not return to the dead token.\n if (manageHref !== null) window.history.replaceState(null, '', manageHref);\n return { ok: true, manageHref };\n },\n [mutate, kit.paths]\n );\n\n const pricing = useMemo<ManagePricing | null>(\n () =>\n service && {\n priceAt: (ts) => kit.wizard.totalPriceOre([{ service }], ts),\n weekendSurchargePct: service.weekendSurchargePct,\n },\n [service, kit.wizard]\n );\n\n /**\n * «Add to calendar». REQUEST / SEQUENCE:1 when this booking is itself a move,\n * under the UID the entry was created with (`rescheduledFromId`); PUBLISH / 0\n * otherwise. The way back in travels with the entry: a REQUEST without it\n * would delete the manage link from the customer's calendar.\n */\n const icsHref = useMemo(() => {\n const serviceLabel =\n booking.serviceName === UNKNOWN_SERVICE\n ? labelText(labels['manage.serviceFallback'])\n : booking.serviceName;\n const name = booking.bookedForName;\n const moved = booking.rescheduledFromId !== null;\n const manageUrl =\n selfManagePath && (siteUrl ? new URL(selfManagePath, siteUrl).toString() : selfManagePath);\n return kit.ics.icsDataUrl(\n kit.ics.buildIcs({\n uid: booking.rescheduledFromId ?? booking.bookingId,\n startTs: booking.startTs,\n endTs: booking.endTs,\n summary: fill(labels[name ? 'manage.ics.titleFor' : 'manage.ics.title'], {\n service: serviceLabel,\n name: name ?? '',\n business: contact.name,\n }),\n description: [\n fill(labels['manage.ics.price'], { price: kit.money.formatMinor(booking.amountOre) }),\n ...(manageUrl ? [fill(labels['manage.ics.manage'], { url: manageUrl })] : []),\n ].join('\\n'),\n ...(where ? { location: where } : {}),\n method: moved ? 'REQUEST' : 'PUBLISH',\n sequence: moved ? 1 : 0,\n })\n );\n }, [booking, labels, selfManagePath, siteUrl, where, contact.name, kit]);\n\n return (\n <ManageScreen\n labels={screenLabels(labels)}\n format={kit.format}\n dayparts={kit.dayparts}\n booking={booking}\n phone={tel}\n address={where}\n bookingHref={bookHref ?? paths.booking}\n portalHref={paths.portal}\n icsHref={icsHref}\n icsFileName={labelText(labels['manage.ics.fileName'])}\n pricing={pricing}\n reschedule={{ slots, failed }}\n onRequestSlots={loadSlots}\n onCancel={onCancel}\n onReschedule={onReschedule}\n classNames={classNames.manage}\n timeClassNames={classNames.time}\n components={{ DayChip: components.DayChip, TimeChip: components.TimeChip }}\n />\n );\n}\n"],"mappings":";;;;;;;;AAYA,MAAM,SAAS;;;;;;AAOf,MAAM,eAAe,IAAI;AACzB,MAAM,gBAAgB,IAAI;;AAG1B,MAAM,kBAAkB;AAExB,MAAM,QAA2B;CAC/B;CACA;CACA;CACA;CACA;AACF;;AAGA,SAAS,YAAY,MAAgC;CACnD,OAAO,MAAM,SAAS,IAAc,IAAK,OAA2B;AACtE;;;;;;;;;;;;;AA+CA,SAAgB,cAAc,EAC5B,SACA,OACA,SACA,UAAU,MACV,YACA,iBAAiB,MACjB,UACA,SACA,GAAG,aACkB;CACrB,MAAM,EAAE,KAAK,YAAY,eAAe,cAAc,SAAS;CAC/D,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,EAAE,SAAS,UAAU;CAC3B,MAAM,MAAM,UAAU,KAAA,IAAY,QAAQ,QAAQ;CAClD,MAAM,QAAQ,YAAY,KAAA,IAAY,QAAQ,UAAU;CACxD,MAAM,OAAO,cAAc,GAAG,MAAM,IAAI;CAExC,MAAM,CAAC,OAAO,YAAY,SAAkC,IAAI;CAChE,MAAM,CAAC,QAAQ,aAAa,SAAS,KAAK;CAE1C,MAAM,EAAE,WAAW,YAAY,YAAY;;;;;CAK3C,MAAM,YAAY,YAAY,YAAY;EACxC,IAAI,cAAc,MAAM;GACtB,UAAU,IAAI;GACd;EACF;EACA,UAAU,KAAK;EACf,MAAM,QAAQ,IAAI,gBAAgB;GAChC,YAAY;GACZ,SAAS,OAAO,KAAK,IAAI,KAAK,IAAI,GAAG,UAAU,YAAY,CAAC;GAC5D,OAAO,OAAO,UAAU,aAAa;GACrC,GAAI,aAAa,EAAE,aAAa,WAAW,IAAI,CAAC;EAClD,CAAC;EACD,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,GAAG,MAAM,IAAI,gBAAgB,OAAO;GACjE,IAAI,CAAC,SAAS,IAAI;IAChB,UAAU,IAAI;IACd;GACF;GACA,MAAM,OAAQ,MAAM,SAAS,KAAK;GAGlC,SAAS,CAAC,GAAI,KAAK,SAAS,CAAC,CAAE,CAAC;EAClC,QAAQ;GACN,UAAU,IAAI;EAChB;CACF,GAAG;EAAC;EAAW;EAAY;EAAS,MAAM;CAAG,CAAC;;CAG9C,MAAM,SAAS,YACb,OAAO,SAAoF;EACzF,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,MAAM;IACjC,QAAQ;IACR,SAAS,EAAE,gBAAgB,mBAAmB;IAC9C,MAAM,KAAK,UAAU,IAAI;GAC3B,CAAC;GACD,MAAM,UAAW,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;GAIvD,IAAI,CAAC,SAAS,IAAI,OAAO;IAAE,IAAI;IAAO,MAAM,YAAY,SAAS,KAAK;GAAE;GACxE,OAAO;IAAE,IAAI;IAAM,aAAa,SAAS;GAAY;EACvD,QAAQ;GACN,OAAO;IAAE,IAAI;IAAO,MAAM;GAAc;EAC1C;CACF,GACA,CAAC,IAAI,CACP;CAEA,MAAM,WAAW,YACf,OAAO,WAAiD;EACtD,MAAM,SAAS,MAAM,OAAO;GAAE,QAAQ;GAAU,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAAG,CAAC;EAC/E,OAAO,OAAO,KAAK,EAAE,IAAI,KAAK,IAAI;CACpC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,eAAe,YACnB,OAAO,SAAgD;EACrD,MAAM,SAAS,MAAM,OAAO;GAAE,QAAQ;GAAc,SAAS,KAAK;EAAQ,CAAC;EAC3E,IAAI,CAAC,OAAO,IAAI,OAAO;EAIvB,MAAM,aAAa,OAAO,cAAc,IAAI,MAAM,WAAW,OAAO,WAAW,IAAI;EAInF,IAAI,eAAe,MAAM,OAAO,QAAQ,aAAa,MAAM,IAAI,UAAU;EACzE,OAAO;GAAE,IAAI;GAAM;EAAW;CAChC,GACA,CAAC,QAAQ,IAAI,KAAK,CACpB;CAEA,MAAM,UAAU,cAEZ,WAAW;EACT,UAAU,OAAO,IAAI,OAAO,cAAc,CAAC,EAAE,QAAQ,CAAC,GAAG,EAAE;EAC3D,qBAAqB,QAAQ;CAC/B,GACF,CAAC,SAAS,IAAI,MAAM,CACtB;;;;;;;CAQA,MAAM,UAAU,cAAc;EAC5B,MAAM,eACJ,QAAQ,gBAAgB,kBACpB,UAAU,OAAO,yBAAyB,IAC1C,QAAQ;EACd,MAAM,OAAO,QAAQ;EACrB,MAAM,QAAQ,QAAQ,sBAAsB;EAC5C,MAAM,YACJ,mBAAmB,UAAU,IAAI,IAAI,gBAAgB,OAAO,CAAC,CAAC,SAAS,IAAI;EAC7E,OAAO,IAAI,IAAI,WACb,IAAI,IAAI,SAAS;GACf,KAAK,QAAQ,qBAAqB,QAAQ;GAC1C,SAAS,QAAQ;GACjB,OAAO,QAAQ;GACf,SAAS,KAAK,OAAO,OAAO,wBAAwB,qBAAqB;IACvE,SAAS;IACT,MAAM,QAAQ;IACd,UAAU,QAAQ;GACpB,CAAC;GACD,aAAa,CACX,KAAK,OAAO,qBAAqB,EAAE,OAAO,IAAI,MAAM,YAAY,QAAQ,SAAS,EAAE,CAAC,GACpF,GAAI,YAAY,CAAC,KAAK,OAAO,sBAAsB,EAAE,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,CAC7E,CAAC,CAAC,KAAK,IAAI;GACX,GAAI,QAAQ,EAAE,UAAU,MAAM,IAAI,CAAC;GACnC,QAAQ,QAAQ,YAAY;GAC5B,UAAU,QAAQ,IAAI;EACxB,CAAC,CACH;CACF,GAAG;EAAC;EAAS;EAAQ;EAAgB;EAAS;EAAO,QAAQ;EAAM;CAAG,CAAC;CAEvE,OACE,oBAAC,cAAD;EACE,QAAQ,aAAa,MAAM;EAC3B,QAAQ,IAAI;EACZ,UAAU,IAAI;EACL;EACT,OAAO;EACP,SAAS;EACT,aAAa,YAAY,MAAM;EAC/B,YAAY,MAAM;EACT;EACT,aAAa,UAAU,OAAO,sBAAsB;EAC3C;EACT,YAAY;GAAE;GAAO;EAAO;EAC5B,gBAAgB;EACN;EACI;EACd,YAAY,WAAW;EACvB,gBAAgB,WAAW;EAC3B,YAAY;GAAE,SAAS,WAAW;GAAS,UAAU,WAAW;EAAS;CAC1E,CAAA;AAEL"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { agePromptCookie, agePromptCookieName, parseAgePromptDismissed } from "./portal/age-prompt.mjs";
|
|
2
|
+
import { bookAgainHref } from "./portal/book-again.mjs";
|
|
3
|
+
import { createChildSummaries } from "./portal/child-summary.mjs";
|
|
4
|
+
import { DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS, PORTAL_TABS, PortalTab, parsePortalTab } from "./portal/tabs.mjs";
|
|
5
|
+
import { PortalTabIcons } from "./portal/sections.mjs";
|
|
6
|
+
import { PortalDashboard, PortalDashboardProps, PortalDashboardUnreachable, PortalDashboardUnreachableProps } from "./portal/dashboard.mjs";
|
|
7
|
+
import { leavePortal } from "./portal/leave.mjs";
|
|
8
|
+
import { SESSION_TOUCH_THROTTLE_MS, SessionTouch, sessionTouchKey } from "./portal/SessionTouch.mjs";
|
|
9
|
+
import { VIPPS_LINK_FLASHES, VippsLinkFlash, vippsFlashPath, vippsLinkFlash } from "./portal/vipps-flash.mjs";
|
|
10
|
+
export { DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS, PORTAL_TABS, PortalDashboard, type PortalDashboardProps, PortalDashboardUnreachable, type PortalDashboardUnreachableProps, type PortalTab, type PortalTabIcons, SESSION_TOUCH_THROTTLE_MS, SessionTouch, VIPPS_LINK_FLASHES, type VippsLinkFlash, agePromptCookie, agePromptCookieName, bookAgainHref, createChildSummaries, leavePortal, parseAgePromptDismissed, parsePortalTab, sessionTouchKey, vippsFlashPath, vippsLinkFlash };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { BookingConfig } from "../core/config.mjs";
|
|
2
|
+
import { BookingLabelsInput } from "./labels/index.mjs";
|
|
3
|
+
import { BookingKit } from "./kit.mjs";
|
|
4
|
+
import { AccountCardSlot, AddChildSheetSlot, BookingCardProps, BookingSkeletonSlot, ChildCardProps, ChildCardsSlot, ConfirmationSlot, DataControlsSlot, DayChipProps, DetailsScreenSlot, FamilyChipProps, FamilyEditorSlot, FamilyMemberCardProps, LoginPanelSlot, LoginSheetSlot, LogoutButtonSlot, ManageScreenSlot, OtpSlotsSlot, PartyLineProps, PersonCardProps, PortalShellSlot, PortalUnreachableSlot, ProfileFormSlot, RebookCardProps, RebookCardsSlot, ServiceCardProps, ServiceScreenSlot, SlotClassNames, StylistCardProps, StylistScreenSlot, SummaryBarSlot, TimeChipProps, TimeScreenSlot, UpcomingBookingsSlot, VippsButtonSlot, VippsLinkRowSlot, VisitHistorySlot, VisitRowProps, WhoScreenSlot } from "@medalsocial/meda/booking";
|
|
5
|
+
import { ComponentType, ReactNode } from "react";
|
|
6
|
+
//#region src/react/Provider.d.ts
|
|
7
|
+
/** Per-screen slot classes (see each meda screen's `…Slot` type for the slots). */
|
|
8
|
+
export interface BookingClassNames {
|
|
9
|
+
wizard?: SlotClassNames<'root' | 'header' | 'back' | 'progress' | 'alert'>;
|
|
10
|
+
who?: SlotClassNames<WhoScreenSlot>;
|
|
11
|
+
addChild?: SlotClassNames<AddChildSheetSlot>;
|
|
12
|
+
service?: SlotClassNames<ServiceScreenSlot>;
|
|
13
|
+
stylist?: SlotClassNames<StylistScreenSlot>;
|
|
14
|
+
time?: SlotClassNames<TimeScreenSlot>;
|
|
15
|
+
details?: SlotClassNames<DetailsScreenSlot>;
|
|
16
|
+
summary?: SlotClassNames<SummaryBarSlot>;
|
|
17
|
+
confirmation?: SlotClassNames<ConfirmationSlot>;
|
|
18
|
+
skeleton?: SlotClassNames<BookingSkeletonSlot>;
|
|
19
|
+
manage?: SlotClassNames<ManageScreenSlot>;
|
|
20
|
+
loginSheet?: SlotClassNames<LoginSheetSlot>;
|
|
21
|
+
loginPanel?: SlotClassNames<LoginPanelSlot>;
|
|
22
|
+
otp?: SlotClassNames<OtpSlotsSlot>;
|
|
23
|
+
vipps?: SlotClassNames<VippsButtonSlot>;
|
|
24
|
+
portalShell?: SlotClassNames<PortalShellSlot>;
|
|
25
|
+
portalUnreachable?: SlotClassNames<PortalUnreachableSlot>;
|
|
26
|
+
upcoming?: SlotClassNames<UpcomingBookingsSlot>;
|
|
27
|
+
history?: SlotClassNames<VisitHistorySlot>;
|
|
28
|
+
rebook?: SlotClassNames<RebookCardsSlot>;
|
|
29
|
+
childCards?: SlotClassNames<ChildCardsSlot>;
|
|
30
|
+
accountCard?: SlotClassNames<AccountCardSlot>;
|
|
31
|
+
familyEditor?: SlotClassNames<FamilyEditorSlot>;
|
|
32
|
+
profileForm?: SlotClassNames<ProfileFormSlot>;
|
|
33
|
+
dataControls?: SlotClassNames<DataControlsSlot>;
|
|
34
|
+
logout?: SlotClassNames<LogoutButtonSlot>;
|
|
35
|
+
vippsLink?: SlotClassNames<VippsLinkRowSlot>;
|
|
36
|
+
}
|
|
37
|
+
/** Card-level renderers, by name; each one is handed to the screen that draws it. */
|
|
38
|
+
export interface BookingComponents {
|
|
39
|
+
PersonCard?: ComponentType<PersonCardProps>;
|
|
40
|
+
ServiceCard?: ComponentType<ServiceCardProps>;
|
|
41
|
+
StylistCard?: ComponentType<StylistCardProps>;
|
|
42
|
+
DayChip?: ComponentType<DayChipProps>;
|
|
43
|
+
TimeChip?: ComponentType<TimeChipProps>;
|
|
44
|
+
FamilyChip?: ComponentType<FamilyChipProps>;
|
|
45
|
+
PartyLine?: ComponentType<PartyLineProps>;
|
|
46
|
+
BookingCard?: ComponentType<BookingCardProps>;
|
|
47
|
+
VisitRow?: ComponentType<VisitRowProps>;
|
|
48
|
+
RebookCard?: ComponentType<RebookCardProps>;
|
|
49
|
+
ChildCard?: ComponentType<ChildCardProps>;
|
|
50
|
+
FamilyMemberCard?: ComponentType<FamilyMemberCardProps>;
|
|
51
|
+
}
|
|
52
|
+
/** The four props every booking component and the provider take. */
|
|
53
|
+
export interface BookingOverrides {
|
|
54
|
+
/** The site's resolved config (`resolveBookingConfig` from `/core`, run on the server). */
|
|
55
|
+
config?: Readonly<BookingConfig>;
|
|
56
|
+
/**
|
|
57
|
+
* Words over `config.labels`. The pack itself is resolved on the server —
|
|
58
|
+
* `mergeLabels(locale, siteWords)` from `@medalsocial/booking/react/shared`
|
|
59
|
+
* — so the built-in packs never ship in the browser bundle.
|
|
60
|
+
*/
|
|
61
|
+
labels?: BookingLabelsInput;
|
|
62
|
+
classNames?: BookingClassNames;
|
|
63
|
+
components?: BookingComponents;
|
|
64
|
+
}
|
|
65
|
+
export interface BookingProviderProps extends BookingOverrides {
|
|
66
|
+
children?: ReactNode;
|
|
67
|
+
}
|
|
68
|
+
export declare function BookingProvider({ config, labels, classNames, components, children }: BookingProviderProps): import("react").JSX.Element;
|
|
69
|
+
/** What a component renders with: the kit, and the overrides that apply to it. */
|
|
70
|
+
export interface ResolvedBooking {
|
|
71
|
+
kit: BookingKit;
|
|
72
|
+
classNames: BookingClassNames;
|
|
73
|
+
components: BookingComponents;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The kit for a component's own props over the nearest provider. Memoised on
|
|
77
|
+
* the inputs, so a re-render with the same props reuses the same clock,
|
|
78
|
+
* machine and label pack.
|
|
79
|
+
*/
|
|
80
|
+
export declare function useBookingKit(props: BookingOverrides, contact?: Partial<Pick<BookingConfig['contact'], 'phone' | 'address'>>): ResolvedBooking;
|
|
81
|
+
//#endregion
|
|
82
|
+
//# sourceMappingURL=Provider.d.mts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createBookingKit } from "./kit.mjs";
|
|
3
|
+
import { createContext, useContext, useMemo } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/react/Provider.tsx
|
|
6
|
+
const BookingContext = createContext(null);
|
|
7
|
+
function BookingProvider({ config, labels, classNames, components, children }) {
|
|
8
|
+
const parent = useContext(BookingContext);
|
|
9
|
+
const value = useMemo(() => ({
|
|
10
|
+
config: config ?? parent?.config,
|
|
11
|
+
labels: parent?.labels && labels ? {
|
|
12
|
+
...parent.labels,
|
|
13
|
+
...labels
|
|
14
|
+
} : labels ?? parent?.labels,
|
|
15
|
+
classNames: mergeClassNames(parent?.classNames, classNames),
|
|
16
|
+
components: {
|
|
17
|
+
...parent?.components,
|
|
18
|
+
...components
|
|
19
|
+
}
|
|
20
|
+
}), [
|
|
21
|
+
config,
|
|
22
|
+
labels,
|
|
23
|
+
classNames,
|
|
24
|
+
components,
|
|
25
|
+
parent
|
|
26
|
+
]);
|
|
27
|
+
return /* @__PURE__ */ jsx(BookingContext.Provider, {
|
|
28
|
+
value,
|
|
29
|
+
children
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The kit for a component's own props over the nearest provider. Memoised on
|
|
34
|
+
* the inputs, so a re-render with the same props reuses the same clock,
|
|
35
|
+
* machine and label pack.
|
|
36
|
+
*/
|
|
37
|
+
function useBookingKit(props, contact) {
|
|
38
|
+
const context = useContext(BookingContext);
|
|
39
|
+
const base = props.config ?? context?.config;
|
|
40
|
+
if (base === void 0) throw new Error("A booking component needs a `config`: pass it, or render it inside <BookingProvider config={…}>.");
|
|
41
|
+
const config = withContact(base, contact);
|
|
42
|
+
const contextLabels = context?.labels;
|
|
43
|
+
const labels = useMemo(() => mergedLabels(config, contextLabels, props.labels), [
|
|
44
|
+
config,
|
|
45
|
+
contextLabels,
|
|
46
|
+
props.labels
|
|
47
|
+
]);
|
|
48
|
+
const kit = useMemo(() => createBookingKit(config, labels), [config, labels]);
|
|
49
|
+
const contextClassNames = context?.classNames;
|
|
50
|
+
const contextComponents = context?.components;
|
|
51
|
+
return {
|
|
52
|
+
kit,
|
|
53
|
+
classNames: useMemo(() => mergeClassNames(contextClassNames, props.classNames), [contextClassNames, props.classNames]),
|
|
54
|
+
components: useMemo(() => ({
|
|
55
|
+
...contextComponents,
|
|
56
|
+
...props.components
|
|
57
|
+
}), [contextComponents, props.components])
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const WITH_CONTACT = /* @__PURE__ */ new WeakMap();
|
|
61
|
+
/**
|
|
62
|
+
* The config with a per-request contact over its own (a page that reads the
|
|
63
|
+
* phone and address from a CMS), one object per pair so the kit is reused.
|
|
64
|
+
*/
|
|
65
|
+
function withContact(config, contact) {
|
|
66
|
+
if (contact === void 0) return config;
|
|
67
|
+
const merged = {
|
|
68
|
+
...config.contact,
|
|
69
|
+
...contact
|
|
70
|
+
};
|
|
71
|
+
const key = JSON.stringify([merged.phone, merged.address]);
|
|
72
|
+
let byKey = WITH_CONTACT.get(config);
|
|
73
|
+
if (!byKey) {
|
|
74
|
+
byKey = /* @__PURE__ */ new Map();
|
|
75
|
+
WITH_CONTACT.set(config, byKey);
|
|
76
|
+
}
|
|
77
|
+
let derived = byKey.get(key);
|
|
78
|
+
if (!derived) {
|
|
79
|
+
derived = Object.freeze({
|
|
80
|
+
...config,
|
|
81
|
+
contact: Object.freeze(merged)
|
|
82
|
+
});
|
|
83
|
+
byKey.set(key, derived);
|
|
84
|
+
}
|
|
85
|
+
return derived;
|
|
86
|
+
}
|
|
87
|
+
/** Stands in for «no labels» as a cache key. */
|
|
88
|
+
const NO_LABELS = {};
|
|
89
|
+
const MERGED = /* @__PURE__ */ new WeakMap();
|
|
90
|
+
/**
|
|
91
|
+
* The pack a component renders with, cached on its three inputs, so every
|
|
92
|
+
* component that shares a config and the same label objects shares one pack
|
|
93
|
+
* — and so one kit (`createBookingKit` caches on the pack's identity).
|
|
94
|
+
*/
|
|
95
|
+
function mergedLabels(config, outer, inner) {
|
|
96
|
+
let byOuter = MERGED.get(config);
|
|
97
|
+
if (!byOuter) {
|
|
98
|
+
byOuter = /* @__PURE__ */ new WeakMap();
|
|
99
|
+
MERGED.set(config, byOuter);
|
|
100
|
+
}
|
|
101
|
+
let byInner = byOuter.get(outer ?? NO_LABELS);
|
|
102
|
+
if (!byInner) {
|
|
103
|
+
byInner = /* @__PURE__ */ new WeakMap();
|
|
104
|
+
byOuter.set(outer ?? NO_LABELS, byInner);
|
|
105
|
+
}
|
|
106
|
+
const known = byInner.get(inner ?? NO_LABELS);
|
|
107
|
+
if (known) return known;
|
|
108
|
+
const merged = {
|
|
109
|
+
...config.labels,
|
|
110
|
+
...outer,
|
|
111
|
+
...inner,
|
|
112
|
+
...config.consent.marketing ? { "details.marketing.text": config.consent.marketing.text } : {}
|
|
113
|
+
};
|
|
114
|
+
if (merged["who.heading"] === void 0) console.warn("[@medalsocial/booking] No label pack: pass `labels: mergeLabels(locale, …)` from '@medalsocial/booking/react/shared' into the config or the provider.");
|
|
115
|
+
byInner.set(inner ?? NO_LABELS, merged);
|
|
116
|
+
return merged;
|
|
117
|
+
}
|
|
118
|
+
/** Screen by screen: a component's own slot classes over the provider's. */
|
|
119
|
+
function mergeClassNames(outer, inner) {
|
|
120
|
+
if (!outer) return inner ?? {};
|
|
121
|
+
if (!inner) return outer;
|
|
122
|
+
const merged = { ...outer };
|
|
123
|
+
for (const [screen, slots] of Object.entries(inner)) merged[screen] = {
|
|
124
|
+
...outer[screen],
|
|
125
|
+
...slots
|
|
126
|
+
};
|
|
127
|
+
return merged;
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { BookingProvider, useBookingKit };
|
|
131
|
+
|
|
132
|
+
//# sourceMappingURL=Provider.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Provider.mjs","names":[],"sources":["../../src/react/Provider.tsx"],"sourcesContent":["/**\n * `<BookingProvider>` — one config, one label pack and one set of overrides\n * for every booking piece under it (the wizard, a sticky bar, a hero chip, the\n * manage page), so they agree about the business without each being told.\n *\n * Every component also takes the same four props itself; a prop wins over the\n * provider, and a component outside any provider needs at least `config`.\n */\n\nimport type {\n AccountCardSlot,\n AddChildSheetSlot,\n BookingCardProps,\n BookingSkeletonSlot,\n ChildCardProps,\n ChildCardsSlot,\n ConfirmationSlot,\n DataControlsSlot,\n DayChipProps,\n DetailsScreenSlot,\n FamilyChipProps,\n FamilyEditorSlot,\n FamilyMemberCardProps,\n LoginPanelSlot,\n LoginSheetSlot,\n LogoutButtonSlot,\n ManageScreenSlot,\n OtpSlotsSlot,\n PartyLineProps,\n PersonCardProps,\n PortalShellSlot,\n PortalUnreachableSlot,\n ProfileFormSlot,\n RebookCardProps,\n RebookCardsSlot,\n ServiceCardProps,\n ServiceScreenSlot,\n SlotClassNames,\n StylistCardProps,\n StylistScreenSlot,\n SummaryBarSlot,\n TimeChipProps,\n TimeScreenSlot,\n UpcomingBookingsSlot,\n VippsButtonSlot,\n VippsLinkRowSlot,\n VisitHistorySlot,\n VisitRowProps,\n WhoScreenSlot,\n} from '@medalsocial/meda/booking';\nimport { type ComponentType, createContext, type ReactNode, useContext, useMemo } from 'react';\nimport type { BookingConfig } from '../core/config';\nimport { type BookingKit, createBookingKit } from './kit';\nimport type { BookingLabels, BookingLabelsInput } from './labels';\n\n/** Per-screen slot classes (see each meda screen's `…Slot` type for the slots). */\nexport interface BookingClassNames {\n wizard?: SlotClassNames<'root' | 'header' | 'back' | 'progress' | 'alert'>;\n who?: SlotClassNames<WhoScreenSlot>;\n addChild?: SlotClassNames<AddChildSheetSlot>;\n service?: SlotClassNames<ServiceScreenSlot>;\n stylist?: SlotClassNames<StylistScreenSlot>;\n time?: SlotClassNames<TimeScreenSlot>;\n details?: SlotClassNames<DetailsScreenSlot>;\n summary?: SlotClassNames<SummaryBarSlot>;\n confirmation?: SlotClassNames<ConfirmationSlot>;\n skeleton?: SlotClassNames<BookingSkeletonSlot>;\n manage?: SlotClassNames<ManageScreenSlot>;\n loginSheet?: SlotClassNames<LoginSheetSlot>;\n loginPanel?: SlotClassNames<LoginPanelSlot>;\n otp?: SlotClassNames<OtpSlotsSlot>;\n vipps?: SlotClassNames<VippsButtonSlot>;\n portalShell?: SlotClassNames<PortalShellSlot>;\n portalUnreachable?: SlotClassNames<PortalUnreachableSlot>;\n upcoming?: SlotClassNames<UpcomingBookingsSlot>;\n history?: SlotClassNames<VisitHistorySlot>;\n rebook?: SlotClassNames<RebookCardsSlot>;\n childCards?: SlotClassNames<ChildCardsSlot>;\n accountCard?: SlotClassNames<AccountCardSlot>;\n familyEditor?: SlotClassNames<FamilyEditorSlot>;\n profileForm?: SlotClassNames<ProfileFormSlot>;\n dataControls?: SlotClassNames<DataControlsSlot>;\n logout?: SlotClassNames<LogoutButtonSlot>;\n vippsLink?: SlotClassNames<VippsLinkRowSlot>;\n}\n\n/** Card-level renderers, by name; each one is handed to the screen that draws it. */\nexport interface BookingComponents {\n PersonCard?: ComponentType<PersonCardProps>;\n ServiceCard?: ComponentType<ServiceCardProps>;\n StylistCard?: ComponentType<StylistCardProps>;\n DayChip?: ComponentType<DayChipProps>;\n TimeChip?: ComponentType<TimeChipProps>;\n FamilyChip?: ComponentType<FamilyChipProps>;\n PartyLine?: ComponentType<PartyLineProps>;\n BookingCard?: ComponentType<BookingCardProps>;\n VisitRow?: ComponentType<VisitRowProps>;\n RebookCard?: ComponentType<RebookCardProps>;\n ChildCard?: ComponentType<ChildCardProps>;\n FamilyMemberCard?: ComponentType<FamilyMemberCardProps>;\n}\n\n/** The four props every booking component and the provider take. */\nexport interface BookingOverrides {\n /** The site's resolved config (`resolveBookingConfig` from `/core`, run on the server). */\n config?: Readonly<BookingConfig>;\n /**\n * Words over `config.labels`. The pack itself is resolved on the server —\n * `mergeLabels(locale, siteWords)` from `@medalsocial/booking/react/shared`\n * — so the built-in packs never ship in the browser bundle.\n */\n labels?: BookingLabelsInput;\n classNames?: BookingClassNames;\n components?: BookingComponents;\n}\n\ninterface BookingContextValue {\n config: Readonly<BookingConfig> | undefined;\n labels: BookingLabelsInput | undefined;\n classNames: BookingClassNames;\n components: BookingComponents;\n}\n\nconst BookingContext = createContext<BookingContextValue | null>(null);\n\nexport interface BookingProviderProps extends BookingOverrides {\n children?: ReactNode;\n}\n\nexport function BookingProvider({\n config,\n labels,\n classNames,\n components,\n children,\n}: BookingProviderProps) {\n const parent = useContext(BookingContext);\n const value = useMemo<BookingContextValue>(\n () => ({\n config: config ?? parent?.config,\n labels:\n parent?.labels && labels ? { ...parent.labels, ...labels } : (labels ?? parent?.labels),\n classNames: mergeClassNames(parent?.classNames, classNames),\n components: { ...parent?.components, ...components },\n }),\n [config, labels, classNames, components, parent]\n );\n return <BookingContext.Provider value={value}>{children}</BookingContext.Provider>;\n}\n\n/** What a component renders with: the kit, and the overrides that apply to it. */\nexport interface ResolvedBooking {\n kit: BookingKit;\n classNames: BookingClassNames;\n components: BookingComponents;\n}\n\n/**\n * The kit for a component's own props over the nearest provider. Memoised on\n * the inputs, so a re-render with the same props reuses the same clock,\n * machine and label pack.\n */\nexport function useBookingKit(\n props: BookingOverrides,\n contact?: Partial<Pick<BookingConfig['contact'], 'phone' | 'address'>>\n): ResolvedBooking {\n const context = useContext(BookingContext);\n const base = props.config ?? context?.config;\n if (base === undefined) {\n throw new Error(\n 'A booking component needs a `config`: pass it, or render it inside <BookingProvider config={…}>.'\n );\n }\n const config = withContact(base, contact);\n const contextLabels = context?.labels;\n const labels = useMemo(\n () => mergedLabels(config, contextLabels, props.labels),\n [config, contextLabels, props.labels]\n );\n const kit = useMemo(() => createBookingKit(config, labels), [config, labels]);\n const contextClassNames = context?.classNames;\n const contextComponents = context?.components;\n const classNames = useMemo(\n () => mergeClassNames(contextClassNames, props.classNames),\n [contextClassNames, props.classNames]\n );\n const components = useMemo(\n () => ({ ...contextComponents, ...props.components }),\n [contextComponents, props.components]\n );\n return { kit, classNames, components };\n}\n\nconst WITH_CONTACT = new WeakMap<object, Map<string, Readonly<BookingConfig>>>();\n\n/**\n * The config with a per-request contact over its own (a page that reads the\n * phone and address from a CMS), one object per pair so the kit is reused.\n */\nfunction withContact(\n config: Readonly<BookingConfig>,\n contact: Partial<Pick<BookingConfig['contact'], 'phone' | 'address'>> | undefined\n): Readonly<BookingConfig> {\n if (contact === undefined) return config;\n const merged = { ...config.contact, ...contact };\n const key = JSON.stringify([merged.phone, merged.address]);\n let byKey = WITH_CONTACT.get(config);\n if (!byKey) {\n byKey = new Map();\n WITH_CONTACT.set(config, byKey);\n }\n let derived = byKey.get(key);\n if (!derived) {\n derived = Object.freeze({ ...config, contact: Object.freeze(merged) });\n byKey.set(key, derived);\n }\n return derived;\n}\n\n/** Stands in for «no labels» as a cache key. */\nconst NO_LABELS: BookingLabelsInput = {};\nconst MERGED = new WeakMap<\n object,\n WeakMap<BookingLabelsInput, WeakMap<BookingLabelsInput, Readonly<BookingLabels>>>\n>();\n\n/**\n * The pack a component renders with, cached on its three inputs, so every\n * component that shares a config and the same label objects shares one pack\n * — and so one kit (`createBookingKit` caches on the pack's identity).\n */\nfunction mergedLabels(\n config: Readonly<BookingConfig>,\n outer: BookingLabelsInput | undefined,\n inner: BookingLabelsInput | undefined\n): Readonly<BookingLabels> {\n let byOuter = MERGED.get(config);\n if (!byOuter) {\n byOuter = new WeakMap();\n MERGED.set(config, byOuter);\n }\n let byInner = byOuter.get(outer ?? NO_LABELS);\n if (!byInner) {\n byInner = new WeakMap();\n byOuter.set(outer ?? NO_LABELS, byInner);\n }\n const known = byInner.get(inner ?? NO_LABELS);\n if (known) return known;\n const merged = {\n ...config.labels,\n ...outer,\n ...inner,\n // The consent sentence is the one recorded with its version, so the box shows it.\n ...(config.consent.marketing\n ? { 'details.marketing.text': config.consent.marketing.text }\n : {}),\n } as BookingLabels;\n if (merged['who.heading'] === undefined) {\n console.warn(\n '[@medalsocial/booking] No label pack: pass `labels: mergeLabels(locale, …)` from ' +\n \"'@medalsocial/booking/react/shared' into the config or the provider.\"\n );\n }\n byInner.set(inner ?? NO_LABELS, merged);\n return merged;\n}\n\n/** Screen by screen: a component's own slot classes over the provider's. */\nfunction mergeClassNames(\n outer: BookingClassNames | undefined,\n inner: BookingClassNames | undefined\n): BookingClassNames {\n if (!outer) return inner ?? {};\n if (!inner) return outer;\n const merged: Record<string, Record<string, string> | undefined> = { ...outer };\n for (const [screen, slots] of Object.entries(inner)) {\n merged[screen] = { ...(outer as Record<string, Record<string, string>>)[screen], ...slots };\n }\n return merged as BookingClassNames;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { PortalProfileDto } from "../core/portal/dto.mjs";
|
|
2
|
+
import { VippsLinkStartResult, VippsStartAction } from "@medalsocial/meda/booking";
|
|
3
|
+
//#region src/react/actions.d.ts
|
|
4
|
+
/** The session is gone (or never was): the visitor goes back to the login. */
|
|
5
|
+
export type SessionFailure = {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: 'session';
|
|
8
|
+
};
|
|
9
|
+
/** The backend refused the input, with a sentence to show. */
|
|
10
|
+
export type InvalidFailure = {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: 'invalid';
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
/** next-safe-action's envelope. */
|
|
16
|
+
export interface SafeActionEnvelope<T> {
|
|
17
|
+
data?: T;
|
|
18
|
+
serverError?: string;
|
|
19
|
+
validationErrors?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** What an action may answer with: its result, or the envelope around it. */
|
|
22
|
+
export type ActionAnswer<T> = T | SafeActionEnvelope<T>;
|
|
23
|
+
export type ProfileActionResult = {
|
|
24
|
+
ok: true;
|
|
25
|
+
profile: PortalProfileDto;
|
|
26
|
+
} | SessionFailure | InvalidFailure;
|
|
27
|
+
export type PersonActionResult = {
|
|
28
|
+
ok: true;
|
|
29
|
+
profile: PortalProfileDto;
|
|
30
|
+
personId: string | null;
|
|
31
|
+
fallback: boolean;
|
|
32
|
+
} | SessionFailure | InvalidFailure;
|
|
33
|
+
/** A person as an action writes it (the wire's snake_case, as the source's schema). */
|
|
34
|
+
export interface PersonActionInput {
|
|
35
|
+
name: string;
|
|
36
|
+
birth_year: number;
|
|
37
|
+
birth_month?: number | null;
|
|
38
|
+
notes?: string | null;
|
|
39
|
+
preferred_resource_id?: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Which person: the id, or the family index where there is none. */
|
|
42
|
+
export interface PersonActionTarget {
|
|
43
|
+
person_id?: string | null;
|
|
44
|
+
index?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface PortalActions {
|
|
47
|
+
/**
|
|
48
|
+
* Ask for a login code. Answers «sent» whether or not the address is known;
|
|
49
|
+
* `invalid` only for an address that cannot be one.
|
|
50
|
+
*/
|
|
51
|
+
startLogin(input: {
|
|
52
|
+
email: string;
|
|
53
|
+
}): Promise<ActionAnswer<{
|
|
54
|
+
status: 'sent';
|
|
55
|
+
} | InvalidFailure>>;
|
|
56
|
+
/** Start a Vipps login: a form action that redirects to Vipps, or answers why not. */
|
|
57
|
+
startVipps?: VippsStartAction;
|
|
58
|
+
/**
|
|
59
|
+
* Start linking Vipps to the logged-in profile — `useActionState`'s shape,
|
|
60
|
+
* like `startVipps`; it is called with `null` and no form data.
|
|
61
|
+
*/
|
|
62
|
+
startVippsLink?: (previous: VippsLinkStartResult, formData?: FormData) => Promise<VippsLinkStartResult>;
|
|
63
|
+
updateProfile(input: {
|
|
64
|
+
first_name?: string;
|
|
65
|
+
last_name?: string;
|
|
66
|
+
phone?: string | null;
|
|
67
|
+
}): Promise<ActionAnswer<ProfileActionResult>>;
|
|
68
|
+
setMarketingConsent(input: {
|
|
69
|
+
granted: boolean;
|
|
70
|
+
}): Promise<ActionAnswer<{
|
|
71
|
+
ok: true;
|
|
72
|
+
marketingConsent: boolean;
|
|
73
|
+
} | SessionFailure | InvalidFailure>>;
|
|
74
|
+
savePerson(input: PersonActionTarget & {
|
|
75
|
+
person: PersonActionInput;
|
|
76
|
+
create: boolean;
|
|
77
|
+
}): Promise<ActionAnswer<PersonActionResult>>;
|
|
78
|
+
removePerson(input: PersonActionTarget): Promise<ActionAnswer<PersonActionResult>>;
|
|
79
|
+
logout(): Promise<ActionAnswer<{
|
|
80
|
+
ok: true;
|
|
81
|
+
}>>;
|
|
82
|
+
exportData(): Promise<ActionAnswer<{
|
|
83
|
+
ok: true;
|
|
84
|
+
filename: string;
|
|
85
|
+
json: string;
|
|
86
|
+
} | SessionFailure>>;
|
|
87
|
+
/** `confirm` is the typed confirmation word; the backend re-checks it. */
|
|
88
|
+
deleteMe(input: {
|
|
89
|
+
confirm: string;
|
|
90
|
+
}): Promise<ActionAnswer<{
|
|
91
|
+
ok: true;
|
|
92
|
+
} | SessionFailure | InvalidFailure>>;
|
|
93
|
+
}
|
|
94
|
+
/** Why an action did not come back `ok`: a dead session, or the sentence to show. */
|
|
95
|
+
export type ActionFailure = {
|
|
96
|
+
kind: 'session';
|
|
97
|
+
} | {
|
|
98
|
+
kind: 'error';
|
|
99
|
+
message: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* `startLogin`'s answer, read for the login form: `sent` only for a result
|
|
103
|
+
* that says so — plain or inside the envelope. The action's own refusal of
|
|
104
|
+
* the address (or zod's) is a bad address; a server error, an empty envelope,
|
|
105
|
+
* an answer that is neither, or no answer at all is `unreachable`, so the
|
|
106
|
+
* form never moves on to a code that was not sent.
|
|
107
|
+
*/
|
|
108
|
+
export declare function readStartLogin(answer: ActionAnswer<{
|
|
109
|
+
status: 'sent';
|
|
110
|
+
} | InvalidFailure> | undefined): {
|
|
111
|
+
ok: true;
|
|
112
|
+
} | {
|
|
113
|
+
ok: false;
|
|
114
|
+
reason: 'unreachable' | 'invalidEmail';
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* An action's answer, read: `{ ok: true, value }`, or the failure a form acts
|
|
118
|
+
* on. The action's own `message` wins over zod's, which wins over
|
|
119
|
+
* `invalidInput`; anything unreadable (a thrown call, a server error, an
|
|
120
|
+
* answer with nothing in it) is `unreachable`.
|
|
121
|
+
*/
|
|
122
|
+
export declare function readAction<T extends object>(answer: ActionAnswer<T> | undefined, words: {
|
|
123
|
+
unreachable: string;
|
|
124
|
+
invalidInput: string;
|
|
125
|
+
}): {
|
|
126
|
+
ok: true;
|
|
127
|
+
value: T;
|
|
128
|
+
} | {
|
|
129
|
+
ok: false;
|
|
130
|
+
failure: ActionFailure;
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
//# sourceMappingURL=actions.d.mts.map
|