@ossy/booking 1.16.2 → 3.0.1
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/README.md +48 -21
- package/package.json +11 -8
- package/src/AvailabilityEditor.jsx +2 -13
- package/src/BookingList.jsx +1 -1
- package/src/BookingProductHome.jsx +38 -0
- package/src/BookingSalesPage.jsx +55 -0
- package/src/Definition.js +2 -2
- package/src/HeroCover.jsx +5 -5
- package/src/SalesSection.jsx +7 -15
- package/src/ServiceCard.jsx +2 -2
- package/src/availability-action-maps.js +39 -0
- package/src/availability-engine.js +18 -14
- package/src/availability-engine.spec.js +36 -4
- package/src/availability-setup.page.jsx +45 -46
- package/src/{availability.resource.js → availability.schema.js} +1 -5
- package/src/booking-cancellation.email.jsx +1 -1
- package/src/booking-card.component.jsx +3 -3
- package/src/booking-confirmation.email.jsx +5 -3
- package/src/booking-detail.page.jsx +15 -10
- package/src/booking-email-token.js +45 -0
- package/src/booking-home-content.js +50 -0
- package/src/booking-list.aggregate.js +65 -0
- package/src/booking-reminder.email.jsx +4 -3
- package/src/booking-request.email.jsx +5 -2
- package/src/booking-resources.js +3 -3
- package/src/{booking.resource.js → booking.schema.js} +12 -3
- package/src/bookings.component.jsx +26 -25
- package/src/bookings.page.jsx +29 -16
- package/src/cancel-booking.action.js +1 -1
- package/src/cancel-booking.task.js +14 -17
- package/src/confirm-booking.action.js +1 -1
- package/src/confirm-booking.api.js +35 -0
- package/src/confirm-booking.task.js +26 -24
- package/src/create-booking.action.js +1 -1
- package/src/create-booking.task.js +73 -37
- package/src/create-service.action.js +5 -1
- package/src/create-service.task.js +7 -7
- package/src/decline-booking.action.js +1 -1
- package/src/decline-booking.api.js +35 -0
- package/src/decline-booking.task.js +15 -18
- package/src/delete-service.action.js +1 -1
- package/src/delete-service.task.js +12 -15
- package/src/en.translations.json +83 -37
- package/src/get-availability.action.js +1 -1
- package/src/get-availability.task.js +5 -4
- package/src/get-available-slots.action.js +1 -1
- package/src/get-available-slots.task.js +16 -13
- package/src/get-services.action.js +1 -1
- package/src/get-services.task.js +13 -6
- package/src/home.page.jsx +14 -46
- package/src/index.js +17 -0
- package/src/list-bookings.action.js +1 -1
- package/src/list-bookings.task.js +33 -16
- package/src/locations.js +6 -4
- package/src/open-availability-setup.action.js +5 -0
- package/src/open-bookings.action.js +5 -0
- package/src/open-create-service.action.js +5 -0
- package/src/open-services.action.js +5 -0
- package/src/provider-availability.flow.js +39 -0
- package/src/provider-fields.js +34 -0
- package/src/provider-onboard.flow.js +25 -0
- package/src/public-booking-form.form.js +7 -0
- package/src/public-booking-form.schema.js +12 -0
- package/src/public-booking.page.jsx +60 -57
- package/src/resolve-workspace-contact.js +55 -0
- package/src/save-availability.action.js +5 -1
- package/src/save-availability.task.js +26 -28
- package/src/schema-ids.js +7 -0
- package/src/select-duration-30.action.js +4 -0
- package/src/select-duration-60.action.js +4 -0
- package/src/select-duration-90.action.js +4 -0
- package/src/send-booking-reminder.task.js +39 -14
- package/src/service-card-slot.component.jsx +4 -4
- package/src/service-detail.component.jsx +4 -4
- package/src/service.form.js +7 -0
- package/src/service.schema.js +14 -0
- package/src/services.page.jsx +62 -58
- package/src/stories/fixtures.js +149 -0
- package/src/sv.translations.json +83 -37
- package/src/time.js +27 -0
- package/src/toggle-availability-day-0.action.js +4 -0
- package/src/toggle-availability-day-1.action.js +4 -0
- package/src/toggle-availability-day-2.action.js +4 -0
- package/src/toggle-availability-day-3.action.js +4 -0
- package/src/toggle-availability-day-4.action.js +4 -0
- package/src/toggle-availability-day-5.action.js +4 -0
- package/src/toggle-availability-day-6.action.js +4 -0
- package/src/update-service.action.js +1 -1
- package/src/update-service.task.js +13 -16
- package/src/workspace-services.js +5 -0
- package/src/moduleStatus.js +0 -2
- package/src/service.resource.js +0 -32
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
import { getBookingResources } from './booking-resources.js'
|
|
2
|
+
import { BookingSchema } from './schema-ids.js'
|
|
2
3
|
import BookingReminderEmail from './booking-reminder.email.jsx'
|
|
4
|
+
import { workspaceIdFromContent } from './provider-fields.js'
|
|
5
|
+
import { resolveWorkspaceContact } from './resolve-workspace-contact.js'
|
|
6
|
+
import { toMs } from './time.js'
|
|
3
7
|
|
|
4
8
|
export const metadata = {
|
|
5
|
-
id: 'booking/send-reminder',
|
|
9
|
+
id: '@ossy/booking/tasks/send-reminder',
|
|
6
10
|
schedule: '0 * * * *', // every hour — checks for bookings starting ~24h from now
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export async function run({ sdk, log, integrations }) {
|
|
10
14
|
const now = Date.now()
|
|
11
|
-
const
|
|
12
|
-
const
|
|
15
|
+
const windowStartMs = now + 23 * 60 * 60 * 1000
|
|
16
|
+
const windowEndMs = now + 25 * 60 * 60 * 1000
|
|
13
17
|
|
|
14
|
-
log?.info(`[booking/send-reminder] Checking bookings between ${
|
|
18
|
+
log?.info(`[booking/tasks/send-reminder] Checking bookings between ${windowStartMs} and ${windowEndMs}`)
|
|
15
19
|
|
|
16
20
|
const allBookings = await getBookingResources({
|
|
17
|
-
|
|
21
|
+
schemaId: BookingSchema.booking,
|
|
18
22
|
})
|
|
19
23
|
|
|
20
24
|
const upcoming = allBookings.filter(b => {
|
|
21
25
|
const { status, startsAt, reminderSentAt } = b.content ?? {}
|
|
22
26
|
if (status !== 'confirmed') return false
|
|
23
27
|
if (reminderSentAt) return false
|
|
24
|
-
|
|
28
|
+
const startsAtMs = toMs(startsAt)
|
|
29
|
+
if (startsAtMs == null) return false
|
|
30
|
+
return startsAtMs >= windowStartMs && startsAtMs <= windowEndMs
|
|
25
31
|
})
|
|
26
32
|
|
|
27
|
-
log?.info(`[booking/send-reminder] Found ${upcoming.length} bookings needing reminders`)
|
|
33
|
+
log?.info(`[booking/tasks/send-reminder] Found ${upcoming.length} bookings needing reminders`)
|
|
28
34
|
|
|
29
35
|
const emailClient = integrations?.get?.('email')
|
|
30
36
|
if (!emailClient) {
|
|
31
|
-
log?.warn('[booking/send-reminder] No email integration available, skipping')
|
|
37
|
+
log?.warn('[booking/tasks/send-reminder] No email integration available, skipping')
|
|
32
38
|
return { sent: 0 }
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
let sent = 0
|
|
36
42
|
|
|
37
43
|
for (const booking of upcoming) {
|
|
38
|
-
const { clientName, clientEmail,
|
|
44
|
+
const { clientName, clientEmail, startsAt, duration } = booking.content ?? {}
|
|
45
|
+
const workspaceId = booking.belongsTo ?? workspaceIdFromContent(booking.content)
|
|
46
|
+
const contact = await resolveWorkspaceContact(workspaceId)
|
|
39
47
|
|
|
40
48
|
if (!clientEmail) continue
|
|
41
49
|
|
|
@@ -44,7 +52,7 @@ export async function run({ sdk, log, integrations }) {
|
|
|
44
52
|
BookingReminderEmail,
|
|
45
53
|
{
|
|
46
54
|
clientName: clientName ?? 'Kund',
|
|
47
|
-
|
|
55
|
+
providerName: contact?.providerName ?? null,
|
|
48
56
|
startAt: startsAt,
|
|
49
57
|
duration: duration ?? 60,
|
|
50
58
|
},
|
|
@@ -55,20 +63,37 @@ export async function run({ sdk, log, integrations }) {
|
|
|
55
63
|
},
|
|
56
64
|
)
|
|
57
65
|
|
|
66
|
+
if (contact?.providerEmail) {
|
|
67
|
+
await emailClient.sendTemplate(
|
|
68
|
+
BookingReminderEmail,
|
|
69
|
+
{
|
|
70
|
+
clientName: clientName ?? 'Kund',
|
|
71
|
+
providerName: contact?.providerName ?? null,
|
|
72
|
+
startAt: startsAt,
|
|
73
|
+
duration: duration ?? 60,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
to: contact.providerEmail,
|
|
77
|
+
from: 'noreply@ossy.se',
|
|
78
|
+
subject: 'Påminnelse: bokning imorgon',
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
// Mark reminder as sent via SDK if available, otherwise skip to avoid
|
|
59
84
|
// needing direct DB writes in a scheduled task
|
|
60
85
|
if (sdk) {
|
|
61
|
-
await sdk.actions.run('resources/update-content', {
|
|
86
|
+
await sdk.actions.run('@ossy/resources/actions/update-content', {
|
|
62
87
|
resourceId: booking.id,
|
|
63
88
|
content: { ...booking.content, reminderSentAt: new Date().toISOString() },
|
|
64
|
-
workspaceId
|
|
89
|
+
workspaceId,
|
|
65
90
|
})
|
|
66
91
|
}
|
|
67
92
|
|
|
68
93
|
sent++
|
|
69
|
-
log?.info(`[booking/send-reminder] Reminder sent to ${clientEmail}`)
|
|
94
|
+
log?.info(`[booking/tasks/send-reminder] Reminder sent to ${clientEmail}`)
|
|
70
95
|
} catch (err) {
|
|
71
|
-
log?.error(`[booking/send-reminder] Failed to send reminder to ${clientEmail}`, err)
|
|
96
|
+
log?.error(`[booking/tasks/send-reminder] Failed to send reminder to ${clientEmail}`, err)
|
|
72
97
|
}
|
|
73
98
|
}
|
|
74
99
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, Text, Badge } from '@ossy/design-system'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: '
|
|
4
|
+
export const metadata = { id: '@ossy/booking/view/service/card' }
|
|
5
5
|
|
|
6
6
|
const formatPrice = (cents, currency) => {
|
|
7
7
|
if (cents === 0) return 'Free'
|
|
@@ -16,7 +16,7 @@ const formatDuration = (minutes) => {
|
|
|
16
16
|
return m === 0 ? `${h}h` : `${h}h ${m}min`
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** Booking service card view —
|
|
19
|
+
/** Booking service card view — `@ossy/booking/view/service/card`. */
|
|
20
20
|
export default function ServiceCardSlotComponent ({ resource }) {
|
|
21
21
|
if (!resource) return null
|
|
22
22
|
|
|
@@ -34,7 +34,7 @@ export default function ServiceCardSlotComponent ({ resource }) {
|
|
|
34
34
|
>
|
|
35
35
|
<View gap="xs" style={{ flex: 1 }}>
|
|
36
36
|
<View layout="row" gap="s" alignItems="center">
|
|
37
|
-
<
|
|
37
|
+
<Text variant="heading-secondary" as="h2">{resource.name || name}</Text>
|
|
38
38
|
{active === false && <Badge label="Inactive" variant="neutral" size="s" />}
|
|
39
39
|
</View>
|
|
40
40
|
{description && <Text size="s" color="secondary">{description}</Text>}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, Text, Badge } from '@ossy/design-system'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: '
|
|
4
|
+
export const metadata = { id: '@ossy/booking/view/service' }
|
|
5
5
|
|
|
6
6
|
const formatPrice = (cents, currency) => {
|
|
7
7
|
if (cents === 0) return 'Free'
|
|
@@ -16,7 +16,7 @@ const formatDuration = (minutes) => {
|
|
|
16
16
|
return m === 0 ? `${h}h` : `${h}h ${m}min`
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** Booking service detail view —
|
|
19
|
+
/** Booking service detail view — `@ossy/booking/view/service`. */
|
|
20
20
|
export default function ServiceDetailComponent ({ resource }) {
|
|
21
21
|
if (!resource) return null
|
|
22
22
|
|
|
@@ -25,7 +25,7 @@ export default function ServiceDetailComponent ({ resource }) {
|
|
|
25
25
|
return (
|
|
26
26
|
<View stack gap="m" inset="m">
|
|
27
27
|
<View layout="row" gap="s" alignItems="center">
|
|
28
|
-
<
|
|
28
|
+
<Text variant="heading-default" as="h1">{resource.name || name}</Text>
|
|
29
29
|
{active === false && <Badge label="Inactive" variant="neutral" size="s" />}
|
|
30
30
|
</View>
|
|
31
31
|
{description && <Text color="secondary">{description}</Text>}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Bookable service — UI fields and flow faker schema. */
|
|
2
|
+
export default {
|
|
3
|
+
id: '@ossy/booking/schema/service',
|
|
4
|
+
name: 'Service',
|
|
5
|
+
icon: 'calendar',
|
|
6
|
+
fields: [
|
|
7
|
+
{ name: 'name', type: 'text', required: true },
|
|
8
|
+
{ name: 'duration', type: 'number', required: true },
|
|
9
|
+
{ name: 'price', type: 'number' },
|
|
10
|
+
{ name: 'currency', type: 'text' },
|
|
11
|
+
{ name: 'description', type: 'textarea' },
|
|
12
|
+
{ name: 'active', type: 'boolean' },
|
|
13
|
+
],
|
|
14
|
+
}
|
package/src/services.page.jsx
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, Text, Button, Alert, Form, FieldFactory, FormStatus, ContentHeader, Page, useLocale } from '@ossy/design-system'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
|
+
import { useDocumentValidator } from '@ossy/resources'
|
|
4
5
|
import { Definition } from './Definition.js'
|
|
5
6
|
import { ServiceCard } from './ServiceCard.jsx'
|
|
6
7
|
import { metadata as CreateService } from './create-service.action.js'
|
|
7
8
|
import { metadata as UpdateService } from './update-service.action.js'
|
|
8
9
|
import { metadata as DeleteService } from './delete-service.action.js'
|
|
9
10
|
import { metadata as GetServices } from './get-services.action.js'
|
|
11
|
+
import { metadata as OpenCreateService } from './open-create-service.action.js'
|
|
12
|
+
import { metadata as ServiceForm } from './service.form.js'
|
|
13
|
+
import serviceTemplate from './service.schema.js'
|
|
10
14
|
import { invokeErrorMessage } from './invoke-error-message.js'
|
|
11
15
|
|
|
12
16
|
export const metadata = {
|
|
@@ -34,8 +38,9 @@ export default function ServicesPage () {
|
|
|
34
38
|
const [error, setError] = useState(null)
|
|
35
39
|
const [formOpen, setFormOpen] = useState(false)
|
|
36
40
|
const [editingId, setEditingId] = useState(null)
|
|
37
|
-
const [
|
|
41
|
+
const [formDefaults, setFormDefaults] = useState(EMPTY_FORM)
|
|
38
42
|
const [saving, setSaving] = useState(false)
|
|
43
|
+
const validate = useDocumentValidator(serviceTemplate)
|
|
39
44
|
|
|
40
45
|
const loadServices = useCallback(async () => {
|
|
41
46
|
setLoading(true)
|
|
@@ -56,13 +61,13 @@ export default function ServicesPage () {
|
|
|
56
61
|
|
|
57
62
|
const openCreate = () => {
|
|
58
63
|
setEditingId(null)
|
|
59
|
-
|
|
64
|
+
setFormDefaults(EMPTY_FORM)
|
|
60
65
|
setFormOpen(true)
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
const openEdit = (service) => {
|
|
64
69
|
setEditingId(service.id)
|
|
65
|
-
|
|
70
|
+
setFormDefaults({
|
|
66
71
|
name: service.name ?? '',
|
|
67
72
|
duration: service.duration ?? 60,
|
|
68
73
|
price: service.price ?? 0,
|
|
@@ -73,13 +78,12 @@ export default function ServicesPage () {
|
|
|
73
78
|
setFormOpen(true)
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
const handleSave = async () => {
|
|
77
|
-
if (!form.name.trim()) return
|
|
81
|
+
const handleSave = async (data) => {
|
|
78
82
|
setSaving(true)
|
|
79
83
|
setError(null)
|
|
80
84
|
try {
|
|
81
85
|
const bookingAction = editingId ? UpdateService : CreateService
|
|
82
|
-
const body = editingId ? { serviceId: editingId, ...
|
|
86
|
+
const body = editingId ? { serviceId: editingId, ...data } : data
|
|
83
87
|
await sdk.invoke(bookingAction, body)
|
|
84
88
|
setFormOpen(false)
|
|
85
89
|
await loadServices()
|
|
@@ -102,63 +106,63 @@ export default function ServicesPage () {
|
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
<Page
|
|
110
|
+
header={(
|
|
111
|
+
<ContentHeader
|
|
112
|
+
inset="none"
|
|
113
|
+
title={(
|
|
114
|
+
<Text variant="heading-default" as="h1">
|
|
115
|
+
{Definition.title} — <Text as="span" text="booking.services.titleSuffix" />
|
|
116
|
+
</Text>
|
|
117
|
+
)}
|
|
118
|
+
description="booking.services.description"
|
|
119
|
+
descriptionMaxWidth="s"
|
|
120
|
+
far={(
|
|
121
|
+
<Button {...OpenCreateService} variant="primary" size="s" onClick={openCreate} />
|
|
122
|
+
)}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
109
125
|
>
|
|
110
|
-
<View inset="s" gap="s">
|
|
111
|
-
<View layout="row" justifyContent="space-between" alignItems="center" style={{ flexShrink: 0, flexWrap: 'wrap', gap: 'var(--space-s)' }}>
|
|
112
|
-
<Title>{Definition.title} — {t('booking.services.titleSuffix')}</Title>
|
|
113
|
-
<Button variant="primary" size="s" onClick={openCreate}>{t('booking.services.addService')}</Button>
|
|
114
|
-
</View>
|
|
115
|
-
<Text style={{ maxWidth: '480px' }}>{t('booking.services.description')}</Text>
|
|
116
|
-
</View>
|
|
117
|
-
|
|
118
126
|
{error && <Alert variant="danger">{error}</Alert>}
|
|
119
127
|
|
|
120
128
|
{formOpen && (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<View gap="
|
|
132
|
-
<Text
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<Button data-action={editingId ? UpdateService.id : CreateService.id} variant="primary" disabled={saving} onClick={handleSave}>
|
|
149
|
-
{saving ? t('booking.services.saving') : t('booking.services.save')}
|
|
150
|
-
</Button>
|
|
151
|
-
<Button variant="neutral" onClick={() => setFormOpen(false)}>{t('booking.services.cancel')}</Button>
|
|
129
|
+
<Form
|
|
130
|
+
key={editingId ?? 'new'}
|
|
131
|
+
id={ServiceForm.id}
|
|
132
|
+
schemaId={ServiceForm.schemaId}
|
|
133
|
+
fields={serviceTemplate.fields}
|
|
134
|
+
defaultData={formDefaults}
|
|
135
|
+
onSubmit={handleSave}
|
|
136
|
+
validate={validate}
|
|
137
|
+
status={saving ? [FormStatus.Submitting] : []}
|
|
138
|
+
>
|
|
139
|
+
<View gap="m" surface="secondary" roundness="m" inset="m">
|
|
140
|
+
<Text
|
|
141
|
+
variant="heading-tertiary"
|
|
142
|
+
as="h3"
|
|
143
|
+
text={editingId ? 'booking.services.editService' : 'booking.services.newService'}
|
|
144
|
+
/>
|
|
145
|
+
<FieldFactory />
|
|
146
|
+
<View layout="row" gap="s">
|
|
147
|
+
<Button
|
|
148
|
+
{...(editingId ? UpdateService : CreateService)}
|
|
149
|
+
variant="primary"
|
|
150
|
+
type="submit"
|
|
151
|
+
disabled={saving}
|
|
152
|
+
label={saving ? 'booking.services.saving' : 'booking.services.save'}
|
|
153
|
+
/>
|
|
154
|
+
<Button variant="neutral" type="button" label="booking.services.cancel" onClick={() => setFormOpen(false)} />
|
|
155
|
+
</View>
|
|
152
156
|
</View>
|
|
153
|
-
</
|
|
157
|
+
</Form>
|
|
154
158
|
)}
|
|
155
159
|
|
|
156
|
-
<View gap="s"
|
|
157
|
-
<
|
|
160
|
+
<View gap="s">
|
|
161
|
+
<Text variant="heading-secondary" as="h2" text="booking.services.yourServices" />
|
|
158
162
|
{loading ? (
|
|
159
|
-
<Text color="secondary"
|
|
163
|
+
<Text color="secondary" text="booking.services.loading" />
|
|
160
164
|
) : services.length === 0 ? (
|
|
161
|
-
<Text color="secondary"
|
|
165
|
+
<Text color="secondary" text="booking.services.empty" />
|
|
162
166
|
) : (
|
|
163
167
|
<View gap="s">
|
|
164
168
|
{services.map(service => (
|
|
@@ -172,14 +176,14 @@ export default function ServicesPage () {
|
|
|
172
176
|
active={service.active !== false}
|
|
173
177
|
/>
|
|
174
178
|
<View layout="row" gap="s">
|
|
175
|
-
<Button variant="link" size="s" onClick={() => openEdit(service)}
|
|
176
|
-
<Button
|
|
179
|
+
<Button variant="link" size="s" label="booking.services.edit" onClick={() => openEdit(service)} />
|
|
180
|
+
<Button {...DeleteService} variant="link" size="s" label="booking.services.delete" onClick={() => handleDelete(service.id)} />
|
|
177
181
|
</View>
|
|
178
182
|
</View>
|
|
179
183
|
))}
|
|
180
184
|
</View>
|
|
181
185
|
)}
|
|
182
186
|
</View>
|
|
183
|
-
</
|
|
187
|
+
</Page>
|
|
184
188
|
)
|
|
185
189
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export const sampleServices = [
|
|
2
|
+
{
|
|
3
|
+
name: 'Initial consultation',
|
|
4
|
+
duration: 60,
|
|
5
|
+
price: 120000,
|
|
6
|
+
currency: 'SEK',
|
|
7
|
+
description: 'A focused first meeting to understand your needs and outline next steps.',
|
|
8
|
+
active: true,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'Follow-up session',
|
|
12
|
+
duration: 45,
|
|
13
|
+
price: 90000,
|
|
14
|
+
currency: 'SEK',
|
|
15
|
+
description: 'Check in on progress and adjust the plan.',
|
|
16
|
+
active: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Legacy workshop',
|
|
20
|
+
duration: 120,
|
|
21
|
+
price: 0,
|
|
22
|
+
currency: 'SEK',
|
|
23
|
+
description: 'No longer offered to new clients.',
|
|
24
|
+
active: false,
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export const sampleBookings = [
|
|
29
|
+
{
|
|
30
|
+
id: 'booking-1',
|
|
31
|
+
clientName: 'Anna Svensson',
|
|
32
|
+
clientEmail: 'anna@example.com',
|
|
33
|
+
startsAt: Date.parse('2026-06-20T09:00:00.000Z'),
|
|
34
|
+
duration: 60,
|
|
35
|
+
status: 'pending',
|
|
36
|
+
paymentStatus: 'unpaid',
|
|
37
|
+
clientMessage: 'Looking forward to discussing the project scope.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'booking-2',
|
|
41
|
+
clientName: 'Erik Johansson',
|
|
42
|
+
clientEmail: 'erik@example.com',
|
|
43
|
+
startsAt: Date.parse('2026-06-22T13:30:00.000Z'),
|
|
44
|
+
duration: 45,
|
|
45
|
+
status: 'confirmed',
|
|
46
|
+
paymentStatus: 'paid',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'booking-3',
|
|
50
|
+
clientName: 'Maria Lind',
|
|
51
|
+
clientEmail: 'maria@example.com',
|
|
52
|
+
startsAt: Date.parse('2026-06-10T10:00:00.000Z'),
|
|
53
|
+
duration: 60,
|
|
54
|
+
status: 'cancelled',
|
|
55
|
+
paymentStatus: 'refunded',
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export const sampleAvailability = [
|
|
60
|
+
{ dayOfWeek: 1, startTime: '09:00', endTime: '12:00' },
|
|
61
|
+
{ dayOfWeek: 1, startTime: '13:00', endTime: '17:00' },
|
|
62
|
+
{ dayOfWeek: 2, startTime: '10:00', endTime: '16:00' },
|
|
63
|
+
{ dayOfWeek: 4, startTime: '08:30', endTime: '11:30' },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
export const sampleServiceFormData = {
|
|
67
|
+
name: 'Strategy session',
|
|
68
|
+
duration: 90,
|
|
69
|
+
price: 150000,
|
|
70
|
+
currency: 'SEK',
|
|
71
|
+
description: 'Deep-dive strategy workshop for growing teams.',
|
|
72
|
+
active: true,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const samplePublicBookingFormData = {
|
|
76
|
+
clientName: 'Anna Svensson',
|
|
77
|
+
clientEmail: 'anna@example.com',
|
|
78
|
+
clientMessage: 'I would like to book the initial consultation.',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const sampleServicesWithIds = sampleServices.map((service, index) => ({
|
|
82
|
+
...service,
|
|
83
|
+
id: `service-${index + 1}`,
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
export const sampleProviderContact = {
|
|
87
|
+
workspaceId: 'demo-studio',
|
|
88
|
+
providerEmail: 'hello@demo.studio',
|
|
89
|
+
providerName: 'Demo Studio',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const samplePublicServicesResponse = {
|
|
93
|
+
services: sampleServicesWithIds.filter((service) => service.active),
|
|
94
|
+
contact: sampleProviderContact,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Slots on upcoming weekdays so the public booking calendar has selectable dates. */
|
|
98
|
+
export function buildSampleSlots ({ fromDate = new Date() } = {}) {
|
|
99
|
+
const slots = []
|
|
100
|
+
const cursor = new Date(fromDate)
|
|
101
|
+
cursor.setHours(0, 0, 0, 0)
|
|
102
|
+
|
|
103
|
+
for (let day = 0; day < 45 && slots.length < 18; day += 1) {
|
|
104
|
+
const date = new Date(cursor)
|
|
105
|
+
date.setDate(cursor.getDate() + day)
|
|
106
|
+
const weekday = date.getDay()
|
|
107
|
+
if (weekday === 0 || weekday === 6) continue
|
|
108
|
+
|
|
109
|
+
for (const hour of [9, 11, 14, 16]) {
|
|
110
|
+
const start = new Date(date)
|
|
111
|
+
start.setHours(hour, 0, 0, 0)
|
|
112
|
+
if (start <= fromDate) continue
|
|
113
|
+
slots.push({
|
|
114
|
+
startAt: start.getTime(),
|
|
115
|
+
duration: 60,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return slots
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const sampleAvailabilitySettings = {
|
|
124
|
+
weeklyWindows: sampleAvailability,
|
|
125
|
+
sessionDurations: [30, 60],
|
|
126
|
+
bufferMinutes: 15,
|
|
127
|
+
timezone: 'Europe/Stockholm',
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const sampleServiceResource = {
|
|
131
|
+
name: 'Initial consultation',
|
|
132
|
+
content: {
|
|
133
|
+
name: 'Initial consultation',
|
|
134
|
+
duration: 60,
|
|
135
|
+
price: 120000,
|
|
136
|
+
currency: 'SEK',
|
|
137
|
+
description: 'A focused first meeting to understand your needs and outline next steps.',
|
|
138
|
+
active: true,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const sampleHeroCover = {
|
|
143
|
+
title: 'Book time with us',
|
|
144
|
+
text: 'Share your availability, publish services, and let clients request appointments online.',
|
|
145
|
+
actions: [
|
|
146
|
+
{ label: 'Get started', variant: 'cta' },
|
|
147
|
+
{ label: 'See how it works', variant: 'neutral' },
|
|
148
|
+
],
|
|
149
|
+
}
|