@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.
Files changed (92) hide show
  1. package/README.md +48 -21
  2. package/package.json +11 -8
  3. package/src/AvailabilityEditor.jsx +2 -13
  4. package/src/BookingList.jsx +1 -1
  5. package/src/BookingProductHome.jsx +38 -0
  6. package/src/BookingSalesPage.jsx +55 -0
  7. package/src/Definition.js +2 -2
  8. package/src/HeroCover.jsx +5 -5
  9. package/src/SalesSection.jsx +7 -15
  10. package/src/ServiceCard.jsx +2 -2
  11. package/src/availability-action-maps.js +39 -0
  12. package/src/availability-engine.js +18 -14
  13. package/src/availability-engine.spec.js +36 -4
  14. package/src/availability-setup.page.jsx +45 -46
  15. package/src/{availability.resource.js → availability.schema.js} +1 -5
  16. package/src/booking-cancellation.email.jsx +1 -1
  17. package/src/booking-card.component.jsx +3 -3
  18. package/src/booking-confirmation.email.jsx +5 -3
  19. package/src/booking-detail.page.jsx +15 -10
  20. package/src/booking-email-token.js +45 -0
  21. package/src/booking-home-content.js +50 -0
  22. package/src/booking-list.aggregate.js +65 -0
  23. package/src/booking-reminder.email.jsx +4 -3
  24. package/src/booking-request.email.jsx +5 -2
  25. package/src/booking-resources.js +3 -3
  26. package/src/{booking.resource.js → booking.schema.js} +12 -3
  27. package/src/bookings.component.jsx +26 -25
  28. package/src/bookings.page.jsx +29 -16
  29. package/src/cancel-booking.action.js +1 -1
  30. package/src/cancel-booking.task.js +14 -17
  31. package/src/confirm-booking.action.js +1 -1
  32. package/src/confirm-booking.api.js +35 -0
  33. package/src/confirm-booking.task.js +26 -24
  34. package/src/create-booking.action.js +1 -1
  35. package/src/create-booking.task.js +73 -37
  36. package/src/create-service.action.js +5 -1
  37. package/src/create-service.task.js +7 -7
  38. package/src/decline-booking.action.js +1 -1
  39. package/src/decline-booking.api.js +35 -0
  40. package/src/decline-booking.task.js +15 -18
  41. package/src/delete-service.action.js +1 -1
  42. package/src/delete-service.task.js +12 -15
  43. package/src/en.translations.json +83 -37
  44. package/src/get-availability.action.js +1 -1
  45. package/src/get-availability.task.js +5 -4
  46. package/src/get-available-slots.action.js +1 -1
  47. package/src/get-available-slots.task.js +16 -13
  48. package/src/get-services.action.js +1 -1
  49. package/src/get-services.task.js +13 -6
  50. package/src/home.page.jsx +14 -46
  51. package/src/index.js +17 -0
  52. package/src/list-bookings.action.js +1 -1
  53. package/src/list-bookings.task.js +33 -16
  54. package/src/locations.js +6 -4
  55. package/src/open-availability-setup.action.js +5 -0
  56. package/src/open-bookings.action.js +5 -0
  57. package/src/open-create-service.action.js +5 -0
  58. package/src/open-services.action.js +5 -0
  59. package/src/provider-availability.flow.js +39 -0
  60. package/src/provider-fields.js +34 -0
  61. package/src/provider-onboard.flow.js +25 -0
  62. package/src/public-booking-form.form.js +7 -0
  63. package/src/public-booking-form.schema.js +12 -0
  64. package/src/public-booking.page.jsx +60 -57
  65. package/src/resolve-workspace-contact.js +55 -0
  66. package/src/save-availability.action.js +5 -1
  67. package/src/save-availability.task.js +26 -28
  68. package/src/schema-ids.js +7 -0
  69. package/src/select-duration-30.action.js +4 -0
  70. package/src/select-duration-60.action.js +4 -0
  71. package/src/select-duration-90.action.js +4 -0
  72. package/src/send-booking-reminder.task.js +39 -14
  73. package/src/service-card-slot.component.jsx +4 -4
  74. package/src/service-detail.component.jsx +4 -4
  75. package/src/service.form.js +7 -0
  76. package/src/service.schema.js +14 -0
  77. package/src/services.page.jsx +62 -58
  78. package/src/stories/fixtures.js +149 -0
  79. package/src/sv.translations.json +83 -37
  80. package/src/time.js +27 -0
  81. package/src/toggle-availability-day-0.action.js +4 -0
  82. package/src/toggle-availability-day-1.action.js +4 -0
  83. package/src/toggle-availability-day-2.action.js +4 -0
  84. package/src/toggle-availability-day-3.action.js +4 -0
  85. package/src/toggle-availability-day-4.action.js +4 -0
  86. package/src/toggle-availability-day-5.action.js +4 -0
  87. package/src/toggle-availability-day-6.action.js +4 -0
  88. package/src/update-service.action.js +1 -1
  89. package/src/update-service.task.js +13 -16
  90. package/src/workspace-services.js +5 -0
  91. package/src/moduleStatus.js +0 -2
  92. 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 windowStart = new Date(now + 23 * 60 * 60 * 1000).toISOString()
12
- const windowEnd = new Date(now + 25 * 60 * 60 * 1000).toISOString()
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 ${windowStart} and ${windowEnd}`)
18
+ log?.info(`[booking/tasks/send-reminder] Checking bookings between ${windowStartMs} and ${windowEndMs}`)
15
19
 
16
20
  const allBookings = await getBookingResources({
17
- type: '@ossy/booking/booking',
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
- return startsAt >= windowStart && startsAt <= windowEnd
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, consultantId, startsAt, duration } = booking.content ?? {}
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
- consultantName: null,
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: consultantId,
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, Title, Text, Badge } from '@ossy/design-system'
2
+ import { View, Text, Badge } from '@ossy/design-system'
3
3
 
4
- export const metadata = { id: 'resource:booking/service/card' }
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 — registered at `resource:booking/service/card`. */
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
- <Title variant="secondary">{resource.name || name}</Title>
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, Title, Text, Badge } from '@ossy/design-system'
2
+ import { View, Text, Badge } from '@ossy/design-system'
3
3
 
4
- export const metadata = { id: 'resource:booking/service/detail' }
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 — registered at `resource:booking/service/detail`. */
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
- <Title>{resource.name || name}</Title>
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,7 @@
1
+ import { BookingSchema } from './schema-ids.js'
2
+
3
+ export const metadata = {
4
+ id: '@ossy/booking/form/service',
5
+ schemaId: BookingSchema.service,
6
+ label: '@ossy/booking/form/service.label',
7
+ }
@@ -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
+ }
@@ -1,12 +1,16 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react'
2
- import { View, Title, Text, Button, Input, Textarea, Alert, useLocale } from '@ossy/design-system'
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 [form, setForm] = useState(EMPTY_FORM)
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
- setForm(EMPTY_FORM)
64
+ setFormDefaults(EMPTY_FORM)
60
65
  setFormOpen(true)
61
66
  }
62
67
 
63
68
  const openEdit = (service) => {
64
69
  setEditingId(service.id)
65
- setForm({
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, ...form } : form
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
- <View
106
- gap="m"
107
- surface="primary"
108
- style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
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
- <View gap="m" surface="secondary" roundness="m" inset="m">
122
- <Title variant="tertiary">{editingId ? t('booking.services.editService') : t('booking.services.newService')}</Title>
123
- <View gap="xs">
124
- <Text weight="medium">{t('booking.services.field.name')}</Text>
125
- <Input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} />
126
- </View>
127
- <View gap="xs">
128
- <Text weight="medium">{t('booking.services.field.duration')}</Text>
129
- <Input type="number" value={String(form.duration)} onChange={e => setForm(f => ({ ...f, duration: Number(e.target.value) || 60 }))} />
130
- </View>
131
- <View gap="xs">
132
- <Text weight="medium">{t('booking.services.field.price')}</Text>
133
- <Input type="number" value={String(form.price)} onChange={e => setForm(f => ({ ...f, price: Number(e.target.value) || 0 }))} />
134
- </View>
135
- <View gap="xs">
136
- <Text weight="medium">{t('booking.services.field.currency')}</Text>
137
- <Input value={form.currency} onChange={e => setForm(f => ({ ...f, currency: e.target.value }))} />
138
- </View>
139
- <View gap="xs">
140
- <Text weight="medium">{t('booking.services.field.description')}</Text>
141
- <Textarea value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} rows={3} />
142
- </View>
143
- <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
144
- <input type="checkbox" checked={form.active} onChange={e => setForm(f => ({ ...f, active: e.target.checked }))} />
145
- <Text>{t('booking.services.field.active')}</Text>
146
- </label>
147
- <View layout="row" gap="s">
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
- </View>
157
+ </Form>
154
158
  )}
155
159
 
156
- <View gap="s" inset="s">
157
- <Title variant="secondary">{t('booking.services.yourServices')}</Title>
160
+ <View gap="s">
161
+ <Text variant="heading-secondary" as="h2" text="booking.services.yourServices" />
158
162
  {loading ? (
159
- <Text color="secondary">{t('booking.services.loading')}</Text>
163
+ <Text color="secondary" text="booking.services.loading" />
160
164
  ) : services.length === 0 ? (
161
- <Text color="secondary">{t('booking.services.empty')}</Text>
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)}>{t('booking.services.edit')}</Button>
176
- <Button data-action={DeleteService.id} variant="link" size="s" onClick={() => handleDelete(service.id)}>{t('booking.services.delete')}</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
- </View>
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
+ }