@ossy/booking 1.15.7 → 1.16.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 (46) hide show
  1. package/README.md +26 -19
  2. package/package.json +7 -7
  3. package/src/AvailabilityEditor.jsx +22 -15
  4. package/src/Definition.js +1 -23
  5. package/src/SalesSection.jsx +9 -4
  6. package/src/availability-setup.page.jsx +94 -126
  7. package/src/booking-card.component.jsx +30 -33
  8. package/src/booking-detail.page.jsx +65 -204
  9. package/src/booking-resources.js +10 -0
  10. package/src/bookings.page.jsx +54 -56
  11. package/src/cancel-booking.action.js +1 -61
  12. package/src/cancel-booking.task.js +60 -0
  13. package/src/confirm-booking.action.js +1 -82
  14. package/src/confirm-booking.task.js +81 -0
  15. package/src/create-booking.action.js +1 -168
  16. package/src/create-booking.task.js +170 -0
  17. package/src/create-service.action.js +1 -0
  18. package/src/create-service.task.js +46 -0
  19. package/src/decline-booking.action.js +1 -68
  20. package/src/decline-booking.task.js +67 -0
  21. package/src/delete-service.action.js +1 -0
  22. package/src/delete-service.task.js +41 -0
  23. package/src/en.translations.json +246 -0
  24. package/src/get-availability.action.js +1 -52
  25. package/src/get-availability.task.js +51 -0
  26. package/src/get-available-slots.action.js +1 -89
  27. package/src/get-available-slots.task.js +99 -0
  28. package/src/get-services.action.js +1 -0
  29. package/src/get-services.task.js +37 -0
  30. package/src/home.page.jsx +48 -5
  31. package/src/index.js +20 -1
  32. package/src/invoke-error-message.js +10 -0
  33. package/src/list-bookings.action.js +1 -36
  34. package/src/list-bookings.task.js +35 -0
  35. package/src/locations.js +14 -0
  36. package/src/public-booking.page.jsx +200 -245
  37. package/src/save-availability.action.js +1 -73
  38. package/src/save-availability.task.js +74 -0
  39. package/src/send-booking-reminder.task.js +2 -2
  40. package/src/services.page.jsx +167 -46
  41. package/src/sv.translations.json +246 -0
  42. package/src/update-service.action.js +1 -0
  43. package/src/update-service.task.js +52 -0
  44. package/src/BookingForm.jsx +0 -82
  45. package/src/availability.page.jsx +0 -41
  46. package/src/booking-page.page.jsx +0 -42
@@ -1,52 +1 @@
1
- import { ResourcesQueries } from '@ossy/resources'
2
-
3
- export const id = 'booking/get-availability'
4
- export const access = 'workspace'
5
-
6
- export async function run({ payload, req, log }) {
7
- const workspaceId = req?.workspaceId
8
-
9
- if (!workspaceId) {
10
- throw Object.assign(new Error('workspaceId is required'), { status: 400 })
11
- }
12
-
13
- log?.info(`[booking/get-availability] Loading availability for workspace ${workspaceId}`)
14
-
15
- const resources = await ResourcesQueries.GetResources({
16
- type: '@ossy/booking/availability',
17
- belongsTo: workspaceId,
18
- })
19
-
20
- if (!resources.length) {
21
- log?.info('[booking/get-availability] No availability configured')
22
- return null
23
- }
24
-
25
- // New format: single resource with weeklyWindows array
26
- const configResource = resources.find(r => Array.isArray(r.content?.weeklyWindows))
27
- if (configResource) {
28
- return {
29
- id: configResource.id,
30
- weeklyWindows: configResource.content.weeklyWindows ?? [],
31
- sessionDurations: configResource.content.sessionDurations ?? [60],
32
- bufferMinutes: configResource.content.bufferMinutes ?? 0,
33
- timezone: configResource.content.timezone ?? 'Europe/Stockholm',
34
- }
35
- }
36
-
37
- // Legacy format: one resource per day — reconstruct config shape
38
- const firstResource = resources[0]
39
- return {
40
- id: firstResource.id,
41
- weeklyWindows: resources
42
- .map(r => ({
43
- dayOfWeek: r.content?.dayOfWeek,
44
- startTime: r.content?.startTime ?? '09:00',
45
- endTime: r.content?.endTime ?? '17:00',
46
- }))
47
- .filter(w => w.dayOfWeek != null),
48
- sessionDurations: [firstResource?.content?.sessionDuration ?? 60],
49
- bufferMinutes: firstResource?.content?.bufferMinutes ?? 0,
50
- timezone: firstResource?.content?.timezone ?? 'Europe/Stockholm',
51
- }
52
- }
1
+ export const metadata = { id: 'booking/get-availability', access: 'workspace' }
@@ -0,0 +1,51 @@
1
+ import { getBookingResources } from './booking-resources.js'
2
+
3
+ export const metadata = { id: 'booking/get-availability' }
4
+
5
+ export async function run({ payload, req, log }) {
6
+ const workspaceId = req?.workspaceId
7
+
8
+ if (!workspaceId) {
9
+ throw Object.assign(new Error('workspaceId is required'), { status: 400 })
10
+ }
11
+
12
+ log?.info(`[booking/get-availability] Loading availability for workspace ${workspaceId}`)
13
+
14
+ const resources = await getBookingResources({
15
+ type: '@ossy/booking/availability',
16
+ belongsTo: workspaceId,
17
+ })
18
+
19
+ if (!resources.length) {
20
+ log?.info('[booking/get-availability] No availability configured')
21
+ return null
22
+ }
23
+
24
+ // New format: single resource with weeklyWindows array
25
+ const configResource = resources.find(r => Array.isArray(r.content?.weeklyWindows))
26
+ if (configResource) {
27
+ return {
28
+ id: configResource.id,
29
+ weeklyWindows: configResource.content.weeklyWindows ?? [],
30
+ sessionDurations: configResource.content.sessionDurations ?? [60],
31
+ bufferMinutes: configResource.content.bufferMinutes ?? 0,
32
+ timezone: configResource.content.timezone ?? 'Europe/Stockholm',
33
+ }
34
+ }
35
+
36
+ // Legacy format: one resource per day — reconstruct config shape
37
+ const firstResource = resources[0]
38
+ return {
39
+ id: firstResource.id,
40
+ weeklyWindows: resources
41
+ .map(r => ({
42
+ dayOfWeek: r.content?.dayOfWeek,
43
+ startTime: r.content?.startTime ?? '09:00',
44
+ endTime: r.content?.endTime ?? '17:00',
45
+ }))
46
+ .filter(w => w.dayOfWeek != null),
47
+ sessionDurations: [firstResource?.content?.sessionDuration ?? 60],
48
+ bufferMinutes: firstResource?.content?.bufferMinutes ?? 0,
49
+ timezone: firstResource?.content?.timezone ?? 'Europe/Stockholm',
50
+ }
51
+ }
@@ -1,89 +1 @@
1
- import { ResourcesQueries } from '@ossy/resources'
2
- import { getAvailableSlots } from './availability-engine.js'
3
-
4
- export const id = 'booking/get-available-slots'
5
- export const access = 'public'
6
-
7
- const DEFAULT_DURATION = 60 // minutes
8
- const DEFAULT_RANGE_DAYS = 30
9
-
10
- export async function run({ payload, log }) {
11
- const { consultantId, duration, from, to } = payload ?? {}
12
-
13
- if (!consultantId) {
14
- throw Object.assign(new Error('consultantId is required'), { status: 400 })
15
- }
16
-
17
- log?.info(`[booking/get-available-slots] Loading slots for consultant ${consultantId}`)
18
-
19
- const availabilityResources = await ResourcesQueries.GetResources({
20
- type: '@ossy/booking/availability',
21
- belongsTo: consultantId,
22
- })
23
-
24
- if (!availabilityResources.length) {
25
- log?.info('[booking/get-available-slots] No availability resources found, returning []')
26
- return []
27
- }
28
-
29
- // New format: single resource with weeklyWindows array in content
30
- const configResource = availabilityResources.find(r => Array.isArray(r.content?.weeklyWindows))
31
- let weeklyWindows, firstResource
32
-
33
- if (configResource) {
34
- weeklyWindows = configResource.content.weeklyWindows ?? []
35
- firstResource = configResource
36
- } else {
37
- // Legacy format: one resource per day
38
- firstResource = availabilityResources[0]
39
- weeklyWindows = availabilityResources
40
- .map(r => ({
41
- dayOfWeek: r.content?.dayOfWeek,
42
- startTime: r.content?.startTime ?? '09:00',
43
- endTime: r.content?.endTime ?? '17:00',
44
- }))
45
- .filter(w => w.dayOfWeek != null)
46
- }
47
-
48
- const availability = {
49
- weeklyWindows,
50
- bufferMinutes: firstResource?.content?.bufferMinutes ?? 0,
51
- blackoutDates: firstResource?.content?.blackoutDates ?? [],
52
- timezone: firstResource?.content?.timezone ?? 'Europe/Stockholm',
53
- }
54
-
55
- const fromDate = from ? new Date(from) : new Date()
56
- const toDate = to
57
- ? new Date(to)
58
- : new Date(Date.now() + DEFAULT_RANGE_DAYS * 24 * 60 * 60 * 1000)
59
-
60
- const allBookingResources = await ResourcesQueries.GetResources({
61
- type: '@ossy/booking/booking',
62
- belongsTo: consultantId,
63
- })
64
-
65
- const confirmedBookings = allBookingResources
66
- .filter(b => b.content?.status === 'confirmed')
67
- .map(b => ({
68
- startAt: b.content.startsAt,
69
- endAt: b.content.endsAt,
70
- status: 'confirmed',
71
- }))
72
-
73
- const sessionDurations = configResource
74
- ? (configResource.content.sessionDurations ?? [DEFAULT_DURATION])
75
- : [firstResource?.content?.sessionDuration ?? DEFAULT_DURATION]
76
-
77
- const slotDuration = duration ?? sessionDurations[0] ?? DEFAULT_DURATION
78
-
79
- const slots = getAvailableSlots({
80
- availability,
81
- bookings: confirmedBookings,
82
- fromDate,
83
- toDate,
84
- duration: slotDuration,
85
- })
86
-
87
- log?.info(`[booking/get-available-slots] Returning ${slots.length} slots`)
88
- return slots
89
- }
1
+ export const metadata = { id: 'booking/get-available-slots', access: 'public' }
@@ -0,0 +1,99 @@
1
+ import { getAvailableSlots } from './availability-engine.js'
2
+ import { getBookingResources } from './booking-resources.js'
3
+
4
+ export const metadata = { id: 'booking/get-available-slots' }
5
+
6
+ const DEFAULT_DURATION = 60 // minutes
7
+ const DEFAULT_RANGE_DAYS = 30
8
+
9
+ export async function run ({ payload, log }) {
10
+ const { consultantId, duration, serviceId, from, to } = payload ?? {}
11
+
12
+ if (!consultantId) {
13
+ throw Object.assign(new Error('consultantId is required'), { status: 400 })
14
+ }
15
+
16
+ log?.info(`[booking/get-available-slots] Loading slots for consultant ${consultantId}`)
17
+
18
+ let slotDuration = duration
19
+
20
+ if (serviceId) {
21
+ const serviceResources = await getBookingResources({
22
+ type: '@ossy/booking/service',
23
+ belongsTo: consultantId,
24
+ })
25
+ const service = serviceResources.find(r => r.id === serviceId)
26
+ if (service?.content?.duration) {
27
+ slotDuration = service.content.duration
28
+ }
29
+ }
30
+
31
+ const availabilityResources = await getBookingResources({
32
+ type: '@ossy/booking/availability',
33
+ belongsTo: consultantId,
34
+ })
35
+
36
+ if (!availabilityResources.length) {
37
+ log?.info('[booking/get-available-slots] No availability resources found, returning []')
38
+ return []
39
+ }
40
+
41
+ const configResource = availabilityResources.find(r => Array.isArray(r.content?.weeklyWindows))
42
+ let weeklyWindows, firstResource
43
+
44
+ if (configResource) {
45
+ weeklyWindows = configResource.content.weeklyWindows ?? []
46
+ firstResource = configResource
47
+ } else {
48
+ firstResource = availabilityResources[0]
49
+ weeklyWindows = availabilityResources
50
+ .map(r => ({
51
+ dayOfWeek: r.content?.dayOfWeek,
52
+ startTime: r.content?.startTime ?? '09:00',
53
+ endTime: r.content?.endTime ?? '17:00',
54
+ }))
55
+ .filter(w => w.dayOfWeek != null)
56
+ }
57
+
58
+ const availability = {
59
+ weeklyWindows,
60
+ bufferMinutes: firstResource?.content?.bufferMinutes ?? 0,
61
+ blackoutDates: firstResource?.content?.blackoutDates ?? [],
62
+ timezone: firstResource?.content?.timezone ?? 'Europe/Stockholm',
63
+ }
64
+
65
+ const fromDate = from ? new Date(from) : new Date()
66
+ const toDate = to
67
+ ? new Date(to)
68
+ : new Date(Date.now() + DEFAULT_RANGE_DAYS * 24 * 60 * 60 * 1000)
69
+
70
+ const allBookingResources = await getBookingResources({
71
+ type: '@ossy/booking/booking',
72
+ belongsTo: consultantId,
73
+ })
74
+
75
+ const confirmedBookings = allBookingResources
76
+ .filter(b => b.content?.status === 'confirmed')
77
+ .map(b => ({
78
+ startAt: b.content.startsAt,
79
+ endAt: b.content.endsAt,
80
+ status: 'confirmed',
81
+ }))
82
+
83
+ const sessionDurations = configResource
84
+ ? (configResource.content.sessionDurations ?? [DEFAULT_DURATION])
85
+ : [firstResource?.content?.sessionDuration ?? DEFAULT_DURATION]
86
+
87
+ const resolvedDuration = slotDuration ?? sessionDurations[0] ?? DEFAULT_DURATION
88
+
89
+ const slots = getAvailableSlots({
90
+ availability,
91
+ bookings: confirmedBookings,
92
+ fromDate,
93
+ toDate,
94
+ duration: resolvedDuration,
95
+ })
96
+
97
+ log?.info(`[booking/get-available-slots] Returning ${slots.length} slots`)
98
+ return slots
99
+ }
@@ -0,0 +1 @@
1
+ export const metadata = { id: 'booking/get-services', access: 'public' }
@@ -0,0 +1,37 @@
1
+ import { getBookingResources } from './booking-resources.js'
2
+
3
+ export const metadata = { id: 'booking/get-services' }
4
+
5
+ export async function run ({ payload, req, log }) {
6
+ const workspaceId = payload?.consultantId ?? payload?.workspaceId ?? req?.workspaceId
7
+
8
+ if (!workspaceId) {
9
+ throw Object.assign(new Error('consultantId or workspaceId is required'), { status: 400 })
10
+ }
11
+
12
+ log?.info(`[booking/get-services] Loading services for ${workspaceId}`)
13
+
14
+ const resources = await getBookingResources({
15
+ type: '@ossy/booking/service',
16
+ belongsTo: workspaceId,
17
+ })
18
+
19
+ const includeInactive = payload?.includeInactive === true
20
+
21
+ const services = resources
22
+ .map(r => ({
23
+ id: r.id,
24
+ name: r.content?.name ?? r.name,
25
+ duration: r.content?.duration ?? 60,
26
+ price: r.content?.price ?? 0,
27
+ currency: r.content?.currency ?? 'SEK',
28
+ description: r.content?.description ?? '',
29
+ active: r.content?.active !== false,
30
+ }))
31
+ .filter(s => includeInactive || s.active)
32
+ .sort((a, b) => (a.name || '').localeCompare(b.name || ''))
33
+
34
+ log?.info(`[booking/get-services] Returning ${services.length} service(s)`)
35
+
36
+ return services
37
+ }
package/src/home.page.jsx CHANGED
@@ -1,13 +1,56 @@
1
1
  import React from 'react'
2
- import { cover } from './cover.js'
3
- import { features } from './features.js'
2
+ import { useLocale } from '@ossy/design-system'
4
3
  import SalesSection from './SalesSection.jsx'
5
4
 
6
5
  export const metadata = {
7
6
  id: 'booking/home',
8
- title: 'Bokningskalender – Ossy',
9
- description: 'Låt kunder boka tid direkt. Dela en länk, kunder väljer tid, du fokuserar på jobbet.',
10
7
  path: { sv: '/boka', en: '/booking' },
11
8
  }
12
9
 
13
- export default () => <SalesSection cover={cover} features={features} />
10
+ function useBookingHomeContent () {
11
+ const { t } = useLocale()
12
+
13
+ const cover = {
14
+ title: t('booking.home.cover.title'),
15
+ text: t('booking.home.cover.text'),
16
+ actions: [
17
+ { label: t('booking.home.cover.ctaPrimary'), variant: 'cta', href: '/sign-up' },
18
+ { label: t('booking.home.cover.ctaSecondary'), variant: 'secondary', href: '/boka/demo' },
19
+ ],
20
+ }
21
+
22
+ const features = [
23
+ {
24
+ title: t('booking.home.features.page.title'),
25
+ icon: 'share',
26
+ text: t('booking.home.features.page.text'),
27
+ },
28
+ {
29
+ title: t('booking.home.features.confirmations.title'),
30
+ icon: 'check-circle',
31
+ text: t('booking.home.features.confirmations.text'),
32
+ },
33
+ {
34
+ title: t('booking.home.features.availability.title'),
35
+ icon: 'calendar',
36
+ text: t('booking.home.features.availability.text'),
37
+ },
38
+ {
39
+ title: t('booking.home.features.email.title'),
40
+ icon: 'mail',
41
+ text: t('booking.home.features.email.text'),
42
+ },
43
+ {
44
+ title: t('booking.home.features.doubleBook.title'),
45
+ icon: 'warning',
46
+ text: t('booking.home.features.doubleBook.text'),
47
+ },
48
+ ]
49
+
50
+ return { cover, features }
51
+ }
52
+
53
+ export default function BookingHomePage () {
54
+ const { cover, features } = useBookingHomeContent()
55
+ return <SalesSection cover={cover} features={features} />
56
+ }
package/src/index.js CHANGED
@@ -1,7 +1,26 @@
1
1
  export { Definition } from './Definition.js'
2
+ export {
3
+ location,
4
+ services,
5
+ bookings,
6
+ availability,
7
+ locationByType,
8
+ } from './locations.js'
9
+ export { metadata as CreateBooking } from './create-booking.action.js'
10
+ export { metadata as CreateService } from './create-service.action.js'
11
+ export { metadata as UpdateService } from './update-service.action.js'
12
+ export { metadata as DeleteService } from './delete-service.action.js'
13
+ export { metadata as SaveAvailability } from './save-availability.action.js'
14
+ export { metadata as ConfirmBooking } from './confirm-booking.action.js'
15
+ export { metadata as DeclineBooking } from './decline-booking.action.js'
16
+ export { metadata as CancelBooking } from './cancel-booking.action.js'
17
+ export { metadata as GetAvailableSlots } from './get-available-slots.action.js'
18
+ export { metadata as GetAvailability } from './get-availability.action.js'
19
+ export { metadata as ListBookings } from './list-bookings.action.js'
20
+ export { metadata as GetServices } from './get-services.action.js'
21
+ export { invokeErrorMessage } from './invoke-error-message.js'
2
22
 
3
23
  export { ServiceCard } from './ServiceCard.jsx'
4
- export { BookingForm } from './BookingForm.jsx'
5
24
  export { BookingList } from './BookingList.jsx'
6
25
  export { AvailabilityEditor } from './AvailabilityEditor.jsx'
7
26
 
@@ -0,0 +1,10 @@
1
+ /** Normalize errors from `sdk.invoke` (Response, string, or Error). */
2
+ export async function invokeErrorMessage(err, fallback) {
3
+ if (typeof err === 'string') return err
4
+ if (err instanceof Response) {
5
+ const data = await err.json().catch(() => ({}))
6
+ return data?.message ?? data?.error ?? fallback
7
+ }
8
+ if (err instanceof Error) return err.message ?? fallback
9
+ return fallback
10
+ }
@@ -1,36 +1 @@
1
- import { ResourcesQueries } from '@ossy/resources'
2
-
3
- export const id = 'booking/list'
4
- export const access = 'workspace'
5
-
6
- export async function run({ payload, req, log }) {
7
- const workspaceId = req?.workspaceId
8
- const status = payload?.status ?? null
9
-
10
- log?.info(`[booking/list] Loading bookings for workspace ${workspaceId}`, { status })
11
-
12
- const resources = await ResourcesQueries.GetResources({
13
- type: '@ossy/booking/booking',
14
- belongsTo: workspaceId,
15
- })
16
-
17
- let bookings = resources.map(r => ({
18
- id: r.id,
19
- ...r.content,
20
- createdAt: r.createdAt,
21
- }))
22
-
23
- if (status) {
24
- bookings = bookings.filter(b => b.status === status)
25
- }
26
-
27
- bookings.sort((a, b) => {
28
- const dateA = new Date(a.startsAt ?? a.createdAt ?? 0).getTime()
29
- const dateB = new Date(b.startsAt ?? b.createdAt ?? 0).getTime()
30
- return dateB - dateA
31
- })
32
-
33
- log?.info(`[booking/list] Found ${bookings.length} booking(s)`)
34
-
35
- return bookings
36
- }
1
+ export const metadata = { id: 'booking/list', access: 'workspace' }
@@ -0,0 +1,35 @@
1
+ import { getBookingResources } from './booking-resources.js'
2
+
3
+ export const metadata = { id: 'booking/list' }
4
+
5
+ export async function run({ payload, req, log }) {
6
+ const workspaceId = req?.workspaceId
7
+ const status = payload?.status ?? null
8
+
9
+ log?.info(`[booking/list] Loading bookings for workspace ${workspaceId}`, { status })
10
+
11
+ const resources = await getBookingResources({
12
+ type: '@ossy/booking/booking',
13
+ belongsTo: workspaceId,
14
+ })
15
+
16
+ let bookings = resources.map(r => ({
17
+ id: r.id,
18
+ ...r.content,
19
+ createdAt: r.createdAt,
20
+ }))
21
+
22
+ if (status) {
23
+ bookings = bookings.filter(b => b.status === status)
24
+ }
25
+
26
+ bookings.sort((a, b) => {
27
+ const dateA = new Date(a.startsAt ?? a.createdAt ?? 0).getTime()
28
+ const dateB = new Date(b.startsAt ?? b.createdAt ?? 0).getTime()
29
+ return dateB - dateA
30
+ })
31
+
32
+ log?.info(`[booking/list] Found ${bookings.length} booking(s)`)
33
+
34
+ return bookings
35
+ }
@@ -0,0 +1,14 @@
1
+ /** Booking feature root. Sub-areas use the paths below. */
2
+ export const location = '/@ossy/booking/'
3
+
4
+ /** Canonical resource locations per area. */
5
+ export const services = '/@ossy/booking/services/'
6
+ export const bookings = '/@ossy/booking/bookings/'
7
+ export const availability = '/@ossy/booking/availability/'
8
+
9
+ /** Map resource type → canonical location (for create actions). */
10
+ export const locationByType = {
11
+ '@ossy/booking/service': services,
12
+ '@ossy/booking/booking': bookings,
13
+ '@ossy/booking/availability': availability,
14
+ }