@ossy/booking 1.15.6 → 1.15.7
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 +10 -6
- package/src/BookingForm.jsx +5 -5
- package/src/HeroCover.jsx +99 -0
- package/src/SalesSection.jsx +72 -0
- package/src/availability-setup.page.jsx +123 -340
- package/src/booking-card.component.jsx +67 -197
- package/src/booking-detail.page.jsx +102 -428
- package/src/bookings.page.jsx +36 -180
- package/src/cover.js +8 -0
- package/src/features.js +27 -0
- package/src/home.page.jsx +4 -364
- package/src/public-booking.page.jsx +163 -520
- package/src/service-card-slot.component.jsx +48 -0
- package/src/service-detail.component.jsx +38 -0
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
2
2
|
import { useRouter } from '@ossy/router-react'
|
|
3
|
+
import { View, Title, Text, Button, Badge, Alert, Textarea } from '@ossy/design-system'
|
|
3
4
|
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Tokens
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
const C = {
|
|
8
|
-
bg: '#fafafa',
|
|
9
|
-
surface: '#ffffff',
|
|
10
|
-
border: '#e4e4e7',
|
|
11
|
-
primary: '#111111',
|
|
12
|
-
secondary: '#52525b',
|
|
13
|
-
muted: '#a1a1aa',
|
|
14
|
-
pendingBg: '#fffbeb',
|
|
15
|
-
pendingText: '#92400e',
|
|
16
|
-
pendingBorder: '#fde68a',
|
|
17
|
-
confirmedBg: '#f0fdf4',
|
|
18
|
-
confirmedText: '#166534',
|
|
19
|
-
confirmedBorder: '#bbf7d0',
|
|
20
|
-
cancelledBg: '#f4f4f5',
|
|
21
|
-
cancelledText: '#52525b',
|
|
22
|
-
cancelledBorder: '#e4e4e7',
|
|
23
|
-
errorText: '#dc2626',
|
|
24
|
-
dangerBorder: '#fca5a5',
|
|
25
|
-
inputBorder: '#d4d4d8',
|
|
26
|
-
}
|
|
27
|
-
const FONT = 'system-ui, -apple-system, sans-serif'
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Page metadata
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
5
|
export const metadata = {
|
|
33
6
|
id: 'booking/booking-detail',
|
|
34
7
|
path: {
|
|
@@ -37,16 +10,25 @@ export const metadata = {
|
|
|
37
10
|
},
|
|
38
11
|
}
|
|
39
12
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
13
|
+
const STATUS_VARIANT = {
|
|
14
|
+
pending: 'warning',
|
|
15
|
+
confirmed: 'success',
|
|
16
|
+
cancelled: 'neutral',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STATUS_LABEL = {
|
|
20
|
+
pending: 'Väntar på bekräftelse',
|
|
21
|
+
confirmed: 'Bekräftad',
|
|
22
|
+
cancelled: 'Avbokad',
|
|
23
|
+
}
|
|
24
|
+
|
|
43
25
|
const SV_MONTHS = [
|
|
44
26
|
'januari', 'februari', 'mars', 'april', 'maj', 'juni',
|
|
45
27
|
'juli', 'augusti', 'september', 'oktober', 'november', 'december',
|
|
46
28
|
]
|
|
47
29
|
const SV_WEEKDAYS = ['Söndag', 'Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag']
|
|
48
30
|
|
|
49
|
-
function formatDateTime(iso) {
|
|
31
|
+
function formatDateTime (iso) {
|
|
50
32
|
if (!iso) return '—'
|
|
51
33
|
const d = new Date(iso)
|
|
52
34
|
const weekday = SV_WEEKDAYS[d.getDay()]
|
|
@@ -64,7 +46,7 @@ function formatDateTime(iso) {
|
|
|
64
46
|
}
|
|
65
47
|
}
|
|
66
48
|
|
|
67
|
-
function formatDuration(minutes) {
|
|
49
|
+
function formatDuration (minutes) {
|
|
68
50
|
if (!minutes) return '—'
|
|
69
51
|
if (minutes < 60) return `${minutes} minuter`
|
|
70
52
|
const h = Math.floor(minutes / 60)
|
|
@@ -72,91 +54,16 @@ function formatDuration(minutes) {
|
|
|
72
54
|
return m ? `${h} timme ${m} min` : `${h} timme`
|
|
73
55
|
}
|
|
74
56
|
|
|
75
|
-
|
|
76
|
-
// Status badge
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
function StatusBadge({ status }) {
|
|
79
|
-
const variants = {
|
|
80
|
-
pending: { bg: C.pendingBg, color: C.pendingText, border: C.pendingBorder, label: 'Väntar på bekräftelse' },
|
|
81
|
-
confirmed: { bg: C.confirmedBg, color: C.confirmedText, border: C.confirmedBorder, label: 'Bekräftad' },
|
|
82
|
-
cancelled: { bg: C.cancelledBg, color: C.cancelledText, border: C.cancelledBorder, label: 'Avbokad' },
|
|
83
|
-
}
|
|
84
|
-
const v = variants[status] ?? variants.cancelled
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<span
|
|
88
|
-
style={{
|
|
89
|
-
display: 'inline-flex',
|
|
90
|
-
alignItems: 'center',
|
|
91
|
-
padding: '4px 12px',
|
|
92
|
-
borderRadius: 999,
|
|
93
|
-
fontSize: 13,
|
|
94
|
-
fontWeight: 600,
|
|
95
|
-
fontFamily: FONT,
|
|
96
|
-
background: v.bg,
|
|
97
|
-
color: v.color,
|
|
98
|
-
border: `1px solid ${v.border}`,
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
101
|
-
{v.label}
|
|
102
|
-
</span>
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
// Detail row
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
function DetailRow({ label, value }) {
|
|
57
|
+
function DetailRow ({ label, value }) {
|
|
110
58
|
return (
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
flexShrink: 0,
|
|
116
|
-
fontSize: 13,
|
|
117
|
-
fontWeight: 600,
|
|
118
|
-
color: C.secondary,
|
|
119
|
-
fontFamily: FONT,
|
|
120
|
-
paddingTop: 1,
|
|
121
|
-
}}
|
|
122
|
-
>
|
|
123
|
-
{label}
|
|
124
|
-
</span>
|
|
125
|
-
<span style={{ fontSize: 14, color: C.primary, fontFamily: FONT, flex: 1 }}>
|
|
126
|
-
{value ?? '—'}
|
|
127
|
-
</span>
|
|
128
|
-
</div>
|
|
59
|
+
<View layout="row" gap="m" style={{ flexWrap: 'wrap' }}>
|
|
60
|
+
<Text weight="medium" color="secondary" style={{ width: 120, flexShrink: 0 }}>{label}</Text>
|
|
61
|
+
<Text style={{ flex: 1 }}>{value ?? '—'}</Text>
|
|
62
|
+
</View>
|
|
129
63
|
)
|
|
130
64
|
}
|
|
131
65
|
|
|
132
|
-
|
|
133
|
-
// Spinner
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
function Spinner() {
|
|
136
|
-
return (
|
|
137
|
-
<>
|
|
138
|
-
<style href="booking/detail-spinner" precedence="low">
|
|
139
|
-
{`@keyframes booking-detail-spin { to { transform: rotate(360deg) } }`}
|
|
140
|
-
</style>
|
|
141
|
-
<div
|
|
142
|
-
style={{
|
|
143
|
-
width: 22,
|
|
144
|
-
height: 22,
|
|
145
|
-
border: '3px solid #e4e4e7',
|
|
146
|
-
borderTopColor: C.primary,
|
|
147
|
-
borderRadius: '50%',
|
|
148
|
-
animation: 'booking-detail-spin 0.7s linear infinite',
|
|
149
|
-
margin: '48px auto',
|
|
150
|
-
}}
|
|
151
|
-
/>
|
|
152
|
-
</>
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// Confirm form
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
function ConfirmForm({ booking, onSuccess, onError }) {
|
|
66
|
+
function ConfirmForm ({ booking, onSuccess, onError }) {
|
|
160
67
|
const [loading, setLoading] = useState(false)
|
|
161
68
|
|
|
162
69
|
const handleConfirm = async () => {
|
|
@@ -180,30 +87,13 @@ function ConfirmForm({ booking, onSuccess, onError }) {
|
|
|
180
87
|
}
|
|
181
88
|
|
|
182
89
|
return (
|
|
183
|
-
<
|
|
184
|
-
onClick={handleConfirm}
|
|
185
|
-
disabled={loading}
|
|
186
|
-
style={{
|
|
187
|
-
padding: '10px 20px',
|
|
188
|
-
background: loading ? C.muted : C.primary,
|
|
189
|
-
color: '#ffffff',
|
|
190
|
-
border: 'none',
|
|
191
|
-
borderRadius: 8,
|
|
192
|
-
fontFamily: FONT,
|
|
193
|
-
fontSize: 14,
|
|
194
|
-
fontWeight: 600,
|
|
195
|
-
cursor: loading ? 'not-allowed' : 'pointer',
|
|
196
|
-
}}
|
|
197
|
-
>
|
|
90
|
+
<Button variant="primary" disabled={loading} onClick={handleConfirm}>
|
|
198
91
|
{loading ? 'Bekräftar…' : 'Bekräfta bokning'}
|
|
199
|
-
</
|
|
92
|
+
</Button>
|
|
200
93
|
)
|
|
201
94
|
}
|
|
202
95
|
|
|
203
|
-
|
|
204
|
-
// Decline form
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
function DeclineForm({ booking, onSuccess, onError }) {
|
|
96
|
+
function DeclineForm ({ booking, onSuccess, onError }) {
|
|
207
97
|
const [reason, setReason] = useState('')
|
|
208
98
|
const [loading, setLoading] = useState(false)
|
|
209
99
|
const [expanded, setExpanded] = useState(false)
|
|
@@ -230,110 +120,38 @@ function DeclineForm({ booking, onSuccess, onError }) {
|
|
|
230
120
|
|
|
231
121
|
if (!expanded) {
|
|
232
122
|
return (
|
|
233
|
-
<
|
|
234
|
-
onClick={() => setExpanded(true)}
|
|
235
|
-
style={{
|
|
236
|
-
padding: '10px 20px',
|
|
237
|
-
background: 'transparent',
|
|
238
|
-
color: C.errorText,
|
|
239
|
-
border: `1px solid ${C.dangerBorder}`,
|
|
240
|
-
borderRadius: 8,
|
|
241
|
-
fontFamily: FONT,
|
|
242
|
-
fontSize: 14,
|
|
243
|
-
fontWeight: 600,
|
|
244
|
-
cursor: 'pointer',
|
|
245
|
-
}}
|
|
246
|
-
>
|
|
123
|
+
<Button variant="danger" onClick={() => setExpanded(true)}>
|
|
247
124
|
Avböj förfrågan
|
|
248
|
-
</
|
|
125
|
+
</Button>
|
|
249
126
|
)
|
|
250
127
|
}
|
|
251
128
|
|
|
252
129
|
return (
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
border: `1px solid ${C.dangerBorder}`,
|
|
257
|
-
borderRadius: 10,
|
|
258
|
-
padding: '16px 20px',
|
|
259
|
-
display: 'flex',
|
|
260
|
-
flexDirection: 'column',
|
|
261
|
-
gap: 12,
|
|
262
|
-
}}
|
|
263
|
-
>
|
|
264
|
-
<span style={{ fontSize: 14, fontWeight: 600, color: C.errorText, fontFamily: FONT }}>
|
|
265
|
-
Avböj bokningsförfrågan
|
|
266
|
-
</span>
|
|
267
|
-
|
|
268
|
-
<div>
|
|
269
|
-
<label
|
|
270
|
-
style={{ display: 'block', fontSize: 13, color: C.secondary, fontFamily: FONT, marginBottom: 6 }}
|
|
271
|
-
>
|
|
272
|
-
Anledning (valfritt — skickas till klienten)
|
|
273
|
-
</label>
|
|
274
|
-
<textarea
|
|
130
|
+
<Alert variant="danger" title="Avböj bokningsförfrågan">
|
|
131
|
+
<View gap="s">
|
|
132
|
+
<Textarea
|
|
275
133
|
value={reason}
|
|
276
134
|
onChange={e => setReason(e.target.value)}
|
|
277
135
|
rows={3}
|
|
278
136
|
placeholder="T.ex. Tyvärr är den begärda tiden inte längre tillgänglig…"
|
|
279
|
-
style={{
|
|
280
|
-
width: '100%',
|
|
281
|
-
padding: '8px 12px',
|
|
282
|
-
border: `1px solid ${C.inputBorder}`,
|
|
283
|
-
borderRadius: 6,
|
|
284
|
-
fontFamily: FONT,
|
|
285
|
-
fontSize: 13,
|
|
286
|
-
color: C.primary,
|
|
287
|
-
background: '#fff',
|
|
288
|
-
boxSizing: 'border-box',
|
|
289
|
-
resize: 'vertical',
|
|
290
|
-
outline: 'none',
|
|
291
|
-
}}
|
|
292
137
|
/>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
<
|
|
297
|
-
onClick={handleDecline}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
fontSize: 13,
|
|
307
|
-
fontWeight: 600,
|
|
308
|
-
cursor: loading ? 'not-allowed' : 'pointer',
|
|
309
|
-
}}
|
|
310
|
-
>
|
|
311
|
-
{loading ? 'Avböjer…' : 'Bekräfta avböjning'}
|
|
312
|
-
</button>
|
|
313
|
-
<button
|
|
314
|
-
onClick={() => setExpanded(false)}
|
|
315
|
-
style={{
|
|
316
|
-
padding: '8px 16px',
|
|
317
|
-
background: 'transparent',
|
|
318
|
-
color: C.secondary,
|
|
319
|
-
border: `1px solid ${C.border}`,
|
|
320
|
-
borderRadius: 6,
|
|
321
|
-
fontFamily: FONT,
|
|
322
|
-
fontSize: 13,
|
|
323
|
-
cursor: 'pointer',
|
|
324
|
-
}}
|
|
325
|
-
>
|
|
326
|
-
Avbryt
|
|
327
|
-
</button>
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
138
|
+
<Text variant="small" color="secondary">
|
|
139
|
+
Anledning (valfritt — skickas till klienten)
|
|
140
|
+
</Text>
|
|
141
|
+
<View layout="row" gap="s">
|
|
142
|
+
<Button variant="danger" disabled={loading} onClick={handleDecline}>
|
|
143
|
+
{loading ? 'Avböjer…' : 'Bekräfta avböjning'}
|
|
144
|
+
</Button>
|
|
145
|
+
<Button variant="neutral" onClick={() => setExpanded(false)}>
|
|
146
|
+
Avbryt
|
|
147
|
+
</Button>
|
|
148
|
+
</View>
|
|
149
|
+
</View>
|
|
150
|
+
</Alert>
|
|
330
151
|
)
|
|
331
152
|
}
|
|
332
153
|
|
|
333
|
-
|
|
334
|
-
// Cancel form
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
function CancelForm({ booking, onSuccess, onError }) {
|
|
154
|
+
function CancelForm ({ booking, onSuccess, onError }) {
|
|
337
155
|
const [loading, setLoading] = useState(false)
|
|
338
156
|
const [expanded, setExpanded] = useState(false)
|
|
339
157
|
|
|
@@ -359,85 +177,32 @@ function CancelForm({ booking, onSuccess, onError }) {
|
|
|
359
177
|
|
|
360
178
|
if (!expanded) {
|
|
361
179
|
return (
|
|
362
|
-
<
|
|
363
|
-
onClick={() => setExpanded(true)}
|
|
364
|
-
style={{
|
|
365
|
-
padding: '10px 20px',
|
|
366
|
-
background: 'transparent',
|
|
367
|
-
color: C.errorText,
|
|
368
|
-
border: `1px solid ${C.dangerBorder}`,
|
|
369
|
-
borderRadius: 8,
|
|
370
|
-
fontFamily: FONT,
|
|
371
|
-
fontSize: 14,
|
|
372
|
-
fontWeight: 600,
|
|
373
|
-
cursor: 'pointer',
|
|
374
|
-
}}
|
|
375
|
-
>
|
|
180
|
+
<Button variant="danger" onClick={() => setExpanded(true)}>
|
|
376
181
|
Avboka möte
|
|
377
|
-
</
|
|
182
|
+
</Button>
|
|
378
183
|
)
|
|
379
184
|
}
|
|
380
185
|
|
|
381
186
|
return (
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
</
|
|
396
|
-
|
|
397
|
-
Klienten meddelas via e-post. Åtgärden kan inte ångras.
|
|
398
|
-
</p>
|
|
399
|
-
<div style={{ display: 'flex', gap: 8 }}>
|
|
400
|
-
<button
|
|
401
|
-
onClick={handleCancel}
|
|
402
|
-
disabled={loading}
|
|
403
|
-
style={{
|
|
404
|
-
padding: '8px 16px',
|
|
405
|
-
background: loading ? C.muted : C.errorText,
|
|
406
|
-
color: '#ffffff',
|
|
407
|
-
border: 'none',
|
|
408
|
-
borderRadius: 6,
|
|
409
|
-
fontFamily: FONT,
|
|
410
|
-
fontSize: 13,
|
|
411
|
-
fontWeight: 600,
|
|
412
|
-
cursor: loading ? 'not-allowed' : 'pointer',
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
{loading ? 'Avbokar…' : 'Bekräfta avbokning'}
|
|
416
|
-
</button>
|
|
417
|
-
<button
|
|
418
|
-
onClick={() => setExpanded(false)}
|
|
419
|
-
style={{
|
|
420
|
-
padding: '8px 16px',
|
|
421
|
-
background: 'transparent',
|
|
422
|
-
color: C.secondary,
|
|
423
|
-
border: `1px solid ${C.border}`,
|
|
424
|
-
borderRadius: 6,
|
|
425
|
-
fontFamily: FONT,
|
|
426
|
-
fontSize: 13,
|
|
427
|
-
cursor: 'pointer',
|
|
428
|
-
}}
|
|
429
|
-
>
|
|
430
|
-
Avbryt
|
|
431
|
-
</button>
|
|
432
|
-
</div>
|
|
433
|
-
</div>
|
|
187
|
+
<Alert variant="danger" title="Avboka bekräftad bokning">
|
|
188
|
+
<View gap="s">
|
|
189
|
+
<Text color="secondary">
|
|
190
|
+
Klienten meddelas via e-post. Åtgärden kan inte ångras.
|
|
191
|
+
</Text>
|
|
192
|
+
<View layout="row" gap="s">
|
|
193
|
+
<Button variant="danger" disabled={loading} onClick={handleCancel}>
|
|
194
|
+
{loading ? 'Avbokar…' : 'Bekräfta avbokning'}
|
|
195
|
+
</Button>
|
|
196
|
+
<Button variant="neutral" onClick={() => setExpanded(false)}>
|
|
197
|
+
Avbryt
|
|
198
|
+
</Button>
|
|
199
|
+
</View>
|
|
200
|
+
</View>
|
|
201
|
+
</Alert>
|
|
434
202
|
)
|
|
435
203
|
}
|
|
436
204
|
|
|
437
|
-
|
|
438
|
-
// Page
|
|
439
|
-
// ---------------------------------------------------------------------------
|
|
440
|
-
export default function BookingDetailPage({ bookingId: bookingIdProp }) {
|
|
205
|
+
export default function BookingDetailPage ({ bookingId: bookingIdProp }) {
|
|
441
206
|
const router = useRouter()
|
|
442
207
|
const bookingId = bookingIdProp ?? router?.params?.bookingId
|
|
443
208
|
|
|
@@ -486,150 +251,59 @@ export default function BookingDetailPage({ bookingId: bookingIdProp }) {
|
|
|
486
251
|
}
|
|
487
252
|
|
|
488
253
|
return (
|
|
489
|
-
<
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
overflowY: 'auto',
|
|
494
|
-
background: C.bg,
|
|
495
|
-
padding: 'var(--space-m, 16px) var(--space-l, 24px)',
|
|
496
|
-
boxSizing: 'border-box',
|
|
497
|
-
}}
|
|
254
|
+
<View
|
|
255
|
+
gap="m"
|
|
256
|
+
surface="primary"
|
|
257
|
+
style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
|
|
498
258
|
>
|
|
499
|
-
|
|
500
|
-
<button
|
|
501
|
-
onClick={goBack}
|
|
502
|
-
style={{
|
|
503
|
-
background: 'none',
|
|
504
|
-
border: 'none',
|
|
505
|
-
cursor: 'pointer',
|
|
506
|
-
color: C.secondary,
|
|
507
|
-
fontFamily: FONT,
|
|
508
|
-
fontSize: 13,
|
|
509
|
-
padding: 0,
|
|
510
|
-
marginBottom: 20,
|
|
511
|
-
display: 'flex',
|
|
512
|
-
alignItems: 'center',
|
|
513
|
-
gap: 4,
|
|
514
|
-
}}
|
|
515
|
-
>
|
|
259
|
+
<Button variant="link" onClick={goBack}>
|
|
516
260
|
← Alla bokningar
|
|
517
|
-
</
|
|
261
|
+
</Button>
|
|
518
262
|
|
|
519
263
|
{loading ? (
|
|
520
|
-
<
|
|
264
|
+
<Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>
|
|
265
|
+
Laddar…
|
|
266
|
+
</Text>
|
|
521
267
|
) : error ? (
|
|
522
|
-
<
|
|
523
|
-
style={{
|
|
524
|
-
padding: 24,
|
|
525
|
-
background: C.surface,
|
|
526
|
-
border: `1px solid ${C.border}`,
|
|
527
|
-
borderRadius: 12,
|
|
528
|
-
fontSize: 14,
|
|
529
|
-
color: C.errorText,
|
|
530
|
-
fontFamily: FONT,
|
|
531
|
-
textAlign: 'center',
|
|
532
|
-
}}
|
|
533
|
-
>
|
|
534
|
-
{error}
|
|
535
|
-
</div>
|
|
268
|
+
<Alert variant="danger">{error}</Alert>
|
|
536
269
|
) : booking ? (
|
|
537
|
-
<
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
<
|
|
551
|
-
<div>
|
|
552
|
-
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 700, color: C.primary }}>
|
|
553
|
-
{booking.clientName ?? '—'}
|
|
554
|
-
</h1>
|
|
555
|
-
<p style={{ margin: '4px 0 0', fontSize: 14, color: C.secondary }}>
|
|
556
|
-
{booking.clientEmail ?? ''}
|
|
557
|
-
</p>
|
|
558
|
-
</div>
|
|
559
|
-
<StatusBadge status={booking.status} />
|
|
560
|
-
</div>
|
|
561
|
-
|
|
562
|
-
<div
|
|
563
|
-
style={{
|
|
564
|
-
height: 1,
|
|
565
|
-
background: C.border,
|
|
566
|
-
}}
|
|
567
|
-
/>
|
|
568
|
-
|
|
569
|
-
{/* Details */}
|
|
570
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
270
|
+
<View gap="m" style={{ maxWidth: 680 }}>
|
|
271
|
+
<View gap="m" surface="secondary" roundness="m" inset="l">
|
|
272
|
+
<View layout="row" justifyContent="space-between" alignItems="flex-start" gap="m" style={{ flexWrap: 'wrap' }}>
|
|
273
|
+
<View gap="xs">
|
|
274
|
+
<Title variant="secondary">{booking.clientName ?? '—'}</Title>
|
|
275
|
+
{booking.clientEmail && <Text color="secondary">{booking.clientEmail}</Text>}
|
|
276
|
+
</View>
|
|
277
|
+
<Badge
|
|
278
|
+
label={STATUS_LABEL[booking.status] ?? booking.status}
|
|
279
|
+
variant={STATUS_VARIANT[booking.status] ?? 'neutral'}
|
|
280
|
+
/>
|
|
281
|
+
</View>
|
|
282
|
+
|
|
283
|
+
<View gap="s">
|
|
571
284
|
<DetailRow label="Datum och tid" value={formatDateTime(booking.startsAt)} />
|
|
572
285
|
<DetailRow label="Längd" value={formatDuration(booking.duration)} />
|
|
573
286
|
{booking.clientMessage && (
|
|
574
|
-
<
|
|
575
|
-
<
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
fontWeight: 600,
|
|
581
|
-
color: C.secondary,
|
|
582
|
-
fontFamily: FONT,
|
|
583
|
-
paddingTop: 1,
|
|
584
|
-
}}
|
|
585
|
-
>
|
|
586
|
-
Meddelande
|
|
587
|
-
</span>
|
|
588
|
-
<div
|
|
589
|
-
style={{
|
|
590
|
-
flex: 1,
|
|
591
|
-
fontSize: 14,
|
|
592
|
-
color: C.primary,
|
|
593
|
-
fontFamily: FONT,
|
|
594
|
-
background: '#fafafa',
|
|
595
|
-
borderRadius: 6,
|
|
596
|
-
padding: '10px 14px',
|
|
597
|
-
borderLeft: `3px solid ${C.border}`,
|
|
598
|
-
whiteSpace: 'pre-wrap',
|
|
599
|
-
}}
|
|
600
|
-
>
|
|
601
|
-
{booking.clientMessage}
|
|
602
|
-
</div>
|
|
603
|
-
</div>
|
|
287
|
+
<View gap="xs">
|
|
288
|
+
<Text weight="medium" color="secondary">Meddelande</Text>
|
|
289
|
+
<View surface="primary" roundness="s" inset="s" style={{ borderLeft: '3px solid var(--separator)' }}>
|
|
290
|
+
<Text style={{ whiteSpace: 'pre-wrap' }}>{booking.clientMessage}</Text>
|
|
291
|
+
</View>
|
|
292
|
+
</View>
|
|
604
293
|
)}
|
|
605
|
-
</
|
|
606
|
-
</
|
|
294
|
+
</View>
|
|
295
|
+
</View>
|
|
607
296
|
|
|
608
|
-
{/* Actions card — only shown for pending or confirmed */}
|
|
609
297
|
{(booking.status === 'pending' || booking.status === 'confirmed') && (
|
|
610
|
-
<
|
|
611
|
-
|
|
612
|
-
background: C.surface,
|
|
613
|
-
border: `1px solid ${C.border}`,
|
|
614
|
-
borderRadius: 12,
|
|
615
|
-
padding: '20px 28px',
|
|
616
|
-
display: 'flex',
|
|
617
|
-
flexDirection: 'column',
|
|
618
|
-
gap: 12,
|
|
619
|
-
}}
|
|
620
|
-
>
|
|
621
|
-
<h2 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: C.primary }}>
|
|
298
|
+
<View gap="s" surface="secondary" roundness="m" inset="l">
|
|
299
|
+
<Title variant="tertiary">
|
|
622
300
|
{booking.status === 'pending' ? 'Hantera förfrågan' : 'Hantera bokning'}
|
|
623
|
-
</
|
|
301
|
+
</Title>
|
|
624
302
|
|
|
625
|
-
{actionError &&
|
|
626
|
-
<div style={{ fontSize: 13, color: C.errorText, fontFamily: FONT }}>
|
|
627
|
-
{actionError}
|
|
628
|
-
</div>
|
|
629
|
-
)}
|
|
303
|
+
{actionError && <Alert variant="danger">{actionError}</Alert>}
|
|
630
304
|
|
|
631
305
|
{booking.status === 'pending' && (
|
|
632
|
-
<
|
|
306
|
+
<View gap="s">
|
|
633
307
|
<ConfirmForm
|
|
634
308
|
booking={booking}
|
|
635
309
|
onSuccess={handleSuccess}
|
|
@@ -640,7 +314,7 @@ export default function BookingDetailPage({ bookingId: bookingIdProp }) {
|
|
|
640
314
|
onSuccess={handleSuccess}
|
|
641
315
|
onError={setActionError}
|
|
642
316
|
/>
|
|
643
|
-
</
|
|
317
|
+
</View>
|
|
644
318
|
)}
|
|
645
319
|
|
|
646
320
|
{booking.status === 'confirmed' && (
|
|
@@ -650,10 +324,10 @@ export default function BookingDetailPage({ bookingId: bookingIdProp }) {
|
|
|
650
324
|
onError={setActionError}
|
|
651
325
|
/>
|
|
652
326
|
)}
|
|
653
|
-
</
|
|
327
|
+
</View>
|
|
654
328
|
)}
|
|
655
|
-
</
|
|
329
|
+
</View>
|
|
656
330
|
) : null}
|
|
657
|
-
</
|
|
331
|
+
</View>
|
|
658
332
|
)
|
|
659
333
|
}
|