@wix/agent-skills 1.9.0 → 1.10.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/package.json +1 -1
- package/skills/wix-headless/references/DESIGN_SYSTEM.md +6 -16
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +1 -1
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +19 -0
- package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +2 -2
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +3 -3
- package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +2 -2
- package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +3 -3
- package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +2 -2
- package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +7 -7
- package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +4 -4
- package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +2 -2
- package/skills/wix-headless/references/astro/templates/Footer.astro +4 -4
- package/skills/wix-headless/references/astro/templates/Navigation.astro +4 -4
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +147 -6
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +21 -4
- package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +11 -2
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +8 -8
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +94 -26
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +53 -10
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +128 -20
- package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +3 -3
- package/skills/wix-headless/references/astro/templates/ecom/cart.astro +3 -3
- package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +17 -17
- package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +3 -3
- package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +4 -4
- package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +19 -18
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +3 -3
- package/skills/wix-headless/references/astro/templates/global.css +17 -15
- package/skills/wix-headless/references/astro/templates/index.astro +4 -4
- package/skills/wix-headless/references/astro/templates/stores/components-stores.css +31 -31
- package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +4 -4
- package/skills/wix-headless/references/bookings/FLOW.md +93 -32
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +8 -3
- package/skills/wix-headless/references/bookings/SERVICES_DATA.md +60 -0
- package/skills/wix-headless/references/custom/bookings/WIRING.md +60 -9
- package/skills/wix-headless/references/shared/DESIGN_MD.md +2 -3
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +4 -4
- package/skills/wix-headless/references/shared/STYLING.md +15 -18
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +2 -2
- package/skills/wix-headless/references/verticals/bookings.md +5 -3
- package/skills/wix-headless/scripts/compose.mjs +4 -19
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -3
- package/skills/wix-manage/references/bookings/create-appointment-service.md +1 -13
- package/skills/wix-manage/references/bookings/create-class-service.md +1 -13
- package/skills/wix-manage/references/bookings/create-course-service.md +1 -13
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
@theme {
|
|
12
12
|
/* {{theme}} — Composer substitutes the full palette here from designTokens:
|
|
13
|
-
every --color-*, --font-*, --
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
every --color-*, --font-*, --container-*, and --radius-* line. The
|
|
14
|
+
required-token set (enforced by compose.mjs) MUST all be present, or
|
|
15
|
+
components render unstyled. Spacing is NOT a token here — it uses
|
|
16
|
+
Tailwind's built-in numeric scale (gap-4, py-24, …); never add a named
|
|
17
|
+
--spacing-* scale, since that namespace also powers the width utilities
|
|
18
|
+
(max-w-*/w-*) and would shadow them. */
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/* Reading-width helper so prose pages center without guessing Tailwind sizes. */
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
/* Shared button primitive — declared with @utility so the variants below can
|
|
26
28
|
@apply it (a plain contract class is NOT @apply-able in Tailwind v4). */
|
|
27
29
|
@utility btn {
|
|
28
|
-
@apply inline-flex items-center justify-center px-
|
|
30
|
+
@apply inline-flex items-center justify-center px-6 py-4 rounded-md font-medium;
|
|
29
31
|
transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -130,7 +132,7 @@
|
|
|
130
132
|
height: 1px;
|
|
131
133
|
background: var(--color-accent);
|
|
132
134
|
opacity: 0.6;
|
|
133
|
-
margin-block:
|
|
135
|
+
margin-block: 1.5rem;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
/* ── Universal site chrome ────────────────────────────────────────────────── */
|
|
@@ -204,8 +206,8 @@ body[data-navigating="true"] .product-grid {
|
|
|
204
206
|
min-width: 12rem;
|
|
205
207
|
display: flex;
|
|
206
208
|
flex-direction: column;
|
|
207
|
-
gap:
|
|
208
|
-
padding:
|
|
209
|
+
gap: 0.25rem;
|
|
210
|
+
padding: 0.75rem;
|
|
209
211
|
background: var(--color-paper);
|
|
210
212
|
border: 1px solid var(--color-rule);
|
|
211
213
|
border-radius: var(--radius-md);
|
|
@@ -223,7 +225,7 @@ body[data-navigating="true"] .product-grid {
|
|
|
223
225
|
.site-nav-sublink {
|
|
224
226
|
font-size: 0.875rem;
|
|
225
227
|
color: var(--color-ink-soft, var(--color-mute));
|
|
226
|
-
padding-block:
|
|
228
|
+
padding-block: 0.25rem;
|
|
227
229
|
}
|
|
228
230
|
.site-nav-sublink:hover,
|
|
229
231
|
.site-nav-sublink[aria-current="page"] {
|
|
@@ -237,13 +239,13 @@ body[data-navigating="true"] .product-grid {
|
|
|
237
239
|
.category-rail-inner {
|
|
238
240
|
display: flex;
|
|
239
241
|
flex-wrap: wrap;
|
|
240
|
-
gap:
|
|
241
|
-
padding-block:
|
|
242
|
+
gap: 0.75rem;
|
|
243
|
+
padding-block: 1rem;
|
|
242
244
|
}
|
|
243
245
|
.category-pill {
|
|
244
246
|
display: inline-flex;
|
|
245
247
|
align-items: center;
|
|
246
|
-
padding:
|
|
248
|
+
padding: 0.25rem 1rem;
|
|
247
249
|
border: 1px solid var(--color-rule);
|
|
248
250
|
border-radius: var(--radius-sm);
|
|
249
251
|
font-size: 0.8125rem;
|
|
@@ -262,12 +264,12 @@ body[data-navigating="true"] .product-grid {
|
|
|
262
264
|
border-color: var(--color-ink);
|
|
263
265
|
}
|
|
264
266
|
.pagination {
|
|
265
|
-
margin-block:
|
|
267
|
+
margin-block: 2rem;
|
|
266
268
|
}
|
|
267
269
|
.pagination-inner {
|
|
268
270
|
display: flex;
|
|
269
271
|
justify-content: space-between;
|
|
270
|
-
gap:
|
|
272
|
+
gap: 1rem;
|
|
271
273
|
}
|
|
272
274
|
.pagination-link {
|
|
273
275
|
color: var(--color-ink);
|
|
@@ -282,5 +284,5 @@ body[data-navigating="true"] .product-grid {
|
|
|
282
284
|
.breadcrumbs {
|
|
283
285
|
font-size: 0.8125rem;
|
|
284
286
|
color: var(--color-mute);
|
|
285
|
-
margin-bottom:
|
|
287
|
+
margin-bottom: 0.75rem;
|
|
286
288
|
}
|
|
@@ -17,8 +17,8 @@ import Layout from '../layouts/Layout.astro';
|
|
|
17
17
|
---
|
|
18
18
|
<Layout>
|
|
19
19
|
<section class="relative">
|
|
20
|
-
<div class="max-w-6xl mx-auto px-
|
|
21
|
-
<div class="flex flex-col gap-
|
|
20
|
+
<div class="max-w-6xl mx-auto px-6 py-24 grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
|
|
21
|
+
<div class="flex flex-col gap-6">
|
|
22
22
|
<h1 class="font-display text-4xl md:text-5xl leading-tight">{{shell.heroHeadline}}</h1>
|
|
23
23
|
<p class="text-lg text-mute max-w-prose">{{shell.heroSub}}</p>
|
|
24
24
|
</div>
|
|
@@ -34,7 +34,7 @@ import Layout from '../layouts/Layout.astro';
|
|
|
34
34
|
|
|
35
35
|
{{home-markers}}
|
|
36
36
|
|
|
37
|
-
<section class="max-w-6xl mx-auto px-
|
|
37
|
+
<section class="max-w-6xl mx-auto px-6 py-24 grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
|
|
38
38
|
<div
|
|
39
39
|
class="about-image rounded-md order-last md:order-first"
|
|
40
40
|
data-decorative-slot="about"
|
|
@@ -42,7 +42,7 @@ import Layout from '../layouts/Layout.astro';
|
|
|
42
42
|
>
|
|
43
43
|
<!-- orchestrator injects <img src={decorativeImages.about} …> here -->
|
|
44
44
|
</div>
|
|
45
|
-
<div class="flex flex-col gap-
|
|
45
|
+
<div class="flex flex-col gap-4 max-w-prose">
|
|
46
46
|
<hr class="editorial-rule w-16 ml-0" />
|
|
47
47
|
<h2 class="font-display text-3xl leading-tight">The story behind {{brand.name}}</h2>
|
|
48
48
|
<p class="text-ink leading-relaxed">
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
.product-grid {
|
|
29
29
|
display: grid;
|
|
30
30
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
31
|
-
gap:
|
|
31
|
+
gap: 2rem;
|
|
32
32
|
transition: opacity 200ms ease;
|
|
33
33
|
}
|
|
34
34
|
body[data-navigating="true"] .product-grid {
|
|
@@ -40,8 +40,8 @@ body[data-navigating="true"] .product-grid {
|
|
|
40
40
|
position: relative;
|
|
41
41
|
display: flex;
|
|
42
42
|
flex-direction: column;
|
|
43
|
-
gap:
|
|
44
|
-
padding-bottom:
|
|
43
|
+
gap: 1rem;
|
|
44
|
+
padding-bottom: 1rem;
|
|
45
45
|
background: var(--color-paper, var(--color-cream));
|
|
46
46
|
border-radius: var(--radius-xl);
|
|
47
47
|
overflow: hidden;
|
|
@@ -59,7 +59,7 @@ body[data-navigating="true"] .product-grid {
|
|
|
59
59
|
|
|
60
60
|
.product-card-media {
|
|
61
61
|
position: relative;
|
|
62
|
-
margin:
|
|
62
|
+
margin: 1rem;
|
|
63
63
|
aspect-ratio: 4 / 5;
|
|
64
64
|
background: var(--color-paper-warm, var(--color-cream));
|
|
65
65
|
border-radius: var(--radius-md);
|
|
@@ -75,8 +75,8 @@ body[data-navigating="true"] .product-grid {
|
|
|
75
75
|
.product-card-body {
|
|
76
76
|
display: flex;
|
|
77
77
|
flex-direction: column;
|
|
78
|
-
gap:
|
|
79
|
-
padding-inline:
|
|
78
|
+
gap: 0.5rem;
|
|
79
|
+
padding-inline: 1rem;
|
|
80
80
|
}
|
|
81
81
|
.product-card-name {
|
|
82
82
|
margin: 0;
|
|
@@ -95,8 +95,8 @@ body[data-navigating="true"] .product-grid {
|
|
|
95
95
|
|
|
96
96
|
.product-card-ribbon {
|
|
97
97
|
position: absolute;
|
|
98
|
-
top:
|
|
99
|
-
left:
|
|
98
|
+
top: 1rem;
|
|
99
|
+
left: 1rem;
|
|
100
100
|
padding: 0.375rem 0.625rem;
|
|
101
101
|
background: var(--color-accent);
|
|
102
102
|
color: var(--color-ink);
|
|
@@ -110,8 +110,8 @@ body[data-navigating="true"] .product-grid {
|
|
|
110
110
|
}
|
|
111
111
|
.product-card-index {
|
|
112
112
|
position: absolute;
|
|
113
|
-
top:
|
|
114
|
-
right:
|
|
113
|
+
top: 1rem;
|
|
114
|
+
right: 1rem;
|
|
115
115
|
padding: 0.375rem 0.5rem;
|
|
116
116
|
background: rgba(255, 255, 255, 0.85);
|
|
117
117
|
color: var(--color-ink);
|
|
@@ -132,19 +132,19 @@ body[data-navigating="true"] .product-grid {
|
|
|
132
132
|
background: var(--color-paper-warm, var(--color-cream));
|
|
133
133
|
border: 1px solid var(--color-rule);
|
|
134
134
|
border-left: 2px solid var(--color-accent);
|
|
135
|
-
padding:
|
|
135
|
+
padding: 1.5rem;
|
|
136
136
|
border-radius: var(--radius-md);
|
|
137
137
|
display: flex;
|
|
138
138
|
flex-direction: column;
|
|
139
|
-
gap:
|
|
139
|
+
gap: 1rem;
|
|
140
140
|
}
|
|
141
141
|
.offer-callout-item {
|
|
142
142
|
display: flex;
|
|
143
143
|
flex-direction: column;
|
|
144
|
-
gap:
|
|
144
|
+
gap: 0.5rem;
|
|
145
145
|
}
|
|
146
146
|
.offer-callout-item + .offer-callout-item {
|
|
147
|
-
padding-top:
|
|
147
|
+
padding-top: 1rem;
|
|
148
148
|
border-top: 1px solid var(--color-rule);
|
|
149
149
|
}
|
|
150
150
|
.offer-callout-badge {
|
|
@@ -190,7 +190,7 @@ body[data-navigating="true"] .product-grid {
|
|
|
190
190
|
of this template used `@apply font-heading text-bark/70 bg-parchment …`,
|
|
191
191
|
which assumed a different token vocabulary than current designer output
|
|
192
192
|
(`paper`/`ink`/`font-display` …) and built-failed on every run that loaded
|
|
193
|
-
it. Direct `var(--color-paper)` / `
|
|
193
|
+
it. Direct `var(--color-paper)` / `0.75rem` calls work against
|
|
194
194
|
any designer-published token set as long as the core scales exist (they
|
|
195
195
|
always do). Use fallback chains (`var(--color-paper-warm, var(--color-cream))`)
|
|
196
196
|
when a token is sometimes-published.
|
|
@@ -200,8 +200,8 @@ body[data-navigating="true"] .product-grid {
|
|
|
200
200
|
.option-group {
|
|
201
201
|
display: flex;
|
|
202
202
|
flex-direction: column;
|
|
203
|
-
gap:
|
|
204
|
-
margin-bottom:
|
|
203
|
+
gap: 0.75rem;
|
|
204
|
+
margin-bottom: 1.5rem;
|
|
205
205
|
}
|
|
206
206
|
.option-label {
|
|
207
207
|
font-family: var(--font-display);
|
|
@@ -213,10 +213,10 @@ body[data-navigating="true"] .product-grid {
|
|
|
213
213
|
.option-choices {
|
|
214
214
|
display: flex;
|
|
215
215
|
flex-wrap: wrap;
|
|
216
|
-
gap:
|
|
216
|
+
gap: 0.75rem;
|
|
217
217
|
}
|
|
218
218
|
.option-pill {
|
|
219
|
-
padding:
|
|
219
|
+
padding: 0.75rem 1rem;
|
|
220
220
|
border: 1px solid var(--color-ink);
|
|
221
221
|
background: transparent;
|
|
222
222
|
color: var(--color-ink);
|
|
@@ -255,14 +255,14 @@ body[data-navigating="true"] .product-grid {
|
|
|
255
255
|
cursor: not-allowed;
|
|
256
256
|
}
|
|
257
257
|
.quantity-value {
|
|
258
|
-
padding:
|
|
258
|
+
padding: 0.75rem 1rem;
|
|
259
259
|
font-variant-numeric: tabular-nums;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/* --- Modifier free-text input (ProductPurchase.tsx, FREE_TEXT render type) --- */
|
|
263
263
|
.modifier-text {
|
|
264
264
|
width: 100%;
|
|
265
|
-
padding:
|
|
265
|
+
padding: 0.75rem;
|
|
266
266
|
font-family: var(--font-body);
|
|
267
267
|
font-size: 0.875rem;
|
|
268
268
|
border: 1px solid var(--color-rule);
|
|
@@ -290,9 +290,9 @@ body[data-navigating="true"] .product-grid {
|
|
|
290
290
|
.back-in-stock-form {
|
|
291
291
|
display: flex;
|
|
292
292
|
flex-direction: column;
|
|
293
|
-
gap:
|
|
294
|
-
margin-top:
|
|
295
|
-
padding:
|
|
293
|
+
gap: 1rem;
|
|
294
|
+
margin-top: 1rem;
|
|
295
|
+
padding: 1.5rem;
|
|
296
296
|
border: 1px solid var(--color-rule);
|
|
297
297
|
border-radius: var(--radius-md);
|
|
298
298
|
background: var(--color-paper, var(--color-cream));
|
|
@@ -300,7 +300,7 @@ body[data-navigating="true"] .product-grid {
|
|
|
300
300
|
.back-in-stock-headline {
|
|
301
301
|
display: flex;
|
|
302
302
|
flex-direction: column;
|
|
303
|
-
gap:
|
|
303
|
+
gap: 0.75rem;
|
|
304
304
|
}
|
|
305
305
|
.back-in-stock-eyebrow {
|
|
306
306
|
font-family: var(--font-display);
|
|
@@ -326,11 +326,11 @@ body[data-navigating="true"] .product-grid {
|
|
|
326
326
|
display: flex;
|
|
327
327
|
flex-direction: row;
|
|
328
328
|
flex-wrap: wrap;
|
|
329
|
-
gap:
|
|
329
|
+
gap: 0.75rem;
|
|
330
330
|
}
|
|
331
331
|
.back-in-stock-input {
|
|
332
332
|
flex: 1;
|
|
333
|
-
padding:
|
|
333
|
+
padding: 0.75rem 1rem;
|
|
334
334
|
font-family: var(--font-body);
|
|
335
335
|
font-size: 0.875rem;
|
|
336
336
|
border: 1px solid var(--color-rule);
|
|
@@ -345,7 +345,7 @@ body[data-navigating="true"] .product-grid {
|
|
|
345
345
|
border-color: var(--color-ink);
|
|
346
346
|
}
|
|
347
347
|
.back-in-stock-submit {
|
|
348
|
-
padding:
|
|
348
|
+
padding: 0.75rem 1.5rem;
|
|
349
349
|
font-family: var(--font-display);
|
|
350
350
|
font-size: 0.75rem;
|
|
351
351
|
text-transform: uppercase;
|
|
@@ -370,8 +370,8 @@ body[data-navigating="true"] .product-grid {
|
|
|
370
370
|
color: var(--color-error, var(--color-accent));
|
|
371
371
|
}
|
|
372
372
|
.back-in-stock-success {
|
|
373
|
-
margin-top:
|
|
374
|
-
padding:
|
|
373
|
+
margin-top: 1rem;
|
|
374
|
+
padding: 1.5rem;
|
|
375
375
|
border: 1px solid var(--color-rule);
|
|
376
376
|
border-radius: var(--radius-md);
|
|
377
377
|
background: var(--color-paper, var(--color-cream));
|
|
@@ -390,7 +390,7 @@ body[data-navigating="true"] .product-grid {
|
|
|
390
390
|
margin: 0;
|
|
391
391
|
}
|
|
392
392
|
.stock-status.back-in-stock-alt {
|
|
393
|
-
margin-top:
|
|
393
|
+
margin-top: 0.75rem;
|
|
394
394
|
font-family: var(--font-body);
|
|
395
395
|
font-size: 0.875rem;
|
|
396
396
|
font-style: italic;
|
|
@@ -89,14 +89,14 @@ const viewContentPayload = {
|
|
|
89
89
|
hasSeoTags={hasSeoTags}
|
|
90
90
|
>
|
|
91
91
|
{hasSeoTags && <SeoTags slot="head" tags={seoTags} />}
|
|
92
|
-
<main class="py-
|
|
93
|
-
<div class="max-w-wide mx-auto px-
|
|
92
|
+
<main class="py-16">
|
|
93
|
+
<div class="max-w-wide mx-auto px-8">
|
|
94
94
|
<a href="/products" class="text-mute text-sm tracking-wide uppercase">← Back to products</a>
|
|
95
|
-
<div class="grid grid-cols-1 md:grid-cols-[7fr_5fr] gap-
|
|
95
|
+
<div class="grid grid-cols-1 md:grid-cols-[7fr_5fr] gap-12 md:gap-24 mt-8 items-start">
|
|
96
96
|
<div class="aspect-[4/5] bg-cream rounded-sm overflow-hidden">
|
|
97
97
|
{imageUrl && <img src={imageUrl} alt={product.name ?? "Product"} class="w-full h-full object-cover" />}
|
|
98
98
|
</div>
|
|
99
|
-
<div class="flex flex-col gap-
|
|
99
|
+
<div class="flex flex-col gap-6 md:sticky md:top-12">
|
|
100
100
|
<h1 class="font-display text-4xl leading-tight">{product.name}</h1>
|
|
101
101
|
{price && <p class="text-xl text-ink-soft">{price}</p>}
|
|
102
102
|
{description && <p class="text-ink-soft leading-relaxed">{description}</p>}
|
|
@@ -13,27 +13,32 @@ framework, translate the examples** — the SDK calls and payloads are identical
|
|
|
13
13
|
only the UI idiom (`useState`/`useEffect` → a store / `onMounted` / signals) and
|
|
14
14
|
the client acquisition differ (see "Client identity" below).
|
|
15
15
|
|
|
16
|
-
The flow
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
scope.** v1 threads a single service selection — the shared state below holds no
|
|
20
|
-
location / staff / add-on choice yet.
|
|
16
|
+
The flow covers **appointments and classes**. APPOINTMENT and CLASS share the
|
|
17
|
+
same booking sequence; they differ only in the availability call (`availabilityTimeSlots`
|
|
18
|
+
vs `eventTimeSlots`, noted inline). **Courses and add-ons are out of scope.**
|
|
21
19
|
|
|
22
20
|
---
|
|
23
21
|
|
|
24
22
|
## 1. The step model
|
|
25
23
|
|
|
26
|
-
A single customer journey,
|
|
24
|
+
A single customer journey, holding one shared selection. The catalog optionally
|
|
25
|
+
filters by **location** and **category**; the slots step optionally filters by
|
|
26
|
+
**staff**. All three auto-skip when there's nothing to choose (≤1 location, ≤1
|
|
27
|
+
category, ≤1 staff), so a plain single-location single-staff site sees the simple
|
|
28
|
+
five-step path:
|
|
27
29
|
|
|
28
30
|
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
0. Location (optional) pick a location — only shown when the site has >1 locations → scopes the catalog + availability
|
|
32
|
+
1. Catalog list bookable services (optionally filtered by location/category) → pick a service
|
|
33
|
+
2. Slots availability calendar (optionally filtered by staff) → pick a time slot
|
|
34
|
+
3. Details the service's booking form (collect contact) → submit
|
|
32
35
|
4. Book run the SDK sequence (§3)
|
|
33
36
|
5a. paid → redirect to the Wix-hosted checkout → returns to the confirmation page
|
|
34
37
|
5b. free/offline → place the order → confirmation page
|
|
35
38
|
```
|
|
36
39
|
|
|
40
|
+
The location/category filters and the staff picker are covered in §§7–8.
|
|
41
|
+
|
|
37
42
|
**The catalog is the entry point but optional** — a visitor can land directly on a
|
|
38
43
|
service page and start at the Slots step. Every step rehydrates from the URL (§2),
|
|
39
44
|
so deep links work.
|
|
@@ -49,8 +54,8 @@ for SEO and run slots + form + book in a `client:only` island.
|
|
|
49
54
|
Steps 1–4 build up one selection (this is exactly `BookParams` + `SelectedSlot`
|
|
50
55
|
in `../astro/templates/bookings/bookingDriver.ts`):
|
|
51
56
|
|
|
52
|
-
- **`service`** — the chosen service object (the booking step reads `service.payment` + `service.bookingPolicy`).
|
|
53
|
-
- **`slot`** — `{ serviceType, serviceId, localStartDate, localEndDate, timezone, scheduleId?, eventId?, locationId?, locationType? }`.
|
|
57
|
+
- **`service`** — the chosen service object (the booking step reads `service.payment` + `service.bookingPolicy`; the slots step reads `service.staffMemberDetails.staffMembers` for the staff picker — §8).
|
|
58
|
+
- **`slot`** — `{ serviceType, serviceId, localStartDate, localEndDate, timezone, scheduleId?, eventId?, locationId?, locationType?, resource? }`. `resource = { _id, name }` is set only when a specific staff was chosen (§8); otherwise the driver emits the ANY_RESOURCE fallback. `locationId`/`locationType` come from the slot's own `location` (the booking books at that location).
|
|
54
59
|
- **`formSubmission`** — the booking-form values, **keyed by each field's `target`** (§4).
|
|
55
60
|
|
|
56
61
|
Hold it however the framework prefers (React state lifted to a coordinator, a
|
|
@@ -89,11 +94,12 @@ Key facts the driver encodes (do not deviate):
|
|
|
89
94
|
cart holds the seat** instead — `placeOrder` (free/offline) or the hosted checkout
|
|
90
95
|
(paid) drives confirmation — so a client-only site completes the whole flow with no
|
|
91
96
|
server elevation.
|
|
92
|
-
- **ANY_RESOURCE fallback (staff)** —
|
|
93
|
-
|
|
97
|
+
- **ANY_RESOURCE fallback (staff)** — when no staff is chosen (the default, and on
|
|
98
|
+
single-staff services), `createBooking` sends
|
|
99
|
+
`resourceSelections:[{ resourceTypeId:"1cd44cf8-756f-41c3-bd90-3e2ffcaf1155", selectionMethod:"ANY_RESOURCE" }]`
|
|
94
100
|
and Wix auto-assigns a bookable staff resource (appointment slots return
|
|
95
|
-
`availableResources:[]` yet book fine this way).
|
|
96
|
-
|
|
101
|
+
`availableResources:[]` yet book fine this way). A specific staff choice sets
|
|
102
|
+
`slot.resource` instead (§8).
|
|
97
103
|
- **Checkout decision** — `isCheckoutRequired`: if the service's booking policy charges
|
|
98
104
|
a **cancellation fee** (`service.bookingPolicy.cancellationFeePolicy.enabled`) →
|
|
99
105
|
checkout (a card must be on file); else total 0 → place; else `FULL_PAYMENT_OFFLINE`
|
|
@@ -106,9 +112,11 @@ Key facts the driver encodes (do not deviate):
|
|
|
106
112
|
Mirror what the Bookings SDK does (`services.queryServices` is the raw SDK, not a component):
|
|
107
113
|
|
|
108
114
|
```js
|
|
109
|
-
// Catalog (list) — the filter MUST include appId; pass conditionalFields, then .find()
|
|
115
|
+
// Catalog (list) — the filter MUST include appId; pass conditionalFields, then .find().
|
|
116
|
+
// Merge the optional category/location filters into the SAME filter object (§7):
|
|
110
117
|
services.queryServices({
|
|
111
|
-
query: { filter: { appId: BOOKING_APP_ID
|
|
118
|
+
query: { filter: { appId: BOOKING_APP_ID /* , "category.id": { $eq: catId },
|
|
119
|
+
"locations.business.id": { $hasSome: [locId] } */ }, paging: { limit: 100 } },
|
|
112
120
|
conditionalFields: ["STAFF_MEMBER_DETAILS"],
|
|
113
121
|
}).find(); // → result.items ; filter out s.hidden
|
|
114
122
|
|
|
@@ -116,13 +124,21 @@ services.queryServices({
|
|
|
116
124
|
services.queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
|
|
117
125
|
.eq("mainSlug.name", slug).eq("appId", BOOKING_APP_ID).limit(1).find();
|
|
118
126
|
|
|
119
|
-
// Availability (APPOINTMENT) — serviceId is a single GUID STRING
|
|
127
|
+
// Availability (APPOINTMENT) — serviceId is a single GUID STRING. The staff (§8)
|
|
128
|
+
// and location filters are OPTIONAL params on the same call:
|
|
120
129
|
availabilityTimeSlots.listAvailabilityTimeSlots({
|
|
121
130
|
serviceId, fromLocalDate, toLocalDate, timeZone, bookable: true, cursorPaging: { limit: 100 },
|
|
131
|
+
// staff: resourceTypes: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [staffResourceId] }],
|
|
132
|
+
// includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID],
|
|
133
|
+
// location: locations: [{ _id: locationId, locationType: "BUSINESS" }],
|
|
122
134
|
}); // slots: result.timeSlots[] (localStartDate/localEndDate/scheduleId at top level)
|
|
123
135
|
|
|
124
|
-
// Availability (CLASS) — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, no scheduleId
|
|
125
|
-
|
|
136
|
+
// Availability (CLASS) — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, no scheduleId.
|
|
137
|
+
// CLASS filters staff + location via eventFilter instead:
|
|
138
|
+
eventTimeSlots.listEventTimeSlots({
|
|
139
|
+
serviceIds: [serviceId], fromLocalDate, toLocalDate, timeZone, includeNonBookable: false,
|
|
140
|
+
// eventFilter: { "resources.id": { $hasSome: [staffResourceId] }, "location.id": [locationId] },
|
|
141
|
+
});
|
|
126
142
|
```
|
|
127
143
|
|
|
128
144
|
`fromLocalDate`/`toLocalDate` are **local** date strings `YYYY-MM-DDThh:mm:ss`
|
|
@@ -157,14 +173,16 @@ The booking form is a **`@wix/forms` form** attached to the service
|
|
|
157
173
|
`target`. The default booking form's targets are snake_case `first_name` /
|
|
158
174
|
`last_name` / `email` / `phone`. Reference example: `../astro/templates/bookings/BookingForm.tsx`.
|
|
159
175
|
|
|
160
|
-
## 5. The
|
|
176
|
+
## 5. The availability calendar (slots step)
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
**Group slots by day — don't render a flat list of every slot with time-only
|
|
179
|
+
labels** (the visitor can't tell which day a slot is on; that flat grid was the
|
|
180
|
+
original usability failure). The calendar **shape is your / the brand's choice** —
|
|
181
|
+
week, month, or N-day. The reference example uses a **week view** (a 7-day strip
|
|
182
|
+
with week navigation → the picked day's times); fetch availability for the visible
|
|
183
|
+
window (`fromLocalDate`/`toLocalDate` = that window's bounds), group by calendar
|
|
184
|
+
day, and offer a **"check next availability"** action that probes forward when the
|
|
185
|
+
window is empty. Reference example:
|
|
168
186
|
`../astro/templates/bookings/AvailabilityCalendar.tsx`.
|
|
169
187
|
|
|
170
188
|
## 6. Client identity (per framework)
|
|
@@ -178,8 +196,51 @@ The SDK calls are identical everywhere; only how you get the client differs:
|
|
|
178
196
|
`createClient({ modules, auth: OAuthStrategy({ clientId: appId }) })` and call
|
|
179
197
|
the same functions on it. CDN imports for `none`, bundled for `own`.
|
|
180
198
|
|
|
181
|
-
## 7.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
## 7. Catalog filters — location & category (both auto-skip)
|
|
200
|
+
|
|
201
|
+
Both filter the **catalog query** and re-render the list. The cleanest
|
|
202
|
+
framework-agnostic shape is a re-query driven by a query param (`?locationId`,
|
|
203
|
+
`?category`) — links/SSR on a static catalog, or a store/router on a SPA. **Show
|
|
204
|
+
each filter only when there is more than one choice**, exactly as the SoT does.
|
|
205
|
+
|
|
206
|
+
**Location** (the SoT is location-first; here it auto-skips ≤1 location):
|
|
207
|
+
1. `services.queryLocations()` → `{ businessLocations: { locations: [{ _id, type, business: { name } }] }, customLocations: { exists }, customerLocations: { exists } }`.
|
|
208
|
+
2. Count = `businessLocations.locations.length + (customLocations.exists ? 1 : 0) + (customerLocations.exists ? 1 : 0)`. **Show the selector only when count > 1.**
|
|
209
|
+
3. The chosen location filters the catalog query:
|
|
210
|
+
- a real business id → `filter["locations.business.id"] = { $hasSome: [id] }`;
|
|
211
|
+
- the synthetic `"custom"` / `"customer"` → `filter["locations.type"] = { $hasSome: ["CUSTOM"|"CUSTOMER"] }` (the SoT's synthetic-id mapping).
|
|
212
|
+
4. **Scope availability to exactly ONE location — always, on a multi-location service.** `listAvailabilityTimeSlots` returns **one slot per location** per time ("if `locations` is not specified, returns time slots for all locations where the service is available"), so an unscoped call on a 2-location service shows every time **twice**. Pass a single-element filter: APPOINTMENT → `locations: [{ _id, locationType: "BUSINESS" }]`; CLASS → `eventFilter: { "location.id": [id] }`. (Staff does **not** multiply rows — one slot carries many `availableResources`; only location does.) The slots step builds its location list from **`queryLocations()`** (the site's real business locations, whose ids the availability engine recognizes) **intersected with the service's own location ids** — not from `service.locations` alone, whose entries can carry an id the availability engine doesn't recognize (scoping to it returns zero slots). A **location picker** defaults to the catalog-carried location (or the first) and scopes the call when there's a real location to scope to; when the site has no business locations the list is empty and the call stays unscoped (one location → no duplicates). The booked slot carries its own `location`, so the booking books at that location (the driver maps it).
|
|
213
|
+
|
|
214
|
+
**Category:**
|
|
215
|
+
1. `categoriesV2.queryCategories().find()` → `result.items: [{ _id, name }]`. **Non-fatal** — if it fails, render the catalog without the bar.
|
|
216
|
+
2. **Show the bar only when `items.length > 1`** (a fresh site has one default category).
|
|
217
|
+
3. The chosen category filters the catalog query: `filter["category.id"] = { $eq: categoryId }`. A service carries its category at `service.category._id`.
|
|
218
|
+
|
|
219
|
+
Reference example: `../astro/templates/bookings/services/index.astro` (both filters, link-driven, auto-skipping).
|
|
220
|
+
|
|
221
|
+
## 8. Staff / resource selection (auto-skips ≤1 staff)
|
|
222
|
+
|
|
223
|
+
A service carries its staff on the service object via the **`STAFF_MEMBER_DETAILS`
|
|
224
|
+
conditional field** (already requested in the catalog/detail query):
|
|
225
|
+
`service.staffMemberDetails.staffMembers: [{ staffMemberId, name }]`. The id used
|
|
226
|
+
for filtering and booking is **`staffMemberId`** — which, **despite the name, IS the
|
|
227
|
+
resource GUID** (it matches the service's `staffMemberIds` and the Staff Members API
|
|
228
|
+
`resourceId`). Below, `resourceId` is that value.
|
|
229
|
+
|
|
230
|
+
1. Show a "Any staff member / pick one" control above the calendar **only when
|
|
231
|
+
there is more than one staff**.
|
|
232
|
+
2. Selecting a staff re-fetches availability filtered to that resource:
|
|
233
|
+
- APPOINTMENT → `resourceTypes: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [resourceId] }]` (plus `includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID]`);
|
|
234
|
+
- CLASS → `eventFilter: { "resources.id": { $hasSome: [resourceId] } }`.
|
|
235
|
+
3. When a slot is picked **with a specific staff selected**, set `slot.resource = { _id: resourceId, name }`. The driver books that resource. With **"any staff"**, leave `resource` unset — the driver emits the **ANY_RESOURCE fallback** and Wix auto-assigns a bookable resource (appointment slots can return `availableResources: []` yet book fine this way).
|
|
236
|
+
|
|
237
|
+
`STAFF_MEMBER_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155"` (exported from `bookingDriver.ts`). Reference example: `../astro/templates/bookings/AvailabilityCalendar.tsx` (the staff `<select>` + the filtered fetch).
|
|
238
|
+
|
|
239
|
+
## 9. Out of scope
|
|
240
|
+
|
|
241
|
+
Waitlist, on-site manage/cancel, payment/deposit breakdown, and multi-service /
|
|
242
|
+
day-range are out of scope. (Waitlist and manage/cancel have **no headless SoT** —
|
|
243
|
+
neither the components nor the vibe plugin implement join/cancel logic, only
|
|
244
|
+
display-only policy flags — so they are deliberately not built here.) Show
|
|
245
|
+
bookable slots only; post-booking self-service is handled by the Wix-hosted flow /
|
|
246
|
+
member area.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bookings-implementer
|
|
3
|
-
description: "Implements the Wix Bookings vertical — services catalog, a service detail page with a week-calendar availability picker, a schema-driven booking form, and confirmation. The booking runs client-side via the @wix SDK (ecom Cart V2 — no confirmBooking). Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
|
|
3
|
+
description: "Implements the Wix Bookings vertical — services catalog (with optional location + category filters), a service detail page with a week-calendar availability picker (with optional staff selection), a schema-driven booking form, and confirmation, for appointments and classes. The booking runs client-side via the @wix SDK (ecom Cart V2 — no confirmBooking). Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Bookings Implementer
|
|
@@ -82,9 +82,14 @@ Bookings-specific CSS lives in `src/styles/components-bookings.css` (pre-copied
|
|
|
82
82
|
| Pass UTC ISO strings to `listAvailabilityTimeSlots` | Use **local** date strings `YYYY-MM-DDThh:mm:ss` (no `Z`) + a separate `timeZone`. |
|
|
83
83
|
| `availability.queryAvailability` / `import { availability }` | APPOINTMENT → `availabilityTimeSlots.listAvailabilityTimeSlots({ serviceId: <string> })`; CLASS → `eventTimeSlots.listEventTimeSlots({ serviceIds: [<string>] })`. The `availability` namespace does not exist. |
|
|
84
84
|
| Read slot time from `timeSlot.slot.startDate` | Slot fields are at the **top level**: `localStartDate`, `localEndDate`, `scheduleId` (APPOINTMENT). CLASS slots carry `eventInfo.eventId` and have **no** `scheduleId`. |
|
|
85
|
-
| Dump every slot in one flat, time-only grid |
|
|
85
|
+
| Dump every slot in one flat, time-only grid | **Group slots by day** — a flat time-only grid hides which day a slot is on. The calendar shape (week / month / N-day) is your choice; the example uses a week strip → the picked day's times. |
|
|
86
|
+
| Read the staff id from `staffMember._id` / `.resourceId` on `staffMemberDetails.staffMembers` | The field is **`staffMemberId`** (on `service.staffMemberDetails.staffMembers[]`) — despite the name it IS the resource GUID (matches the service's `staffMemberIds`). Filter availability: APPOINTMENT `resourceTypes:[{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds:[staffMemberId] }]`; CLASS `eventFilter:{ "resources.id": { $hasSome:[staffMemberId] } }`. Record the choice as `slot.resource = { _id: staffMemberId, name }`; leave it unset for "any staff" (ANY_RESOURCE fallback). Show the picker only when >1 staff. |
|
|
87
|
+
| Always show the location selector / staff picker / category bar | **Auto-skip** each when there's nothing to choose: location count ≤1 (`businessLocations.length + custom?.exists + customer?.exists`), staff ≤1, categories ≤1. Single-location single-staff sites see the simple flow. |
|
|
88
|
+
| Filter the catalog by a synthetic location id, or hardcode the location filter | Real business id → `filter["locations.business.id"] = { $hasSome:[id] }`; synthetic `"custom"`/`"customer"` → `filter["locations.type"] = { $hasSome:["CUSTOM"|"CUSTOMER"] }`. Carry the choice to the detail page so availability is scoped too. |
|
|
89
|
+
| Call `listAvailabilityTimeSlots` **without** a `locations` filter on a multi-location service | It returns **one slot per location** per time → every time appears **N×** (duplicated rows). **Always scope availability to one business location** (`locations: [{ _id, locationType:"BUSINESS" }]`; CLASS: `eventFilter:{ "location.id":[id] }`). Build the calendar's location list from **`queryLocations()`** (the site's real business locations) intersected with the service's location ids — NOT from `service.locations` alone (its entries can carry an id the availability engine doesn't recognize → scoping to it returns 0 slots even though the service has availability). Default to the carried/first, picker when >1; no business locations → unscoped. Staff does **not** cause duplicates — one slot carries many `availableResources`; only location multiplies rows. |
|
|
90
|
+
| Let a `queryCategories` failure break the catalog | Categories are **non-fatal** — wrap in try/catch and render the catalog without the bar. Category filter: `filter["category.id"] = { $eq: categoryId }`; a service's category is `service.category._id`. |
|
|
86
91
|
| Read service slug from `service.slug` / name from `service.info.name` | V2 is flat: slug `service.mainSlug.name` (fallback `supportedSlugs[0].name`), `service.name`, `service.description`, `service.tagLine`; duration `service.schedule.availabilityConstraints.sessionDurations[0]`; price `service.payment.fixed.price.value` (string); media `service.media.mainMedia.image.url`. |
|
|
87
92
|
| Use `service.id` / `result.booking.id` | Entity ids are `_id` (underscore): `service._id`, `booking._id`. |
|
|
88
93
|
| Mount the calendar/form without `client:only="react"` | Availability + booking are timezone/session-specific — always `client:only="react"`. SSR only the read pages (catalog/detail) for SEO. |
|
|
89
94
|
| Omit try/catch on the booking | `createBooking` can reject (e.g. a slot taken between fetch and submit, or strict phone validation). Catch and surface a friendly message; don't crash. |
|
|
90
|
-
| Ship a
|
|
95
|
+
| Ship a payment/deposit breakdown, multi-service stacking, waitlist, or on-site manage/cancel | Out of scope. **Waitlist and manage/cancel have no headless SoT** (the components + vibe plugin expose only display-only policy flags — no join/cancel logic) — do not invent them on the REST/anonymous-token layer. Post-booking self-service is handled by the Wix-hosted flow / member area. |
|
|
@@ -93,6 +93,66 @@ curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H
|
|
|
93
93
|
|
|
94
94
|
---
|
|
95
95
|
|
|
96
|
+
## Step 3b — Create categories (OPTIONAL — only to surface the catalog category filter)
|
|
97
|
+
|
|
98
|
+
A fresh Bookings install has **one** default category ("Our Services"), so the
|
|
99
|
+
front-end's category filter bar stays hidden (it shows only when >1 category
|
|
100
|
+
exists). Create extra categories **only when the build wants a category filter**
|
|
101
|
+
(e.g. `intent.bookings.categories` is set, or several distinct service groups).
|
|
102
|
+
Skip this step otherwise — it's not needed for a basic seed.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Create a category. Returns { category: { id, ... } } — save the id.
|
|
106
|
+
curl -sS -X POST \
|
|
107
|
+
-H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H "Content-Type: application/json" \
|
|
108
|
+
-d '{ "category": { "name": "<Category name, e.g. Massage>" } }' \
|
|
109
|
+
"https://www.wixapis.com/bookings/v2/categories"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then **assign each service to a category** by including `category` in the Step-4
|
|
113
|
+
create payload: `"category": { "id": "<categoryId>" }`. (If a create rejects
|
|
114
|
+
`category`, assign it after creation via Update Service — `PATCH
|
|
115
|
+
/_api/bookings/v2/services/{id}` with `service.category.id` + the current
|
|
116
|
+
`revision` — per the docs' "create a category and connect a service" flow.) A
|
|
117
|
+
service can belong to **one** category. Distribute services across 2+ categories
|
|
118
|
+
so the filter is meaningful.
|
|
119
|
+
|
|
120
|
+
> The front-end reads categories via `categoriesV2.queryCategories().find()` and
|
|
121
|
+
> filters services with `filter["category.id"] = { $eq: id }`. No category data →
|
|
122
|
+
> no filter bar (graceful).
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Step 3c — Multiple business locations (OPTIONAL — only for a multi-location demo)
|
|
127
|
+
|
|
128
|
+
The front-end's location selector auto-skips when the site has ≤1 location, so a
|
|
129
|
+
single-location seed needs nothing here (and the booking flow is unaffected).
|
|
130
|
+
Create a second business location **only** for a multi-location build
|
|
131
|
+
(`intent.bookings.multiLocation`):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Create a business location. Returns { location: { id, ... } }.
|
|
135
|
+
curl -sS -X POST \
|
|
136
|
+
-H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H "Content-Type: application/json" \
|
|
137
|
+
-d '{ "location": { "name": "<Branch name>", "status": "ACTIVE", "locationType": "BUSINESS",
|
|
138
|
+
"address": { "city": "<City>", "country": "<ISO country>" } } }' \
|
|
139
|
+
"https://www.wixapis.com/locations/v1/locations"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then attach locations to each service's `locations` array on create (Step 4):
|
|
143
|
+
`"locations": [ { "type": "BUSINESS", "business": { "id": "<locationId>" } }, … ]`.
|
|
144
|
+
|
|
145
|
+
> **Caveat (verified-behavior limitation, not a bug):** filtering *availability*
|
|
146
|
+
> by location also depends on each staff member's **working-hours location
|
|
147
|
+
> coverage** (the `STAFF_WORKING_HOURS_LOCATIONS` setting). Seeding that reliably
|
|
148
|
+
> is as fragile as custom working hours (see Step 3's warning), so for a seed,
|
|
149
|
+
> create the location + attach it to services to exercise the **catalog** location
|
|
150
|
+
> filter, and configure per-location staff hours from the Bookings dashboard if you
|
|
151
|
+
> need per-location availability to differ. The selector + service filtering work
|
|
152
|
+
> from the seed alone; per-location *slot* differences need the dashboard step.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
96
156
|
## Step 4 — Create services (Services V2 API)
|
|
97
157
|
|
|
98
158
|
**Endpoint:** `POST https://www.wixapis.com/_api/bookings/v2/services`
|