astro-tractstack 2.3.2 → 2.3.4
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/bin/create-tractstack.js +7 -4
- package/dist/index.js +51 -8
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +279 -118
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +328 -65
- package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
- package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
- package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
- package/templates/custom/shopify/cart.astro +7 -1
- package/templates/src/components/Header.astro +4 -2
- package/templates/src/components/compositor/Node.tsx +39 -9
- package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
- package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/form/advanced/APIConfigSection.tsx +249 -4
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
- package/templates/src/constants.ts +2 -0
- package/templates/src/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- package/templates/src/pages/api/google/oauth/callback.ts +50 -0
- package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
- package/templates/src/pages/api/google/oauth/start.ts +32 -0
- package/templates/src/pages/api/google/oauth/status.ts +32 -0
- package/templates/src/pages/api/sales/list.ts +66 -0
- package/templates/src/pages/api/sales/metrics.ts +60 -0
- package/templates/src/pages/context/[...contextSlug].astro +50 -31
- package/templates/src/pages/privacy.astro +84 -0
- package/templates/src/pages/storykeep/advanced.astro +4 -1
- package/templates/src/pages/terms.astro +47 -0
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/stores/shopify.ts +5 -0
- package/templates/src/types/tractstack.ts +87 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +20 -0
- package/templates/src/utils/api/bookingHelpers.ts +3 -1
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +14 -1
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/templates/src/utils/booking/appointmentMode.ts +135 -0
- package/templates/src/utils/customHelpers.ts +287 -2
- package/utils/inject-files.ts +47 -4
- package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
- package/templates/src/utils/actions/actionButton.ts +0 -103
- package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
type FormEvent,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import { ulid } from 'ulid';
|
|
3
10
|
import { useStore } from '@nanostores/react';
|
|
4
11
|
import { Dialog } from '@ark-ui/react/dialog';
|
|
@@ -9,12 +16,26 @@ import {
|
|
|
9
16
|
customerDetails,
|
|
10
17
|
setCustomerDetails,
|
|
11
18
|
CART_STATES,
|
|
19
|
+
preferredAppointmentMode,
|
|
12
20
|
transactionTraceId,
|
|
13
21
|
isShopifyHandoff,
|
|
14
22
|
} from '@/stores/shopify';
|
|
15
23
|
import { bookingHelpers } from '@/utils/api/bookingHelpers';
|
|
24
|
+
import {
|
|
25
|
+
deriveAppointmentConstraints,
|
|
26
|
+
pickInitialAppointmentMode,
|
|
27
|
+
} from '@/utils/booking/appointmentMode';
|
|
16
28
|
import { NativeBookingCalendar } from './NativeBookingCalendar';
|
|
17
29
|
import { ProfileStorage } from '@/utils/profileStorage';
|
|
30
|
+
import {
|
|
31
|
+
buildShopifyCheckoutLines,
|
|
32
|
+
getServiceLinkedProduct,
|
|
33
|
+
getServiceVariantIdFromCanonicalProduct,
|
|
34
|
+
getSharedFeeChargeLineSummary,
|
|
35
|
+
hasGidBackedCheckout,
|
|
36
|
+
isSharedFeeService,
|
|
37
|
+
parsePrimaryShopifyProductData,
|
|
38
|
+
} from '@/utils/customHelpers';
|
|
18
39
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
19
40
|
|
|
20
41
|
type CheckoutState =
|
|
@@ -24,14 +45,19 @@ type CheckoutState =
|
|
|
24
45
|
| 'SUMMARY'
|
|
25
46
|
| 'PROCESSING'
|
|
26
47
|
| 'SUCCESS';
|
|
48
|
+
type AppointmentMode = 'IN_PERSON' | 'REMOTE';
|
|
27
49
|
|
|
28
50
|
interface CheckoutModalProps {
|
|
29
51
|
maxLength: number;
|
|
52
|
+
allowRemote?: boolean;
|
|
53
|
+
remoteOnly?: boolean;
|
|
30
54
|
resources?: ResourceNode[];
|
|
31
55
|
}
|
|
32
56
|
|
|
33
57
|
export default function CheckoutModal({
|
|
34
58
|
maxLength,
|
|
59
|
+
allowRemote = false,
|
|
60
|
+
remoteOnly = false,
|
|
35
61
|
resources = [],
|
|
36
62
|
}: CheckoutModalProps) {
|
|
37
63
|
const $globalCartState = useStore(cartState);
|
|
@@ -57,6 +83,24 @@ export default function CheckoutModal({
|
|
|
57
83
|
start: Date;
|
|
58
84
|
end: Date;
|
|
59
85
|
} | null>(null);
|
|
86
|
+
const [appointmentMode, setAppointmentMode] = useState<AppointmentMode>(() =>
|
|
87
|
+
pickInitialAppointmentMode(
|
|
88
|
+
deriveAppointmentConstraints(cartStore.get(), resources, {
|
|
89
|
+
allowRemote,
|
|
90
|
+
remoteOnly,
|
|
91
|
+
}),
|
|
92
|
+
preferredAppointmentMode.get()
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
const appointmentModeRef = useRef<AppointmentMode>(
|
|
96
|
+
pickInitialAppointmentMode(
|
|
97
|
+
deriveAppointmentConstraints(cartStore.get(), resources, {
|
|
98
|
+
allowRemote,
|
|
99
|
+
remoteOnly,
|
|
100
|
+
}),
|
|
101
|
+
preferredAppointmentMode.get()
|
|
102
|
+
)
|
|
103
|
+
);
|
|
60
104
|
const [error, setError] = useState<string | null>(null);
|
|
61
105
|
|
|
62
106
|
const isOpen =
|
|
@@ -67,21 +111,36 @@ export default function CheckoutModal({
|
|
|
67
111
|
const enrichedCart = useMemo(() => {
|
|
68
112
|
return Object.values($cartItems).map((item: any) => {
|
|
69
113
|
const resource = resources.find((r) => r.id === item.resourceId);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
114
|
+
const productResources = resources.filter(
|
|
115
|
+
(r) => r.categorySlug === 'product'
|
|
116
|
+
);
|
|
117
|
+
const priceSource =
|
|
118
|
+
resource?.categorySlug === 'service'
|
|
119
|
+
? getServiceLinkedProduct(resource, resources) || resource
|
|
120
|
+
: resource;
|
|
121
|
+
const productData = parsePrimaryShopifyProductData(priceSource) || {};
|
|
122
|
+
const fallbackVariantId =
|
|
123
|
+
resource?.categorySlug === 'service'
|
|
124
|
+
? getServiceVariantIdFromCanonicalProduct(resource, resources)
|
|
125
|
+
: undefined;
|
|
126
|
+
const resolvedVariantId = item.variantId || fallbackVariantId;
|
|
78
127
|
const variant = (productData?.variants || []).find(
|
|
79
|
-
(v: any) => v.id ===
|
|
128
|
+
(v: any) => v.id === resolvedVariantId
|
|
80
129
|
);
|
|
130
|
+
const sharedFeeService =
|
|
131
|
+
resource?.categorySlug === 'service' &&
|
|
132
|
+
isSharedFeeService(resource, productResources);
|
|
133
|
+
const resolvedTitle =
|
|
134
|
+
resource?.categorySlug === 'service'
|
|
135
|
+
? resource.title
|
|
136
|
+
: productData?.title || resource?.title || 'Loading...';
|
|
81
137
|
return {
|
|
82
138
|
...item,
|
|
83
|
-
title:
|
|
139
|
+
title: resolvedTitle,
|
|
84
140
|
price: variant?.price?.amount || '0.00',
|
|
141
|
+
currencyCode: variant?.price?.currencyCode || 'USD',
|
|
142
|
+
sharedFeeService,
|
|
143
|
+
resourceNode: resource,
|
|
85
144
|
resource: {
|
|
86
145
|
id: item.resourceId,
|
|
87
146
|
needsBooking:
|
|
@@ -98,9 +157,18 @@ export default function CheckoutModal({
|
|
|
98
157
|
() => enrichedCart.some((item) => item.resource?.needsBooking),
|
|
99
158
|
[enrichedCart]
|
|
100
159
|
);
|
|
160
|
+
const checkoutLines = useMemo(
|
|
161
|
+
() => buildShopifyCheckoutLines($cartItems, resources),
|
|
162
|
+
[$cartItems, resources]
|
|
163
|
+
);
|
|
164
|
+
const sharedFeeChargeLine = useMemo(
|
|
165
|
+
() => getSharedFeeChargeLineSummary($cartItems, resources),
|
|
166
|
+
[$cartItems, resources]
|
|
167
|
+
);
|
|
101
168
|
const needsPayment = useMemo(
|
|
102
|
-
() =>
|
|
103
|
-
|
|
169
|
+
() =>
|
|
170
|
+
hasGidBackedCheckout($cartItems, resources) || checkoutLines.length > 0,
|
|
171
|
+
[$cartItems, resources, checkoutLines]
|
|
104
172
|
);
|
|
105
173
|
const totalDuration = useMemo(() => {
|
|
106
174
|
const rawMinutes = enrichedCart.reduce(
|
|
@@ -111,6 +179,51 @@ export default function CheckoutModal({
|
|
|
111
179
|
return Math.min(Math.ceil(rawMinutes / 15) * 15, maxLength);
|
|
112
180
|
}, [enrichedCart]);
|
|
113
181
|
|
|
182
|
+
const appointmentConstraints = useMemo(
|
|
183
|
+
() =>
|
|
184
|
+
deriveAppointmentConstraints($cartItems, resources, {
|
|
185
|
+
allowRemote,
|
|
186
|
+
remoteOnly,
|
|
187
|
+
}),
|
|
188
|
+
[$cartItems, resources, allowRemote, remoteOnly]
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const { effectiveRemoteOnly, inPersonAvailable } = appointmentConstraints;
|
|
192
|
+
|
|
193
|
+
const remoteAvailable =
|
|
194
|
+
appointmentConstraints.effectiveRemoteOnly ||
|
|
195
|
+
(needsBooking &&
|
|
196
|
+
appointmentConstraints.effectiveAllowRemote &&
|
|
197
|
+
appointmentConstraints.serviceResources.length > 0 &&
|
|
198
|
+
appointmentConstraints.allServicesAllowRemote);
|
|
199
|
+
|
|
200
|
+
const applyAppointmentMode = useCallback((nextMode: AppointmentMode) => {
|
|
201
|
+
appointmentModeRef.current = nextMode;
|
|
202
|
+
setAppointmentMode((currentMode) =>
|
|
203
|
+
currentMode === nextMode ? currentMode : nextMode
|
|
204
|
+
);
|
|
205
|
+
preferredAppointmentMode.set(nextMode);
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (selectedSlot) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (effectiveRemoteOnly) {
|
|
213
|
+
applyAppointmentMode('REMOTE');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!remoteAvailable && appointmentMode === 'REMOTE') {
|
|
217
|
+
applyAppointmentMode('IN_PERSON');
|
|
218
|
+
}
|
|
219
|
+
}, [
|
|
220
|
+
effectiveRemoteOnly,
|
|
221
|
+
remoteAvailable,
|
|
222
|
+
appointmentMode,
|
|
223
|
+
selectedSlot,
|
|
224
|
+
applyAppointmentMode,
|
|
225
|
+
]);
|
|
226
|
+
|
|
114
227
|
useEffect(() => {
|
|
115
228
|
const profile = ProfileStorage.getProfileData();
|
|
116
229
|
if (ProfileStorage.isProfileUnlocked() && profile.email) {
|
|
@@ -120,6 +233,21 @@ export default function CheckoutModal({
|
|
|
120
233
|
}
|
|
121
234
|
}, [ProfileStorage.isProfileUnlocked(), $customer.email]);
|
|
122
235
|
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (needsBooking) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (selectedSlot) {
|
|
241
|
+
setSelectedSlot(null);
|
|
242
|
+
}
|
|
243
|
+
if (shopTimeZone) {
|
|
244
|
+
setShopTimeZone(undefined);
|
|
245
|
+
}
|
|
246
|
+
if (internalState === 'BOOKING') {
|
|
247
|
+
setInternalState('SUMMARY');
|
|
248
|
+
}
|
|
249
|
+
}, [needsBooking, selectedSlot, shopTimeZone, internalState]);
|
|
250
|
+
|
|
123
251
|
useEffect(() => {
|
|
124
252
|
if (
|
|
125
253
|
$globalCartState !== CART_STATES.CHECKOUT ||
|
|
@@ -146,6 +274,36 @@ export default function CheckoutModal({
|
|
|
146
274
|
}
|
|
147
275
|
}, [$globalCartState, needsBooking, $customer.leadId, internalState]);
|
|
148
276
|
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (!isOpen || selectedSlot) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
internalState !== 'IDENTITY_EMAIL' &&
|
|
284
|
+
internalState !== 'IDENTITY_NEW_USER'
|
|
285
|
+
) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (effectiveRemoteOnly) {
|
|
290
|
+
applyAppointmentMode('REMOTE');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const next = pickInitialAppointmentMode(
|
|
295
|
+
appointmentConstraints,
|
|
296
|
+
preferredAppointmentMode.get()
|
|
297
|
+
);
|
|
298
|
+
applyAppointmentMode(next);
|
|
299
|
+
}, [
|
|
300
|
+
isOpen,
|
|
301
|
+
selectedSlot,
|
|
302
|
+
internalState,
|
|
303
|
+
appointmentConstraints,
|
|
304
|
+
applyAppointmentMode,
|
|
305
|
+
]);
|
|
306
|
+
|
|
149
307
|
const handleClose = async () => {
|
|
150
308
|
const redirect = internalState === 'SUCCESS';
|
|
151
309
|
const currentTraceId = transactionTraceId.get();
|
|
@@ -163,6 +321,8 @@ export default function CheckoutModal({
|
|
|
163
321
|
transactionTraceId.set('');
|
|
164
322
|
cartState.set(CART_STATES.READY);
|
|
165
323
|
setInternalState('IDENTITY_EMAIL');
|
|
324
|
+
setSelectedSlot(null);
|
|
325
|
+
setShopTimeZone(undefined);
|
|
166
326
|
setError(null);
|
|
167
327
|
if (redirect) window.location.href = `/`;
|
|
168
328
|
};
|
|
@@ -249,7 +409,8 @@ export default function CheckoutModal({
|
|
|
249
409
|
transactionTraceId.get(),
|
|
250
410
|
start.toISOString(),
|
|
251
411
|
end.toISOString(),
|
|
252
|
-
cartResourceIds
|
|
412
|
+
cartResourceIds,
|
|
413
|
+
appointmentModeRef.current
|
|
253
414
|
);
|
|
254
415
|
if (response && (response.success || response.status === 'PENDING')) {
|
|
255
416
|
setInternalState('SUMMARY');
|
|
@@ -293,6 +454,8 @@ export default function CheckoutModal({
|
|
|
293
454
|
);
|
|
294
455
|
if (response && response.success) {
|
|
295
456
|
cartStore.set({});
|
|
457
|
+
setSelectedSlot(null);
|
|
458
|
+
setShopTimeZone(undefined);
|
|
296
459
|
setInternalState('SUCCESS');
|
|
297
460
|
} else {
|
|
298
461
|
setError(response?.error || 'Failed to confirm booking.');
|
|
@@ -308,46 +471,43 @@ export default function CheckoutModal({
|
|
|
308
471
|
setError(null);
|
|
309
472
|
setInternalState('PROCESSING');
|
|
310
473
|
try {
|
|
474
|
+
const lines = buildShopifyCheckoutLines(cartStore.get(), resources);
|
|
475
|
+
if (lines.length === 0) {
|
|
476
|
+
throw new Error('No checkout lines available.');
|
|
477
|
+
}
|
|
311
478
|
const response = await fetch('/api/shopify/createCart', {
|
|
312
479
|
method: 'POST',
|
|
313
480
|
headers: { 'Content-Type': 'application/json' },
|
|
314
481
|
body: JSON.stringify({
|
|
315
|
-
lines
|
|
316
|
-
.filter((i) => i.variantId)
|
|
317
|
-
.map((i) => ({
|
|
318
|
-
merchandiseId: i.variantId,
|
|
319
|
-
quantity: i.quantity || 1,
|
|
320
|
-
})),
|
|
482
|
+
lines,
|
|
321
483
|
email: $customer.email,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
: {}),
|
|
484
|
+
attributes: [
|
|
485
|
+
{ key: 'bookingId', value: transactionTraceId.get() },
|
|
486
|
+
{ key: 'leadId', value: $customer.leadId },
|
|
487
|
+
...(needsBooking && selectedSlot
|
|
488
|
+
? [
|
|
489
|
+
{
|
|
490
|
+
key: 'Appointment Date',
|
|
491
|
+
value: selectedSlot.start.toLocaleDateString('en-US', {
|
|
492
|
+
timeZone: shopTimeZone,
|
|
493
|
+
}),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
key: 'Appointment Time',
|
|
497
|
+
value: selectedSlot.start.toLocaleTimeString('en-US', {
|
|
498
|
+
hour: '2-digit',
|
|
499
|
+
minute: '2-digit',
|
|
500
|
+
timeZone: shopTimeZone,
|
|
501
|
+
}),
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
key: 'Appointment Mode',
|
|
505
|
+
value:
|
|
506
|
+
appointmentMode === 'REMOTE' ? 'Remote' : 'In Person',
|
|
507
|
+
},
|
|
508
|
+
]
|
|
509
|
+
: []),
|
|
510
|
+
],
|
|
351
511
|
}),
|
|
352
512
|
});
|
|
353
513
|
const result = await response.json();
|
|
@@ -357,6 +517,8 @@ export default function CheckoutModal({
|
|
|
357
517
|
isShopifyHandoff.set(true);
|
|
358
518
|
cartStore.set({});
|
|
359
519
|
cartState.set(CART_STATES.READY);
|
|
520
|
+
setSelectedSlot(null);
|
|
521
|
+
setShopTimeZone(undefined);
|
|
360
522
|
window.location.href = result.checkoutUrl;
|
|
361
523
|
} else {
|
|
362
524
|
throw new Error('No checkout URL');
|
|
@@ -462,36 +624,137 @@ export default function CheckoutModal({
|
|
|
462
624
|
</form>
|
|
463
625
|
)}
|
|
464
626
|
{internalState === 'BOOKING' && (
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
627
|
+
<div className="space-y-6">
|
|
628
|
+
{needsBooking && (inPersonAvailable || remoteAvailable) && (
|
|
629
|
+
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
|
630
|
+
<p className="text-xs font-bold uppercase tracking-wide text-gray-500">
|
|
631
|
+
Appointment Mode
|
|
632
|
+
</p>
|
|
633
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
634
|
+
{inPersonAvailable && (
|
|
635
|
+
<button
|
|
636
|
+
type="button"
|
|
637
|
+
onClick={() => {
|
|
638
|
+
applyAppointmentMode('IN_PERSON');
|
|
639
|
+
}}
|
|
640
|
+
className={`rounded-md px-3 py-2 text-sm font-bold ${
|
|
641
|
+
appointmentMode === 'IN_PERSON'
|
|
642
|
+
? 'bg-black text-white'
|
|
643
|
+
: 'border border-gray-300 bg-white text-gray-700'
|
|
644
|
+
}`}
|
|
645
|
+
>
|
|
646
|
+
In Person
|
|
647
|
+
</button>
|
|
648
|
+
)}
|
|
649
|
+
{remoteAvailable && (
|
|
650
|
+
<button
|
|
651
|
+
type="button"
|
|
652
|
+
onClick={() => {
|
|
653
|
+
applyAppointmentMode('REMOTE');
|
|
654
|
+
}}
|
|
655
|
+
className={`rounded-md px-3 py-2 text-sm font-bold ${
|
|
656
|
+
appointmentMode === 'REMOTE'
|
|
657
|
+
? 'bg-black text-white'
|
|
658
|
+
: 'border border-gray-300 bg-white text-gray-700'
|
|
659
|
+
}`}
|
|
660
|
+
>
|
|
661
|
+
Remote
|
|
662
|
+
</button>
|
|
663
|
+
)}
|
|
664
|
+
</div>
|
|
665
|
+
{effectiveRemoteOnly && (
|
|
666
|
+
<p className="mt-2 text-xs font-bold text-gray-500">
|
|
667
|
+
Remote mode is required by shop or service settings.
|
|
668
|
+
</p>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
)}
|
|
672
|
+
<NativeBookingCalendar
|
|
673
|
+
totalDurationMinutes={totalDuration}
|
|
674
|
+
onSlotSelected={handleSlotSelection}
|
|
675
|
+
/>
|
|
676
|
+
</div>
|
|
469
677
|
)}
|
|
470
678
|
{internalState === 'SUMMARY' && (
|
|
471
679
|
<div className="space-y-6">
|
|
680
|
+
{needsBooking && (
|
|
681
|
+
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
|
682
|
+
<p className="text-xs font-bold uppercase tracking-wide text-gray-500">
|
|
683
|
+
Appointment Mode
|
|
684
|
+
</p>
|
|
685
|
+
<p className="mt-3 text-sm font-bold text-gray-900">
|
|
686
|
+
{appointmentMode === 'REMOTE' ? 'Remote' : 'In Person'}
|
|
687
|
+
</p>
|
|
688
|
+
{effectiveRemoteOnly && (
|
|
689
|
+
<p className="mt-2 text-xs font-bold text-gray-500">
|
|
690
|
+
Remote mode is required by shop or service settings.
|
|
691
|
+
</p>
|
|
692
|
+
)}
|
|
693
|
+
</div>
|
|
694
|
+
)}
|
|
695
|
+
|
|
472
696
|
<div className="rounded-xl border border-gray-200 bg-gray-50 p-6">
|
|
473
697
|
<h3 className="mb-4 font-bold text-gray-900">
|
|
474
698
|
Order Summary
|
|
475
699
|
</h3>
|
|
476
|
-
{enrichedCart.map((item, idx) =>
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
700
|
+
{enrichedCart.map((item, idx) => {
|
|
701
|
+
if (item.sharedFeeService) {
|
|
702
|
+
return (
|
|
703
|
+
<div
|
|
704
|
+
key={idx}
|
|
705
|
+
className="flex justify-between text-sm text-gray-700"
|
|
706
|
+
>
|
|
707
|
+
<span>
|
|
708
|
+
{item.resourceNode?.title || item.title}
|
|
709
|
+
</span>
|
|
710
|
+
</div>
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return (
|
|
715
|
+
<div
|
|
716
|
+
key={idx}
|
|
717
|
+
className="flex justify-between text-sm text-gray-700"
|
|
718
|
+
>
|
|
719
|
+
<span>{item.title}</span>
|
|
720
|
+
<span className="font-bold">
|
|
721
|
+
$
|
|
722
|
+
{(
|
|
723
|
+
parseFloat(item.price) * (item.quantity || 1)
|
|
724
|
+
).toFixed(2)}
|
|
725
|
+
{item.currencyCode ? ` ${item.currencyCode}` : ''}
|
|
726
|
+
</span>
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
})}
|
|
730
|
+
{sharedFeeChargeLine && (
|
|
731
|
+
<div className="mt-2 flex justify-between border-t border-gray-200 pt-2 text-sm text-gray-700">
|
|
732
|
+
<div>
|
|
733
|
+
<span className="font-bold">
|
|
734
|
+
{sharedFeeChargeLine.title}
|
|
735
|
+
</span>
|
|
736
|
+
{sharedFeeChargeLine.description && (
|
|
737
|
+
<p className="text-xs text-gray-500">
|
|
738
|
+
{sharedFeeChargeLine.description}
|
|
739
|
+
</p>
|
|
740
|
+
)}
|
|
741
|
+
</div>
|
|
482
742
|
<span className="font-bold">
|
|
483
|
-
$
|
|
484
|
-
{
|
|
485
|
-
parseFloat(item.price) * (item.quantity || 1)
|
|
486
|
-
).toFixed(2)}
|
|
743
|
+
${parseFloat(sharedFeeChargeLine.amount).toFixed(2)}{' '}
|
|
744
|
+
{sharedFeeChargeLine.currencyCode}
|
|
487
745
|
</span>
|
|
488
746
|
</div>
|
|
489
|
-
)
|
|
490
|
-
{selectedSlot && (
|
|
747
|
+
)}
|
|
748
|
+
{needsBooking && selectedSlot && (
|
|
491
749
|
<div className="mt-6 border-t border-gray-200 pt-6">
|
|
492
750
|
<p className="text-xs font-bold uppercase text-gray-500">
|
|
493
751
|
Appointment
|
|
494
752
|
</p>
|
|
753
|
+
<p className="mt-1 text-xs font-bold uppercase tracking-wide text-gray-500">
|
|
754
|
+
{appointmentMode === 'REMOTE'
|
|
755
|
+
? 'Remote'
|
|
756
|
+
: 'In Person'}
|
|
757
|
+
</p>
|
|
495
758
|
<p className="mt-1 text-sm font-bold text-gray-900">
|
|
496
759
|
{selectedSlot.start.toLocaleDateString('en-US', {
|
|
497
760
|
timeZone: shopTimeZone,
|