@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.
- package/README.md +26 -19
- package/package.json +7 -7
- package/src/availability-setup.page.jsx +18 -17
- package/src/booking-card.component.jsx +6 -0
- package/src/booking-detail.page.jsx +28 -23
- package/src/booking-resources.js +10 -0
- package/src/bookings.page.jsx +27 -39
- package/src/cancel-booking.action.js +1 -61
- package/src/cancel-booking.task.js +60 -0
- package/src/confirm-booking.action.js +1 -82
- package/src/confirm-booking.task.js +81 -0
- package/src/create-booking.action.js +1 -168
- package/src/create-booking.task.js +170 -0
- package/src/create-service.action.js +1 -0
- package/src/create-service.task.js +46 -0
- package/src/decline-booking.action.js +1 -68
- package/src/decline-booking.task.js +67 -0
- package/src/delete-service.action.js +1 -0
- package/src/delete-service.task.js +41 -0
- package/src/en.translations.json +42 -9
- package/src/get-availability.action.js +1 -52
- package/src/get-availability.task.js +51 -0
- package/src/get-available-slots.action.js +1 -89
- package/src/get-available-slots.task.js +99 -0
- package/src/get-services.action.js +1 -0
- package/src/get-services.task.js +37 -0
- package/src/index.js +20 -1
- package/src/invoke-error-message.js +10 -0
- package/src/list-bookings.action.js +1 -36
- package/src/list-bookings.task.js +35 -0
- package/src/locations.js +14 -0
- package/src/public-booking.page.jsx +138 -42
- package/src/save-availability.action.js +1 -73
- package/src/save-availability.task.js +74 -0
- package/src/send-booking-reminder.task.js +2 -2
- package/src/services.page.jsx +157 -37
- package/src/sv.translations.json +42 -9
- package/src/update-service.action.js +1 -0
- package/src/update-service.task.js +52 -0
- package/src/BookingForm.jsx +0 -72
- package/src/availability.page.jsx +0 -43
- 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
|
+
}
|
package/src/en.translations.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"booking
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/locations.js
ADDED
|
@@ -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
|
+
}
|