@ossy/booking 1.16.2 → 1.16.3
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 +44 -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 +1 -1
- 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 +7 -11
- 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/moduleStatus.js +30 -2
- 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 +58 -56
- 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/AvailabilityEditor.stories.jsx +54 -0
- package/src/stories/AvailabilitySetupPage.stories.jsx +44 -0
- package/src/stories/BookingCard.stories.jsx +57 -0
- package/src/stories/BookingList.stories.jsx +30 -0
- package/src/stories/BookingsComponent.stories.jsx +31 -0
- package/src/stories/BookingsPage.stories.jsx +66 -0
- package/src/stories/HeroCover.stories.jsx +42 -0
- package/src/stories/PublicBookingForm.stories.jsx +66 -0
- package/src/stories/PublicBookingPage.stories.jsx +78 -0
- package/src/stories/ServiceCard.stories.jsx +52 -0
- package/src/stories/ServiceDetail.stories.jsx +52 -0
- package/src/stories/ServiceForm.stories.jsx +64 -0
- package/src/stories/ServicesPage.stories.jsx +53 -0
- package/src/stories/decorators.jsx +68 -0
- package/src/stories/fixtures.js +149 -0
- package/src/stories/story-helpers.jsx +85 -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/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,54 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { View, Text } from '@ossy/design-system'
|
|
3
|
+
import { AvailabilityEditor } from '../AvailabilityEditor.jsx'
|
|
4
|
+
import { sampleAvailability } from './fixtures.js'
|
|
5
|
+
|
|
6
|
+
const savedPreviewStyle = {
|
|
7
|
+
margin: 0,
|
|
8
|
+
padding: 'var(--space-s)',
|
|
9
|
+
background: 'var(--color-secondary)',
|
|
10
|
+
color: 'var(--text-default-color, CanvasText)',
|
|
11
|
+
borderRadius: 'var(--space-xs)',
|
|
12
|
+
border: '1px solid var(--separator-primary)',
|
|
13
|
+
fontSize: 12,
|
|
14
|
+
overflow: 'auto',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Booking/Components/AvailabilityEditor',
|
|
19
|
+
component: AvailabilityEditor,
|
|
20
|
+
parameters: {
|
|
21
|
+
layout: 'padded',
|
|
22
|
+
docs: {
|
|
23
|
+
story: {
|
|
24
|
+
inline: true,
|
|
25
|
+
height: 'auto',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function AvailabilityEditorShell ({ availability = [] }) {
|
|
32
|
+
const [saved, setSaved] = useState(null)
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View gap="m" style={{ maxWidth: 720, width: '100%' }}>
|
|
36
|
+
<AvailabilityEditor
|
|
37
|
+
availability={availability}
|
|
38
|
+
onSave={(windows) => setSaved(windows)}
|
|
39
|
+
/>
|
|
40
|
+
{saved && (
|
|
41
|
+
<View gap="xs">
|
|
42
|
+
<Text size="s" color="secondary">Saved windows (JSON)</Text>
|
|
43
|
+
<pre style={savedPreviewStyle}>{JSON.stringify(saved, null, 2)}</pre>
|
|
44
|
+
</View>
|
|
45
|
+
)}
|
|
46
|
+
</View>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const Empty = () => <AvailabilityEditorShell />
|
|
51
|
+
|
|
52
|
+
export const WithExistingAvailability = () => (
|
|
53
|
+
<AvailabilityEditorShell availability={sampleAvailability} />
|
|
54
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import AvailabilitySetupPage from '../availability-setup.page.jsx'
|
|
3
|
+
import { withMockRouter, withMockSdk } from './decorators.jsx'
|
|
4
|
+
import { sampleAvailabilitySettings, sampleProviderContact } from './fixtures.js'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Booking/Pages/AvailabilitySetupPage',
|
|
8
|
+
component: AvailabilitySetupPage,
|
|
9
|
+
decorators: [withMockRouter, withMockSdk],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'fullscreen',
|
|
12
|
+
docs: {
|
|
13
|
+
story: {
|
|
14
|
+
inline: false,
|
|
15
|
+
height: '720px',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const LoadedSchedule = {
|
|
22
|
+
parameters: {
|
|
23
|
+
mockSdk: {},
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const EmptySchedule = {
|
|
28
|
+
parameters: {
|
|
29
|
+
mockSdk: {
|
|
30
|
+
availability: null,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const WithBookingLink = {
|
|
36
|
+
args: {
|
|
37
|
+
workspaceId: sampleProviderContact.workspaceId,
|
|
38
|
+
},
|
|
39
|
+
parameters: {
|
|
40
|
+
mockSdk: {
|
|
41
|
+
availability: sampleAvailabilitySettings,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
import { BookingCard } from '../booking-card.component.jsx'
|
|
4
|
+
import { sampleBookings } from './fixtures.js'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Booking/Components/BookingCard',
|
|
8
|
+
component: BookingCard,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
story: {
|
|
13
|
+
inline: true,
|
|
14
|
+
height: 'auto',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cardShell = (booking, handlers = {}) => (
|
|
21
|
+
<View style={{ maxWidth: 560, width: '100%' }}>
|
|
22
|
+
<BookingCard booking={booking} {...handlers} />
|
|
23
|
+
</View>
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const mockAction = (label) => async () => {
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, 400))
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.log(`[Storybook] ${label}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Pending = () =>
|
|
33
|
+
cardShell(sampleBookings[0], {
|
|
34
|
+
onConfirm: mockAction('confirm'),
|
|
35
|
+
onDecline: mockAction('decline'),
|
|
36
|
+
onView: mockAction('view'),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export const Confirmed = () =>
|
|
40
|
+
cardShell(sampleBookings[1], {
|
|
41
|
+
onCancel: mockAction('cancel'),
|
|
42
|
+
onView: mockAction('view'),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const Cancelled = () => cardShell(sampleBookings[2])
|
|
46
|
+
|
|
47
|
+
export const WithMessage = () =>
|
|
48
|
+
cardShell(
|
|
49
|
+
{
|
|
50
|
+
...sampleBookings[1],
|
|
51
|
+
clientMessage:
|
|
52
|
+
'Please focus on onboarding improvements during our session. I have a few specific questions about team workflows.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
onCancel: mockAction('cancel'),
|
|
56
|
+
},
|
|
57
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
import { BookingList } from '../BookingList.jsx'
|
|
4
|
+
import { sampleBookings } from './fixtures.js'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Booking/Components/BookingList',
|
|
8
|
+
component: BookingList,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
story: {
|
|
13
|
+
inline: true,
|
|
14
|
+
height: 'auto',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const listShell = (bookings) => (
|
|
21
|
+
<View style={{ maxWidth: 640, width: '100%' }}>
|
|
22
|
+
<BookingList bookings={bookings} />
|
|
23
|
+
</View>
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export const Empty = () => listShell([])
|
|
27
|
+
|
|
28
|
+
export const Populated = () => listShell(sampleBookings)
|
|
29
|
+
|
|
30
|
+
export const SinglePending = () => listShell([sampleBookings[0]])
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
import { BookingsContent } from '../bookings.component.jsx'
|
|
4
|
+
import { BookingList } from '../BookingList.jsx'
|
|
5
|
+
import { sampleBookings } from './fixtures.js'
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Booking/Components/BookingsComponent',
|
|
9
|
+
component: BookingsContent,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
docs: {
|
|
13
|
+
story: {
|
|
14
|
+
inline: true,
|
|
15
|
+
height: 'auto',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MarketingShell = () => <BookingsContent />
|
|
22
|
+
|
|
23
|
+
export const WithSampleBookings = () => (
|
|
24
|
+
<View
|
|
25
|
+
gap="m"
|
|
26
|
+
surface="primary"
|
|
27
|
+
style={{ padding: 'var(--space-m) var(--space-l)', maxWidth: 640, width: '100%' }}
|
|
28
|
+
>
|
|
29
|
+
<BookingList bookings={sampleBookings} />
|
|
30
|
+
</View>
|
|
31
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import BookingsPage from '../bookings.page.jsx'
|
|
3
|
+
import { withMockRouter, withMockSdk } from './decorators.jsx'
|
|
4
|
+
import { useSelectBookingsTab } from './story-helpers.jsx'
|
|
5
|
+
import { sampleBookings } from './fixtures.js'
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Booking/Pages/BookingsPage',
|
|
9
|
+
component: BookingsPage,
|
|
10
|
+
decorators: [withMockRouter, withMockSdk],
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'fullscreen',
|
|
13
|
+
docs: {
|
|
14
|
+
story: {
|
|
15
|
+
inline: false,
|
|
16
|
+
height: '640px',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PendingTab = {
|
|
23
|
+
parameters: {
|
|
24
|
+
mockSdk: {},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ConfirmedTabStory () {
|
|
29
|
+
useSelectBookingsTab('confirmed')
|
|
30
|
+
return <BookingsPage />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ConfirmedTab = {
|
|
34
|
+
render: () => <ConfirmedTabStory />,
|
|
35
|
+
parameters: {
|
|
36
|
+
mockSdk: {},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function AllTabStory () {
|
|
41
|
+
useSelectBookingsTab('all')
|
|
42
|
+
return <BookingsPage />
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const AllTab = {
|
|
46
|
+
render: () => <AllTabStory />,
|
|
47
|
+
parameters: {
|
|
48
|
+
mockSdk: {},
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const EmptyPending = {
|
|
53
|
+
parameters: {
|
|
54
|
+
mockSdk: {
|
|
55
|
+
bookings: sampleBookings.filter((booking) => booking.status !== 'pending'),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const LoadError = {
|
|
61
|
+
parameters: {
|
|
62
|
+
mockSdk: {
|
|
63
|
+
'@ossy/booking/actions/list': () => Promise.reject(new Error('Failed to load bookings')),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|