@ossy/booking 1.15.5 → 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.
@@ -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-card' }
4
+ export const metadata = { id: 'resource:booking/booking/card' }
4
5
 
5
- // ---------------------------------------------------------------------------
6
- // Tokens
7
- // ---------------------------------------------------------------------------
8
- const C = {
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 FONT = 'system-ui, -apple-system, sans-serif'
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
- // Status badge
76
- // ---------------------------------------------------------------------------
77
- function StatusBadge({ status }) {
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(booking)
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 = booking.status === 'pending'
173
- const isConfirmed = booking.status === 'confirmed'
74
+ const isPending = data.status === 'pending'
75
+ const isConfirmed = data.status === 'confirmed'
174
76
 
175
77
  return (
176
- <div
177
- style={{
178
- background: C.surface,
179
- border: `1px solid ${C.border}`,
180
- borderRadius: 10,
181
- padding: '16px 20px',
182
- fontFamily: FONT,
183
- display: 'flex',
184
- flexDirection: 'column',
185
- gap: 12,
186
- }}
187
- >
188
- {/* Header row */}
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
- <button
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
- </button>
95
+ </Button>
218
96
  )}
219
- </div>
97
+ </View>
220
98
 
221
- {/* Booking details */}
222
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 20px', fontSize: 13, color: C.secondary }}>
223
- <span>📅 {formatDateTime(booking.startsAt)}</span>
224
- <span>⏱ {formatDuration(booking.duration)}</span>
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
- {/* Message snippet */}
228
- {booking.clientMessage && (
229
- <div
230
- style={{
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
- <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
111
+ <View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
246
112
  {isPending && onConfirm && (
247
- <ActionButton
248
- label="Bekräfta"
249
- loading={loadingAction === 'confirm'}
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
- <ActionButton
255
- label="Avböj"
256
- danger
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
- <ActionButton
263
- label="Avboka"
264
- danger
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
- </div>
139
+ </View>
270
140
  )}
271
141
 
272
142
  {error && (
273
- <div style={{ fontSize: 12, color: C.errorText }}>{error}</div>
143
+ <Alert variant="danger">{error}</Alert>
274
144
  )}
275
- </div>
145
+ </View>
276
146
  )
277
147
  }
278
148