@ossy/booking 3.10.0 → 3.11.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.
- package/package.json +8 -8
- package/src/BookingSalesPage.jsx +34 -33
- package/src/ServiceCard.jsx +6 -2
- package/src/booking-cancellation.email.jsx +4 -0
- package/src/booking-card.component.jsx +1 -1
- package/src/booking-confirmation.email.jsx +4 -0
- package/src/booking-reminder.email.jsx +4 -0
- package/src/booking-request.email.jsx +4 -0
- package/src/bookings.page.jsx +8 -4
- package/src/cancel-booking.flow.js +30 -0
- package/src/cancel-booking.task.js +2 -0
- package/src/client-booking.flow.js +89 -0
- package/src/confirm-booking-inbox.flow.js +42 -0
- package/src/confirm-booking.flow.js +32 -0
- package/src/confirm-booking.task.js +4 -1
- package/src/create-booking.task.js +2 -1
- package/src/decline-booking-inbox.flow.js +43 -0
- package/src/decline-booking.flow.js +32 -0
- package/src/decline-booking.task.js +2 -0
- package/src/delete-service.flow.js +40 -0
- package/src/disable-booking.flow.js +58 -0
- package/src/en.translations.json +6 -0
- package/src/index.js +2 -0
- package/src/open-delete-service.action.js +7 -0
- package/src/open-edit-service.action.js +7 -0
- package/src/provider-availability-weekend.flow.js +34 -0
- package/src/provider-catalog.flow.js +35 -0
- package/src/provider-onboard.flow.js +3 -3
- package/src/public-booking.page.jsx +6 -2
- package/src/service-card-slot.component.jsx +6 -2
- package/src/service-detail.component.jsx +6 -2
- package/src/service.schema.js +2 -2
- package/src/services.page.jsx +66 -10
- package/src/sv.translations.json +6 -0
- package/src/update-service.flow.js +42 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/booking",
|
|
3
3
|
"description": "Booking feature package — services, availability, and appointment management for Ossy providers",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.11.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@ossy/config": "^3.0.9",
|
|
18
|
-
"@ossy/email": "^3.0
|
|
19
|
-
"@ossy/event-store": "^3.
|
|
18
|
+
"@ossy/email": "^3.11.0",
|
|
19
|
+
"@ossy/event-store": "^3.11.1",
|
|
20
20
|
"@ossy/observability": "^3.0.9",
|
|
21
|
-
"@ossy/platform": "^3.
|
|
22
|
-
"@ossy/resources": "^3.
|
|
23
|
-
"@ossy/users": "^3.
|
|
24
|
-
"@ossy/workspaces": "^3.
|
|
21
|
+
"@ossy/platform": "^3.11.1",
|
|
22
|
+
"@ossy/resources": "^3.11.1",
|
|
23
|
+
"@ossy/users": "^3.11.1",
|
|
24
|
+
"@ossy/workspaces": "^3.11.1"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@ossy/app": ">=1.0.0",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"/src",
|
|
39
39
|
"README.md"
|
|
40
40
|
],
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "f279de3535b6bc76631cbc6b943dae8f92a1565f"
|
|
42
42
|
}
|
package/src/BookingSalesPage.jsx
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
|
-
import React, { useState, useCallback } from 'react'
|
|
1
|
+
import React, { useState, useCallback, useMemo } from 'react'
|
|
2
2
|
import { GetWorkspace, EnableService } from '@ossy/workspaces'
|
|
3
|
-
import { useSdk
|
|
4
|
-
import { Text, View,
|
|
3
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
4
|
+
import { Text, View, useLocale } from '@ossy/design-system'
|
|
5
5
|
import SalesSection from './SalesSection.jsx'
|
|
6
6
|
import { useBookingHomeContent } from './booking-home-content.js'
|
|
7
7
|
|
|
8
8
|
export default function BookingSalesPage ({ isAuthenticated }) {
|
|
9
9
|
const { t } = useLocale()
|
|
10
|
-
const { invoke,
|
|
10
|
+
const { invoke, read } = useSdk()
|
|
11
|
+
// Shared GetWorkspace cache with useShellWorkspace — refetch (not bare invalidate)
|
|
12
|
+
// awaits the post-enable read so home can switch sales → product reliably.
|
|
13
|
+
const { refetch: refetchWorkspace } = read(GetWorkspace, undefined, {
|
|
14
|
+
enabled: !!isAuthenticated,
|
|
15
|
+
})
|
|
11
16
|
const [error, setError] = useState(null)
|
|
12
17
|
const { cover: baseCover, features } = useBookingHomeContent()
|
|
13
18
|
|
|
14
19
|
const handleEnable = useCallback(() => {
|
|
15
20
|
setError(null)
|
|
16
21
|
invoke(EnableService, { service: '@ossy/booking' })
|
|
17
|
-
.then(() =>
|
|
22
|
+
.then(() => refetchWorkspace())
|
|
18
23
|
.catch(() => setError(t('booking.sales.enableError')))
|
|
19
|
-
}, [invoke,
|
|
24
|
+
}, [invoke, refetchWorkspace, t])
|
|
20
25
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
const cover = useMemo(() => {
|
|
27
|
+
if (!isAuthenticated) return baseCover
|
|
28
|
+
return {
|
|
29
|
+
...baseCover,
|
|
30
|
+
// Hero CTA (not a sibling below the height:100% sales scroller) so pointer
|
|
31
|
+
// events reach the React onClick — required for Enable booking e2e.
|
|
32
|
+
actions: [
|
|
33
|
+
{
|
|
34
|
+
...EnableService,
|
|
35
|
+
'data-service': '@ossy/booking',
|
|
36
|
+
variant: 'cta',
|
|
37
|
+
// HeroCover passes label as Button children (already translated).
|
|
38
|
+
label: t('booking.sales.enable'),
|
|
39
|
+
onClick: handleEnable,
|
|
40
|
+
},
|
|
41
|
+
{ label: t('booking.home.cover.ctaSecondary'), variant: 'secondary', href: '/boka/demo' },
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
}, [baseCover, handleEnable, isAuthenticated, t])
|
|
38
45
|
|
|
39
46
|
return (
|
|
40
47
|
<View data-booking-status="off">
|
|
41
48
|
<SalesSection cover={cover} features={features} />
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<Text variant="heading-secondary" as="h2" text="booking.sales.enable" />
|
|
47
|
-
<Text style={{ maxWidth: '600px' }} text="booking.sales.enableDescription" />
|
|
48
|
-
<Button {...enableAction} variant="cta" />
|
|
49
|
-
{error && <Text variant="small">{error}</Text>}
|
|
50
|
-
</View>
|
|
51
|
-
</Page>
|
|
49
|
+
{error && (
|
|
50
|
+
<View inset="l">
|
|
51
|
+
<Text variant="small">{error}</Text>
|
|
52
|
+
</View>
|
|
52
53
|
)}
|
|
53
54
|
</View>
|
|
54
55
|
)
|
package/src/ServiceCard.jsx
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, Badge } from '@ossy/design-system'
|
|
3
3
|
|
|
4
|
-
const formatPrice = (cents, currency) => {
|
|
4
|
+
const formatPrice = (cents, currency = 'SEK') => {
|
|
5
5
|
if (cents === 0) return 'Free'
|
|
6
|
-
|
|
6
|
+
try {
|
|
7
|
+
return new Intl.NumberFormat('sv-SE', { style: 'currency', currency }).format(cents / 100)
|
|
8
|
+
} catch {
|
|
9
|
+
return `${(cents / 100).toFixed(2)} ${currency}`
|
|
10
|
+
}
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
const formatDuration = (minutes) => {
|
|
@@ -69,7 +69,7 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
|
|
|
69
69
|
const isConfirmed = data.status === 'confirmed'
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
|
-
<View surface="secondary" roundness="m" inset="m" gap="s">
|
|
72
|
+
<View surface="secondary" roundness="m" inset="m" gap="s" data-booking-status={data.status}>
|
|
73
73
|
<View layout="row" justifyContent="space-between" alignItems="flex-start" gap="m">
|
|
74
74
|
<View gap="xs" style={{ flex: 1, minWidth: 0 }}>
|
|
75
75
|
<View layout="row" gap="s" alignItems="center" style={{ flexWrap: 'wrap' }}>
|
package/src/bookings.page.jsx
CHANGED
|
@@ -22,17 +22,21 @@ export default function BookingsPage () {
|
|
|
22
22
|
const { t } = useLocale()
|
|
23
23
|
const sdk = useSdk()
|
|
24
24
|
const router = useRouter()
|
|
25
|
-
const
|
|
25
|
+
const initialTab = ['pending', 'confirmed', 'all'].includes(router.searchParams?.tab)
|
|
26
|
+
? router.searchParams.tab
|
|
27
|
+
: 'pending'
|
|
28
|
+
const [activeTab, setActiveTab] = useState(initialTab)
|
|
26
29
|
const [bookings, setBookings] = useState([])
|
|
27
30
|
const [loading, setLoading] = useState(true)
|
|
28
31
|
const [error, setError] = useState(null)
|
|
29
32
|
|
|
30
33
|
const TABS = useMemo(() => [
|
|
31
|
-
{ id: 'pending', label: t('booking.bookings.tab.pending') },
|
|
32
|
-
{ id: 'confirmed', label: t('booking.bookings.tab.confirmed') },
|
|
33
|
-
{ id: 'all', label: t('booking.bookings.tab.all') },
|
|
34
|
+
{ id: 'pending', label: t('booking.bookings.tab.pending'), 'data-bookings-tab': 'pending' },
|
|
35
|
+
{ id: 'confirmed', label: t('booking.bookings.tab.confirmed'), 'data-bookings-tab': 'confirmed' },
|
|
36
|
+
{ id: 'all', label: t('booking.bookings.tab.all'), 'data-bookings-tab': 'all' },
|
|
34
37
|
], [t])
|
|
35
38
|
|
|
39
|
+
|
|
36
40
|
const EMPTY_MESSAGES = useMemo(() => ({
|
|
37
41
|
pending: {
|
|
38
42
|
heading: t('booking.bookings.empty.pending.heading'),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import confirmBooking from './confirm-booking.flow.js'
|
|
2
|
+
import { metadata as CancelBooking } from './cancel-booking.action.js'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
id: '@ossy/booking/flows/cancel-booking',
|
|
6
|
+
feature: 'booking',
|
|
7
|
+
requires: ['server', 'database'],
|
|
8
|
+
timeout: 150_000,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Provider cancels a confirmed booking from the bookings list.
|
|
13
|
+
*
|
|
14
|
+
* Composes after confirm-booking. Opens the confirmed tab via search param,
|
|
15
|
+
* cancels, then asserts cancelled status on the all tab.
|
|
16
|
+
*/
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Cancel booking',
|
|
19
|
+
description:
|
|
20
|
+
'Client books; provider confirms via email; provider cancels from the confirmed bookings list',
|
|
21
|
+
steps: [
|
|
22
|
+
...confirmBooking.steps,
|
|
23
|
+
{ page: '@booking/bookings', search: { tab: 'confirmed' } },
|
|
24
|
+
{ result: { action: CancelBooking, timeout: 20000 } },
|
|
25
|
+
{ action: CancelBooking },
|
|
26
|
+
{ result: { action: CancelBooking, hidden: true, timeout: 15000 } },
|
|
27
|
+
{ page: '@booking/bookings', search: { tab: 'all' } },
|
|
28
|
+
{ result: { selector: '[data-booking-status="cancelled"]', timeout: 15000 } },
|
|
29
|
+
],
|
|
30
|
+
}
|
|
@@ -26,6 +26,8 @@ export async function run ({ payload, req, log, integrations }) {
|
|
|
26
26
|
bookingId,
|
|
27
27
|
ResourcesEvents.Patched({
|
|
28
28
|
createdBy: req?.userId ?? 'system',
|
|
29
|
+
// Required for BookingListProjection.scopeFromEvent (live projection updates).
|
|
30
|
+
belongsTo: booking.belongsTo,
|
|
29
31
|
content: { ...booking.content, status: 'cancelled' },
|
|
30
32
|
}),
|
|
31
33
|
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import providerOnboard from './provider-onboard.flow.js'
|
|
2
|
+
import { metadata as OpenServices } from './open-services.action.js'
|
|
3
|
+
import { metadata as OpenCreateService } from './open-create-service.action.js'
|
|
4
|
+
import { metadata as CreateService } from './create-service.action.js'
|
|
5
|
+
import { metadata as OpenDeleteService } from './open-delete-service.action.js'
|
|
6
|
+
import { metadata as ServiceForm } from './service.form.js'
|
|
7
|
+
import { metadata as SaveAvailability } from './save-availability.action.js'
|
|
8
|
+
import { metadata as OpenAvailabilitySetup } from './open-availability-setup.action.js'
|
|
9
|
+
import { metadata as CreateBooking } from './create-booking.action.js'
|
|
10
|
+
import { metadata as PublicBookingForm } from './public-booking-form.form.js'
|
|
11
|
+
import {
|
|
12
|
+
ToggleAvailabilityDay1,
|
|
13
|
+
ToggleAvailabilityDay2,
|
|
14
|
+
ToggleAvailabilityDay3,
|
|
15
|
+
ToggleAvailabilityDay4,
|
|
16
|
+
ToggleAvailabilityDay5,
|
|
17
|
+
SelectDuration60,
|
|
18
|
+
} from './availability-action-maps.js'
|
|
19
|
+
|
|
20
|
+
export const metadata = {
|
|
21
|
+
id: '@ossy/booking/flows/client-booking',
|
|
22
|
+
feature: 'booking',
|
|
23
|
+
requires: ['server', 'database'],
|
|
24
|
+
timeout: 90_000,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Client books via the public page after the provider has a catalog service
|
|
29
|
+
* and weekly availability. Composes provider onboard + catalog + availability.
|
|
30
|
+
*
|
|
31
|
+
* Next: confirm-booking (provider confirms via request email).
|
|
32
|
+
*/
|
|
33
|
+
export default {
|
|
34
|
+
title: 'Book as client',
|
|
35
|
+
description:
|
|
36
|
+
'Provider enables booking, adds a service and availability; a client picks a slot and submits a booking request',
|
|
37
|
+
steps: [
|
|
38
|
+
...providerOnboard.steps,
|
|
39
|
+
|
|
40
|
+
// Catalog service
|
|
41
|
+
{ action: OpenServices },
|
|
42
|
+
{ result: { selector: '[data-services-status]', timeout: 15000 } },
|
|
43
|
+
{ result: { action: OpenCreateService, timeout: 10000 } },
|
|
44
|
+
{ action: OpenCreateService },
|
|
45
|
+
{ result: { action: CreateService, timeout: 15000 } },
|
|
46
|
+
{ form: ServiceForm },
|
|
47
|
+
{ action: CreateService },
|
|
48
|
+
{ result: { action: OpenDeleteService, timeout: 15000 } },
|
|
49
|
+
|
|
50
|
+
// Availability + capture public booking slug
|
|
51
|
+
{ page: '@booking/home' },
|
|
52
|
+
{ result: { action: OpenAvailabilitySetup, timeout: 10000 } },
|
|
53
|
+
{ action: OpenAvailabilitySetup },
|
|
54
|
+
{ result: { action: SaveAvailability, timeout: 10000 } },
|
|
55
|
+
{ action: ToggleAvailabilityDay1 },
|
|
56
|
+
{ action: ToggleAvailabilityDay2 },
|
|
57
|
+
{ action: ToggleAvailabilityDay3 },
|
|
58
|
+
{ action: ToggleAvailabilityDay4 },
|
|
59
|
+
{ action: ToggleAvailabilityDay5 },
|
|
60
|
+
{ action: SelectDuration60 },
|
|
61
|
+
{ action: SaveAvailability },
|
|
62
|
+
{ result: { selector: '[data-availability-status="saved"]', timeout: 10000 } },
|
|
63
|
+
{
|
|
64
|
+
capture: {
|
|
65
|
+
providerSlug: { selector: '[data-provider-slug]', attr: 'data-provider-slug' },
|
|
66
|
+
providerEmail: '$email',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Public client booking journey
|
|
71
|
+
{ page: '@public-booking', params: { providerSlug: '$providerSlug' } },
|
|
72
|
+
{ result: { action: 'public-booking/continue', timeout: 15000 } },
|
|
73
|
+
{ pickFirst: 'service' },
|
|
74
|
+
{ action: 'public-booking/continue' },
|
|
75
|
+
{
|
|
76
|
+
result: {
|
|
77
|
+
selector: '[data-action="public-booking/pick-date"]:not([disabled])',
|
|
78
|
+
timeout: 20000,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{ pickFirst: 'date' },
|
|
82
|
+
{ pickFirst: 'slot' },
|
|
83
|
+
{ action: 'public-booking/continue' },
|
|
84
|
+
{ result: { action: CreateBooking, timeout: 10000 } },
|
|
85
|
+
{ form: PublicBookingForm },
|
|
86
|
+
{ action: CreateBooking },
|
|
87
|
+
{ result: { selector: '[data-flow-stage="booking-pending"]', timeout: 20000 } },
|
|
88
|
+
],
|
|
89
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import clientBooking from './client-booking.flow.js'
|
|
2
|
+
import { metadata as ConfirmBooking } from './confirm-booking.action.js'
|
|
3
|
+
import { metadata as OpenBookings } from './open-bookings.action.js'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: '@ossy/booking/flows/confirm-booking-inbox',
|
|
7
|
+
feature: 'booking',
|
|
8
|
+
requires: ['server', 'database'],
|
|
9
|
+
timeout: 150_000,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Provider confirms a pending booking from the in-app bookings inbox
|
|
14
|
+
* (not the request email link).
|
|
15
|
+
*
|
|
16
|
+
* Composes after client-booking; session remains the provider.
|
|
17
|
+
*/
|
|
18
|
+
export default {
|
|
19
|
+
title: 'Confirm booking from inbox',
|
|
20
|
+
description:
|
|
21
|
+
'Client submits a booking request; provider opens bookings inbox and confirms the pending card',
|
|
22
|
+
steps: [
|
|
23
|
+
...clientBooking.steps,
|
|
24
|
+
{ page: '@booking/home' },
|
|
25
|
+
{ result: { selector: '[data-booking-status="on"]', timeout: 15000 } },
|
|
26
|
+
{ result: { action: OpenBookings, timeout: 10000 } },
|
|
27
|
+
{ action: OpenBookings },
|
|
28
|
+
{ result: { page: '@booking/bookings', timeout: 15000 } },
|
|
29
|
+
{ result: { selector: '[data-booking-status="pending"]', timeout: 20000 } },
|
|
30
|
+
{ result: { action: ConfirmBooking, timeout: 10000 } },
|
|
31
|
+
{ action: ConfirmBooking },
|
|
32
|
+
{
|
|
33
|
+
result: {
|
|
34
|
+
selector: '[data-booking-status="pending"]',
|
|
35
|
+
hidden: true,
|
|
36
|
+
timeout: 15000,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{ page: '@booking/bookings', search: { tab: 'confirmed' } },
|
|
40
|
+
{ result: { selector: '[data-booking-status="confirmed"]', timeout: 15000 } },
|
|
41
|
+
],
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import clientBooking from './client-booking.flow.js'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
id: '@ossy/booking/flows/confirm-booking',
|
|
5
|
+
feature: 'booking',
|
|
6
|
+
requires: ['server', 'database'],
|
|
7
|
+
timeout: 120_000,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provider confirms a pending booking via the request email link.
|
|
12
|
+
*
|
|
13
|
+
* Composes after client-booking. Captures `providerEmail` before the client
|
|
14
|
+
* form overwrites `$email` with the client address.
|
|
15
|
+
*/
|
|
16
|
+
export default {
|
|
17
|
+
title: 'Confirm booking',
|
|
18
|
+
description:
|
|
19
|
+
'Client submits a booking request; provider confirms via the booking-request email link',
|
|
20
|
+
steps: [
|
|
21
|
+
...clientBooking.steps,
|
|
22
|
+
{
|
|
23
|
+
email: {
|
|
24
|
+
to: '$providerEmail',
|
|
25
|
+
id: '@ossy/booking/emails/request',
|
|
26
|
+
click: 'Bekräfta bokning',
|
|
27
|
+
timeout: 20000,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{ result: { url: /\/api\/v0\/booking\/confirm/, timeout: 15000 } },
|
|
31
|
+
],
|
|
32
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ResourcesEvents, mutateResource, viewResource } from '@ossy/resources/server'
|
|
2
|
-
import BookingConfirmationEmail from './booking-confirmation.email.jsx'
|
|
2
|
+
import BookingConfirmationEmail, { id as bookingConfirmationEmailId } from './booking-confirmation.email.jsx'
|
|
3
3
|
import { resolveProviderName, workspaceIdFromContent } from './provider-fields.js'
|
|
4
4
|
import { resolveWorkspaceContact } from './resolve-workspace-contact.js'
|
|
5
5
|
|
|
@@ -33,6 +33,8 @@ export async function run ({ payload, req, log, integrations }) {
|
|
|
33
33
|
bookingId,
|
|
34
34
|
ResourcesEvents.Patched({
|
|
35
35
|
createdBy: req?.userId ?? 'system',
|
|
36
|
+
// Required for BookingListProjection.scopeFromEvent (live projection updates).
|
|
37
|
+
belongsTo: booking.belongsTo,
|
|
36
38
|
content: { ...booking.content, status: 'confirmed' },
|
|
37
39
|
}),
|
|
38
40
|
)
|
|
@@ -55,6 +57,7 @@ export async function run ({ payload, req, log, integrations }) {
|
|
|
55
57
|
to: booking.content.clientEmail,
|
|
56
58
|
from: 'noreply@ossy.se',
|
|
57
59
|
subject: 'Bokningsbekräftelse',
|
|
60
|
+
templateId: bookingConfirmationEmailId,
|
|
58
61
|
},
|
|
59
62
|
)
|
|
60
63
|
log?.info(`[booking/tasks/confirm] Confirmation email sent to ${booking.content.clientEmail}`)
|
|
@@ -7,7 +7,7 @@ import { toMs } from './time.js'
|
|
|
7
7
|
import { getBookingResources } from './booking-resources.js'
|
|
8
8
|
import { createBookingEmailToken } from './booking-email-token.js'
|
|
9
9
|
import { bookings } from './locations.js'
|
|
10
|
-
import BookingRequestEmail from './booking-request.email.jsx'
|
|
10
|
+
import BookingRequestEmail, { id as bookingRequestEmailId } from './booking-request.email.jsx'
|
|
11
11
|
import {
|
|
12
12
|
resolveProviderEmail,
|
|
13
13
|
resolveProviderName,
|
|
@@ -178,6 +178,7 @@ export async function run({ payload, req, log, integrations }) {
|
|
|
178
178
|
to: providerEmail,
|
|
179
179
|
from: 'noreply@ossy.se',
|
|
180
180
|
subject: 'Ny bokningsförfrågan',
|
|
181
|
+
templateId: bookingRequestEmailId,
|
|
181
182
|
},
|
|
182
183
|
)
|
|
183
184
|
log?.info(`[booking/tasks/create] Booking request email sent to provider ${providerEmail}`)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import clientBooking from './client-booking.flow.js'
|
|
2
|
+
import { metadata as DeclineBooking } from './decline-booking.action.js'
|
|
3
|
+
import { metadata as OpenBookings } from './open-bookings.action.js'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: '@ossy/booking/flows/decline-booking-inbox',
|
|
7
|
+
feature: 'booking',
|
|
8
|
+
requires: ['server', 'database'],
|
|
9
|
+
timeout: 150_000,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Provider declines a pending booking from the in-app bookings inbox
|
|
14
|
+
* (not the request email link).
|
|
15
|
+
*
|
|
16
|
+
* Composes after client-booking; session remains the provider.
|
|
17
|
+
* Decline sets status to cancelled (same as cancel-booking task).
|
|
18
|
+
*/
|
|
19
|
+
export default {
|
|
20
|
+
title: 'Decline booking from inbox',
|
|
21
|
+
description:
|
|
22
|
+
'Client submits a booking request; provider opens bookings inbox and declines the pending card',
|
|
23
|
+
steps: [
|
|
24
|
+
...clientBooking.steps,
|
|
25
|
+
{ page: '@booking/home' },
|
|
26
|
+
{ result: { selector: '[data-booking-status="on"]', timeout: 15000 } },
|
|
27
|
+
{ result: { action: OpenBookings, timeout: 10000 } },
|
|
28
|
+
{ action: OpenBookings },
|
|
29
|
+
{ result: { page: '@booking/bookings', timeout: 15000 } },
|
|
30
|
+
{ result: { selector: '[data-booking-status="pending"]', timeout: 20000 } },
|
|
31
|
+
{ result: { action: DeclineBooking, timeout: 10000 } },
|
|
32
|
+
{ action: DeclineBooking },
|
|
33
|
+
{
|
|
34
|
+
result: {
|
|
35
|
+
selector: '[data-booking-status="pending"]',
|
|
36
|
+
hidden: true,
|
|
37
|
+
timeout: 15000,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{ page: '@booking/bookings', search: { tab: 'all' } },
|
|
41
|
+
{ result: { selector: '[data-booking-status="cancelled"]', timeout: 15000 } },
|
|
42
|
+
],
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import clientBooking from './client-booking.flow.js'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
id: '@ossy/booking/flows/decline-booking',
|
|
5
|
+
feature: 'booking',
|
|
6
|
+
requires: ['server', 'database'],
|
|
7
|
+
timeout: 120_000,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provider declines a pending booking via the request email link.
|
|
12
|
+
*
|
|
13
|
+
* Composes after client-booking. Captures `providerEmail` before the client
|
|
14
|
+
* form overwrites `$email` with the client address.
|
|
15
|
+
*/
|
|
16
|
+
export default {
|
|
17
|
+
title: 'Decline booking',
|
|
18
|
+
description:
|
|
19
|
+
'Client submits a booking request; provider declines via the booking-request email link',
|
|
20
|
+
steps: [
|
|
21
|
+
...clientBooking.steps,
|
|
22
|
+
{
|
|
23
|
+
email: {
|
|
24
|
+
to: '$providerEmail',
|
|
25
|
+
id: '@ossy/booking/emails/request',
|
|
26
|
+
click: 'Avböj förfrågan',
|
|
27
|
+
timeout: 20000,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{ result: { url: /\/api\/v0\/booking\/decline/, timeout: 15000 } },
|
|
31
|
+
],
|
|
32
|
+
}
|
|
@@ -31,6 +31,8 @@ export async function run ({ payload, req, log, integrations }) {
|
|
|
31
31
|
bookingId,
|
|
32
32
|
ResourcesEvents.Patched({
|
|
33
33
|
createdBy: req?.userId ?? 'system',
|
|
34
|
+
// Required for BookingListProjection.scopeFromEvent (live projection updates).
|
|
35
|
+
belongsTo: booking.belongsTo,
|
|
34
36
|
content: { ...booking.content, status: 'cancelled', declineReason: reason ?? '' },
|
|
35
37
|
}),
|
|
36
38
|
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import providerCatalogFlow from './provider-catalog.flow.js'
|
|
2
|
+
import { metadata as OpenDeleteService } from './open-delete-service.action.js'
|
|
3
|
+
import { metadata as DeleteService } from './delete-service.action.js'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: '@ossy/booking/flows/delete-service',
|
|
7
|
+
feature: 'booking',
|
|
8
|
+
requires: ['server', 'database'],
|
|
9
|
+
timeout: 120_000,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* After creating a catalog service, open delete from the services list, confirm
|
|
14
|
+
* via Guide, and assert the catalog is empty again.
|
|
15
|
+
*/
|
|
16
|
+
export default {
|
|
17
|
+
title: 'Delete booking service',
|
|
18
|
+
description:
|
|
19
|
+
'Signed-in provider creates a catalog service, deletes it from the confirm dialog, and sees an empty services list',
|
|
20
|
+
steps: [
|
|
21
|
+
...providerCatalogFlow.steps,
|
|
22
|
+
{ result: { action: OpenDeleteService, timeout: 10000 } },
|
|
23
|
+
{ action: OpenDeleteService },
|
|
24
|
+
{ result: { action: DeleteService, timeout: 10000 } },
|
|
25
|
+
{ action: DeleteService },
|
|
26
|
+
{
|
|
27
|
+
result: {
|
|
28
|
+
selector: '[data-services-status="empty"]',
|
|
29
|
+
timeout: 20000,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
result: {
|
|
34
|
+
selector: '[data-service-id]',
|
|
35
|
+
hidden: true,
|
|
36
|
+
timeout: 10000,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import providerOnboardFlow from './provider-onboard.flow.js'
|
|
2
|
+
import { DisableService, EnableService } from '@ossy/workspaces'
|
|
3
|
+
|
|
4
|
+
const BOOKING = '@ossy/booking'
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: '@ossy/booking/flows/disable-booking',
|
|
8
|
+
feature: 'booking',
|
|
9
|
+
requires: ['server', 'database'],
|
|
10
|
+
timeout: 120_000,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* After enabling booking, disable the package from the catalog detail
|
|
15
|
+
* toggle and confirm the booking home returns to the sales (off) state.
|
|
16
|
+
*/
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Disable booking',
|
|
19
|
+
description:
|
|
20
|
+
'Signed-in user enables booking, disables it from the package catalog, and sees the sales home again',
|
|
21
|
+
steps: [
|
|
22
|
+
...providerOnboardFlow.steps,
|
|
23
|
+
{ page: '@packages/detail', params: { packageSlug: 'booking' } },
|
|
24
|
+
{
|
|
25
|
+
result: {
|
|
26
|
+
action: { ...DisableService, service: BOOKING },
|
|
27
|
+
timeout: 15000,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
result: {
|
|
32
|
+
selector: `[data-package-service="${BOOKING}"][data-service-enabled="true"]`,
|
|
33
|
+
timeout: 10000,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{ action: { ...DisableService, service: BOOKING } },
|
|
37
|
+
{
|
|
38
|
+
result: {
|
|
39
|
+
action: { ...EnableService, service: BOOKING },
|
|
40
|
+
timeout: 20000,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
result: {
|
|
45
|
+
selector: `[data-package-service="${BOOKING}"][data-service-enabled="false"]`,
|
|
46
|
+
timeout: 10000,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ page: '@booking/home' },
|
|
50
|
+
{ result: { selector: '[data-booking-status="off"]', timeout: 20000 } },
|
|
51
|
+
{
|
|
52
|
+
result: {
|
|
53
|
+
action: { ...EnableService, service: BOOKING },
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
package/src/en.translations.json
CHANGED
|
@@ -242,6 +242,10 @@
|
|
|
242
242
|
"@ossy/booking/actions/update-service.description": "Update an existing bookable service",
|
|
243
243
|
"@ossy/booking/actions/delete-service.label": "Delete service",
|
|
244
244
|
"@ossy/booking/actions/delete-service.description": "Remove a bookable service",
|
|
245
|
+
"@ossy/booking/actions/open-delete-service.label": "Delete service",
|
|
246
|
+
"@ossy/booking/actions/open-delete-service.description": "Open delete confirmation for a bookable service",
|
|
247
|
+
"@ossy/booking/actions/open-edit-service.label": "Edit service",
|
|
248
|
+
"@ossy/booking/actions/open-edit-service.description": "Open the form to edit an existing bookable service",
|
|
245
249
|
"@ossy/booking/actions/open-availability-setup.label": "Set up availability",
|
|
246
250
|
"@ossy/booking/actions/open-availability-setup.description": "Open availability settings",
|
|
247
251
|
"@ossy/booking/actions/open-services.label": "Manage services",
|
|
@@ -276,6 +280,8 @@
|
|
|
276
280
|
"booking.services.errorSave": "Could not save service",
|
|
277
281
|
"booking.services.errorDelete": "Could not delete service",
|
|
278
282
|
"booking.services.deleteConfirm": "Delete this service?",
|
|
283
|
+
"booking.services.deleteTitle": "Delete this service?",
|
|
284
|
+
"booking.services.deleteText": "This removes the service from your catalog. Existing bookings are not changed.",
|
|
279
285
|
"booking.services.editService": "Edit service",
|
|
280
286
|
"booking.services.newService": "New service",
|
|
281
287
|
"booking.services.save": "Save",
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export { metadata as ServiceForm } from './service.form.js'
|
|
|
12
12
|
export { metadata as PublicBookingForm } from './public-booking-form.form.js'
|
|
13
13
|
export { metadata as UpdateService } from './update-service.action.js'
|
|
14
14
|
export { metadata as DeleteService } from './delete-service.action.js'
|
|
15
|
+
export { metadata as OpenDeleteService } from './open-delete-service.action.js'
|
|
16
|
+
export { metadata as OpenEditService } from './open-edit-service.action.js'
|
|
15
17
|
export { metadata as SaveAvailability } from './save-availability.action.js'
|
|
16
18
|
export { metadata as ConfirmBooking } from './confirm-booking.action.js'
|
|
17
19
|
export { metadata as DeclineBooking } from './decline-booking.action.js'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import providerOnboard from './provider-onboard.flow.js'
|
|
2
|
+
import { metadata as SaveAvailability } from './save-availability.action.js'
|
|
3
|
+
import { metadata as OpenAvailabilitySetup } from './open-availability-setup.action.js'
|
|
4
|
+
import {
|
|
5
|
+
ToggleAvailabilityDay0,
|
|
6
|
+
ToggleAvailabilityDay6,
|
|
7
|
+
SelectDuration90,
|
|
8
|
+
} from './availability-action-maps.js'
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
id: '@ossy/booking/flows/provider-availability-weekend',
|
|
12
|
+
feature: 'booking',
|
|
13
|
+
requires: ['server', 'database'],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Issue #5 — cover weekend day toggles and 90-minute duration (sibling of the
|
|
18
|
+
* Mon–Fri + 60m provider-availability flow).
|
|
19
|
+
*/
|
|
20
|
+
export default {
|
|
21
|
+
title: 'Set up weekend availability',
|
|
22
|
+
description:
|
|
23
|
+
'New service provider enables booking and saves Saturday/Sunday availability with 90-minute slots',
|
|
24
|
+
steps: [
|
|
25
|
+
...providerOnboard.steps,
|
|
26
|
+
{ action: OpenAvailabilitySetup },
|
|
27
|
+
{ result: { action: SaveAvailability, timeout: 10000 } },
|
|
28
|
+
{ action: ToggleAvailabilityDay0 },
|
|
29
|
+
{ action: ToggleAvailabilityDay6 },
|
|
30
|
+
{ action: SelectDuration90 },
|
|
31
|
+
{ action: SaveAvailability },
|
|
32
|
+
{ result: { selector: '[data-availability-status="saved"]', timeout: 10000 } },
|
|
33
|
+
],
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import providerOnboard from './provider-onboard.flow.js'
|
|
2
|
+
import { metadata as OpenServices } from './open-services.action.js'
|
|
3
|
+
import { metadata as OpenCreateService } from './open-create-service.action.js'
|
|
4
|
+
import { metadata as CreateService } from './create-service.action.js'
|
|
5
|
+
import { metadata as OpenDeleteService } from './open-delete-service.action.js'
|
|
6
|
+
import { metadata as ServiceForm } from './service.form.js'
|
|
7
|
+
|
|
8
|
+
export const metadata = {
|
|
9
|
+
id: '@ossy/booking/flows/provider-catalog',
|
|
10
|
+
feature: 'booking',
|
|
11
|
+
requires: ['server', 'database'],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Step 2 — provider enables booking and adds a catalog service.
|
|
16
|
+
*
|
|
17
|
+
* Composes after provider-onboard. client-booking / confirm-booking compose
|
|
18
|
+
* catalog + availability after onboard for the public booking journey.
|
|
19
|
+
*/
|
|
20
|
+
export default {
|
|
21
|
+
title: 'Create booking service',
|
|
22
|
+
description:
|
|
23
|
+
'New service provider registers, verifies email, enables booking, and creates a catalog service',
|
|
24
|
+
steps: [
|
|
25
|
+
...providerOnboard.steps,
|
|
26
|
+
{ action: OpenServices },
|
|
27
|
+
{ result: { selector: '[data-services-status]', timeout: 15000 } },
|
|
28
|
+
{ result: { action: OpenCreateService, timeout: 10000 } },
|
|
29
|
+
{ action: OpenCreateService },
|
|
30
|
+
{ result: { action: CreateService, timeout: 15000 } },
|
|
31
|
+
{ form: ServiceForm },
|
|
32
|
+
{ action: CreateService },
|
|
33
|
+
{ result: { action: OpenDeleteService, timeout: 15000 } },
|
|
34
|
+
],
|
|
35
|
+
}
|
|
@@ -10,7 +10,7 @@ export const metadata = {
|
|
|
10
10
|
/**
|
|
11
11
|
* Step 1 — provider signs up and enables the booking capability for their workspace.
|
|
12
12
|
*
|
|
13
|
-
* Next
|
|
13
|
+
* Next: provider-catalog (service setup), then client booking / confirm.
|
|
14
14
|
*/
|
|
15
15
|
export default {
|
|
16
16
|
title: 'Enable booking',
|
|
@@ -18,8 +18,8 @@ export default {
|
|
|
18
18
|
steps: [
|
|
19
19
|
...signUpFlow.steps,
|
|
20
20
|
{ page: '@booking/home' },
|
|
21
|
-
{ result: { action: { ...EnableService, service: '@ossy/booking' }, timeout:
|
|
21
|
+
{ result: { action: { ...EnableService, service: '@ossy/booking' }, timeout: 15000 } },
|
|
22
22
|
{ action: { ...EnableService, service: '@ossy/booking' } },
|
|
23
|
-
{ result: { selector: '[data-booking-status="on"]', timeout:
|
|
23
|
+
{ result: { selector: '[data-booking-status="on"]', timeout: 20000 } },
|
|
24
24
|
],
|
|
25
25
|
}
|
|
@@ -151,7 +151,7 @@ function BookingForm ({ slot, service, providerSlug, providerContact, onSuccess,
|
|
|
151
151
|
|
|
152
152
|
function PendingScreen ({ slot, t, language }) {
|
|
153
153
|
return (
|
|
154
|
-
<View gap="m" alignItems="center" style={{ textAlign: 'center' }}>
|
|
154
|
+
<View gap="m" alignItems="center" style={{ textAlign: 'center' }} data-flow-stage="booking-pending">
|
|
155
155
|
<Text variant="heading-secondary" as="h2" text="publicBooking.pending.title" />
|
|
156
156
|
<Text color="secondary" text="publicBooking.pending.body" />
|
|
157
157
|
<Alert variant="warning" title={t('publicBooking.pending.requestedTime')}>
|
|
@@ -175,7 +175,11 @@ function ServicePicker ({ services, selectedService, onSelect, t, language }) {
|
|
|
175
175
|
}
|
|
176
176
|
const formatPrice = (cents, currency) => {
|
|
177
177
|
if (!cents) return t('publicBooking.serviceFree')
|
|
178
|
-
|
|
178
|
+
try {
|
|
179
|
+
return new Intl.NumberFormat(language === 'sv' ? 'sv-SE' : 'en-GB', { style: 'currency', currency: currency || 'SEK' }).format(cents / 100)
|
|
180
|
+
} catch {
|
|
181
|
+
return `${(cents / 100).toFixed(2)} ${currency || 'SEK'}`
|
|
182
|
+
}
|
|
179
183
|
}
|
|
180
184
|
return (
|
|
181
185
|
<View gap="s">
|
|
@@ -3,9 +3,13 @@ import { View, Text, Badge } from '@ossy/design-system'
|
|
|
3
3
|
|
|
4
4
|
export const metadata = { id: '@ossy/booking/view/service/card' }
|
|
5
5
|
|
|
6
|
-
const formatPrice = (cents, currency) => {
|
|
6
|
+
const formatPrice = (cents, currency = 'SEK') => {
|
|
7
7
|
if (cents === 0) return 'Free'
|
|
8
|
-
|
|
8
|
+
try {
|
|
9
|
+
return new Intl.NumberFormat('sv-SE', { style: 'currency', currency }).format(cents / 100)
|
|
10
|
+
} catch {
|
|
11
|
+
return `${(cents / 100).toFixed(2)} ${currency}`
|
|
12
|
+
}
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
const formatDuration = (minutes) => {
|
|
@@ -3,9 +3,13 @@ import { View, Text, Badge } from '@ossy/design-system'
|
|
|
3
3
|
|
|
4
4
|
export const metadata = { id: '@ossy/booking/view/service' }
|
|
5
5
|
|
|
6
|
-
const formatPrice = (cents, currency) => {
|
|
6
|
+
const formatPrice = (cents, currency = 'SEK') => {
|
|
7
7
|
if (cents === 0) return 'Free'
|
|
8
|
-
|
|
8
|
+
try {
|
|
9
|
+
return new Intl.NumberFormat('sv-SE', { style: 'currency', currency }).format(cents / 100)
|
|
10
|
+
} catch {
|
|
11
|
+
return `${(cents / 100).toFixed(2)} ${currency}`
|
|
12
|
+
}
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
const formatDuration = (minutes) => {
|
package/src/service.schema.js
CHANGED
|
@@ -5,9 +5,9 @@ export default {
|
|
|
5
5
|
icon: 'calendar',
|
|
6
6
|
fields: [
|
|
7
7
|
{ name: 'name', type: 'text', required: true },
|
|
8
|
-
{ name: 'duration', type: '
|
|
8
|
+
{ name: 'duration', type: 'select', required: true, options: ['30', '60', '90'] },
|
|
9
9
|
{ name: 'price', type: 'number' },
|
|
10
|
-
{ name: 'currency', type: '
|
|
10
|
+
{ name: 'currency', type: 'select', options: ['SEK', 'EUR', 'USD', 'NOK', 'DKK'] },
|
|
11
11
|
{ name: 'description', type: 'textarea' },
|
|
12
12
|
{ name: 'active', type: 'boolean' },
|
|
13
13
|
],
|
package/src/services.page.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import { View, Text, Button, Alert, Form, FieldFactory, FormStatus, ContentHeader, Page, useLocale } from '@ossy/design-system'
|
|
2
|
+
import { View, Text, Button, Alert, Form, FieldFactory, FormStatus, ContentHeader, Page, Overlay, Guide, useLocale } from '@ossy/design-system'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
4
|
import { useDocumentValidator } from '@ossy/resources'
|
|
5
5
|
import { Definition } from './Definition.js'
|
|
@@ -7,6 +7,8 @@ import { ServiceCard } from './ServiceCard.jsx'
|
|
|
7
7
|
import { metadata as CreateService } from './create-service.action.js'
|
|
8
8
|
import { metadata as UpdateService } from './update-service.action.js'
|
|
9
9
|
import { metadata as DeleteService } from './delete-service.action.js'
|
|
10
|
+
import { metadata as OpenDeleteService } from './open-delete-service.action.js'
|
|
11
|
+
import { metadata as OpenEditService } from './open-edit-service.action.js'
|
|
10
12
|
import { metadata as GetServices } from './get-services.action.js'
|
|
11
13
|
import { metadata as OpenCreateService } from './open-create-service.action.js'
|
|
12
14
|
import { metadata as ServiceForm } from './service.form.js'
|
|
@@ -23,7 +25,7 @@ export const metadata = {
|
|
|
23
25
|
|
|
24
26
|
const EMPTY_FORM = {
|
|
25
27
|
name: '',
|
|
26
|
-
duration: 60,
|
|
28
|
+
duration: '60',
|
|
27
29
|
price: 0,
|
|
28
30
|
currency: 'SEK',
|
|
29
31
|
description: '',
|
|
@@ -40,6 +42,8 @@ export default function ServicesPage () {
|
|
|
40
42
|
const [editingId, setEditingId] = useState(null)
|
|
41
43
|
const [formDefaults, setFormDefaults] = useState(EMPTY_FORM)
|
|
42
44
|
const [saving, setSaving] = useState(false)
|
|
45
|
+
const [serviceToDelete, setServiceToDelete] = useState(null)
|
|
46
|
+
const [deleting, setDeleting] = useState(false)
|
|
43
47
|
const validate = useDocumentValidator(serviceTemplate)
|
|
44
48
|
|
|
45
49
|
const loadServices = useCallback(async () => {
|
|
@@ -69,7 +73,7 @@ export default function ServicesPage () {
|
|
|
69
73
|
setEditingId(service.id)
|
|
70
74
|
setFormDefaults({
|
|
71
75
|
name: service.name ?? '',
|
|
72
|
-
duration: service.duration ?? 60,
|
|
76
|
+
duration: String(service.duration ?? 60),
|
|
73
77
|
price: service.price ?? 0,
|
|
74
78
|
currency: service.currency ?? 'SEK',
|
|
75
79
|
description: service.description ?? '',
|
|
@@ -94,14 +98,18 @@ export default function ServicesPage () {
|
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
const
|
|
98
|
-
if (!
|
|
101
|
+
const confirmDelete = async () => {
|
|
102
|
+
if (!serviceToDelete?.id) return
|
|
103
|
+
setDeleting(true)
|
|
99
104
|
setError(null)
|
|
100
105
|
try {
|
|
101
|
-
await sdk.invoke(DeleteService, { serviceId })
|
|
106
|
+
await sdk.invoke(DeleteService, { serviceId: serviceToDelete.id })
|
|
107
|
+
setServiceToDelete(null)
|
|
102
108
|
await loadServices()
|
|
103
109
|
} catch (err) {
|
|
104
110
|
setError(await invokeErrorMessage(err, t('booking.services.errorDelete')))
|
|
111
|
+
} finally {
|
|
112
|
+
setDeleting(false)
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -157,7 +165,10 @@ export default function ServicesPage () {
|
|
|
157
165
|
</Form>
|
|
158
166
|
)}
|
|
159
167
|
|
|
160
|
-
<View
|
|
168
|
+
<View
|
|
169
|
+
gap="s"
|
|
170
|
+
data-services-status={loading ? 'loading' : services.length === 0 ? 'empty' : 'populated'}
|
|
171
|
+
>
|
|
161
172
|
<Text variant="heading-secondary" as="h2" text="booking.services.yourServices" />
|
|
162
173
|
{loading ? (
|
|
163
174
|
<Text color="secondary" text="booking.services.loading" />
|
|
@@ -166,7 +177,12 @@ export default function ServicesPage () {
|
|
|
166
177
|
) : (
|
|
167
178
|
<View gap="s">
|
|
168
179
|
{services.map(service => (
|
|
169
|
-
<View
|
|
180
|
+
<View
|
|
181
|
+
key={service.id}
|
|
182
|
+
gap="xs"
|
|
183
|
+
data-service-id={service.id}
|
|
184
|
+
data-service-name={service.name}
|
|
185
|
+
>
|
|
170
186
|
<ServiceCard
|
|
171
187
|
name={service.name}
|
|
172
188
|
duration={service.duration}
|
|
@@ -176,14 +192,54 @@ export default function ServicesPage () {
|
|
|
176
192
|
active={service.active !== false}
|
|
177
193
|
/>
|
|
178
194
|
<View layout="row" gap="s">
|
|
179
|
-
<Button
|
|
180
|
-
|
|
195
|
+
<Button
|
|
196
|
+
{...OpenEditService}
|
|
197
|
+
variant="link"
|
|
198
|
+
size="s"
|
|
199
|
+
label="booking.services.edit"
|
|
200
|
+
onClick={() => openEdit(service)}
|
|
201
|
+
/>
|
|
202
|
+
<Button
|
|
203
|
+
{...OpenDeleteService}
|
|
204
|
+
variant="link"
|
|
205
|
+
size="s"
|
|
206
|
+
label="booking.services.delete"
|
|
207
|
+
onClick={() => setServiceToDelete(service)}
|
|
208
|
+
/>
|
|
181
209
|
</View>
|
|
182
210
|
</View>
|
|
183
211
|
))}
|
|
184
212
|
</View>
|
|
185
213
|
)}
|
|
186
214
|
</View>
|
|
215
|
+
|
|
216
|
+
<Overlay isVisible={Boolean(serviceToDelete)} onClose={() => !deleting && setServiceToDelete(null)}>
|
|
217
|
+
<View layout="off-center-s" style={{ height: '100%' }}>
|
|
218
|
+
<View data-region="content">
|
|
219
|
+
<View surface="primary" roundness="s" inset="l">
|
|
220
|
+
<Guide
|
|
221
|
+
title="booking.services.deleteTitle"
|
|
222
|
+
text="booking.services.deleteText"
|
|
223
|
+
actions={[
|
|
224
|
+
{
|
|
225
|
+
label: 'booking.services.cancel',
|
|
226
|
+
variant: 'command',
|
|
227
|
+
disabled: deleting,
|
|
228
|
+
onClick: () => setServiceToDelete(null),
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
...DeleteService,
|
|
232
|
+
label: 'booking.services.delete',
|
|
233
|
+
variant: 'command-danger',
|
|
234
|
+
disabled: deleting,
|
|
235
|
+
onClick: confirmDelete,
|
|
236
|
+
},
|
|
237
|
+
]}
|
|
238
|
+
/>
|
|
239
|
+
</View>
|
|
240
|
+
</View>
|
|
241
|
+
</View>
|
|
242
|
+
</Overlay>
|
|
187
243
|
</Page>
|
|
188
244
|
)
|
|
189
245
|
}
|
package/src/sv.translations.json
CHANGED
|
@@ -242,6 +242,10 @@
|
|
|
242
242
|
"@ossy/booking/actions/update-service.description": "Uppdatera en befintlig bokningsbar tjänst",
|
|
243
243
|
"@ossy/booking/actions/delete-service.label": "Ta bort tjänst",
|
|
244
244
|
"@ossy/booking/actions/delete-service.description": "Ta bort en bokningsbar tjänst",
|
|
245
|
+
"@ossy/booking/actions/open-delete-service.label": "Ta bort tjänst",
|
|
246
|
+
"@ossy/booking/actions/open-delete-service.description": "Öppna bekräftelse för att ta bort en bokningsbar tjänst",
|
|
247
|
+
"@ossy/booking/actions/open-edit-service.label": "Redigera tjänst",
|
|
248
|
+
"@ossy/booking/actions/open-edit-service.description": "Öppna formuläret för att redigera en befintlig bokningsbar tjänst",
|
|
245
249
|
"@ossy/booking/actions/open-availability-setup.label": "Ställ in tillgänglighet",
|
|
246
250
|
"@ossy/booking/actions/open-availability-setup.description": "Öppna tillgänglighetsinställningar",
|
|
247
251
|
"@ossy/booking/actions/open-services.label": "Hantera tjänster",
|
|
@@ -276,6 +280,8 @@
|
|
|
276
280
|
"booking.services.errorSave": "Kunde inte spara tjänst",
|
|
277
281
|
"booking.services.errorDelete": "Kunde inte ta bort tjänst",
|
|
278
282
|
"booking.services.deleteConfirm": "Ta bort denna tjänst?",
|
|
283
|
+
"booking.services.deleteTitle": "Ta bort denna tjänst?",
|
|
284
|
+
"booking.services.deleteText": "Detta tar bort tjänsten från din katalog. Befintliga bokningar påverkas inte.",
|
|
279
285
|
"booking.services.editService": "Redigera tjänst",
|
|
280
286
|
"booking.services.newService": "Ny tjänst",
|
|
281
287
|
"booking.services.save": "Spara",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import providerCatalogFlow from './provider-catalog.flow.js'
|
|
2
|
+
import { metadata as OpenEditService } from './open-edit-service.action.js'
|
|
3
|
+
import { metadata as UpdateService } from './update-service.action.js'
|
|
4
|
+
import { metadata as ServiceForm } from './service.form.js'
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: '@ossy/booking/flows/update-service',
|
|
8
|
+
feature: 'booking',
|
|
9
|
+
requires: ['server', 'database'],
|
|
10
|
+
timeout: 120_000,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* After creating a catalog service, open edit, change fields, save via
|
|
15
|
+
* UpdateService, and assert the list shows the updated name.
|
|
16
|
+
*/
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Update booking service',
|
|
19
|
+
description:
|
|
20
|
+
'Signed-in provider creates a catalog service, edits it from the services list, and sees the updated name',
|
|
21
|
+
steps: [
|
|
22
|
+
...providerCatalogFlow.steps,
|
|
23
|
+
{ result: { action: OpenEditService, timeout: 10000 } },
|
|
24
|
+
{ action: OpenEditService },
|
|
25
|
+
{ result: { action: UpdateService, timeout: 10000 } },
|
|
26
|
+
{ form: ServiceForm, fields: { name: 'e2e-updated-service' } },
|
|
27
|
+
{ action: UpdateService },
|
|
28
|
+
{
|
|
29
|
+
result: {
|
|
30
|
+
selector: '[data-service-name="e2e-updated-service"]',
|
|
31
|
+
timeout: 20000,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
result: {
|
|
36
|
+
selector: '[data-action="@ossy/booking/actions/update-service"]',
|
|
37
|
+
hidden: true,
|
|
38
|
+
timeout: 10000,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
}
|