@ossy/booking 1.15.6 → 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/README.md +12 -0
- package/package.json +7 -7
- package/src/AvailabilityEditor.jsx +30 -19
- package/src/BookingForm.jsx +21 -31
- package/src/Definition.js +1 -23
- package/src/HeroCover.jsx +99 -0
- package/src/SalesSection.jsx +77 -0
- package/src/availability-setup.page.jsx +147 -397
- package/src/availability.page.jsx +25 -23
- package/src/booking-card.component.jsx +76 -215
- package/src/booking-detail.page.jsx +103 -573
- package/src/booking-page.page.jsx +4 -7
- package/src/bookings.page.jsx +62 -196
- package/src/cover.js +8 -0
- package/src/en.translations.json +213 -0
- package/src/features.js +27 -0
- package/src/home.page.jsx +47 -364
- package/src/public-booking.page.jsx +138 -636
- package/src/service-card-slot.component.jsx +48 -0
- package/src/service-detail.component.jsx +38 -0
- package/src/services.page.jsx +44 -43
- package/src/sv.translations.json +213 -0
|
@@ -1,46 +1,20 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
surfaceEnabled: '#f8f9ff',
|
|
17
|
-
border: '#e2e2e2',
|
|
18
|
-
borderEnabled: '#c7d2fe',
|
|
19
|
-
primary: '#111111',
|
|
20
|
-
primaryText: '#ffffff',
|
|
21
|
-
secondary: '#555555',
|
|
22
|
-
muted: '#999999',
|
|
23
|
-
success: '#16a34a',
|
|
24
|
-
successBg: '#f0fdf4',
|
|
25
|
-
successBorder: '#bbf7d0',
|
|
26
|
-
error: '#dc2626',
|
|
27
|
-
errorBg: '#fef2f2',
|
|
28
|
-
errorBorder: '#fecaca',
|
|
29
|
-
accent: '#4f46e5',
|
|
30
|
-
accentBg: '#eef2ff',
|
|
31
|
-
accentBorder: '#c7d2fe',
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Title,
|
|
5
|
+
Text,
|
|
6
|
+
Button,
|
|
7
|
+
Input,
|
|
8
|
+
Select,
|
|
9
|
+
Alert,
|
|
10
|
+
useLocale,
|
|
11
|
+
} from '@ossy/design-system'
|
|
12
|
+
|
|
13
|
+
export const metadata = {
|
|
14
|
+
id: 'booking/availability-setup',
|
|
15
|
+
path: { sv: '/tillganglighet', en: '/availability' },
|
|
32
16
|
}
|
|
33
17
|
|
|
34
|
-
const DAYS = [
|
|
35
|
-
{ label: 'Måndag', dayOfWeek: 1 },
|
|
36
|
-
{ label: 'Tisdag', dayOfWeek: 2 },
|
|
37
|
-
{ label: 'Onsdag', dayOfWeek: 3 },
|
|
38
|
-
{ label: 'Torsdag', dayOfWeek: 4 },
|
|
39
|
-
{ label: 'Fredag', dayOfWeek: 5 },
|
|
40
|
-
{ label: 'Lördag', dayOfWeek: 6 },
|
|
41
|
-
{ label: 'Söndag', dayOfWeek: 0 },
|
|
42
|
-
]
|
|
43
|
-
|
|
44
18
|
const SESSION_DURATIONS = [30, 60, 90]
|
|
45
19
|
const BUFFER_OPTIONS = [0, 15, 30]
|
|
46
20
|
const TIMEZONE_OPTIONS = [
|
|
@@ -53,11 +27,7 @@ const TIMEZONE_OPTIONS = [
|
|
|
53
27
|
'Asia/Tokyo',
|
|
54
28
|
]
|
|
55
29
|
|
|
56
|
-
|
|
57
|
-
// Helpers
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
function buildDayState(weeklyWindows = []) {
|
|
30
|
+
function buildDayState (weeklyWindows = []) {
|
|
61
31
|
const byDay = {}
|
|
62
32
|
for (const w of weeklyWindows) {
|
|
63
33
|
byDay[w.dayOfWeek] = {
|
|
@@ -66,121 +36,67 @@ function buildDayState(weeklyWindows = []) {
|
|
|
66
36
|
endTime: w.endTime ?? '17:00',
|
|
67
37
|
}
|
|
68
38
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
}))
|
|
75
52
|
}
|
|
76
53
|
|
|
77
|
-
|
|
78
|
-
// Sub-components
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
function Toggle({ checked, onChange }) {
|
|
54
|
+
function DayRow ({ label, day, onToggle, onTimeChange, t }) {
|
|
82
55
|
return (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
border: 'none',
|
|
92
|
-
background: checked ? COLOR.accent : '#d1d5db',
|
|
93
|
-
cursor: 'pointer',
|
|
94
|
-
position: 'relative',
|
|
95
|
-
flexShrink: 0,
|
|
96
|
-
transition: 'background 0.15s',
|
|
97
|
-
padding: 0,
|
|
98
|
-
}}
|
|
56
|
+
<View
|
|
57
|
+
layout="row"
|
|
58
|
+
gap="m"
|
|
59
|
+
alignItems="center"
|
|
60
|
+
surface={day.enabled ? 'secondary' : 'primary'}
|
|
61
|
+
roundness="m"
|
|
62
|
+
inset="s"
|
|
63
|
+
style={{ border: '1px solid var(--separator)' }}
|
|
99
64
|
>
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
width: 16,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
transition: 'left 0.15s',
|
|
110
|
-
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
</button>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function TimeInput({ value, onChange }) {
|
|
118
|
-
return (
|
|
119
|
-
<input
|
|
120
|
-
type="time"
|
|
121
|
-
value={value}
|
|
122
|
-
onChange={e => onChange(e.target.value)}
|
|
123
|
-
style={{
|
|
124
|
-
padding: '5px 8px',
|
|
125
|
-
border: `1px solid ${COLOR.border}`,
|
|
126
|
-
borderRadius: 6,
|
|
127
|
-
fontFamily: FONT,
|
|
128
|
-
fontSize: 13,
|
|
129
|
-
color: COLOR.primary,
|
|
130
|
-
background: COLOR.surface,
|
|
131
|
-
outline: 'none',
|
|
132
|
-
}}
|
|
133
|
-
/>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function DayRow({ label, dayOfWeek, day, onToggle, onTimeChange }) {
|
|
138
|
-
return (
|
|
139
|
-
<div
|
|
140
|
-
style={{
|
|
141
|
-
display: 'flex',
|
|
142
|
-
alignItems: 'center',
|
|
143
|
-
gap: 12,
|
|
144
|
-
padding: '10px 14px',
|
|
145
|
-
borderRadius: 8,
|
|
146
|
-
background: day.enabled ? COLOR.surfaceEnabled : COLOR.surfaceAlt,
|
|
147
|
-
border: `1px solid ${day.enabled ? COLOR.borderEnabled : COLOR.border}`,
|
|
148
|
-
transition: 'background 0.15s, border-color 0.15s',
|
|
149
|
-
}}
|
|
150
|
-
>
|
|
151
|
-
<Toggle checked={day.enabled} onChange={onToggle} />
|
|
152
|
-
|
|
153
|
-
<span
|
|
154
|
-
style={{
|
|
155
|
-
width: 70,
|
|
156
|
-
fontSize: 14,
|
|
157
|
-
fontWeight: 600,
|
|
158
|
-
color: day.enabled ? COLOR.primary : COLOR.muted,
|
|
159
|
-
flexShrink: 0,
|
|
160
|
-
userSelect: 'none',
|
|
161
|
-
}}
|
|
162
|
-
>
|
|
163
|
-
{label}
|
|
164
|
-
</span>
|
|
65
|
+
<label style={{ display: 'flex', alignItems: 'center', flexShrink: 0, cursor: 'pointer' }}>
|
|
66
|
+
<input
|
|
67
|
+
type="checkbox"
|
|
68
|
+
checked={day.enabled}
|
|
69
|
+
onChange={() => onToggle()}
|
|
70
|
+
style={{ width: 16, height: 16, accentColor: 'var(--color-accent)' }}
|
|
71
|
+
/>
|
|
72
|
+
</label>
|
|
73
|
+
<Text weight="medium" style={{ width: '4.5rem', flexShrink: 0 }}>{label}</Text>
|
|
165
74
|
|
|
166
75
|
{day.enabled ? (
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
</
|
|
76
|
+
<View layout="row" gap="s" alignItems="center" style={{ flex: 1, flexWrap: 'wrap' }}>
|
|
77
|
+
<Input type="time" value={day.startTime} onChange={e => onTimeChange('startTime', e.target.value)} />
|
|
78
|
+
<Text color="secondary">–</Text>
|
|
79
|
+
<Input type="time" value={day.endTime} onChange={e => onTimeChange('endTime', e.target.value)} />
|
|
80
|
+
</View>
|
|
172
81
|
) : (
|
|
173
|
-
<
|
|
82
|
+
<Text color="secondary" size="s">{t('booking.availabilitySetup.unavailable')}</Text>
|
|
174
83
|
)}
|
|
175
|
-
</
|
|
84
|
+
</View>
|
|
176
85
|
)
|
|
177
86
|
}
|
|
178
87
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
], [])
|
|
182
99
|
|
|
183
|
-
export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
184
100
|
const [dayState, setDayState] = useState(() => buildDayState())
|
|
185
101
|
const [sessionDurations, setSessionDurations] = useState([60])
|
|
186
102
|
const [bufferMinutes, setBufferMinutes] = useState(0)
|
|
@@ -193,7 +109,6 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
|
193
109
|
|
|
194
110
|
const bookingUrl = `https://ossy.se/boka/${workspaceId ?? ''}`
|
|
195
111
|
|
|
196
|
-
// Load existing availability on mount
|
|
197
112
|
useEffect(() => {
|
|
198
113
|
fetch('/actions/booking/get-availability')
|
|
199
114
|
.then(r => (r.ok ? r.json() : null))
|
|
@@ -215,18 +130,14 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
|
215
130
|
}, [])
|
|
216
131
|
|
|
217
132
|
const toggleDay = (dayOfWeek) => {
|
|
218
|
-
setDayState(prev => (
|
|
219
|
-
...
|
|
220
|
-
|
|
221
|
-
}))
|
|
133
|
+
setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek
|
|
134
|
+
? { ...d, enabled: !d.enabled }
|
|
135
|
+
: d))
|
|
222
136
|
setSaved(false)
|
|
223
137
|
}
|
|
224
138
|
|
|
225
139
|
const updateDayTime = (dayOfWeek, field, value) => {
|
|
226
|
-
setDayState(prev => ({
|
|
227
|
-
...prev,
|
|
228
|
-
[dayOfWeek]: { ...prev[dayOfWeek], [field]: value },
|
|
229
|
-
}))
|
|
140
|
+
setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek ? { ...d, [field]: value } : d))
|
|
230
141
|
setSaved(false)
|
|
231
142
|
}
|
|
232
143
|
|
|
@@ -245,10 +156,8 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
|
245
156
|
setSaving(true)
|
|
246
157
|
setError(null)
|
|
247
158
|
|
|
248
|
-
const weeklyWindows =
|
|
249
|
-
dayOfWeek
|
|
250
|
-
startTime: dayState[d.dayOfWeek].startTime,
|
|
251
|
-
endTime: dayState[d.dayOfWeek].endTime,
|
|
159
|
+
const weeklyWindows = dayState.filter(d => d.enabled).map(({ dayOfWeek, startTime, endTime }) => ({
|
|
160
|
+
dayOfWeek, startTime, endTime,
|
|
252
161
|
}))
|
|
253
162
|
|
|
254
163
|
try {
|
|
@@ -260,7 +169,7 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
|
260
169
|
|
|
261
170
|
if (!res.ok) {
|
|
262
171
|
const data = await res.json().catch(() => ({}))
|
|
263
|
-
throw new Error(data?.message ??
|
|
172
|
+
throw new Error(data?.message ?? t('booking.availabilitySetup.errorSave', { status: res.status }))
|
|
264
173
|
}
|
|
265
174
|
|
|
266
175
|
setSaved(true)
|
|
@@ -278,265 +187,106 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
|
278
187
|
})
|
|
279
188
|
}
|
|
280
189
|
|
|
281
|
-
// ---------------------------------------------------------------------------
|
|
282
|
-
// Styles
|
|
283
|
-
// ---------------------------------------------------------------------------
|
|
284
|
-
|
|
285
|
-
const cardStyle = {
|
|
286
|
-
marginBottom: 16,
|
|
287
|
-
padding: '20px 24px',
|
|
288
|
-
background: COLOR.surface,
|
|
289
|
-
border: `1px solid ${COLOR.border}`,
|
|
290
|
-
borderRadius: 12,
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const sectionHeadingStyle = {
|
|
294
|
-
margin: '0 0 14px',
|
|
295
|
-
fontSize: 12,
|
|
296
|
-
fontWeight: 700,
|
|
297
|
-
color: COLOR.muted,
|
|
298
|
-
textTransform: 'uppercase',
|
|
299
|
-
letterSpacing: '0.06em',
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const selectStyle = {
|
|
303
|
-
padding: '8px 12px',
|
|
304
|
-
border: `1px solid ${COLOR.border}`,
|
|
305
|
-
borderRadius: 8,
|
|
306
|
-
fontFamily: FONT,
|
|
307
|
-
fontSize: 14,
|
|
308
|
-
color: COLOR.primary,
|
|
309
|
-
background: COLOR.surface,
|
|
310
|
-
cursor: 'pointer',
|
|
311
|
-
outline: 'none',
|
|
312
|
-
}
|
|
313
|
-
|
|
314
190
|
if (loading) {
|
|
315
191
|
return (
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
</
|
|
192
|
+
<View surface="primary" inset="l" style={{ height: '100%', overflowY: 'auto' }}>
|
|
193
|
+
<Text color="secondary">{t('booking.availabilitySetup.loading')}</Text>
|
|
194
|
+
</View>
|
|
319
195
|
)
|
|
320
196
|
}
|
|
321
197
|
|
|
322
198
|
return (
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
padding: '28px 24px',
|
|
328
|
-
boxSizing: 'border-box',
|
|
329
|
-
}}
|
|
199
|
+
<View
|
|
200
|
+
gap="m"
|
|
201
|
+
surface="primary"
|
|
202
|
+
style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto', maxWidth: 620 }}
|
|
330
203
|
>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
{/* Weekly schedule */}
|
|
343
|
-
<div style={cardStyle}>
|
|
344
|
-
<p style={sectionHeadingStyle}>Veckoschema</p>
|
|
345
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
346
|
-
{DAYS.map(({ label, dayOfWeek }) => (
|
|
347
|
-
<DayRow
|
|
348
|
-
key={dayOfWeek}
|
|
349
|
-
label={label}
|
|
350
|
-
dayOfWeek={dayOfWeek}
|
|
351
|
-
day={dayState[dayOfWeek]}
|
|
352
|
-
onToggle={() => toggleDay(dayOfWeek)}
|
|
353
|
-
onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
|
|
354
|
-
/>
|
|
355
|
-
))}
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
|
|
359
|
-
{/* Session durations */}
|
|
360
|
-
<div style={cardStyle}>
|
|
361
|
-
<p style={sectionHeadingStyle}>Sessionslängd</p>
|
|
362
|
-
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
363
|
-
{SESSION_DURATIONS.map(dur => {
|
|
364
|
-
const active = sessionDurations.includes(dur)
|
|
204
|
+
<View inset="s" gap="xs">
|
|
205
|
+
<Title variant="secondary">{t('booking.availabilitySetup.title')}</Title>
|
|
206
|
+
<Text style={{ maxWidth: 480 }}>{t('booking.availabilitySetup.description')}</Text>
|
|
207
|
+
</View>
|
|
208
|
+
|
|
209
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
210
|
+
<Title variant="tertiary">{t('booking.availabilitySetup.weeklySchedule')}</Title>
|
|
211
|
+
<View gap="xs">
|
|
212
|
+
{dayDefs.map(({ dayOfWeek, key }) => {
|
|
213
|
+
const day = dayState.find(d => d.dayOfWeek === dayOfWeek)
|
|
365
214
|
return (
|
|
366
|
-
<
|
|
367
|
-
key={
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
color: active ? COLOR.accent : COLOR.secondary,
|
|
375
|
-
fontFamily: FONT,
|
|
376
|
-
fontSize: 14,
|
|
377
|
-
fontWeight: 600,
|
|
378
|
-
cursor: 'pointer',
|
|
379
|
-
transition: 'all 0.15s',
|
|
380
|
-
}}
|
|
381
|
-
>
|
|
382
|
-
{dur} min
|
|
383
|
-
</button>
|
|
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
|
+
/>
|
|
384
223
|
)
|
|
385
224
|
})}
|
|
386
|
-
</
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
225
|
+
</View>
|
|
226
|
+
</View>
|
|
227
|
+
|
|
228
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
229
|
+
<Title variant="tertiary">{t('booking.availabilitySetup.sessionDuration')}</Title>
|
|
230
|
+
<View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
|
|
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
|
+
))}
|
|
240
|
+
</View>
|
|
241
|
+
<Text size="s" color="secondary">{t('booking.availabilitySetup.sessionDurationHint')}</Text>
|
|
242
|
+
</View>
|
|
391
243
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
<
|
|
395
|
-
htmlFor="buffer-select"
|
|
396
|
-
style={{ display: 'block', fontWeight: 600, fontSize: 14, color: COLOR.primary, marginBottom: 8 }}
|
|
397
|
-
>
|
|
398
|
-
Bufferttid mellan bokningar
|
|
399
|
-
</label>
|
|
400
|
-
<select
|
|
244
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
245
|
+
<Text weight="medium">{t('booking.availabilitySetup.bufferLabel')}</Text>
|
|
246
|
+
<Select
|
|
401
247
|
id="buffer-select"
|
|
402
248
|
value={bufferMinutes}
|
|
403
|
-
onChange={e => {
|
|
404
|
-
setBufferMinutes(Number(e.target.value))
|
|
405
|
-
setSaved(false)
|
|
406
|
-
}}
|
|
407
|
-
style={{ ...selectStyle, minWidth: 190 }}
|
|
249
|
+
onChange={e => { setBufferMinutes(Number(e.target.value)); setSaved(false) }}
|
|
408
250
|
>
|
|
409
251
|
{BUFFER_OPTIONS.map(opt => (
|
|
410
252
|
<option key={opt} value={opt}>
|
|
411
|
-
{opt === 0 ? '
|
|
253
|
+
{opt === 0 ? t('booking.availabilitySetup.bufferNone') : t('booking.availabilitySetup.bufferMinutes', { minutes: opt })}
|
|
412
254
|
</option>
|
|
413
255
|
))}
|
|
414
|
-
</
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
</p>
|
|
418
|
-
</div>
|
|
256
|
+
</Select>
|
|
257
|
+
<Text size="s" color="secondary">{t('booking.availabilitySetup.bufferHint')}</Text>
|
|
258
|
+
</View>
|
|
419
259
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
<
|
|
423
|
-
htmlFor="tz-select"
|
|
424
|
-
style={{ display: 'block', fontWeight: 600, fontSize: 14, color: COLOR.primary, marginBottom: 8 }}
|
|
425
|
-
>
|
|
426
|
-
Tidszon
|
|
427
|
-
</label>
|
|
428
|
-
<select
|
|
429
|
-
id="tz-select"
|
|
430
|
-
value={timezone}
|
|
431
|
-
onChange={e => {
|
|
432
|
-
setTimezone(e.target.value)
|
|
433
|
-
setSaved(false)
|
|
434
|
-
}}
|
|
435
|
-
style={{ ...selectStyle, minWidth: 220 }}
|
|
436
|
-
>
|
|
260
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
261
|
+
<Text weight="medium">{t('booking.availabilitySetup.timezone')}</Text>
|
|
262
|
+
<Select id="tz-select" value={timezone} onChange={e => { setTimezone(e.target.value); setSaved(false) }}>
|
|
437
263
|
{TIMEZONE_OPTIONS.map(tz => (
|
|
438
|
-
<option key={tz} value={tz}>
|
|
439
|
-
{tz}
|
|
440
|
-
</option>
|
|
264
|
+
<option key={tz} value={tz}>{tz}</option>
|
|
441
265
|
))}
|
|
442
|
-
</
|
|
443
|
-
</
|
|
266
|
+
</Select>
|
|
267
|
+
</View>
|
|
444
268
|
|
|
445
|
-
{
|
|
446
|
-
{error && (
|
|
447
|
-
<div
|
|
448
|
-
style={{
|
|
449
|
-
marginBottom: 16,
|
|
450
|
-
padding: '12px 16px',
|
|
451
|
-
background: COLOR.errorBg,
|
|
452
|
-
border: `1px solid ${COLOR.errorBorder}`,
|
|
453
|
-
borderRadius: 8,
|
|
454
|
-
color: COLOR.error,
|
|
455
|
-
fontSize: 14,
|
|
456
|
-
}}
|
|
457
|
-
>
|
|
458
|
-
{error}
|
|
459
|
-
</div>
|
|
460
|
-
)}
|
|
269
|
+
{error && <Alert variant="danger">{error}</Alert>}
|
|
461
270
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
disabled={saving}
|
|
466
|
-
style={{
|
|
467
|
-
padding: '12px 28px',
|
|
468
|
-
background: saving ? COLOR.muted : COLOR.primary,
|
|
469
|
-
color: COLOR.primaryText,
|
|
470
|
-
border: 'none',
|
|
471
|
-
borderRadius: 8,
|
|
472
|
-
fontFamily: FONT,
|
|
473
|
-
fontSize: 15,
|
|
474
|
-
fontWeight: 600,
|
|
475
|
-
cursor: saving ? 'not-allowed' : 'pointer',
|
|
476
|
-
marginBottom: 20,
|
|
477
|
-
transition: 'background 0.15s',
|
|
478
|
-
}}
|
|
479
|
-
>
|
|
480
|
-
{saving ? 'Sparar…' : 'Spara tillgänglighet'}
|
|
481
|
-
</button>
|
|
271
|
+
<Button variant="primary" disabled={saving} onClick={handleSave}>
|
|
272
|
+
{saving ? t('booking.availabilitySetup.saving') : t('booking.availabilitySetup.save')}
|
|
273
|
+
</Button>
|
|
482
274
|
|
|
483
|
-
{/* Booking URL — shown after successful save */}
|
|
484
275
|
{saved && workspaceId && (
|
|
485
|
-
<
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
</p>
|
|
499
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
500
|
-
<code
|
|
501
|
-
style={{
|
|
502
|
-
display: 'block',
|
|
503
|
-
padding: '8px 12px',
|
|
504
|
-
background: COLOR.surface,
|
|
505
|
-
border: `1px solid ${COLOR.border}`,
|
|
506
|
-
borderRadius: 6,
|
|
507
|
-
fontSize: 13,
|
|
508
|
-
color: COLOR.primary,
|
|
509
|
-
fontFamily: 'ui-monospace, monospace',
|
|
510
|
-
flex: 1,
|
|
511
|
-
overflow: 'hidden',
|
|
512
|
-
textOverflow: 'ellipsis',
|
|
513
|
-
whiteSpace: 'nowrap',
|
|
514
|
-
}}
|
|
515
|
-
>
|
|
516
|
-
{bookingUrl}
|
|
517
|
-
</code>
|
|
518
|
-
<button
|
|
519
|
-
onClick={handleCopyLink}
|
|
520
|
-
style={{
|
|
521
|
-
padding: '8px 16px',
|
|
522
|
-
background: copied ? COLOR.success : COLOR.primary,
|
|
523
|
-
color: COLOR.primaryText,
|
|
524
|
-
border: 'none',
|
|
525
|
-
borderRadius: 6,
|
|
526
|
-
fontFamily: FONT,
|
|
527
|
-
fontSize: 13,
|
|
528
|
-
fontWeight: 600,
|
|
529
|
-
cursor: 'pointer',
|
|
530
|
-
flexShrink: 0,
|
|
531
|
-
transition: 'background 0.15s',
|
|
532
|
-
whiteSpace: 'nowrap',
|
|
533
|
-
}}
|
|
534
|
-
>
|
|
535
|
-
{copied ? '✓ Kopierad!' : 'Kopiera länk'}
|
|
536
|
-
</button>
|
|
537
|
-
</div>
|
|
538
|
-
</div>
|
|
276
|
+
<Alert variant="success" title={t('booking.availabilitySetup.savedTitle')}>
|
|
277
|
+
<View gap="s">
|
|
278
|
+
<Text color="secondary">{t('booking.availabilitySetup.savedBody')}</Text>
|
|
279
|
+
<View layout="row" gap="s" alignItems="center">
|
|
280
|
+
<Text as="code" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'ui-monospace, monospace' }}>
|
|
281
|
+
{bookingUrl}
|
|
282
|
+
</Text>
|
|
283
|
+
<Button variant={copied ? 'cta' : 'primary'} onClick={handleCopyLink}>
|
|
284
|
+
{copied ? t('booking.availabilitySetup.copied') : t('booking.availabilitySetup.copyLink')}
|
|
285
|
+
</Button>
|
|
286
|
+
</View>
|
|
287
|
+
</View>
|
|
288
|
+
</Alert>
|
|
539
289
|
)}
|
|
540
|
-
</
|
|
290
|
+
</View>
|
|
541
291
|
)
|
|
542
292
|
}
|