@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,47 +1,27 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
+
import { View, Title, Text, Button, Badge, Alert } from '@ossy/design-system'
|
|
2
3
|
|
|
3
|
-
export const metadata = { id: 'booking/booking
|
|
4
|
+
export const metadata = { id: 'resource:booking/booking/card' }
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
surface: '#ffffff',
|
|
10
|
-
border: '#e4e4e7',
|
|
11
|
-
primary: '#111111',
|
|
12
|
-
secondary: '#52525b',
|
|
13
|
-
muted: '#a1a1aa',
|
|
14
|
-
accent: '#111111',
|
|
15
|
-
pendingBg: '#fffbeb',
|
|
16
|
-
pendingText: '#92400e',
|
|
17
|
-
pendingBorder: '#fde68a',
|
|
18
|
-
confirmedBg: '#f0fdf4',
|
|
19
|
-
confirmedText: '#166534',
|
|
20
|
-
confirmedBorder: '#bbf7d0',
|
|
21
|
-
cancelledBg: '#f4f4f5',
|
|
22
|
-
cancelledText: '#52525b',
|
|
23
|
-
cancelledBorder: '#e4e4e7',
|
|
24
|
-
dangerBg: '#fef2f2',
|
|
25
|
-
dangerText: '#991b1b',
|
|
26
|
-
dangerHover: '#dc2626',
|
|
27
|
-
successBg: '#f0fdf4',
|
|
28
|
-
successText: '#166534',
|
|
29
|
-
successHover: '#16a34a',
|
|
30
|
-
errorText: '#dc2626',
|
|
6
|
+
const STATUS_VARIANT = {
|
|
7
|
+
pending: 'warning',
|
|
8
|
+
confirmed: 'success',
|
|
9
|
+
cancelled: 'neutral',
|
|
31
10
|
}
|
|
32
11
|
|
|
33
|
-
const
|
|
12
|
+
const STATUS_LABEL = {
|
|
13
|
+
pending: 'Väntar',
|
|
14
|
+
confirmed: 'Bekräftad',
|
|
15
|
+
cancelled: 'Avbokad',
|
|
16
|
+
}
|
|
34
17
|
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Helpers
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
18
|
const SV_MONTHS = [
|
|
39
19
|
'jan', 'feb', 'mar', 'apr', 'maj', 'jun',
|
|
40
20
|
'jul', 'aug', 'sep', 'okt', 'nov', 'dec',
|
|
41
21
|
]
|
|
42
22
|
const SV_WEEKDAYS = ['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör']
|
|
43
23
|
|
|
44
|
-
function formatDateTime(iso) {
|
|
24
|
+
function formatDateTime (iso) {
|
|
45
25
|
if (!iso) return '—'
|
|
46
26
|
const d = new Date(iso)
|
|
47
27
|
const weekday = SV_WEEKDAYS[d.getDay()]
|
|
@@ -58,7 +38,7 @@ function formatDateTime(iso) {
|
|
|
58
38
|
}
|
|
59
39
|
}
|
|
60
40
|
|
|
61
|
-
function formatDuration(minutes) {
|
|
41
|
+
function formatDuration (minutes) {
|
|
62
42
|
if (!minutes) return '—'
|
|
63
43
|
if (minutes < 60) return `${minutes} min`
|
|
64
44
|
const h = Math.floor(minutes / 60)
|
|
@@ -66,94 +46,16 @@ function formatDuration(minutes) {
|
|
|
66
46
|
return m ? `${h} h ${m} min` : `${h} h`
|
|
67
47
|
}
|
|
68
48
|
|
|
69
|
-
function truncate(text, max = 120) {
|
|
49
|
+
function truncate (text, max = 120) {
|
|
70
50
|
if (!text) return ''
|
|
71
51
|
return text.length > max ? `${text.slice(0, max).trimEnd()}…` : text
|
|
72
52
|
}
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const variants = {
|
|
79
|
-
pending: { bg: C.pendingBg, color: C.pendingText, border: C.pendingBorder, label: 'Väntar' },
|
|
80
|
-
confirmed: { bg: C.confirmedBg, color: C.confirmedText, border: C.confirmedBorder, label: 'Bekräftad' },
|
|
81
|
-
cancelled: { bg: C.cancelledBg, color: C.cancelledText, border: C.cancelledBorder, label: 'Avbokad' },
|
|
82
|
-
}
|
|
83
|
-
const v = variants[status] ?? variants.cancelled
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<span
|
|
87
|
-
style={{
|
|
88
|
-
display: 'inline-flex',
|
|
89
|
-
alignItems: 'center',
|
|
90
|
-
padding: '2px 10px',
|
|
91
|
-
borderRadius: 999,
|
|
92
|
-
fontSize: 11,
|
|
93
|
-
fontWeight: 600,
|
|
94
|
-
fontFamily: FONT,
|
|
95
|
-
background: v.bg,
|
|
96
|
-
color: v.color,
|
|
97
|
-
border: `1px solid ${v.border}`,
|
|
98
|
-
whiteSpace: 'nowrap',
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
101
|
-
{v.label}
|
|
102
|
-
</span>
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
// Action button
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
function ActionButton({ label, onClick, loading, danger, disabled }) {
|
|
110
|
-
const [hovered, setHovered] = useState(false)
|
|
111
|
-
const base = {
|
|
112
|
-
display: 'inline-flex',
|
|
113
|
-
alignItems: 'center',
|
|
114
|
-
gap: 4,
|
|
115
|
-
padding: '5px 12px',
|
|
116
|
-
borderRadius: 6,
|
|
117
|
-
fontSize: 12,
|
|
118
|
-
fontWeight: 600,
|
|
119
|
-
fontFamily: FONT,
|
|
120
|
-
cursor: loading || disabled ? 'not-allowed' : 'pointer',
|
|
121
|
-
border: '1px solid',
|
|
122
|
-
transition: 'background 0.1s, color 0.1s',
|
|
123
|
-
whiteSpace: 'nowrap',
|
|
124
|
-
opacity: loading || disabled ? 0.6 : 1,
|
|
125
|
-
}
|
|
126
|
-
const style = danger
|
|
127
|
-
? {
|
|
128
|
-
...base,
|
|
129
|
-
background: hovered ? C.dangerBg : 'transparent',
|
|
130
|
-
color: C.dangerText,
|
|
131
|
-
borderColor: C.pendingBorder,
|
|
132
|
-
}
|
|
133
|
-
: {
|
|
134
|
-
...base,
|
|
135
|
-
background: hovered ? C.accent : C.primary,
|
|
136
|
-
color: '#ffffff',
|
|
137
|
-
borderColor: 'transparent',
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<button
|
|
142
|
-
style={style}
|
|
143
|
-
onClick={onClick}
|
|
144
|
-
disabled={loading || disabled}
|
|
145
|
-
onMouseEnter={() => setHovered(true)}
|
|
146
|
-
onMouseLeave={() => setHovered(false)}
|
|
147
|
-
>
|
|
148
|
-
{loading ? 'Väntar…' : label}
|
|
149
|
-
</button>
|
|
150
|
-
)
|
|
151
|
-
}
|
|
54
|
+
/** Booking card view — registered at `resource:booking/booking/card`. */
|
|
55
|
+
export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel, onView }) {
|
|
56
|
+
const data = booking ?? resource?.content ?? resource
|
|
57
|
+
if (!data) return null
|
|
152
58
|
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
// BookingCard
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
export function BookingCard({ booking, onConfirm, onDecline, onCancel, onView }) {
|
|
157
59
|
const [loadingAction, setLoadingAction] = useState(null)
|
|
158
60
|
const [error, setError] = useState(null)
|
|
159
61
|
|
|
@@ -161,7 +63,7 @@ export function BookingCard({ booking, onConfirm, onDecline, onCancel, onView })
|
|
|
161
63
|
setLoadingAction(actionName)
|
|
162
64
|
setError(null)
|
|
163
65
|
try {
|
|
164
|
-
await actionFn(
|
|
66
|
+
await actionFn(data)
|
|
165
67
|
} catch (err) {
|
|
166
68
|
setError(err?.message ?? 'Något gick fel')
|
|
167
69
|
} finally {
|
|
@@ -169,110 +71,78 @@ export function BookingCard({ booking, onConfirm, onDecline, onCancel, onView })
|
|
|
169
71
|
}
|
|
170
72
|
}
|
|
171
73
|
|
|
172
|
-
const isPending =
|
|
173
|
-
const isConfirmed =
|
|
74
|
+
const isPending = data.status === 'pending'
|
|
75
|
+
const isConfirmed = data.status === 'confirmed'
|
|
174
76
|
|
|
175
77
|
return (
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
|
|
190
|
-
<div style={{ flex: 1, minWidth: 0 }}>
|
|
191
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
192
|
-
<span style={{ fontSize: 15, fontWeight: 600, color: C.primary }}>
|
|
193
|
-
{booking.clientName ?? '—'}
|
|
194
|
-
</span>
|
|
195
|
-
<StatusBadge status={booking.status} />
|
|
196
|
-
</div>
|
|
197
|
-
<div style={{ fontSize: 13, color: C.secondary, marginTop: 2 }}>
|
|
198
|
-
{booking.clientEmail ?? ''}
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
78
|
+
<View surface="secondary" roundness="m" inset="m" gap="s">
|
|
79
|
+
<View layout="row" justifyContent="space-between" alignItems="flex-start" gap="m">
|
|
80
|
+
<View gap="xs" style={{ flex: 1, minWidth: 0 }}>
|
|
81
|
+
<View layout="row" gap="s" alignItems="center" style={{ flexWrap: 'wrap' }}>
|
|
82
|
+
<Title variant="tertiary">{data.clientName ?? '—'}</Title>
|
|
83
|
+
<Badge
|
|
84
|
+
label={STATUS_LABEL[data.status] ?? data.status}
|
|
85
|
+
variant={STATUS_VARIANT[data.status] ?? 'neutral'}
|
|
86
|
+
size="s"
|
|
87
|
+
/>
|
|
88
|
+
</View>
|
|
89
|
+
{data.clientEmail && <Text size="s" color="secondary">{data.clientEmail}</Text>}
|
|
90
|
+
</View>
|
|
201
91
|
|
|
202
92
|
{onView && (
|
|
203
|
-
<
|
|
204
|
-
onClick={() => onView(booking)}
|
|
205
|
-
style={{
|
|
206
|
-
background: 'none',
|
|
207
|
-
border: 'none',
|
|
208
|
-
color: C.muted,
|
|
209
|
-
fontSize: 12,
|
|
210
|
-
fontFamily: FONT,
|
|
211
|
-
cursor: 'pointer',
|
|
212
|
-
padding: '2px 4px',
|
|
213
|
-
flexShrink: 0,
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
93
|
+
<Button variant="link" onClick={() => onView(data)}>
|
|
216
94
|
Visa →
|
|
217
|
-
</
|
|
95
|
+
</Button>
|
|
218
96
|
)}
|
|
219
|
-
</
|
|
97
|
+
</View>
|
|
220
98
|
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
</div>
|
|
99
|
+
<View layout="row" gap="l" style={{ flexWrap: 'wrap' }}>
|
|
100
|
+
<Text size="s" color="secondary">{formatDateTime(data.startsAt)}</Text>
|
|
101
|
+
<Text size="s" color="secondary">{formatDuration(data.duration)}</Text>
|
|
102
|
+
</View>
|
|
226
103
|
|
|
227
|
-
{
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
fontSize: 13,
|
|
232
|
-
color: C.secondary,
|
|
233
|
-
background: '#fafafa',
|
|
234
|
-
borderRadius: 6,
|
|
235
|
-
padding: '8px 12px',
|
|
236
|
-
borderLeft: `3px solid ${C.border}`,
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
{truncate(booking.clientMessage)}
|
|
240
|
-
</div>
|
|
104
|
+
{data.clientMessage && (
|
|
105
|
+
<View surface="primary" roundness="s" inset="s" style={{ borderLeft: '3px solid var(--separator)' }}>
|
|
106
|
+
<Text size="s" color="secondary">{truncate(data.clientMessage)}</Text>
|
|
107
|
+
</View>
|
|
241
108
|
)}
|
|
242
109
|
|
|
243
|
-
{/* Actions */}
|
|
244
110
|
{(isPending || isConfirmed) && (
|
|
245
|
-
<
|
|
111
|
+
<View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
|
|
246
112
|
{isPending && onConfirm && (
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
113
|
+
<Button
|
|
114
|
+
variant="primary"
|
|
115
|
+
disabled={loadingAction === 'confirm'}
|
|
250
116
|
onClick={() => handle(onConfirm, 'confirm')}
|
|
251
|
-
|
|
117
|
+
>
|
|
118
|
+
{loadingAction === 'confirm' ? 'Väntar…' : 'Bekräfta'}
|
|
119
|
+
</Button>
|
|
252
120
|
)}
|
|
253
121
|
{isPending && onDecline && (
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
loading={loadingAction === 'decline'}
|
|
122
|
+
<Button
|
|
123
|
+
variant="danger"
|
|
124
|
+
disabled={loadingAction === 'decline'}
|
|
258
125
|
onClick={() => handle(onDecline, 'decline')}
|
|
259
|
-
|
|
126
|
+
>
|
|
127
|
+
{loadingAction === 'decline' ? 'Väntar…' : 'Avböj'}
|
|
128
|
+
</Button>
|
|
260
129
|
)}
|
|
261
130
|
{isConfirmed && onCancel && (
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
loading={loadingAction === 'cancel'}
|
|
131
|
+
<Button
|
|
132
|
+
variant="danger"
|
|
133
|
+
disabled={loadingAction === 'cancel'}
|
|
266
134
|
onClick={() => handle(onCancel, 'cancel')}
|
|
267
|
-
|
|
135
|
+
>
|
|
136
|
+
{loadingAction === 'cancel' ? 'Väntar…' : 'Avboka'}
|
|
137
|
+
</Button>
|
|
268
138
|
)}
|
|
269
|
-
</
|
|
139
|
+
</View>
|
|
270
140
|
)}
|
|
271
141
|
|
|
272
142
|
{error && (
|
|
273
|
-
<
|
|
143
|
+
<Alert variant="danger">{error}</Alert>
|
|
274
144
|
)}
|
|
275
|
-
</
|
|
145
|
+
</View>
|
|
276
146
|
)
|
|
277
147
|
}
|
|
278
148
|
|