@ossy/booking 1.16.3 → 3.0.2

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 CHANGED
@@ -67,3 +67,7 @@ Tasks write resources via `commitResource` / `mutateResource` from `@ossy/resour
67
67
  | `booking-card.component.jsx` | `@ossy/booking/view/booking/card` |
68
68
 
69
69
  See [SPEC.md](./SPEC.md) for the full product specification.
70
+
71
+ ## Preview
72
+
73
+ Booking pages and flows are exercised in **`@ossy/app-test`** on the production build path. Enable the package in `devEntitlements`, then open Booking from the sidebar or `/packages/booking`. See [`packages/app-test/README.md`](../app-test/README.md).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/booking",
3
3
  "description": "Booking feature package — services, availability, and appointment management for Ossy providers",
4
- "version": "1.16.3",
4
+ "version": "3.0.2",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "module": "./src/index.js",
@@ -14,13 +14,14 @@
14
14
  "src": "./src"
15
15
  },
16
16
  "dependencies": {
17
- "@ossy/email": "^1.6.3",
18
- "@ossy/event-store": "^1.8.3",
19
- "@ossy/observability": "^1.8.3",
20
- "@ossy/platform": "^1.39.3",
21
- "@ossy/resources": "^1.12.3",
22
- "@ossy/users": "^1.13.3",
23
- "@ossy/workspaces": "^1.17.3"
17
+ "@ossy/config": "^3.0.2",
18
+ "@ossy/email": "^3.0.2",
19
+ "@ossy/event-store": "^3.0.2",
20
+ "@ossy/observability": "^3.0.2",
21
+ "@ossy/platform": "^3.0.2",
22
+ "@ossy/resources": "^3.0.2",
23
+ "@ossy/users": "^3.0.2",
24
+ "@ossy/workspaces": "^3.0.2"
24
25
  },
25
26
  "peerDependencies": {
26
27
  "@ossy/app": ">=1.0.0",
@@ -37,5 +38,5 @@
37
38
  "/src",
38
39
  "README.md"
39
40
  ],
40
- "gitHead": "a0d89185a17f8de8ce328c3a648c108ff1d61d8f"
41
+ "gitHead": "f0a8af3c370bbaffe4f7388c14f9345a5c6c56e5"
41
42
  }
package/src/Definition.js CHANGED
@@ -3,5 +3,5 @@ export const Definition = {
3
3
  title: 'Booking',
4
4
  description: 'Services, availability windows, and client appointment management for service providers.',
5
5
  icon: 'calendar',
6
- statuses: ['beta'],
6
+ status: ['beta'],
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import jwt from 'jsonwebtoken'
2
- import { ConfigService } from '@ossy/platform'
2
+ import { ConfigService } from '@ossy/config'
3
3
 
4
4
  const TOKEN_TYPES = {
5
5
  confirm: 'BookingConfirm',
@@ -1,33 +1,38 @@
1
1
  import React from 'react'
2
2
  import { View, Text, Tags, Page } from '@ossy/design-system'
3
3
  import { Definition } from './Definition.js'
4
- import { moduleStatusTags } from './moduleStatus.js'
5
4
  import { BookingList } from './BookingList.jsx'
6
5
 
7
6
  export const metadata = {
8
7
  id: 'booking/bookings',
9
8
  }
10
9
 
11
- export const BookingsContent = () => (
12
- <Page>
13
- <View gap="s">
14
- <View layout="row" gap="s" alignItems="center">
15
- <Text variant="heading-default" as="h1">{Definition.title} — Bookings</Text>
16
- {moduleStatusTags(Definition).length > 0 && (
17
- <Tags tags={moduleStatusTags(Definition)} size="s" />
18
- )}
19
- </View>
10
+ export const BookingsContent = () => {
11
+ const statusTags = (Definition.status ?? [])
12
+ .map((s) => ({ beta: 'Beta', 'coming-soon': 'Coming Soon' }[s]))
13
+ .filter(Boolean)
14
+
15
+ return (
16
+ <Page>
17
+ <View gap="s">
18
+ <View layout="row" gap="s" alignItems="center">
19
+ <Text variant="heading-default" as="h1">{Definition.title} — Bookings</Text>
20
+ {statusTags.length > 0 && (
21
+ <Tags tags={statusTags} size="s" />
22
+ )}
23
+ </View>
20
24
 
21
- <Text style={{ maxWidth: '400px' }}>
22
- Incoming appointments from your clients.
23
- </Text>
24
- </View>
25
+ <Text style={{ maxWidth: '400px' }}>
26
+ Incoming appointments from your clients.
27
+ </Text>
28
+ </View>
25
29
 
26
- <View gap="s">
27
- <Text variant="heading-secondary" as="h2">Upcoming</Text>
28
- <BookingList bookings={[]} />
29
- </View>
30
- </Page>
31
- )
30
+ <View gap="s">
31
+ <Text variant="heading-secondary" as="h2">Upcoming</Text>
32
+ <BookingList bookings={[]} />
33
+ </View>
34
+ </Page>
35
+ )
36
+ }
32
37
 
33
38
  export default BookingsContent
@@ -1,5 +1,5 @@
1
1
  import { nanoid } from 'nanoid'
2
- import { ConfigService } from '@ossy/platform'
2
+ import { ConfigService } from '@ossy/config'
3
3
  import { ResourcesEvents, commitResource } from '@ossy/resources/server'
4
4
  import { BookingSchema } from './schema-ids.js'
5
5
  import { getAvailableSlots } from './availability-engine.js'
@@ -165,6 +165,7 @@ function PendingScreen ({ slot, t, language }) {
165
165
  export const metadata = {
166
166
  id: 'public-booking',
167
167
  public: true,
168
+ layout: '@ossy/app/layout/blank',
168
169
  path: { sv: '/boka/:providerSlug', en: '/book/:providerSlug' },
169
170
  }
170
171
 
@@ -254,7 +255,7 @@ export default function PublicBookingPage ({ providerSlug }) {
254
255
 
255
256
  return (
256
257
  <View surface="base" layout="off-center-s" inset="s" style={{ minHeight: '100dvh' }}>
257
- <View inset="m" roundness="l" surface="primary" slot="content" gap="l" style={{ maxWidth: 720, width: '100%' }}>
258
+ <View inset="m" roundness="l" surface="primary" data-region="content" gap="l" style={{ maxWidth: 720, width: '100%' }}>
258
259
  <View gap="xs" style={{ borderBottom: '1px solid var(--separator)', paddingBottom: 'var(--space-m)' }}>
259
260
  <Text variant="heading-secondary" as="h2" text="publicBooking.title" />
260
261
  <Text color="secondary" text="publicBooking.description" />
@@ -1,30 +0,0 @@
1
- export const MODULE_STATUS = {
2
- beta: 'beta',
3
- comingSoon: 'coming-soon',
4
- stable: 'stable',
5
- hidden: 'hidden',
6
- }
7
-
8
- const STATUS_TO_TAG_LABEL = {
9
- [MODULE_STATUS.beta]: 'Beta',
10
- [MODULE_STATUS.comingSoon]: 'Coming Soon',
11
- [MODULE_STATUS.stable]: null,
12
- [MODULE_STATUS.hidden]: null,
13
- }
14
-
15
- export function moduleStatuses(definition) {
16
- const fromArray = Array.isArray(definition?.statuses) ? definition.statuses : []
17
- const fromLegacy = definition?.status && typeof definition.status === 'string' ? [definition.status] : []
18
- if (fromArray.length) return [...new Set(fromArray)]
19
- if (fromLegacy.length) return [...new Set(fromLegacy)]
20
- return []
21
- }
22
-
23
- export function moduleStatusTags(definition) {
24
- const labels = []
25
- for (const s of moduleStatuses(definition)) {
26
- const label = STATUS_TO_TAG_LABEL[s]
27
- if (label) labels.push(label)
28
- }
29
- return [...new Set(labels)]
30
- }
@@ -1,54 +0,0 @@
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
- )
@@ -1,44 +0,0 @@
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
- }
@@ -1,57 +0,0 @@
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
- )
@@ -1,30 +0,0 @@
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]])
@@ -1,31 +0,0 @@
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
- )
@@ -1,66 +0,0 @@
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
- }
@@ -1,42 +0,0 @@
1
- import React from 'react'
2
- import { HeroCover } from '../HeroCover.jsx'
3
- import { sampleHeroCover } from './fixtures.js'
4
-
5
- export default {
6
- title: 'Booking/Components/HeroCover',
7
- component: HeroCover,
8
- parameters: {
9
- layout: 'padded',
10
- docs: {
11
- story: {
12
- inline: true,
13
- height: 'auto',
14
- },
15
- },
16
- },
17
- }
18
-
19
- export const Default = () => <HeroCover {...sampleHeroCover} />
20
-
21
- export const SingleAction = () => (
22
- <HeroCover
23
- {...sampleHeroCover}
24
- actions={[{ label: 'Start booking setup', variant: 'cta' }]}
25
- />
26
- )
27
-
28
- export const CompactTitle = () => (
29
- <HeroCover
30
- title="Simple scheduling for small teams"
31
- titleMaxWidth="640px"
32
- text="Publish services, collect requests, and confirm appointments from one workspace."
33
- actions={[{ label: 'Explore booking', variant: 'primary' }]}
34
- />
35
- )
36
-
37
- export const Minimal = () => (
38
- <HeroCover
39
- title="Booking"
40
- text="Let clients request time with you online."
41
- />
42
- )
@@ -1,66 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { Button, View, Form, FieldFactory } from '@ossy/design-system'
3
- import publicBookingResource from '../public-booking-form.schema.js'
4
- import { metadata as publicBookingFormMeta } from '../public-booking-form.form.js'
5
- import { samplePublicBookingFormData } from './fixtures.js'
6
-
7
- const dataPreviewStyle = {
8
- margin: 0,
9
- padding: 'var(--space-s)',
10
- background: 'var(--color-secondary)',
11
- color: 'var(--text-default-color, CanvasText)',
12
- borderRadius: 'var(--space-xs)',
13
- border: '1px solid var(--separator-primary)',
14
- fontSize: 12,
15
- overflow: 'auto',
16
- }
17
-
18
- export default {
19
- title: 'Booking/Forms/PublicBookingForm',
20
- component: Form,
21
- parameters: {
22
- layout: 'padded',
23
- docs: {
24
- story: {
25
- inline: true,
26
- height: 'auto',
27
- },
28
- },
29
- },
30
- }
31
-
32
- function PublicBookingFormShell ({ initial = {} }) {
33
- const [preview, setPreview] = useState(initial)
34
-
35
- return (
36
- <View
37
- surface="primary"
38
- inset="m"
39
- roundness="m"
40
- gap="m"
41
- style={{ maxWidth: 480, width: '100%', fontFamily: 'system-ui, sans-serif' }}
42
- >
43
- <Form
44
- id="storybook/booking/public-booking"
45
- schemaId={publicBookingFormMeta.schemaId}
46
- fields={publicBookingResource.fields}
47
- defaultData={initial}
48
- onChange={({ data }) => setPreview(data)}
49
- onSubmit={(data) => setPreview(data)}
50
- gap="m"
51
- >
52
- <FieldFactory />
53
- <Button type="submit" variant="cta">
54
- Request booking
55
- </Button>
56
- </Form>
57
- <pre style={dataPreviewStyle}>{JSON.stringify(preview, null, 2)}</pre>
58
- </View>
59
- )
60
- }
61
-
62
- export const Empty = () => <PublicBookingFormShell />
63
-
64
- export const Prefilled = () => (
65
- <PublicBookingFormShell initial={samplePublicBookingFormData} />
66
- )
@@ -1,78 +0,0 @@
1
- import React from 'react'
2
- import PublicBookingPage from '../public-booking.page.jsx'
3
- import { withMockSdk } from './decorators.jsx'
4
- import { useAdvancePublicBookingStep } from './story-helpers.jsx'
5
- import { sampleProviderContact } from './fixtures.js'
6
-
7
- export default {
8
- title: 'Booking/Pages/PublicBookingPage',
9
- component: PublicBookingPage,
10
- decorators: [withMockSdk],
11
- parameters: {
12
- layout: 'fullscreen',
13
- docs: {
14
- story: {
15
- inline: false,
16
- height: '720px',
17
- },
18
- },
19
- },
20
- }
21
-
22
- const pageArgs = {
23
- providerSlug: sampleProviderContact.workspaceId,
24
- }
25
-
26
- export const ServiceSelection = {
27
- args: pageArgs,
28
- parameters: {
29
- mockSdk: {},
30
- },
31
- }
32
-
33
- function CalendarStepStory () {
34
- useAdvancePublicBookingStep('pick-slot')
35
- return <PublicBookingPage providerSlug={pageArgs.providerSlug} />
36
- }
37
-
38
- export const CalendarAndSlots = {
39
- render: () => <CalendarStepStory />,
40
- args: pageArgs,
41
- parameters: {
42
- mockSdk: {},
43
- },
44
- }
45
-
46
- function ClientDetailsStepStory () {
47
- useAdvancePublicBookingStep('fill-form')
48
- return <PublicBookingPage providerSlug={pageArgs.providerSlug} />
49
- }
50
-
51
- export const ClientDetailsForm = {
52
- render: () => <ClientDetailsStepStory />,
53
- args: pageArgs,
54
- parameters: {
55
- mockSdk: {},
56
- },
57
- }
58
-
59
- export const NoServices = {
60
- args: pageArgs,
61
- parameters: {
62
- mockSdk: {
63
- publicServices: {
64
- services: [],
65
- contact: sampleProviderContact,
66
- },
67
- },
68
- },
69
- }
70
-
71
- export const ServicesLoadError = {
72
- args: pageArgs,
73
- parameters: {
74
- mockSdk: {
75
- '@ossy/booking/actions/get-services': () => Promise.reject(new Error('Provider unavailable')),
76
- },
77
- },
78
- }
@@ -1,52 +0,0 @@
1
- import React from 'react'
2
- import { View } from '@ossy/design-system'
3
- import { ServiceCard } from '../ServiceCard.jsx'
4
- import { sampleServices } from './fixtures.js'
5
-
6
- export default {
7
- title: 'Booking/Components/ServiceCard',
8
- component: ServiceCard,
9
- parameters: {
10
- layout: 'padded',
11
- docs: {
12
- story: {
13
- inline: true,
14
- height: 'auto',
15
- },
16
- },
17
- },
18
- }
19
-
20
- const cardShell = (props) => (
21
- <View style={{ maxWidth: 520, width: '100%' }}>
22
- <ServiceCard {...props} />
23
- </View>
24
- )
25
-
26
- export const Default = () => cardShell(sampleServices[0])
27
-
28
- export const FreeService = () =>
29
- cardShell({
30
- ...sampleServices[0],
31
- name: 'Intro call',
32
- price: 0,
33
- duration: 30,
34
- description: 'A short complimentary introduction.',
35
- })
36
-
37
- export const Inactive = () => cardShell(sampleServices[2])
38
-
39
- export const LongDescription = () =>
40
- cardShell({
41
- ...sampleServices[0],
42
- description:
43
- 'A comprehensive first meeting covering goals, constraints, timeline, and deliverables. We leave with a shared understanding of scope and a proposed next step.',
44
- })
45
-
46
- export const AllVariants = () => (
47
- <View gap="m" style={{ maxWidth: 520, width: '100%' }}>
48
- {sampleServices.map((service) => (
49
- <ServiceCard key={service.name} {...service} />
50
- ))}
51
- </View>
52
- )
@@ -1,52 +0,0 @@
1
- import React from 'react'
2
- import { View } from '@ossy/design-system'
3
- import ServiceDetailComponent from '../service-detail.component.jsx'
4
- import { sampleServiceResource } from './fixtures.js'
5
-
6
- export default {
7
- title: 'Booking/Components/ServiceDetail',
8
- component: ServiceDetailComponent,
9
- parameters: {
10
- layout: 'padded',
11
- docs: {
12
- story: {
13
- inline: true,
14
- height: 'auto',
15
- },
16
- },
17
- },
18
- }
19
-
20
- const detailShell = (resource) => (
21
- <View surface="primary" roundness="m" style={{ maxWidth: 560, width: '100%' }}>
22
- <ServiceDetailComponent resource={resource} />
23
- </View>
24
- )
25
-
26
- export const ActiveService = () => detailShell(sampleServiceResource)
27
-
28
- export const FreeService = () =>
29
- detailShell({
30
- ...sampleServiceResource,
31
- name: 'Intro call',
32
- content: {
33
- ...sampleServiceResource.content,
34
- name: 'Intro call',
35
- price: 0,
36
- duration: 30,
37
- description: 'A short complimentary introduction call.',
38
- },
39
- })
40
-
41
- export const InactiveService = () =>
42
- detailShell({
43
- ...sampleServiceResource,
44
- content: {
45
- ...sampleServiceResource.content,
46
- name: 'Legacy workshop',
47
- active: false,
48
- price: 0,
49
- duration: 120,
50
- description: 'No longer offered to new clients.',
51
- },
52
- })
@@ -1,64 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { Button, View, Form, FieldFactory } from '@ossy/design-system'
3
- import serviceResource from '../service.schema.js'
4
- import { metadata as serviceFormMeta } from '../service.form.js'
5
- import { sampleServiceFormData } from './fixtures.js'
6
-
7
- const dataPreviewStyle = {
8
- margin: 0,
9
- padding: 'var(--space-s)',
10
- background: 'var(--color-secondary)',
11
- color: 'var(--text-default-color, CanvasText)',
12
- borderRadius: 'var(--space-xs)',
13
- border: '1px solid var(--separator-primary)',
14
- fontSize: 12,
15
- overflow: 'auto',
16
- }
17
-
18
- export default {
19
- title: 'Booking/Forms/ServiceForm',
20
- component: Form,
21
- parameters: {
22
- layout: 'padded',
23
- docs: {
24
- story: {
25
- inline: true,
26
- height: 'auto',
27
- },
28
- },
29
- },
30
- }
31
-
32
- function ServiceFormShell ({ initial = sampleServiceFormData }) {
33
- const [preview, setPreview] = useState(initial)
34
-
35
- return (
36
- <View
37
- surface="primary"
38
- inset="m"
39
- roundness="m"
40
- gap="m"
41
- style={{ maxWidth: 480, width: '100%', fontFamily: 'system-ui, sans-serif' }}
42
- >
43
- <Form
44
- id="storybook/booking/service"
45
- schemaId={serviceFormMeta.schemaId}
46
- fields={serviceResource.fields}
47
- defaultData={initial}
48
- onChange={({ data }) => setPreview(data)}
49
- onSubmit={(data) => setPreview(data)}
50
- gap="m"
51
- >
52
- <FieldFactory />
53
- <Button type="submit" variant="cta">
54
- Save service
55
- </Button>
56
- </Form>
57
- <pre style={dataPreviewStyle}>{JSON.stringify(preview, null, 2)}</pre>
58
- </View>
59
- )
60
- }
61
-
62
- export const Default = () => <ServiceFormShell />
63
-
64
- export const Empty = () => <ServiceFormShell initial={{ active: true, currency: 'SEK' }} />
@@ -1,53 +0,0 @@
1
- import React from 'react'
2
- import ServicesPage from '../services.page.jsx'
3
- import { withMockSdk } from './decorators.jsx'
4
- import { useOpenServicesCreateForm } from './story-helpers.jsx'
5
-
6
- export default {
7
- title: 'Booking/Pages/ServicesPage',
8
- component: ServicesPage,
9
- decorators: [withMockSdk],
10
- parameters: {
11
- layout: 'fullscreen',
12
- docs: {
13
- story: {
14
- inline: false,
15
- height: '640px',
16
- },
17
- },
18
- },
19
- }
20
-
21
- export const ListWithServices = {
22
- parameters: {
23
- mockSdk: {},
24
- },
25
- }
26
-
27
- export const EmptyList = {
28
- parameters: {
29
- mockSdk: {
30
- services: [],
31
- },
32
- },
33
- }
34
-
35
- function CreateFormOpenStory () {
36
- useOpenServicesCreateForm()
37
- return <ServicesPage />
38
- }
39
-
40
- export const CreateFormOpen = {
41
- render: () => <CreateFormOpenStory />,
42
- parameters: {
43
- mockSdk: {},
44
- },
45
- }
46
-
47
- export const LoadError = {
48
- parameters: {
49
- mockSdk: {
50
- '@ossy/booking/actions/get-services': () => Promise.reject(new Error('Failed to load services')),
51
- },
52
- },
53
- }
@@ -1,68 +0,0 @@
1
- import {
2
- createMockRouterDecorator,
3
- createMockSdkDecorator,
4
- resolveMockActionId,
5
- } from '../../../../.storybook/decorators/index.jsx'
6
- import {
7
- buildSampleSlots,
8
- sampleAvailabilitySettings,
9
- sampleBookings,
10
- samplePublicServicesResponse,
11
- sampleServicesWithIds,
12
- } from './fixtures.js'
13
-
14
- export { resolveMockActionId }
15
-
16
- export function createMockSdkHandlers (overrides = {}) {
17
- return {
18
- '@ossy/booking/actions/get-services': async (payload) => {
19
- if (payload?.providerSlug) {
20
- if ('publicServices' in overrides) return overrides.publicServices
21
- return samplePublicServicesResponse
22
- }
23
- if ('services' in overrides) return overrides.services
24
- return sampleServicesWithIds
25
- },
26
- '@ossy/booking/actions/list': async () => (
27
- 'bookings' in overrides ? overrides.bookings : sampleBookings
28
- ),
29
- '@ossy/booking/actions/get-available-slots': async () => (
30
- 'slots' in overrides ? overrides.slots : buildSampleSlots()
31
- ),
32
- '@ossy/booking/actions/get-availability': async () => (
33
- 'availability' in overrides ? overrides.availability : sampleAvailabilitySettings
34
- ),
35
- '@ossy/booking/actions/create-service': async () => ({}),
36
- '@ossy/booking/actions/update-service': async () => ({}),
37
- '@ossy/booking/actions/delete-service': async () => ({}),
38
- '@ossy/booking/actions/save-availability': async () => ({}),
39
- '@ossy/booking/actions/create': async () => ({}),
40
- '@ossy/booking/actions/confirm': async () => ({}),
41
- '@ossy/booking/actions/decline': async () => ({}),
42
- '@ossy/booking/actions/cancel': async () => ({}),
43
- }
44
- }
45
-
46
- const defaultMockRouter = {
47
- href: 'http://localhost/en/bookings',
48
- pages: [],
49
- language: 'en',
50
- defaultLanguage: 'en',
51
- supportedLanguages: ['en', 'sv'],
52
- params: {},
53
- searchParams: {},
54
- navigate: () => {},
55
- getHref: ({ id, params = {} } = {}) => {
56
- if (id === 'booking/booking-detail' && params.bookingId) {
57
- return `/en/bookings/${params.bookingId}`
58
- }
59
- if (id === 'public-booking' && params.providerSlug) {
60
- return `/en/book/${params.providerSlug}`
61
- }
62
- return `/en/${String(id).replace(/\//g, '-')}`
63
- },
64
- back: () => {},
65
- }
66
-
67
- export const withMockSdk = createMockSdkDecorator(createMockSdkHandlers)
68
- export const withMockRouter = createMockRouterDecorator(defaultMockRouter)
@@ -1,85 +0,0 @@
1
- import { useEffect } from 'react'
2
-
3
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
4
-
5
- async function waitForSelector (selector, { attempts = 30, interval = 100 } = {}) {
6
- for (let attempt = 0; attempt < attempts; attempt += 1) {
7
- const element = document.querySelector(selector)
8
- if (element) return element
9
- await sleep(interval)
10
- }
11
- return null
12
- }
13
-
14
- export function useOpenServicesCreateForm () {
15
- useEffect(() => {
16
- let cancelled = false
17
-
18
- async function openForm () {
19
- await sleep(200)
20
- if (cancelled) return
21
- const button = await waitForSelector('[data-action="@ossy/booking/actions/open-create-service"]')
22
- button?.click()
23
- }
24
-
25
- openForm()
26
- return () => { cancelled = true }
27
- }, [])
28
- }
29
-
30
- export function useSelectBookingsTab (tabId) {
31
- useEffect(() => {
32
- let cancelled = false
33
-
34
- async function selectTab () {
35
- await sleep(300)
36
- if (cancelled) return
37
- const tabsRoot = await waitForSelector('[data-tabs]')
38
- if (!tabsRoot) return
39
- const tabIndex = { pending: 0, confirmed: 1, all: 2 }[tabId] ?? 0
40
- tabsRoot.querySelectorAll('button')[tabIndex]?.click()
41
- }
42
-
43
- selectTab()
44
- return () => { cancelled = true }
45
- }, [tabId])
46
- }
47
-
48
- /**
49
- * Advances PublicBookingPage through its wizard for calendar / form step stories.
50
- * @param {'pick-slot' | 'fill-form'} targetStep
51
- */
52
- export function useAdvancePublicBookingStep (targetStep) {
53
- useEffect(() => {
54
- let cancelled = false
55
-
56
- async function advance () {
57
- if (targetStep === 'pick-service') return
58
-
59
- await sleep(300)
60
- if (cancelled) return
61
-
62
- const serviceButton = await waitForSelector('[data-action="public-booking/pick-service"]')
63
- serviceButton?.click()
64
-
65
- await sleep(150)
66
- document.querySelector('[data-action="public-booking/continue"]')?.click()
67
-
68
- if (targetStep === 'pick-slot') return
69
-
70
- await sleep(400)
71
- const dateButton = await waitForSelector('[data-action="public-booking/pick-date"]:not([disabled])')
72
- dateButton?.click()
73
-
74
- await sleep(200)
75
- const slotButton = await waitForSelector('[data-action="public-booking/pick-slot"]')
76
- slotButton?.click()
77
-
78
- await sleep(150)
79
- document.querySelector('[data-action="public-booking/continue"]')?.click()
80
- }
81
-
82
- advance()
83
- return () => { cancelled = true }
84
- }, [targetStep])
85
- }