@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
package/src/sv.translations.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"booking
|
|
2
|
+
"booking/home.documentTitle": "Bokningskalender – Ossy",
|
|
3
3
|
"booking.home.cover.title": "Låt kunder boka tid direkt",
|
|
4
4
|
"booking.home.cover.text": "Dela en länk. Kunder väljer tid. Du fokuserar på jobbet.",
|
|
5
5
|
"booking.home.cover.ctaPrimary": "Kom igång gratis",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"booking.home.features.doubleBook.text": "En kalender här, en länk där – vi håller koll så samma tid inte bokas två gånger.",
|
|
17
17
|
"booking.sales.overview": "Översikt",
|
|
18
18
|
"booking.sales.features": "Funktioner",
|
|
19
|
-
"booking
|
|
19
|
+
"booking/bookings.documentTitle": "Bokningar",
|
|
20
20
|
"booking.bookings.titleSuffix": "Bokningar",
|
|
21
21
|
"booking.bookings.description": "Hantera bokningsförfrågningar och bekräftade möten.",
|
|
22
22
|
"booking.bookings.tab.pending": "Förfrågningar",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"booking.bookings.errorConfirm": "Bekräftning misslyckades",
|
|
34
34
|
"booking.bookings.errorDecline": "Avböjning misslyckades",
|
|
35
35
|
"booking.bookings.errorCancel": "Avbokning misslyckades",
|
|
36
|
-
"booking.
|
|
36
|
+
"booking/booking-detail.documentTitle": "Bokningsdetaljer",
|
|
37
37
|
"booking.bookingDetail.back": "← Alla bokningar",
|
|
38
38
|
"booking.bookingDetail.loading": "Laddar…",
|
|
39
39
|
"booking.bookingDetail.notFound": "Bokningen hittades inte",
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"booking.bookingDetail.cancelling": "Avbokar…",
|
|
67
67
|
"booking.bookingDetail.errorCancel": "Avbokning misslyckades",
|
|
68
68
|
"booking.bookingDetail.cancelButton": "Avbryt",
|
|
69
|
-
"booking
|
|
69
|
+
"booking/availability.documentTitle": "Tillgänglighet",
|
|
70
70
|
"booking.availability.titleSuffix": "Tillgänglighet",
|
|
71
71
|
"booking.availability.description": "Ange din veckotillgänglighet. Kunder kan bara boka tider inom dessa fönster.",
|
|
72
|
-
"booking.
|
|
72
|
+
"booking/availability-setup.documentTitle": "Tillgänglighet",
|
|
73
73
|
"booking.availabilitySetup.title": "Tillgänglighet",
|
|
74
74
|
"booking.availabilitySetup.description": "Ange när du är tillgänglig för bokningar. Klienter kan bara boka tider inom dessa fönster.",
|
|
75
75
|
"booking.availabilitySetup.loading": "Laddar…",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"booking.availabilitySetup.savedBody": "Din bokningslänk är redo att delas med klienter.",
|
|
98
98
|
"booking.availabilitySetup.copyLink": "Kopiera länk",
|
|
99
99
|
"booking.availabilitySetup.copied": "Kopierad!",
|
|
100
|
-
"booking
|
|
100
|
+
"booking/services.documentTitle": "Tjänster",
|
|
101
101
|
"booking.services.titleSuffix": "Tjänster",
|
|
102
102
|
"booking.services.description": "Definiera vad du erbjuder — namn, längd och pris. Kunder ser detta vid bokning.",
|
|
103
103
|
"booking.services.addService": "Lägg till tjänst",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"booking.services.example.introCall.description": "Ett gratis 30-minuters intro för att se om vi passar ihop.",
|
|
107
107
|
"booking.services.example.strategy.name": "Strategimöte",
|
|
108
108
|
"booking.services.example.strategy.description": "Fördjupat strategimöte.",
|
|
109
|
-
"booking
|
|
109
|
+
"booking/book.documentTitle": "Boka ett möte",
|
|
110
110
|
"booking.book.title": "Boka ett möte",
|
|
111
111
|
"booking.book.description": "Välj en tid som passar dig och bekräfta din bokning.",
|
|
112
112
|
"booking.book.form.chooseService": "Välj en tjänst",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"booking.availabilityEditor.day.thu": "Tor",
|
|
136
136
|
"booking.availabilityEditor.day.fri": "Fre",
|
|
137
137
|
"booking.availabilityEditor.day.sat": "Lör",
|
|
138
|
-
"
|
|
138
|
+
"public-booking.documentTitle": "Boka en tid",
|
|
139
139
|
"publicBooking.title": "Boka en tid",
|
|
140
140
|
"publicBooking.description": "Välj ett datum och en tid som passar dig.",
|
|
141
141
|
"publicBooking.loadingSlots": "Laddar tider…",
|
|
@@ -209,5 +209,38 @@
|
|
|
209
209
|
"booking/confirm.label": "Bekräfta bokning",
|
|
210
210
|
"booking/confirm.description": "Bekräfta en väntande bokningsförfrågan",
|
|
211
211
|
"booking/cancel.label": "Avboka bokning",
|
|
212
|
-
"booking/cancel.description": "Avboka en bekräftad bokning"
|
|
212
|
+
"booking/cancel.description": "Avboka en bekräftad bokning",
|
|
213
|
+
"booking/get-services.label": "Hämta tjänster",
|
|
214
|
+
"booking/get-services.description": "Lista bokningsbara tjänster för konsult eller workspace",
|
|
215
|
+
"booking/create-service.label": "Skapa tjänst",
|
|
216
|
+
"booking/create-service.description": "Skapa en ny bokningsbar tjänst",
|
|
217
|
+
"booking/update-service.label": "Uppdatera tjänst",
|
|
218
|
+
"booking/update-service.description": "Uppdatera en befintlig bokningsbar tjänst",
|
|
219
|
+
"booking/delete-service.label": "Ta bort tjänst",
|
|
220
|
+
"booking/delete-service.description": "Ta bort en bokningsbar tjänst",
|
|
221
|
+
"booking.services.loading": "Laddar…",
|
|
222
|
+
"booking.services.empty": "Inga tjänster ännu. Lägg till din första tjänst.",
|
|
223
|
+
"booking.services.errorLoad": "Kunde inte ladda tjänster ({status})",
|
|
224
|
+
"booking.services.errorSave": "Kunde inte spara tjänst",
|
|
225
|
+
"booking.services.errorDelete": "Kunde inte ta bort tjänst",
|
|
226
|
+
"booking.services.deleteConfirm": "Ta bort denna tjänst?",
|
|
227
|
+
"booking.services.editService": "Redigera tjänst",
|
|
228
|
+
"booking.services.newService": "Ny tjänst",
|
|
229
|
+
"booking.services.save": "Spara",
|
|
230
|
+
"booking.services.saving": "Sparar…",
|
|
231
|
+
"booking.services.cancel": "Avbryt",
|
|
232
|
+
"booking.services.edit": "Redigera",
|
|
233
|
+
"booking.services.delete": "Ta bort",
|
|
234
|
+
"booking.services.field.name": "Namn",
|
|
235
|
+
"booking.services.field.duration": "Längd (minuter)",
|
|
236
|
+
"booking.services.field.price": "Pris (öre)",
|
|
237
|
+
"booking.services.field.currency": "Valuta",
|
|
238
|
+
"booking.services.field.description": "Beskrivning",
|
|
239
|
+
"booking.services.field.active": "Aktiv (synlig för klienter)",
|
|
240
|
+
"publicBooking.chooseService": "Välj tjänst",
|
|
241
|
+
"publicBooking.loadingServices": "Laddar tjänster…",
|
|
242
|
+
"publicBooking.servicesError": "Kunde inte ladda tjänster.",
|
|
243
|
+
"publicBooking.noServices": "Inga bokningsbara tjänster finns just nu.",
|
|
244
|
+
"publicBooking.serviceFree": "Gratis",
|
|
245
|
+
"publicBooking.serviceMeta": "{duration} min · {price}"
|
|
213
246
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const metadata = { id: 'booking/update-service', access: 'workspace' }
|
|
@@ -0,0 +1,52 @@
|
|
|
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/update-service' }
|
|
6
|
+
|
|
7
|
+
export async function run ({ payload, req, log }) {
|
|
8
|
+
const workspaceId = req?.workspaceId
|
|
9
|
+
const { serviceId, name, duration, price, currency, description, active } = 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
|
+
const content = {
|
|
29
|
+
...resource.content,
|
|
30
|
+
...(name != null ? { name: String(name).trim() } : {}),
|
|
31
|
+
...(duration != null ? { duration: Number(duration) || 60 } : {}),
|
|
32
|
+
...(price != null ? { price: Number(price) || 0 } : {}),
|
|
33
|
+
...(currency != null ? { currency } : {}),
|
|
34
|
+
...(description != null ? { description: String(description).trim() } : {}),
|
|
35
|
+
...(active != null ? { active: active !== false } : {}),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log?.info(`[booking/update-service] Updating service ${serviceId}`)
|
|
39
|
+
|
|
40
|
+
await Aggregate.Of(Resource, serviceId)
|
|
41
|
+
.then(
|
|
42
|
+
Aggregate.Add(
|
|
43
|
+
ResourcesEvents.ContentUpdated({
|
|
44
|
+
createdBy: req?.userId ?? 'system',
|
|
45
|
+
content,
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
.then(Aggregate.Save())
|
|
50
|
+
|
|
51
|
+
return { id: serviceId, ...content }
|
|
52
|
+
}
|
package/src/BookingForm.jsx
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { View, Title, Text, Button, Input, useLocale } from '@ossy/design-system'
|
|
3
|
-
|
|
4
|
-
export const BookingForm = ({ workspaceId }) => {
|
|
5
|
-
const { t } = useLocale()
|
|
6
|
-
const [step, setStep] = useState('pick-service')
|
|
7
|
-
const [selectedService, setSelectedService] = useState(null)
|
|
8
|
-
const [selectedSlot, setSelectedSlot] = useState(null)
|
|
9
|
-
const [clientName, setClientName] = useState('')
|
|
10
|
-
const [clientEmail, setClientEmail] = useState('')
|
|
11
|
-
|
|
12
|
-
if (step === 'pick-service') {
|
|
13
|
-
return (
|
|
14
|
-
<View gap="m">
|
|
15
|
-
<Title variant="secondary">{t('booking.book.form.chooseService')}</Title>
|
|
16
|
-
<Text color="secondary">{t('booking.book.form.loadingServices', { workspaceId })}</Text>
|
|
17
|
-
<Button variant="secondary" onClick={() => setStep('pick-slot')}>
|
|
18
|
-
{t('booking.book.form.continuePlaceholder')}
|
|
19
|
-
</Button>
|
|
20
|
-
</View>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (step === 'pick-slot') {
|
|
25
|
-
return (
|
|
26
|
-
<View gap="m">
|
|
27
|
-
<Title variant="secondary">{t('booking.book.form.pickTime')}</Title>
|
|
28
|
-
<Text color="secondary">{t('booking.book.form.slotsPlaceholder')}</Text>
|
|
29
|
-
<View layout="row" gap="s">
|
|
30
|
-
<Button variant="ghost" onClick={() => setStep('pick-service')}>{t('booking.book.form.back')}</Button>
|
|
31
|
-
<Button variant="secondary" onClick={() => setStep('enter-details')}>
|
|
32
|
-
{t('booking.book.form.continuePlaceholder')}
|
|
33
|
-
</Button>
|
|
34
|
-
</View>
|
|
35
|
-
</View>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (step === 'enter-details') {
|
|
40
|
-
return (
|
|
41
|
-
<View gap="m">
|
|
42
|
-
<Title variant="secondary">{t('booking.book.form.yourDetails')}</Title>
|
|
43
|
-
<Input
|
|
44
|
-
label={t('booking.book.form.nameLabel')}
|
|
45
|
-
value={clientName}
|
|
46
|
-
onChange={setClientName}
|
|
47
|
-
placeholder={t('booking.book.form.namePlaceholder')}
|
|
48
|
-
/>
|
|
49
|
-
<Input
|
|
50
|
-
label={t('booking.book.form.emailLabel')}
|
|
51
|
-
type="email"
|
|
52
|
-
value={clientEmail}
|
|
53
|
-
onChange={setClientEmail}
|
|
54
|
-
placeholder={t('booking.book.form.emailPlaceholder')}
|
|
55
|
-
/>
|
|
56
|
-
<View layout="row" gap="s">
|
|
57
|
-
<Button variant="ghost" onClick={() => setStep('pick-slot')}>{t('booking.book.form.back')}</Button>
|
|
58
|
-
<Button variant="primary" onClick={() => setStep('confirm')}>
|
|
59
|
-
{t('booking.book.form.confirmPay')}
|
|
60
|
-
</Button>
|
|
61
|
-
</View>
|
|
62
|
-
</View>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<View gap="m">
|
|
68
|
-
<Title variant="secondary">{t('booking.book.form.confirmed')}</Title>
|
|
69
|
-
<Text>{t('booking.book.form.checkEmail')}</Text>
|
|
70
|
-
</View>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { View, Title, Text, Tags, useLocale } from '@ossy/design-system'
|
|
3
|
-
import { Definition } from './Definition.js'
|
|
4
|
-
import { moduleStatusTags } from './moduleStatus.js'
|
|
5
|
-
import { AvailabilityEditor } from './AvailabilityEditor.jsx'
|
|
6
|
-
|
|
7
|
-
export const metadata = {
|
|
8
|
-
id: 'booking/availability',
|
|
9
|
-
path: {
|
|
10
|
-
sv: '/tillganglighet',
|
|
11
|
-
en: '/availability',
|
|
12
|
-
},
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const AvailabilityPage = () => {
|
|
16
|
-
const { t } = useLocale()
|
|
17
|
-
return (
|
|
18
|
-
<View
|
|
19
|
-
gap="m"
|
|
20
|
-
surface="primary"
|
|
21
|
-
style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
|
|
22
|
-
>
|
|
23
|
-
<View inset="s" gap="s">
|
|
24
|
-
<View layout="row" gap="s" alignItems="center">
|
|
25
|
-
<Title>{Definition.title} — {t('booking.availability.titleSuffix')}</Title>
|
|
26
|
-
{moduleStatusTags(Definition).length > 0 && (
|
|
27
|
-
<Tags tags={moduleStatusTags(Definition)} size="s" />
|
|
28
|
-
)}
|
|
29
|
-
</View>
|
|
30
|
-
|
|
31
|
-
<Text style={{ maxWidth: '400px' }}>
|
|
32
|
-
{t('booking.availability.description')}
|
|
33
|
-
</Text>
|
|
34
|
-
</View>
|
|
35
|
-
|
|
36
|
-
<View inset="s">
|
|
37
|
-
<AvailabilityEditor availability={[]} />
|
|
38
|
-
</View>
|
|
39
|
-
</View>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export default AvailabilityPage
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { View, Title, Text, useLocale } from '@ossy/design-system'
|
|
3
|
-
import { useRouter } from '@ossy/router-react'
|
|
4
|
-
import { BookingForm } from './BookingForm.jsx'
|
|
5
|
-
|
|
6
|
-
export const metadata = {
|
|
7
|
-
id: 'booking/book',
|
|
8
|
-
public: true,
|
|
9
|
-
path: {
|
|
10
|
-
sv: '/boka/:workspaceId',
|
|
11
|
-
en: '/book/:workspaceId',
|
|
12
|
-
},
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const BookingPage = () => {
|
|
16
|
-
const { t } = useLocale()
|
|
17
|
-
const router = useRouter()
|
|
18
|
-
const { workspaceId } = router.params
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<View
|
|
22
|
-
surface="base"
|
|
23
|
-
layout="off-center-s"
|
|
24
|
-
inset="s"
|
|
25
|
-
style={{ minHeight: '100dvh' }}
|
|
26
|
-
>
|
|
27
|
-
<View inset="m" roundness="l" surface="primary" slot="content" gap="m">
|
|
28
|
-
<View gap="xs">
|
|
29
|
-
<Title>{t('booking.book.title')}</Title>
|
|
30
|
-
<Text>{t('booking.book.description')}</Text>
|
|
31
|
-
</View>
|
|
32
|
-
|
|
33
|
-
<BookingForm workspaceId={workspaceId} />
|
|
34
|
-
</View>
|
|
35
|
-
</View>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export default BookingPage
|