@ossy/booking 1.16.0 → 1.16.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.
Files changed (42) hide show
  1. package/README.md +26 -19
  2. package/package.json +7 -7
  3. package/src/availability-setup.page.jsx +18 -17
  4. package/src/booking-card.component.jsx +6 -0
  5. package/src/booking-detail.page.jsx +28 -23
  6. package/src/booking-resources.js +10 -0
  7. package/src/bookings.page.jsx +27 -39
  8. package/src/cancel-booking.action.js +1 -61
  9. package/src/cancel-booking.task.js +60 -0
  10. package/src/confirm-booking.action.js +1 -82
  11. package/src/confirm-booking.task.js +81 -0
  12. package/src/create-booking.action.js +1 -168
  13. package/src/create-booking.task.js +170 -0
  14. package/src/create-service.action.js +1 -0
  15. package/src/create-service.task.js +46 -0
  16. package/src/decline-booking.action.js +1 -68
  17. package/src/decline-booking.task.js +67 -0
  18. package/src/delete-service.action.js +1 -0
  19. package/src/delete-service.task.js +41 -0
  20. package/src/en.translations.json +42 -9
  21. package/src/get-availability.action.js +1 -52
  22. package/src/get-availability.task.js +51 -0
  23. package/src/get-available-slots.action.js +1 -89
  24. package/src/get-available-slots.task.js +99 -0
  25. package/src/get-services.action.js +1 -0
  26. package/src/get-services.task.js +37 -0
  27. package/src/index.js +20 -1
  28. package/src/invoke-error-message.js +10 -0
  29. package/src/list-bookings.action.js +1 -36
  30. package/src/list-bookings.task.js +35 -0
  31. package/src/locations.js +14 -0
  32. package/src/public-booking.page.jsx +138 -42
  33. package/src/save-availability.action.js +1 -73
  34. package/src/save-availability.task.js +74 -0
  35. package/src/send-booking-reminder.task.js +2 -2
  36. package/src/services.page.jsx +157 -37
  37. package/src/sv.translations.json +42 -9
  38. package/src/update-service.action.js +1 -0
  39. package/src/update-service.task.js +52 -0
  40. package/src/BookingForm.jsx +0 -72
  41. package/src/availability.page.jsx +0 -43
  42. package/src/booking-page.page.jsx +0 -39
@@ -0,0 +1,41 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Resource, ResourcesEvents } from '@ossy/resources/server'
3
+ import { getBookingResources } from './booking-resources.js'
4
+
5
+ export const metadata = { id: 'booking/delete-service' }
6
+
7
+ export async function run ({ payload, req, log }) {
8
+ const workspaceId = req?.workspaceId
9
+ const { serviceId } = payload ?? {}
10
+
11
+ if (!workspaceId) {
12
+ throw Object.assign(new Error('workspaceId is required'), { status: 400 })
13
+ }
14
+ if (!serviceId) {
15
+ throw Object.assign(new Error('serviceId is required'), { status: 400 })
16
+ }
17
+
18
+ const existing = await getBookingResources({
19
+ type: '@ossy/booking/service',
20
+ belongsTo: workspaceId,
21
+ })
22
+
23
+ const resource = existing.find(r => r.id === serviceId)
24
+ if (!resource) {
25
+ throw Object.assign(new Error('Service not found'), { status: 404 })
26
+ }
27
+
28
+ log?.info(`[booking/delete-service] Deleting service ${serviceId}`)
29
+
30
+ await Aggregate.Of(Resource, serviceId)
31
+ .then(
32
+ Aggregate.Add(
33
+ ResourcesEvents.Deleted({
34
+ createdBy: req?.userId ?? 'system',
35
+ }),
36
+ ),
37
+ )
38
+ .then(Aggregate.Save())
39
+
40
+ return { id: serviceId, deleted: true }
41
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "booking.home.documentTitle": "Booking calendar – Ossy",
2
+ "booking/home.documentTitle": "Booking calendar – Ossy",
3
3
  "booking.home.cover.title": "Let clients book time directly",
4
4
  "booking.home.cover.text": "Share a link. Clients pick a slot. You focus on the work.",
5
5
  "booking.home.cover.ctaPrimary": "Get started free",
@@ -16,7 +16,7 @@
16
16
  "booking.home.features.doubleBook.text": "One calendar here, one link there — we keep track so the same slot isn't booked twice.",
17
17
  "booking.sales.overview": "Overview",
18
18
  "booking.sales.features": "Features",
19
- "booking.bookings.documentTitle": "Bookings",
19
+ "booking/bookings.documentTitle": "Bookings",
20
20
  "booking.bookings.titleSuffix": "Bookings",
21
21
  "booking.bookings.description": "Manage booking requests and confirmed meetings.",
22
22
  "booking.bookings.tab.pending": "Requests",
@@ -33,7 +33,7 @@
33
33
  "booking.bookings.errorConfirm": "Confirmation failed",
34
34
  "booking.bookings.errorDecline": "Decline failed",
35
35
  "booking.bookings.errorCancel": "Cancellation failed",
36
- "booking.bookingDetail.documentTitle": "Booking details",
36
+ "booking/booking-detail.documentTitle": "Booking details",
37
37
  "booking.bookingDetail.back": "← All bookings",
38
38
  "booking.bookingDetail.loading": "Loading…",
39
39
  "booking.bookingDetail.notFound": "Booking not found",
@@ -66,10 +66,10 @@
66
66
  "booking.bookingDetail.cancelling": "Cancelling…",
67
67
  "booking.bookingDetail.errorCancel": "Cancellation failed",
68
68
  "booking.bookingDetail.cancelButton": "Cancel",
69
- "booking.availability.documentTitle": "Availability",
69
+ "booking/availability.documentTitle": "Availability",
70
70
  "booking.availability.titleSuffix": "Availability",
71
71
  "booking.availability.description": "Set your weekly availability. Clients can only book slots within these windows.",
72
- "booking.availabilitySetup.documentTitle": "Availability",
72
+ "booking/availability-setup.documentTitle": "Availability",
73
73
  "booking.availabilitySetup.title": "Availability",
74
74
  "booking.availabilitySetup.description": "Set when you are available for bookings. Clients can only book times within these windows.",
75
75
  "booking.availabilitySetup.loading": "Loading…",
@@ -97,7 +97,7 @@
97
97
  "booking.availabilitySetup.savedBody": "Your booking link is ready to share with clients.",
98
98
  "booking.availabilitySetup.copyLink": "Copy link",
99
99
  "booking.availabilitySetup.copied": "Copied!",
100
- "booking.services.documentTitle": "Services",
100
+ "booking/services.documentTitle": "Services",
101
101
  "booking.services.titleSuffix": "Services",
102
102
  "booking.services.description": "Define what you offer — name, duration, and price. Clients will see these when booking.",
103
103
  "booking.services.addService": "Add service",
@@ -106,7 +106,7 @@
106
106
  "booking.services.example.introCall.description": "A free 30-minute intro to see if we're a good fit.",
107
107
  "booking.services.example.strategy.name": "Strategy session",
108
108
  "booking.services.example.strategy.description": "Deep-dive strategy session.",
109
- "booking.book.documentTitle": "Book a session",
109
+ "booking/book.documentTitle": "Book a session",
110
110
  "booking.book.title": "Book a session",
111
111
  "booking.book.description": "Pick a time that works for you and confirm your appointment.",
112
112
  "booking.book.form.chooseService": "Choose a service",
@@ -135,7 +135,7 @@
135
135
  "booking.availabilityEditor.day.thu": "Thu",
136
136
  "booking.availabilityEditor.day.fri": "Fri",
137
137
  "booking.availabilityEditor.day.sat": "Sat",
138
- "publicBooking.documentTitle": "Book an appointment",
138
+ "public-booking.documentTitle": "Book an appointment",
139
139
  "publicBooking.title": "Book an appointment",
140
140
  "publicBooking.description": "Choose a date and time that works for you.",
141
141
  "publicBooking.loadingSlots": "Loading times…",
@@ -209,5 +209,38 @@
209
209
  "booking/confirm.label": "Confirm booking",
210
210
  "booking/confirm.description": "Confirm a pending booking request",
211
211
  "booking/cancel.label": "Cancel booking",
212
- "booking/cancel.description": "Cancel a confirmed booking"
212
+ "booking/cancel.description": "Cancel a confirmed booking",
213
+ "booking/get-services.label": "Get services",
214
+ "booking/get-services.description": "List bookable services for a consultant or workspace",
215
+ "booking/create-service.label": "Create service",
216
+ "booking/create-service.description": "Create a new bookable service",
217
+ "booking/update-service.label": "Update service",
218
+ "booking/update-service.description": "Update an existing bookable service",
219
+ "booking/delete-service.label": "Delete service",
220
+ "booking/delete-service.description": "Remove a bookable service",
221
+ "booking.services.loading": "Loading…",
222
+ "booking.services.empty": "No services yet. Add your first service to get started.",
223
+ "booking.services.errorLoad": "Could not load services ({status})",
224
+ "booking.services.errorSave": "Could not save service",
225
+ "booking.services.errorDelete": "Could not delete service",
226
+ "booking.services.deleteConfirm": "Delete this service?",
227
+ "booking.services.editService": "Edit service",
228
+ "booking.services.newService": "New service",
229
+ "booking.services.save": "Save",
230
+ "booking.services.saving": "Saving…",
231
+ "booking.services.cancel": "Cancel",
232
+ "booking.services.edit": "Edit",
233
+ "booking.services.delete": "Delete",
234
+ "booking.services.field.name": "Name",
235
+ "booking.services.field.duration": "Duration (minutes)",
236
+ "booking.services.field.price": "Price (öre/cents)",
237
+ "booking.services.field.currency": "Currency",
238
+ "booking.services.field.description": "Description",
239
+ "booking.services.field.active": "Active (visible to clients)",
240
+ "publicBooking.chooseService": "Choose a service",
241
+ "publicBooking.loadingServices": "Loading services…",
242
+ "publicBooking.servicesError": "Could not load services.",
243
+ "publicBooking.noServices": "No bookable services are available right now.",
244
+ "publicBooking.serviceFree": "Free",
245
+ "publicBooking.serviceMeta": "{duration} min · {price}"
213
246
  }
@@ -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/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
+ }