@ossy/booking 1.15.7 → 1.16.0
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 +7 -7
- package/src/AvailabilityEditor.jsx +22 -15
- package/src/BookingForm.jsx +18 -28
- package/src/Definition.js +1 -23
- package/src/SalesSection.jsx +9 -4
- package/src/availability-setup.page.jsx +77 -110
- package/src/availability.page.jsx +25 -23
- package/src/booking-card.component.jsx +24 -33
- package/src/booking-detail.page.jsx +61 -205
- package/src/booking-page.page.jsx +4 -7
- package/src/bookings.page.jsx +32 -22
- package/src/en.translations.json +213 -0
- package/src/home.page.jsx +48 -5
- package/src/public-booking.page.jsx +81 -222
- package/src/services.page.jsx +44 -43
- package/src/sv.translations.json +213 -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 consultants",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.16.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"src": "./src"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@ossy/email": "^1.
|
|
18
|
-
"@ossy/event-store": "^1.
|
|
19
|
-
"@ossy/observability": "^1.
|
|
20
|
-
"@ossy/platform": "^1.
|
|
21
|
-
"@ossy/resources": "^1.
|
|
17
|
+
"@ossy/email": "^1.6.0",
|
|
18
|
+
"@ossy/event-store": "^1.8.0",
|
|
19
|
+
"@ossy/observability": "^1.8.0",
|
|
20
|
+
"@ossy/platform": "^1.39.0",
|
|
21
|
+
"@ossy/resources": "^1.12.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@ossy/design-system": ">=1.0.0",
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"/src",
|
|
35
35
|
"README.md"
|
|
36
36
|
],
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "d9d31182be64a448d575da432af2830d909ad4b9"
|
|
38
38
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { View,
|
|
3
|
-
|
|
4
|
-
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
1
|
+
import React, { useState, useMemo } from 'react'
|
|
2
|
+
import { View, Text, Button, Input, useLocale } from '@ossy/design-system'
|
|
5
3
|
|
|
6
4
|
const DEFAULT_WINDOW = { enabled: false, startTime: '09:00', endTime: '17:00', bufferMinutes: 0 }
|
|
7
5
|
|
|
8
6
|
const buildInitialState = (availability = []) => {
|
|
9
|
-
const state =
|
|
7
|
+
const state = Array.from({ length: 7 }, () => ({ ...DEFAULT_WINDOW }))
|
|
10
8
|
for (const window of availability) {
|
|
11
9
|
if (window.dayOfWeek >= 0 && window.dayOfWeek <= 6) {
|
|
12
10
|
state[window.dayOfWeek] = {
|
|
@@ -21,8 +19,19 @@ const buildInitialState = (availability = []) => {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
22
|
+
const { t } = useLocale()
|
|
24
23
|
const [days, setDays] = useState(() => buildInitialState(availability))
|
|
25
24
|
|
|
25
|
+
const dayKeys = useMemo(() => [
|
|
26
|
+
'booking.availabilityEditor.day.sun',
|
|
27
|
+
'booking.availabilityEditor.day.mon',
|
|
28
|
+
'booking.availabilityEditor.day.tue',
|
|
29
|
+
'booking.availabilityEditor.day.wed',
|
|
30
|
+
'booking.availabilityEditor.day.thu',
|
|
31
|
+
'booking.availabilityEditor.day.fri',
|
|
32
|
+
'booking.availabilityEditor.day.sat',
|
|
33
|
+
], [])
|
|
34
|
+
|
|
26
35
|
const update = (idx, patch) =>
|
|
27
36
|
setDays((prev) => prev.map((d, i) => (i === idx ? { ...d, ...patch } : d)))
|
|
28
37
|
|
|
@@ -37,16 +46,15 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
37
46
|
bufferMinutes: Number(bufferMinutes),
|
|
38
47
|
}))
|
|
39
48
|
|
|
40
|
-
// TODO: dispatch booking.set-availability action with `windows`
|
|
41
49
|
onSave?.(windows)
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
return (
|
|
45
53
|
<View gap="m">
|
|
46
54
|
<View gap="s">
|
|
47
|
-
{
|
|
55
|
+
{dayKeys.map((key, idx) => (
|
|
48
56
|
<View
|
|
49
|
-
key={
|
|
57
|
+
key={key}
|
|
50
58
|
layout="row"
|
|
51
59
|
gap="m"
|
|
52
60
|
alignItems="center"
|
|
@@ -62,7 +70,7 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
62
70
|
style={{ width: 16, height: 16, accentColor: 'var(--color-accent)' }}
|
|
63
71
|
/>
|
|
64
72
|
</label>
|
|
65
|
-
<Text weight="medium" style={{ width: '2.5rem', flexShrink: 0 }}>{
|
|
73
|
+
<Text weight="medium" style={{ width: '2.5rem', flexShrink: 0 }}>{t(key)}</Text>
|
|
66
74
|
|
|
67
75
|
{days[idx].enabled ? (
|
|
68
76
|
<View layout="row" gap="s" alignItems="center" style={{ flex: 1 }}>
|
|
@@ -72,14 +80,14 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
72
80
|
onChange={(v) => update(idx, { startTime: v })}
|
|
73
81
|
size="s"
|
|
74
82
|
/>
|
|
75
|
-
<Text color="secondary">to</Text>
|
|
83
|
+
<Text color="secondary">{t('booking.availabilityEditor.to')}</Text>
|
|
76
84
|
<Input
|
|
77
85
|
type="time"
|
|
78
86
|
value={days[idx].endTime}
|
|
79
87
|
onChange={(v) => update(idx, { endTime: v })}
|
|
80
88
|
size="s"
|
|
81
89
|
/>
|
|
82
|
-
<Text color="secondary" size="s">
|
|
90
|
+
<Text color="secondary" size="s">{t('booking.availabilityEditor.buffer')}</Text>
|
|
83
91
|
<Input
|
|
84
92
|
type="number"
|
|
85
93
|
value={String(days[idx].bufferMinutes)}
|
|
@@ -87,17 +95,16 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
87
95
|
size="s"
|
|
88
96
|
style={{ width: '4rem' }}
|
|
89
97
|
/>
|
|
90
|
-
<Text color="secondary" size="s">min</Text>
|
|
98
|
+
<Text color="secondary" size="s">{t('booking.availabilityEditor.min')}</Text>
|
|
91
99
|
</View>
|
|
92
100
|
) : (
|
|
93
|
-
<Text color="secondary" size="s">
|
|
101
|
+
<Text color="secondary" size="s">{t('booking.availabilityEditor.unavailable')}</Text>
|
|
94
102
|
)}
|
|
95
103
|
</View>
|
|
96
104
|
))}
|
|
97
105
|
</View>
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
<Button variant="primary" onClick={handleSave}>Save availability</Button>
|
|
107
|
+
<Button variant="primary" onClick={handleSave}>{t('booking.availabilityEditor.save')}</Button>
|
|
101
108
|
</View>
|
|
102
109
|
)
|
|
103
110
|
}
|
package/src/BookingForm.jsx
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { View, Title, Text, Button, Input,
|
|
2
|
+
import { View, Title, Text, Button, Input, useLocale } from '@ossy/design-system'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Public client-facing form.
|
|
6
|
-
*
|
|
7
|
-
* TODO:
|
|
8
|
-
* - Load services for workspaceId via booking.get-services
|
|
9
|
-
* - Load available slots for selected service via booking.get-available-slots
|
|
10
|
-
* - On submit: call booking.create-booking action, then redirect to Stripe Checkout
|
|
11
|
-
*/
|
|
12
4
|
export const BookingForm = ({ workspaceId }) => {
|
|
5
|
+
const { t } = useLocale()
|
|
13
6
|
const [step, setStep] = useState('pick-service')
|
|
14
7
|
const [selectedService, setSelectedService] = useState(null)
|
|
15
8
|
const [selectedSlot, setSelectedSlot] = useState(null)
|
|
@@ -19,11 +12,10 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
19
12
|
if (step === 'pick-service') {
|
|
20
13
|
return (
|
|
21
14
|
<View gap="m">
|
|
22
|
-
<Title variant="secondary">
|
|
23
|
-
{
|
|
24
|
-
<Text color="secondary">Loading services for workspace {workspaceId}…</Text>
|
|
15
|
+
<Title variant="secondary">{t('booking.book.form.chooseService')}</Title>
|
|
16
|
+
<Text color="secondary">{t('booking.book.form.loadingServices', { workspaceId })}</Text>
|
|
25
17
|
<Button variant="secondary" onClick={() => setStep('pick-slot')}>
|
|
26
|
-
|
|
18
|
+
{t('booking.book.form.continuePlaceholder')}
|
|
27
19
|
</Button>
|
|
28
20
|
</View>
|
|
29
21
|
)
|
|
@@ -32,13 +24,12 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
32
24
|
if (step === 'pick-slot') {
|
|
33
25
|
return (
|
|
34
26
|
<View gap="m">
|
|
35
|
-
<Title variant="secondary">
|
|
36
|
-
{
|
|
37
|
-
<Text color="secondary">Available slots will appear here.</Text>
|
|
27
|
+
<Title variant="secondary">{t('booking.book.form.pickTime')}</Title>
|
|
28
|
+
<Text color="secondary">{t('booking.book.form.slotsPlaceholder')}</Text>
|
|
38
29
|
<View layout="row" gap="s">
|
|
39
|
-
<Button variant="ghost" onClick={() => setStep('pick-service')}>
|
|
30
|
+
<Button variant="ghost" onClick={() => setStep('pick-service')}>{t('booking.book.form.back')}</Button>
|
|
40
31
|
<Button variant="secondary" onClick={() => setStep('enter-details')}>
|
|
41
|
-
|
|
32
|
+
{t('booking.book.form.continuePlaceholder')}
|
|
42
33
|
</Button>
|
|
43
34
|
</View>
|
|
44
35
|
</View>
|
|
@@ -48,25 +39,24 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
48
39
|
if (step === 'enter-details') {
|
|
49
40
|
return (
|
|
50
41
|
<View gap="m">
|
|
51
|
-
<Title variant="secondary">
|
|
42
|
+
<Title variant="secondary">{t('booking.book.form.yourDetails')}</Title>
|
|
52
43
|
<Input
|
|
53
|
-
label=
|
|
44
|
+
label={t('booking.book.form.nameLabel')}
|
|
54
45
|
value={clientName}
|
|
55
46
|
onChange={setClientName}
|
|
56
|
-
placeholder=
|
|
47
|
+
placeholder={t('booking.book.form.namePlaceholder')}
|
|
57
48
|
/>
|
|
58
49
|
<Input
|
|
59
|
-
label=
|
|
50
|
+
label={t('booking.book.form.emailLabel')}
|
|
60
51
|
type="email"
|
|
61
52
|
value={clientEmail}
|
|
62
53
|
onChange={setClientEmail}
|
|
63
|
-
placeholder=
|
|
54
|
+
placeholder={t('booking.book.form.emailPlaceholder')}
|
|
64
55
|
/>
|
|
65
56
|
<View layout="row" gap="s">
|
|
66
|
-
<Button variant="ghost" onClick={() => setStep('pick-slot')}>
|
|
67
|
-
{/* TODO: submit booking action then redirect to Stripe */}
|
|
57
|
+
<Button variant="ghost" onClick={() => setStep('pick-slot')}>{t('booking.book.form.back')}</Button>
|
|
68
58
|
<Button variant="primary" onClick={() => setStep('confirm')}>
|
|
69
|
-
|
|
59
|
+
{t('booking.book.form.confirmPay')}
|
|
70
60
|
</Button>
|
|
71
61
|
</View>
|
|
72
62
|
</View>
|
|
@@ -75,8 +65,8 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
75
65
|
|
|
76
66
|
return (
|
|
77
67
|
<View gap="m">
|
|
78
|
-
<Title variant="secondary">
|
|
79
|
-
<Text>
|
|
68
|
+
<Title variant="secondary">{t('booking.book.form.confirmed')}</Title>
|
|
69
|
+
<Text>{t('booking.book.form.checkEmail')}</Text>
|
|
80
70
|
</View>
|
|
81
71
|
)
|
|
82
72
|
}
|
package/src/Definition.js
CHANGED
|
@@ -2,28 +2,6 @@ export const Definition = {
|
|
|
2
2
|
id: 'booking',
|
|
3
3
|
title: 'Booking',
|
|
4
4
|
description: 'Services, availability windows, and client appointment management for consultants.',
|
|
5
|
-
|
|
6
|
-
id: 'booking',
|
|
7
|
-
enabled: true,
|
|
8
|
-
},
|
|
5
|
+
icon: 'calendar',
|
|
9
6
|
statuses: ['beta'],
|
|
10
|
-
actions: [
|
|
11
|
-
'booking.create-service',
|
|
12
|
-
'booking.update-service',
|
|
13
|
-
'booking.delete-service',
|
|
14
|
-
'booking.create-booking',
|
|
15
|
-
'booking.confirm-booking',
|
|
16
|
-
'booking.cancel-booking',
|
|
17
|
-
'booking.set-availability',
|
|
18
|
-
],
|
|
19
|
-
views: [
|
|
20
|
-
'booking.get-services',
|
|
21
|
-
'booking.get-bookings',
|
|
22
|
-
'booking.get-availability',
|
|
23
|
-
'booking.get-available-slots',
|
|
24
|
-
],
|
|
25
|
-
tasks: [
|
|
26
|
-
'booking.send-confirmation-email',
|
|
27
|
-
'booking.sync-stripe-payment-status',
|
|
28
|
-
],
|
|
29
7
|
}
|
package/src/SalesSection.jsx
CHANGED
|
@@ -6,20 +6,24 @@ import {
|
|
|
6
6
|
Icon,
|
|
7
7
|
PageSection,
|
|
8
8
|
MarkdownViewer,
|
|
9
|
+
useLocale,
|
|
9
10
|
} from '@ossy/design-system'
|
|
10
11
|
import { HeroCover } from './HeroCover.jsx'
|
|
11
12
|
|
|
12
13
|
export default ({
|
|
13
14
|
cover,
|
|
14
15
|
features,
|
|
15
|
-
}) =>
|
|
16
|
+
}) => {
|
|
17
|
+
const { t } = useLocale()
|
|
18
|
+
|
|
19
|
+
return (
|
|
16
20
|
<PageSection maxWidth="xl" gap="l">
|
|
17
21
|
<View gap="l" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
|
|
18
22
|
<HeroCover {...cover} />
|
|
19
23
|
|
|
20
24
|
<View gap="m" inset="s">
|
|
21
25
|
<Title variant="secondary">
|
|
22
|
-
|
|
26
|
+
{t('booking.sales.overview')}
|
|
23
27
|
</Title>
|
|
24
28
|
|
|
25
29
|
<View gap="m" layout="row-wrap">
|
|
@@ -47,7 +51,7 @@ export default ({
|
|
|
47
51
|
|
|
48
52
|
<View gap="m" inset="s">
|
|
49
53
|
<Title variant="secondary">
|
|
50
|
-
|
|
54
|
+
{t('booking.sales.features')}
|
|
51
55
|
</Title>
|
|
52
56
|
|
|
53
57
|
{features.map(x => (
|
|
@@ -69,4 +73,5 @@ export default ({
|
|
|
69
73
|
</View>
|
|
70
74
|
</View>
|
|
71
75
|
</PageSection>
|
|
72
|
-
)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Title,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Input,
|
|
8
8
|
Select,
|
|
9
9
|
Alert,
|
|
10
|
+
useLocale,
|
|
10
11
|
} from '@ossy/design-system'
|
|
11
12
|
|
|
12
13
|
export const metadata = {
|
|
@@ -14,16 +15,6 @@ export const metadata = {
|
|
|
14
15
|
path: { sv: '/tillganglighet', en: '/availability' },
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const DAYS = [
|
|
18
|
-
{ label: 'Måndag', dayOfWeek: 1 },
|
|
19
|
-
{ label: 'Tisdag', dayOfWeek: 2 },
|
|
20
|
-
{ label: 'Onsdag', dayOfWeek: 3 },
|
|
21
|
-
{ label: 'Torsdag', dayOfWeek: 4 },
|
|
22
|
-
{ label: 'Fredag', dayOfWeek: 5 },
|
|
23
|
-
{ label: 'Lördag', dayOfWeek: 6 },
|
|
24
|
-
{ label: 'Söndag', dayOfWeek: 0 },
|
|
25
|
-
]
|
|
26
|
-
|
|
27
18
|
const SESSION_DURATIONS = [30, 60, 90]
|
|
28
19
|
const BUFFER_OPTIONS = [0, 15, 30]
|
|
29
20
|
const TIMEZONE_OPTIONS = [
|
|
@@ -45,15 +36,22 @@ function buildDayState (weeklyWindows = []) {
|
|
|
45
36
|
endTime: w.endTime ?? '17:00',
|
|
46
37
|
}
|
|
47
38
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
const dayOrder = [
|
|
40
|
+
{ dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
|
|
41
|
+
{ dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
|
|
42
|
+
{ dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
|
|
43
|
+
{ dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
|
|
44
|
+
{ dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
|
|
45
|
+
{ dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
|
|
46
|
+
{ dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
|
|
47
|
+
]
|
|
48
|
+
return dayOrder.map(({ dayOfWeek }) => ({
|
|
49
|
+
dayOfWeek,
|
|
50
|
+
...(byDay[dayOfWeek] ?? { enabled: false, startTime: '09:00', endTime: '17:00' }),
|
|
51
|
+
}))
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
function DayRow ({ label, day, onToggle, onTimeChange }) {
|
|
54
|
+
function DayRow ({ label, day, onToggle, onTimeChange, t }) {
|
|
57
55
|
return (
|
|
58
56
|
<View
|
|
59
57
|
layout="row"
|
|
@@ -76,26 +74,29 @@ function DayRow ({ label, day, onToggle, onTimeChange }) {
|
|
|
76
74
|
|
|
77
75
|
{day.enabled ? (
|
|
78
76
|
<View layout="row" gap="s" alignItems="center" style={{ flex: 1, flexWrap: 'wrap' }}>
|
|
79
|
-
<Input
|
|
80
|
-
type="time"
|
|
81
|
-
value={day.startTime}
|
|
82
|
-
onChange={e => onTimeChange('startTime', e.target.value)}
|
|
83
|
-
/>
|
|
77
|
+
<Input type="time" value={day.startTime} onChange={e => onTimeChange('startTime', e.target.value)} />
|
|
84
78
|
<Text color="secondary">–</Text>
|
|
85
|
-
<Input
|
|
86
|
-
type="time"
|
|
87
|
-
value={day.endTime}
|
|
88
|
-
onChange={e => onTimeChange('endTime', e.target.value)}
|
|
89
|
-
/>
|
|
79
|
+
<Input type="time" value={day.endTime} onChange={e => onTimeChange('endTime', e.target.value)} />
|
|
90
80
|
</View>
|
|
91
81
|
) : (
|
|
92
|
-
<Text color="secondary" size="s">
|
|
82
|
+
<Text color="secondary" size="s">{t('booking.availabilitySetup.unavailable')}</Text>
|
|
93
83
|
)}
|
|
94
84
|
</View>
|
|
95
85
|
)
|
|
96
86
|
}
|
|
97
87
|
|
|
98
88
|
export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
89
|
+
const { t } = useLocale()
|
|
90
|
+
const dayDefs = useMemo(() => [
|
|
91
|
+
{ dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
|
|
92
|
+
{ dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
|
|
93
|
+
{ dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
|
|
94
|
+
{ dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
|
|
95
|
+
{ dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
|
|
96
|
+
{ dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
|
|
97
|
+
{ dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
|
|
98
|
+
], [])
|
|
99
|
+
|
|
99
100
|
const [dayState, setDayState] = useState(() => buildDayState())
|
|
100
101
|
const [sessionDurations, setSessionDurations] = useState([60])
|
|
101
102
|
const [bufferMinutes, setBufferMinutes] = useState(0)
|
|
@@ -129,18 +130,14 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
|
129
130
|
}, [])
|
|
130
131
|
|
|
131
132
|
const toggleDay = (dayOfWeek) => {
|
|
132
|
-
setDayState(prev => (
|
|
133
|
-
...
|
|
134
|
-
|
|
135
|
-
}))
|
|
133
|
+
setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek
|
|
134
|
+
? { ...d, enabled: !d.enabled }
|
|
135
|
+
: d))
|
|
136
136
|
setSaved(false)
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const updateDayTime = (dayOfWeek, field, value) => {
|
|
140
|
-
setDayState(prev => ({
|
|
141
|
-
...prev,
|
|
142
|
-
[dayOfWeek]: { ...prev[dayOfWeek], [field]: value },
|
|
143
|
-
}))
|
|
140
|
+
setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek ? { ...d, [field]: value } : d))
|
|
144
141
|
setSaved(false)
|
|
145
142
|
}
|
|
146
143
|
|
|
@@ -159,10 +156,8 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
|
159
156
|
setSaving(true)
|
|
160
157
|
setError(null)
|
|
161
158
|
|
|
162
|
-
const weeklyWindows =
|
|
163
|
-
dayOfWeek
|
|
164
|
-
startTime: dayState[d.dayOfWeek].startTime,
|
|
165
|
-
endTime: dayState[d.dayOfWeek].endTime,
|
|
159
|
+
const weeklyWindows = dayState.filter(d => d.enabled).map(({ dayOfWeek, startTime, endTime }) => ({
|
|
160
|
+
dayOfWeek, startTime, endTime,
|
|
166
161
|
}))
|
|
167
162
|
|
|
168
163
|
try {
|
|
@@ -174,7 +169,7 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
|
174
169
|
|
|
175
170
|
if (!res.ok) {
|
|
176
171
|
const data = await res.json().catch(() => ({}))
|
|
177
|
-
throw new Error(data?.message ??
|
|
172
|
+
throw new Error(data?.message ?? t('booking.availabilitySetup.errorSave', { status: res.status }))
|
|
178
173
|
}
|
|
179
174
|
|
|
180
175
|
setSaved(true)
|
|
@@ -195,7 +190,7 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
|
195
190
|
if (loading) {
|
|
196
191
|
return (
|
|
197
192
|
<View surface="primary" inset="l" style={{ height: '100%', overflowY: 'auto' }}>
|
|
198
|
-
<Text color="secondary">
|
|
193
|
+
<Text color="secondary">{t('booking.availabilitySetup.loading')}</Text>
|
|
199
194
|
</View>
|
|
200
195
|
)
|
|
201
196
|
}
|
|
@@ -207,83 +202,66 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
|
207
202
|
style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto', maxWidth: 620 }}
|
|
208
203
|
>
|
|
209
204
|
<View inset="s" gap="xs">
|
|
210
|
-
<Title variant="secondary">
|
|
211
|
-
<Text style={{ maxWidth: 480 }}>
|
|
212
|
-
Ange när du är tillgänglig för bokningar. Klienter kan bara boka tider inom dessa fönster.
|
|
213
|
-
</Text>
|
|
205
|
+
<Title variant="secondary">{t('booking.availabilitySetup.title')}</Title>
|
|
206
|
+
<Text style={{ maxWidth: 480 }}>{t('booking.availabilitySetup.description')}</Text>
|
|
214
207
|
</View>
|
|
215
208
|
|
|
216
209
|
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
217
|
-
<Title variant="tertiary">
|
|
210
|
+
<Title variant="tertiary">{t('booking.availabilitySetup.weeklySchedule')}</Title>
|
|
218
211
|
<View gap="xs">
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
212
|
+
{dayDefs.map(({ dayOfWeek, key }) => {
|
|
213
|
+
const day = dayState.find(d => d.dayOfWeek === dayOfWeek)
|
|
214
|
+
return (
|
|
215
|
+
<DayRow
|
|
216
|
+
key={dayOfWeek}
|
|
217
|
+
label={t(key)}
|
|
218
|
+
day={day}
|
|
219
|
+
onToggle={() => toggleDay(dayOfWeek)}
|
|
220
|
+
onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
|
|
221
|
+
t={t}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
})}
|
|
228
225
|
</View>
|
|
229
226
|
</View>
|
|
230
227
|
|
|
231
228
|
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
232
|
-
<Title variant="tertiary">
|
|
229
|
+
<Title variant="tertiary">{t('booking.availabilitySetup.sessionDuration')}</Title>
|
|
233
230
|
<View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
|
|
234
|
-
{SESSION_DURATIONS.map(dur =>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
</Button>
|
|
244
|
-
)
|
|
245
|
-
})}
|
|
231
|
+
{SESSION_DURATIONS.map(dur => (
|
|
232
|
+
<Button
|
|
233
|
+
key={dur}
|
|
234
|
+
variant={sessionDurations.includes(dur) ? 'tab-active' : 'tab'}
|
|
235
|
+
onClick={() => toggleDuration(dur)}
|
|
236
|
+
>
|
|
237
|
+
{t('booking.availabilitySetup.durationMin', { minutes: dur })}
|
|
238
|
+
</Button>
|
|
239
|
+
))}
|
|
246
240
|
</View>
|
|
247
|
-
<Text size="s" color="secondary">
|
|
248
|
-
Välj de sessionslängder klienter kan välja bland.
|
|
249
|
-
</Text>
|
|
241
|
+
<Text size="s" color="secondary">{t('booking.availabilitySetup.sessionDurationHint')}</Text>
|
|
250
242
|
</View>
|
|
251
243
|
|
|
252
244
|
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
253
|
-
<Text weight="medium">
|
|
245
|
+
<Text weight="medium">{t('booking.availabilitySetup.bufferLabel')}</Text>
|
|
254
246
|
<Select
|
|
255
247
|
id="buffer-select"
|
|
256
248
|
value={bufferMinutes}
|
|
257
|
-
onChange={e => {
|
|
258
|
-
setBufferMinutes(Number(e.target.value))
|
|
259
|
-
setSaved(false)
|
|
260
|
-
}}
|
|
249
|
+
onChange={e => { setBufferMinutes(Number(e.target.value)); setSaved(false) }}
|
|
261
250
|
>
|
|
262
251
|
{BUFFER_OPTIONS.map(opt => (
|
|
263
252
|
<option key={opt} value={opt}>
|
|
264
|
-
{opt === 0 ? '
|
|
253
|
+
{opt === 0 ? t('booking.availabilitySetup.bufferNone') : t('booking.availabilitySetup.bufferMinutes', { minutes: opt })}
|
|
265
254
|
</option>
|
|
266
255
|
))}
|
|
267
256
|
</Select>
|
|
268
|
-
<Text size="s" color="secondary">
|
|
269
|
-
Automatisk paustid som läggs till efter varje bokning.
|
|
270
|
-
</Text>
|
|
257
|
+
<Text size="s" color="secondary">{t('booking.availabilitySetup.bufferHint')}</Text>
|
|
271
258
|
</View>
|
|
272
259
|
|
|
273
260
|
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
274
|
-
<Text weight="medium">
|
|
275
|
-
<Select
|
|
276
|
-
id="tz-select"
|
|
277
|
-
value={timezone}
|
|
278
|
-
onChange={e => {
|
|
279
|
-
setTimezone(e.target.value)
|
|
280
|
-
setSaved(false)
|
|
281
|
-
}}
|
|
282
|
-
>
|
|
261
|
+
<Text weight="medium">{t('booking.availabilitySetup.timezone')}</Text>
|
|
262
|
+
<Select id="tz-select" value={timezone} onChange={e => { setTimezone(e.target.value); setSaved(false) }}>
|
|
283
263
|
{TIMEZONE_OPTIONS.map(tz => (
|
|
284
|
-
<option key={tz} value={tz}>
|
|
285
|
-
{tz}
|
|
286
|
-
</option>
|
|
264
|
+
<option key={tz} value={tz}>{tz}</option>
|
|
287
265
|
))}
|
|
288
266
|
</Select>
|
|
289
267
|
</View>
|
|
@@ -291,30 +269,19 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
|
291
269
|
{error && <Alert variant="danger">{error}</Alert>}
|
|
292
270
|
|
|
293
271
|
<Button variant="primary" disabled={saving} onClick={handleSave}>
|
|
294
|
-
{saving ? '
|
|
272
|
+
{saving ? t('booking.availabilitySetup.saving') : t('booking.availabilitySetup.save')}
|
|
295
273
|
</Button>
|
|
296
274
|
|
|
297
275
|
{saved && workspaceId && (
|
|
298
|
-
<Alert variant="success" title=
|
|
276
|
+
<Alert variant="success" title={t('booking.availabilitySetup.savedTitle')}>
|
|
299
277
|
<View gap="s">
|
|
300
|
-
<Text color="secondary">
|
|
301
|
-
Din bokningslänk är redo att delas med klienter.
|
|
302
|
-
</Text>
|
|
278
|
+
<Text color="secondary">{t('booking.availabilitySetup.savedBody')}</Text>
|
|
303
279
|
<View layout="row" gap="s" alignItems="center">
|
|
304
|
-
<Text
|
|
305
|
-
as="code"
|
|
306
|
-
style={{
|
|
307
|
-
flex: 1,
|
|
308
|
-
overflow: 'hidden',
|
|
309
|
-
textOverflow: 'ellipsis',
|
|
310
|
-
whiteSpace: 'nowrap',
|
|
311
|
-
fontFamily: 'ui-monospace, monospace',
|
|
312
|
-
}}
|
|
313
|
-
>
|
|
280
|
+
<Text as="code" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'ui-monospace, monospace' }}>
|
|
314
281
|
{bookingUrl}
|
|
315
282
|
</Text>
|
|
316
283
|
<Button variant={copied ? 'cta' : 'primary'} onClick={handleCopyLink}>
|
|
317
|
-
{copied ? '
|
|
284
|
+
{copied ? t('booking.availabilitySetup.copied') : t('booking.availabilitySetup.copyLink')}
|
|
318
285
|
</Button>
|
|
319
286
|
</View>
|
|
320
287
|
</View>
|