@ossy/booking 1.15.7 → 1.16.1

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.
Files changed (46) hide show
  1. package/README.md +26 -19
  2. package/package.json +7 -7
  3. package/src/AvailabilityEditor.jsx +22 -15
  4. package/src/Definition.js +1 -23
  5. package/src/SalesSection.jsx +9 -4
  6. package/src/availability-setup.page.jsx +94 -126
  7. package/src/booking-card.component.jsx +30 -33
  8. package/src/booking-detail.page.jsx +65 -204
  9. package/src/booking-resources.js +10 -0
  10. package/src/bookings.page.jsx +54 -56
  11. package/src/cancel-booking.action.js +1 -61
  12. package/src/cancel-booking.task.js +60 -0
  13. package/src/confirm-booking.action.js +1 -82
  14. package/src/confirm-booking.task.js +81 -0
  15. package/src/create-booking.action.js +1 -168
  16. package/src/create-booking.task.js +170 -0
  17. package/src/create-service.action.js +1 -0
  18. package/src/create-service.task.js +46 -0
  19. package/src/decline-booking.action.js +1 -68
  20. package/src/decline-booking.task.js +67 -0
  21. package/src/delete-service.action.js +1 -0
  22. package/src/delete-service.task.js +41 -0
  23. package/src/en.translations.json +246 -0
  24. package/src/get-availability.action.js +1 -52
  25. package/src/get-availability.task.js +51 -0
  26. package/src/get-available-slots.action.js +1 -89
  27. package/src/get-available-slots.task.js +99 -0
  28. package/src/get-services.action.js +1 -0
  29. package/src/get-services.task.js +37 -0
  30. package/src/home.page.jsx +48 -5
  31. package/src/index.js +20 -1
  32. package/src/invoke-error-message.js +10 -0
  33. package/src/list-bookings.action.js +1 -36
  34. package/src/list-bookings.task.js +35 -0
  35. package/src/locations.js +14 -0
  36. package/src/public-booking.page.jsx +200 -245
  37. package/src/save-availability.action.js +1 -73
  38. package/src/save-availability.task.js +74 -0
  39. package/src/send-booking-reminder.task.js +2 -2
  40. package/src/services.page.jsx +167 -46
  41. package/src/sv.translations.json +246 -0
  42. package/src/update-service.action.js +1 -0
  43. package/src/update-service.task.js +52 -0
  44. package/src/BookingForm.jsx +0 -82
  45. package/src/availability.page.jsx +0 -41
  46. package/src/booking-page.page.jsx +0 -42
@@ -1,5 +1,8 @@
1
1
  import React, { useState } from 'react'
2
- import { View, Title, Text, Button, Badge, Alert } from '@ossy/design-system'
2
+ import { View, Title, Text, Button, Badge, Alert, useLocale } from '@ossy/design-system'
3
+ import { metadata as ConfirmBooking } from './confirm-booking.action.js'
4
+ import { metadata as DeclineBooking } from './decline-booking.action.js'
5
+ import { metadata as CancelBooking } from './cancel-booking.action.js'
3
6
 
4
7
  export const metadata = { id: 'resource:booking/booking/card' }
5
8
 
@@ -9,36 +12,21 @@ const STATUS_VARIANT = {
9
12
  cancelled: 'neutral',
10
13
  }
11
14
 
12
- const STATUS_LABEL = {
13
- pending: 'Väntar',
14
- confirmed: 'Bekräftad',
15
- cancelled: 'Avbokad',
16
- }
17
-
18
- const SV_MONTHS = [
19
- 'jan', 'feb', 'mar', 'apr', 'maj', 'jun',
20
- 'jul', 'aug', 'sep', 'okt', 'nov', 'dec',
21
- ]
22
- const SV_WEEKDAYS = ['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör']
23
-
24
- function formatDateTime (iso) {
15
+ function formatDateTime (iso, t, language) {
25
16
  if (!iso) return '—'
26
17
  const d = new Date(iso)
27
- const weekday = SV_WEEKDAYS[d.getDay()]
28
- const day = d.getDate()
29
- const month = SV_MONTHS[d.getMonth()]
30
- const hh = String(d.getHours()).padStart(2, '0')
31
- const mm = String(d.getMinutes()).padStart(2, '0')
18
+ const locale = language === 'sv' ? 'sv-SE' : 'en-GB'
32
19
  try {
33
- const tz = new Intl.DateTimeFormat('sv-SE', { timeZoneName: 'short', hour: 'numeric' })
34
- .formatToParts(d).find(p => p.type === 'timeZoneName')?.value ?? ''
35
- return `${weekday} ${day} ${month} kl. ${hh}:${mm}${tz ? ` (${tz})` : ''}`
20
+ return new Intl.DateTimeFormat(locale, {
21
+ weekday: 'short', day: 'numeric', month: 'short',
22
+ hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
23
+ }).format(d)
36
24
  } catch {
37
- return `${weekday} ${day} ${month} kl. ${hh}:${mm}`
25
+ return d.toLocaleString(locale)
38
26
  }
39
27
  }
40
28
 
41
- function formatDuration (minutes) {
29
+ function formatDuration (minutes, t) {
42
30
  if (!minutes) return '—'
43
31
  if (minutes < 60) return `${minutes} min`
44
32
  const h = Math.floor(minutes / 60)
@@ -51,21 +39,27 @@ function truncate (text, max = 120) {
51
39
  return text.length > max ? `${text.slice(0, max).trimEnd()}…` : text
52
40
  }
53
41
 
54
- /** Booking card view — registered at `resource:booking/booking/card`. */
55
42
  export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel, onView }) {
43
+ const { t, language } = useLocale()
56
44
  const data = booking ?? resource?.content ?? resource
57
45
  if (!data) return null
58
46
 
59
47
  const [loadingAction, setLoadingAction] = useState(null)
60
48
  const [error, setError] = useState(null)
61
49
 
50
+ const statusLabel = {
51
+ pending: t('booking.card.status.pending'),
52
+ confirmed: t('booking.card.status.confirmed'),
53
+ cancelled: t('booking.card.status.cancelled'),
54
+ }
55
+
62
56
  const handle = async (actionFn, actionName) => {
63
57
  setLoadingAction(actionName)
64
58
  setError(null)
65
59
  try {
66
60
  await actionFn(data)
67
61
  } catch (err) {
68
- setError(err?.message ?? 'Något gick fel')
62
+ setError(err?.message ?? t('booking.card.errorGeneric'))
69
63
  } finally {
70
64
  setLoadingAction(null)
71
65
  }
@@ -81,7 +75,7 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
81
75
  <View layout="row" gap="s" alignItems="center" style={{ flexWrap: 'wrap' }}>
82
76
  <Title variant="tertiary">{data.clientName ?? '—'}</Title>
83
77
  <Badge
84
- label={STATUS_LABEL[data.status] ?? data.status}
78
+ label={statusLabel[data.status] ?? data.status}
85
79
  variant={STATUS_VARIANT[data.status] ?? 'neutral'}
86
80
  size="s"
87
81
  />
@@ -91,14 +85,14 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
91
85
 
92
86
  {onView && (
93
87
  <Button variant="link" onClick={() => onView(data)}>
94
- Visa →
88
+ {t('booking.card.view')}
95
89
  </Button>
96
90
  )}
97
91
  </View>
98
92
 
99
93
  <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>
94
+ <Text size="s" color="secondary">{formatDateTime(data.startsAt, t, language)}</Text>
95
+ <Text size="s" color="secondary">{formatDuration(data.duration, t)}</Text>
102
96
  </View>
103
97
 
104
98
  {data.clientMessage && (
@@ -111,29 +105,32 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
111
105
  <View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
112
106
  {isPending && onConfirm && (
113
107
  <Button
108
+ data-action={ConfirmBooking.id}
114
109
  variant="primary"
115
110
  disabled={loadingAction === 'confirm'}
116
111
  onClick={() => handle(onConfirm, 'confirm')}
117
112
  >
118
- {loadingAction === 'confirm' ? 'Väntar…' : 'Bekräfta'}
113
+ {loadingAction === 'confirm' ? t('booking.card.waiting') : t('booking.card.confirm')}
119
114
  </Button>
120
115
  )}
121
116
  {isPending && onDecline && (
122
117
  <Button
118
+ data-action={DeclineBooking.id}
123
119
  variant="danger"
124
120
  disabled={loadingAction === 'decline'}
125
121
  onClick={() => handle(onDecline, 'decline')}
126
122
  >
127
- {loadingAction === 'decline' ? 'Väntar…' : 'Avböj'}
123
+ {loadingAction === 'decline' ? t('booking.card.waiting') : t('booking.card.decline')}
128
124
  </Button>
129
125
  )}
130
126
  {isConfirmed && onCancel && (
131
127
  <Button
128
+ data-action={CancelBooking.id}
132
129
  variant="danger"
133
130
  disabled={loadingAction === 'cancel'}
134
131
  onClick={() => handle(onCancel, 'cancel')}
135
132
  >
136
- {loadingAction === 'cancel' ? 'Väntar…' : 'Avboka'}
133
+ {loadingAction === 'cancel' ? t('booking.card.waiting') : t('booking.card.cancel')}
137
134
  </Button>
138
135
  )}
139
136
  </View>
@@ -1,57 +1,36 @@
1
- import React, { useState, useEffect, useCallback } from 'react'
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react'
2
2
  import { useRouter } from '@ossy/router-react'
3
- import { View, Title, Text, Button, Badge, Alert, Textarea } from '@ossy/design-system'
3
+ import { View, Title, Text, Button, Badge, Alert, Textarea, useLocale } from '@ossy/design-system'
4
+ import { useSdk } from '@ossy/sdk-react'
5
+ import { metadata as ConfirmBooking } from './confirm-booking.action.js'
6
+ import { metadata as DeclineBooking } from './decline-booking.action.js'
7
+ import { metadata as CancelBooking } from './cancel-booking.action.js'
8
+ import { metadata as ListBookings } from './list-bookings.action.js'
9
+ import { invokeErrorMessage } from './invoke-error-message.js'
4
10
 
5
11
  export const metadata = {
6
12
  id: 'booking/booking-detail',
7
- path: {
8
- sv: '/bokningar/:bookingId',
9
- en: '/bookings/:bookingId',
10
- },
13
+ path: { sv: '/bokningar/:bookingId', en: '/bookings/:bookingId' },
11
14
  }
12
15
 
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
-
25
- const SV_MONTHS = [
26
- 'januari', 'februari', 'mars', 'april', 'maj', 'juni',
27
- 'juli', 'augusti', 'september', 'oktober', 'november', 'december',
28
- ]
29
- const SV_WEEKDAYS = ['Söndag', 'Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag']
16
+ const STATUS_VARIANT = { pending: 'warning', confirmed: 'success', cancelled: 'neutral' }
30
17
 
31
- function formatDateTime (iso) {
18
+ function formatDateTime (iso, language) {
32
19
  if (!iso) return '—'
33
- const d = new Date(iso)
34
- const weekday = SV_WEEKDAYS[d.getDay()]
35
- const day = d.getDate()
36
- const month = SV_MONTHS[d.getMonth()]
37
- const year = d.getFullYear()
38
- const hh = String(d.getHours()).padStart(2, '0')
39
- const mm = String(d.getMinutes()).padStart(2, '0')
20
+ const locale = language === 'sv' ? 'sv-SE' : 'en-GB'
40
21
  try {
41
- const tz = new Intl.DateTimeFormat('sv-SE', { timeZoneName: 'short', hour: 'numeric' })
42
- .formatToParts(d).find(p => p.type === 'timeZoneName')?.value ?? ''
43
- return `${weekday} ${day} ${month} ${year} kl. ${hh}:${mm}${tz ? ` (${tz})` : ''}`
22
+ return new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }).format(new Date(iso))
44
23
  } catch {
45
- return `${weekday} ${day} ${month} ${year} kl. ${hh}:${mm}`
24
+ return new Date(iso).toLocaleString(locale)
46
25
  }
47
26
  }
48
27
 
49
- function formatDuration (minutes) {
28
+ function formatDuration (minutes, t) {
50
29
  if (!minutes) return '—'
51
- if (minutes < 60) return `${minutes} minuter`
30
+ if (minutes < 60) return t('booking.bookingDetail.durationMinutes', { minutes })
52
31
  const h = Math.floor(minutes / 60)
53
32
  const m = minutes % 60
54
- return m ? `${h} timme ${m} min` : `${h} timme`
33
+ return m ? t('booking.bookingDetail.durationHoursMinutes', { hours: h, minutes: m }) : t('booking.bookingDetail.durationHours', { hours: h })
55
34
  }
56
35
 
57
36
  function DetailRow ({ label, value }) {
@@ -63,139 +42,66 @@ function DetailRow ({ label, value }) {
63
42
  )
64
43
  }
65
44
 
66
- function ConfirmForm ({ booking, onSuccess, onError }) {
45
+ function ConfirmForm ({ booking, onSuccess, onError, t, sdk }) {
67
46
  const [loading, setLoading] = useState(false)
68
-
69
47
  const handleConfirm = async () => {
70
48
  setLoading(true)
71
49
  try {
72
- const res = await fetch('/actions/booking/confirm', {
73
- method: 'POST',
74
- headers: { 'Content-Type': 'application/json' },
75
- body: JSON.stringify({ bookingId: booking.id }),
76
- })
77
- if (!res.ok) {
78
- const data = await res.json().catch(() => ({}))
79
- throw new Error(data?.message ?? 'Bekräftning misslyckades')
80
- }
50
+ await sdk.invoke(ConfirmBooking, { bookingId: booking.id })
81
51
  onSuccess()
82
- } catch (err) {
83
- onError(err.message)
84
- } finally {
85
- setLoading(false)
86
- }
52
+ } catch (err) { onError(await invokeErrorMessage(err, t('booking.bookingDetail.errorConfirm'))) } finally { setLoading(false) }
87
53
  }
88
-
89
54
  return (
90
- <Button variant="primary" disabled={loading} onClick={handleConfirm}>
91
- {loading ? 'Bekräftar…' : 'Bekräfta bokning'}
55
+ <Button data-action={ConfirmBooking.id} variant="primary" disabled={loading} onClick={handleConfirm}>
56
+ {loading ? t('booking.bookingDetail.confirming') : t('booking.bookingDetail.confirm')}
92
57
  </Button>
93
58
  )
94
59
  }
95
60
 
96
- function DeclineForm ({ booking, onSuccess, onError }) {
61
+ function DeclineForm ({ booking, onSuccess, onError, t, sdk }) {
97
62
  const [reason, setReason] = useState('')
98
63
  const [loading, setLoading] = useState(false)
99
64
  const [expanded, setExpanded] = useState(false)
100
-
101
65
  const handleDecline = async () => {
102
66
  setLoading(true)
103
67
  try {
104
- const res = await fetch('/actions/booking/decline', {
105
- method: 'POST',
106
- headers: { 'Content-Type': 'application/json' },
107
- body: JSON.stringify({ bookingId: booking.id, reason: reason.trim() || null }),
108
- })
109
- if (!res.ok) {
110
- const data = await res.json().catch(() => ({}))
111
- throw new Error(data?.message ?? 'Avböjning misslyckades')
112
- }
68
+ await sdk.invoke(DeclineBooking, { bookingId: booking.id, reason: reason.trim() || null })
113
69
  onSuccess()
114
- } catch (err) {
115
- onError(err.message)
116
- } finally {
117
- setLoading(false)
118
- }
70
+ } catch (err) { onError(await invokeErrorMessage(err, t('booking.bookingDetail.errorDecline'))) } finally { setLoading(false) }
119
71
  }
120
-
121
- if (!expanded) {
122
- return (
123
- <Button variant="danger" onClick={() => setExpanded(true)}>
124
- Avböj förfrågan
125
- </Button>
126
- )
127
- }
128
-
72
+ if (!expanded) return <Button variant="danger" onClick={() => setExpanded(true)}>{t('booking.bookingDetail.decline')}</Button>
129
73
  return (
130
- <Alert variant="danger" title="Avböj bokningsförfrågan">
74
+ <Alert variant="danger" title={t('booking.bookingDetail.declineTitle')}>
131
75
  <View gap="s">
132
- <Textarea
133
- value={reason}
134
- onChange={e => setReason(e.target.value)}
135
- rows={3}
136
- placeholder="T.ex. Tyvärr är den begärda tiden inte längre tillgänglig…"
137
- />
138
- <Text variant="small" color="secondary">
139
- Anledning (valfritt — skickas till klienten)
140
- </Text>
76
+ <Textarea value={reason} onChange={e => setReason(e.target.value)} rows={3} placeholder={t('booking.bookingDetail.declinePlaceholder')} />
77
+ <Text variant="small" color="secondary">{t('booking.bookingDetail.declineReasonHint')}</Text>
141
78
  <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>
79
+ <Button data-action={DeclineBooking.id} variant="danger" disabled={loading} onClick={handleDecline}>{loading ? t('booking.bookingDetail.declining') : t('booking.bookingDetail.declineConfirm')}</Button>
80
+ <Button variant="neutral" onClick={() => setExpanded(false)}>{t('booking.bookingDetail.cancelButton')}</Button>
148
81
  </View>
149
82
  </View>
150
83
  </Alert>
151
84
  )
152
85
  }
153
86
 
154
- function CancelForm ({ booking, onSuccess, onError }) {
87
+ function CancelForm ({ booking, onSuccess, onError, t, sdk }) {
155
88
  const [loading, setLoading] = useState(false)
156
89
  const [expanded, setExpanded] = useState(false)
157
-
158
90
  const handleCancel = async () => {
159
91
  setLoading(true)
160
92
  try {
161
- const res = await fetch('/actions/booking/cancel', {
162
- method: 'POST',
163
- headers: { 'Content-Type': 'application/json' },
164
- body: JSON.stringify({ bookingId: booking.id }),
165
- })
166
- if (!res.ok) {
167
- const data = await res.json().catch(() => ({}))
168
- throw new Error(data?.message ?? 'Avbokning misslyckades')
169
- }
93
+ await sdk.invoke(CancelBooking, { bookingId: booking.id })
170
94
  onSuccess()
171
- } catch (err) {
172
- onError(err.message)
173
- } finally {
174
- setLoading(false)
175
- }
176
- }
177
-
178
- if (!expanded) {
179
- return (
180
- <Button variant="danger" onClick={() => setExpanded(true)}>
181
- Avboka möte
182
- </Button>
183
- )
95
+ } catch (err) { onError(await invokeErrorMessage(err, t('booking.bookingDetail.errorCancel'))) } finally { setLoading(false) }
184
96
  }
185
-
97
+ if (!expanded) return <Button variant="danger" onClick={() => setExpanded(true)}>{t('booking.bookingDetail.cancelMeeting')}</Button>
186
98
  return (
187
- <Alert variant="danger" title="Avboka bekräftad bokning">
99
+ <Alert variant="danger" title={t('booking.bookingDetail.cancelTitle')}>
188
100
  <View gap="s">
189
- <Text color="secondary">
190
- Klienten meddelas via e-post. Åtgärden kan inte ångras.
191
- </Text>
101
+ <Text color="secondary">{t('booking.bookingDetail.cancelBody')}</Text>
192
102
  <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>
103
+ <Button data-action={CancelBooking.id} variant="danger" disabled={loading} onClick={handleCancel}>{loading ? t('booking.bookingDetail.cancelling') : t('booking.bookingDetail.cancelConfirm')}</Button>
104
+ <Button variant="neutral" onClick={() => setExpanded(false)}>{t('booking.bookingDetail.cancelButton')}</Button>
199
105
  </View>
200
106
  </View>
201
107
  </Alert>
@@ -203,67 +109,44 @@ function CancelForm ({ booking, onSuccess, onError }) {
203
109
  }
204
110
 
205
111
  export default function BookingDetailPage ({ bookingId: bookingIdProp }) {
112
+ const { t, language } = useLocale()
113
+ const sdk = useSdk()
206
114
  const router = useRouter()
207
115
  const bookingId = bookingIdProp ?? router?.params?.bookingId
208
-
209
116
  const [booking, setBooking] = useState(null)
210
117
  const [loading, setLoading] = useState(true)
211
118
  const [error, setError] = useState(null)
212
119
  const [actionError, setActionError] = useState(null)
213
120
 
121
+ const statusLabel = useMemo(() => ({
122
+ pending: t('booking.bookingDetail.status.pending'),
123
+ confirmed: t('booking.bookingDetail.status.confirmed'),
124
+ cancelled: t('booking.bookingDetail.status.cancelled'),
125
+ }), [t])
126
+
214
127
  const loadBooking = useCallback(async () => {
215
128
  if (!bookingId) return
216
- setLoading(true)
217
- setError(null)
129
+ setLoading(true); setError(null)
218
130
  try {
219
- const res = await fetch('/actions/booking/list', {
220
- method: 'POST',
221
- headers: { 'Content-Type': 'application/json' },
222
- body: JSON.stringify({}),
223
- })
224
- if (!res.ok) {
225
- const data = await res.json().catch(() => ({}))
226
- throw new Error(data?.message ?? `Fel ${res.status}`)
227
- }
228
- const all = await res.json()
131
+ const all = await sdk.invoke(ListBookings, {})
229
132
  const found = all.find(b => b.id === bookingId)
230
- if (!found) throw new Error('Bokningen hittades inte')
133
+ if (!found) throw new Error(t('booking.bookingDetail.notFound'))
231
134
  setBooking(found)
232
- } catch (err) {
233
- setError(err.message)
234
- } finally {
235
- setLoading(false)
236
- }
237
- }, [bookingId])
238
-
239
- useEffect(() => {
240
- loadBooking()
241
- }, [loadBooking])
135
+ } catch (err) { setError(err.message) } finally { setLoading(false) }
136
+ }, [bookingId, sdk, t])
242
137
 
243
- const handleSuccess = () => {
244
- setActionError(null)
245
- loadBooking()
246
- }
138
+ useEffect(() => { loadBooking() }, [loadBooking])
247
139
 
248
140
  const goBack = () => {
249
141
  if (window.history.length > 1) window.history.back()
250
- else window.location.href = '/bokningar'
142
+ else window.location.href = router.getHref('booking/bookings')
251
143
  }
252
144
 
253
145
  return (
254
- <View
255
- gap="m"
256
- surface="primary"
257
- style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
258
- >
259
- <Button variant="link" onClick={goBack}>
260
- ← Alla bokningar
261
- </Button>
262
-
146
+ <View gap="m" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
147
+ <Button variant="link" onClick={goBack}>{t('booking.bookingDetail.back')}</Button>
263
148
  {loading ? (
264
- <Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>
265
- Laddar…
266
- </Text>
149
+ <Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>{t('booking.bookingDetail.loading')}</Text>
267
150
  ) : error ? (
268
151
  <Alert variant="danger">{error}</Alert>
269
152
  ) : booking ? (
@@ -274,18 +157,14 @@ export default function BookingDetailPage ({ bookingId: bookingIdProp }) {
274
157
  <Title variant="secondary">{booking.clientName ?? '—'}</Title>
275
158
  {booking.clientEmail && <Text color="secondary">{booking.clientEmail}</Text>}
276
159
  </View>
277
- <Badge
278
- label={STATUS_LABEL[booking.status] ?? booking.status}
279
- variant={STATUS_VARIANT[booking.status] ?? 'neutral'}
280
- />
160
+ <Badge label={statusLabel[booking.status] ?? booking.status} variant={STATUS_VARIANT[booking.status] ?? 'neutral'} />
281
161
  </View>
282
-
283
162
  <View gap="s">
284
- <DetailRow label="Datum och tid" value={formatDateTime(booking.startsAt)} />
285
- <DetailRow label="Längd" value={formatDuration(booking.duration)} />
163
+ <DetailRow label={t('booking.bookingDetail.label.dateTime')} value={formatDateTime(booking.startsAt, language)} />
164
+ <DetailRow label={t('booking.bookingDetail.label.duration')} value={formatDuration(booking.duration, t)} />
286
165
  {booking.clientMessage && (
287
166
  <View gap="xs">
288
- <Text weight="medium" color="secondary">Meddelande</Text>
167
+ <Text weight="medium" color="secondary">{t('booking.bookingDetail.label.message')}</Text>
289
168
  <View surface="primary" roundness="s" inset="s" style={{ borderLeft: '3px solid var(--separator)' }}>
290
169
  <Text style={{ whiteSpace: 'pre-wrap' }}>{booking.clientMessage}</Text>
291
170
  </View>
@@ -293,36 +172,18 @@ export default function BookingDetailPage ({ bookingId: bookingIdProp }) {
293
172
  )}
294
173
  </View>
295
174
  </View>
296
-
297
175
  {(booking.status === 'pending' || booking.status === 'confirmed') && (
298
176
  <View gap="s" surface="secondary" roundness="m" inset="l">
299
- <Title variant="tertiary">
300
- {booking.status === 'pending' ? 'Hantera förfrågan' : 'Hantera bokning'}
301
- </Title>
302
-
177
+ <Title variant="tertiary">{booking.status === 'pending' ? t('booking.bookingDetail.manageRequest') : t('booking.bookingDetail.manageBooking')}</Title>
303
178
  {actionError && <Alert variant="danger">{actionError}</Alert>}
304
-
305
179
  {booking.status === 'pending' && (
306
180
  <View gap="s">
307
- <ConfirmForm
308
- booking={booking}
309
- onSuccess={handleSuccess}
310
- onError={setActionError}
311
- />
312
- <DeclineForm
313
- booking={booking}
314
- onSuccess={handleSuccess}
315
- onError={setActionError}
316
- />
181
+ <ConfirmForm booking={booking} sdk={sdk} onSuccess={() => { setActionError(null); loadBooking() }} onError={setActionError} t={t} />
182
+ <DeclineForm booking={booking} sdk={sdk} onSuccess={() => { setActionError(null); loadBooking() }} onError={setActionError} t={t} />
317
183
  </View>
318
184
  )}
319
-
320
185
  {booking.status === 'confirmed' && (
321
- <CancelForm
322
- booking={booking}
323
- onSuccess={handleSuccess}
324
- onError={setActionError}
325
- />
186
+ <CancelForm booking={booking} sdk={sdk} onSuccess={() => { setActionError(null); loadBooking() }} onError={setActionError} t={t} />
326
187
  )}
327
188
  </View>
328
189
  )}
@@ -0,0 +1,10 @@
1
+ import { ResourcesQueries } from '@ossy/resources/server'
2
+
3
+ /**
4
+ * List booking resources by type (and optional workspace).
5
+ */
6
+ export function getBookingResources ({ type, belongsTo }) {
7
+ const query = { type }
8
+ if (belongsTo) query.belongsTo = belongsTo
9
+ return ResourcesQueries.GetResources(query)
10
+ }