@ossy/booking 1.15.7 → 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.
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { View, Title, Text, Tags } from '@ossy/design-system'
2
+ import { View, Title, Text, Tags, useLocale } from '@ossy/design-system'
3
3
  import { Definition } from './Definition.js'
4
4
  import { moduleStatusTags } from './moduleStatus.js'
5
5
  import { AvailabilityEditor } from './AvailabilityEditor.jsx'
@@ -12,30 +12,32 @@ export const metadata = {
12
12
  },
13
13
  }
14
14
 
15
- const AvailabilityPage = () => (
16
- <View
17
- gap="m"
18
- surface="primary"
19
- style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
20
- >
21
- <View inset="s" gap="s">
22
- <View layout="row" gap="s" alignItems="center">
23
- <Title>{Definition.title} Availability</Title>
24
- {moduleStatusTags(Definition).length > 0 && (
25
- <Tags tags={moduleStatusTags(Definition)} size="s" />
26
- )}
27
- </View>
15
+ const AvailabilityPage = () => {
16
+ const { t } = useLocale()
17
+ return (
18
+ <View
19
+ gap="m"
20
+ surface="primary"
21
+ style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
22
+ >
23
+ <View inset="s" gap="s">
24
+ <View layout="row" gap="s" alignItems="center">
25
+ <Title>{Definition.title} {t('booking.availability.titleSuffix')}</Title>
26
+ {moduleStatusTags(Definition).length > 0 && (
27
+ <Tags tags={moduleStatusTags(Definition)} size="s" />
28
+ )}
29
+ </View>
28
30
 
29
- <Text style={{ maxWidth: '400px' }}>
30
- Set your weekly availability. Clients can only book slots within these windows.
31
- </Text>
32
- </View>
31
+ <Text style={{ maxWidth: '400px' }}>
32
+ {t('booking.availability.description')}
33
+ </Text>
34
+ </View>
33
35
 
34
- <View inset="s">
35
- {/* TODO: load existing availability via booking.get-availability, save via booking.set-availability */}
36
- <AvailabilityEditor availability={[]} />
36
+ <View inset="s">
37
+ <AvailabilityEditor availability={[]} />
38
+ </View>
37
39
  </View>
38
- </View>
39
- )
40
+ )
41
+ }
40
42
 
41
43
  export default AvailabilityPage
@@ -1,5 +1,5 @@
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
3
 
4
4
  export const metadata = { id: 'resource:booking/booking/card' }
5
5
 
@@ -9,36 +9,21 @@ const STATUS_VARIANT = {
9
9
  cancelled: 'neutral',
10
10
  }
11
11
 
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) {
12
+ function formatDateTime (iso, t, language) {
25
13
  if (!iso) return '—'
26
14
  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')
15
+ const locale = language === 'sv' ? 'sv-SE' : 'en-GB'
32
16
  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})` : ''}`
17
+ return new Intl.DateTimeFormat(locale, {
18
+ weekday: 'short', day: 'numeric', month: 'short',
19
+ hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
20
+ }).format(d)
36
21
  } catch {
37
- return `${weekday} ${day} ${month} kl. ${hh}:${mm}`
22
+ return d.toLocaleString(locale)
38
23
  }
39
24
  }
40
25
 
41
- function formatDuration (minutes) {
26
+ function formatDuration (minutes, t) {
42
27
  if (!minutes) return '—'
43
28
  if (minutes < 60) return `${minutes} min`
44
29
  const h = Math.floor(minutes / 60)
@@ -51,21 +36,27 @@ function truncate (text, max = 120) {
51
36
  return text.length > max ? `${text.slice(0, max).trimEnd()}…` : text
52
37
  }
53
38
 
54
- /** Booking card view — registered at `resource:booking/booking/card`. */
55
39
  export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel, onView }) {
40
+ const { t, language } = useLocale()
56
41
  const data = booking ?? resource?.content ?? resource
57
42
  if (!data) return null
58
43
 
59
44
  const [loadingAction, setLoadingAction] = useState(null)
60
45
  const [error, setError] = useState(null)
61
46
 
47
+ const statusLabel = {
48
+ pending: t('booking.card.status.pending'),
49
+ confirmed: t('booking.card.status.confirmed'),
50
+ cancelled: t('booking.card.status.cancelled'),
51
+ }
52
+
62
53
  const handle = async (actionFn, actionName) => {
63
54
  setLoadingAction(actionName)
64
55
  setError(null)
65
56
  try {
66
57
  await actionFn(data)
67
58
  } catch (err) {
68
- setError(err?.message ?? 'Något gick fel')
59
+ setError(err?.message ?? t('booking.card.errorGeneric'))
69
60
  } finally {
70
61
  setLoadingAction(null)
71
62
  }
@@ -81,7 +72,7 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
81
72
  <View layout="row" gap="s" alignItems="center" style={{ flexWrap: 'wrap' }}>
82
73
  <Title variant="tertiary">{data.clientName ?? '—'}</Title>
83
74
  <Badge
84
- label={STATUS_LABEL[data.status] ?? data.status}
75
+ label={statusLabel[data.status] ?? data.status}
85
76
  variant={STATUS_VARIANT[data.status] ?? 'neutral'}
86
77
  size="s"
87
78
  />
@@ -91,14 +82,14 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
91
82
 
92
83
  {onView && (
93
84
  <Button variant="link" onClick={() => onView(data)}>
94
- Visa →
85
+ {t('booking.card.view')}
95
86
  </Button>
96
87
  )}
97
88
  </View>
98
89
 
99
90
  <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>
91
+ <Text size="s" color="secondary">{formatDateTime(data.startsAt, t, language)}</Text>
92
+ <Text size="s" color="secondary">{formatDuration(data.duration, t)}</Text>
102
93
  </View>
103
94
 
104
95
  {data.clientMessage && (
@@ -115,7 +106,7 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
115
106
  disabled={loadingAction === 'confirm'}
116
107
  onClick={() => handle(onConfirm, 'confirm')}
117
108
  >
118
- {loadingAction === 'confirm' ? 'Väntar…' : 'Bekräfta'}
109
+ {loadingAction === 'confirm' ? t('booking.card.waiting') : t('booking.card.confirm')}
119
110
  </Button>
120
111
  )}
121
112
  {isPending && onDecline && (
@@ -124,7 +115,7 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
124
115
  disabled={loadingAction === 'decline'}
125
116
  onClick={() => handle(onDecline, 'decline')}
126
117
  >
127
- {loadingAction === 'decline' ? 'Väntar…' : 'Avböj'}
118
+ {loadingAction === 'decline' ? t('booking.card.waiting') : t('booking.card.decline')}
128
119
  </Button>
129
120
  )}
130
121
  {isConfirmed && onCancel && (
@@ -133,7 +124,7 @@ export function BookingCard ({ booking, resource, onConfirm, onDecline, onCancel
133
124
  disabled={loadingAction === 'cancel'}
134
125
  onClick={() => handle(onCancel, 'cancel')}
135
126
  >
136
- {loadingAction === 'cancel' ? 'Väntar…' : 'Avboka'}
127
+ {loadingAction === 'cancel' ? t('booking.card.waiting') : t('booking.card.cancel')}
137
128
  </Button>
138
129
  )}
139
130
  </View>
@@ -1,57 +1,30 @@
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
4
 
5
5
  export const metadata = {
6
6
  id: 'booking/booking-detail',
7
- path: {
8
- sv: '/bokningar/:bookingId',
9
- en: '/bookings/:bookingId',
10
- },
7
+ path: { sv: '/bokningar/:bookingId', en: '/bookings/:bookingId' },
11
8
  }
12
9
 
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']
10
+ const STATUS_VARIANT = { pending: 'warning', confirmed: 'success', cancelled: 'neutral' }
30
11
 
31
- function formatDateTime (iso) {
12
+ function formatDateTime (iso, language) {
32
13
  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')
14
+ const locale = language === 'sv' ? 'sv-SE' : 'en-GB'
40
15
  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})` : ''}`
16
+ 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
17
  } catch {
45
- return `${weekday} ${day} ${month} ${year} kl. ${hh}:${mm}`
18
+ return new Date(iso).toLocaleString(locale)
46
19
  }
47
20
  }
48
21
 
49
- function formatDuration (minutes) {
22
+ function formatDuration (minutes, t) {
50
23
  if (!minutes) return '—'
51
- if (minutes < 60) return `${minutes} minuter`
24
+ if (minutes < 60) return t('booking.bookingDetail.durationMinutes', { minutes })
52
25
  const h = Math.floor(minutes / 60)
53
26
  const m = minutes % 60
54
- return m ? `${h} timme ${m} min` : `${h} timme`
27
+ return m ? t('booking.bookingDetail.durationHoursMinutes', { hours: h, minutes: m }) : t('booking.bookingDetail.durationHours', { hours: h })
55
28
  }
56
29
 
57
30
  function DetailRow ({ label, value }) {
@@ -63,139 +36,69 @@ function DetailRow ({ label, value }) {
63
36
  )
64
37
  }
65
38
 
66
- function ConfirmForm ({ booking, onSuccess, onError }) {
39
+ function ConfirmForm ({ booking, onSuccess, onError, t }) {
67
40
  const [loading, setLoading] = useState(false)
68
-
69
41
  const handleConfirm = async () => {
70
42
  setLoading(true)
71
43
  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
- }
44
+ const res = await fetch('/actions/booking/confirm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bookingId: booking.id }) })
45
+ if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data?.message ?? t('booking.bookingDetail.errorConfirm')) }
81
46
  onSuccess()
82
- } catch (err) {
83
- onError(err.message)
84
- } finally {
85
- setLoading(false)
86
- }
47
+ } catch (err) { onError(err.message) } finally { setLoading(false) }
87
48
  }
88
-
89
49
  return (
90
50
  <Button variant="primary" disabled={loading} onClick={handleConfirm}>
91
- {loading ? 'Bekräftar…' : 'Bekräfta bokning'}
51
+ {loading ? t('booking.bookingDetail.confirming') : t('booking.bookingDetail.confirm')}
92
52
  </Button>
93
53
  )
94
54
  }
95
55
 
96
- function DeclineForm ({ booking, onSuccess, onError }) {
56
+ function DeclineForm ({ booking, onSuccess, onError, t }) {
97
57
  const [reason, setReason] = useState('')
98
58
  const [loading, setLoading] = useState(false)
99
59
  const [expanded, setExpanded] = useState(false)
100
-
101
60
  const handleDecline = async () => {
102
61
  setLoading(true)
103
62
  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
- }
63
+ const res = await fetch('/actions/booking/decline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bookingId: booking.id, reason: reason.trim() || null }) })
64
+ if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data?.message ?? t('booking.bookingDetail.errorDecline')) }
113
65
  onSuccess()
114
- } catch (err) {
115
- onError(err.message)
116
- } finally {
117
- setLoading(false)
118
- }
66
+ } catch (err) { onError(err.message) } finally { setLoading(false) }
119
67
  }
120
-
121
- if (!expanded) {
122
- return (
123
- <Button variant="danger" onClick={() => setExpanded(true)}>
124
- Avböj förfrågan
125
- </Button>
126
- )
127
- }
128
-
68
+ if (!expanded) return <Button variant="danger" onClick={() => setExpanded(true)}>{t('booking.bookingDetail.decline')}</Button>
129
69
  return (
130
- <Alert variant="danger" title="Avböj bokningsförfrågan">
70
+ <Alert variant="danger" title={t('booking.bookingDetail.declineTitle')}>
131
71
  <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>
72
+ <Textarea value={reason} onChange={e => setReason(e.target.value)} rows={3} placeholder={t('booking.bookingDetail.declinePlaceholder')} />
73
+ <Text variant="small" color="secondary">{t('booking.bookingDetail.declineReasonHint')}</Text>
141
74
  <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>
75
+ <Button variant="danger" disabled={loading} onClick={handleDecline}>{loading ? t('booking.bookingDetail.declining') : t('booking.bookingDetail.declineConfirm')}</Button>
76
+ <Button variant="neutral" onClick={() => setExpanded(false)}>{t('booking.bookingDetail.cancelButton')}</Button>
148
77
  </View>
149
78
  </View>
150
79
  </Alert>
151
80
  )
152
81
  }
153
82
 
154
- function CancelForm ({ booking, onSuccess, onError }) {
83
+ function CancelForm ({ booking, onSuccess, onError, t }) {
155
84
  const [loading, setLoading] = useState(false)
156
85
  const [expanded, setExpanded] = useState(false)
157
-
158
86
  const handleCancel = async () => {
159
87
  setLoading(true)
160
88
  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
- }
89
+ const res = await fetch('/actions/booking/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bookingId: booking.id }) })
90
+ if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data?.message ?? t('booking.bookingDetail.errorCancel')) }
170
91
  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
- )
92
+ } catch (err) { onError(err.message) } finally { setLoading(false) }
184
93
  }
185
-
94
+ if (!expanded) return <Button variant="danger" onClick={() => setExpanded(true)}>{t('booking.bookingDetail.cancelMeeting')}</Button>
186
95
  return (
187
- <Alert variant="danger" title="Avboka bekräftad bokning">
96
+ <Alert variant="danger" title={t('booking.bookingDetail.cancelTitle')}>
188
97
  <View gap="s">
189
- <Text color="secondary">
190
- Klienten meddelas via e-post. Åtgärden kan inte ångras.
191
- </Text>
98
+ <Text color="secondary">{t('booking.bookingDetail.cancelBody')}</Text>
192
99
  <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>
100
+ <Button variant="danger" disabled={loading} onClick={handleCancel}>{loading ? t('booking.bookingDetail.cancelling') : t('booking.bookingDetail.cancelConfirm')}</Button>
101
+ <Button variant="neutral" onClick={() => setExpanded(false)}>{t('booking.bookingDetail.cancelButton')}</Button>
199
102
  </View>
200
103
  </View>
201
104
  </Alert>
@@ -203,67 +106,42 @@ function CancelForm ({ booking, onSuccess, onError }) {
203
106
  }
204
107
 
205
108
  export default function BookingDetailPage ({ bookingId: bookingIdProp }) {
109
+ const { t, language } = useLocale()
206
110
  const router = useRouter()
207
111
  const bookingId = bookingIdProp ?? router?.params?.bookingId
208
-
209
112
  const [booking, setBooking] = useState(null)
210
113
  const [loading, setLoading] = useState(true)
211
114
  const [error, setError] = useState(null)
212
115
  const [actionError, setActionError] = useState(null)
213
116
 
117
+ const statusLabel = useMemo(() => ({
118
+ pending: t('booking.bookingDetail.status.pending'),
119
+ confirmed: t('booking.bookingDetail.status.confirmed'),
120
+ cancelled: t('booking.bookingDetail.status.cancelled'),
121
+ }), [t])
122
+
214
123
  const loadBooking = useCallback(async () => {
215
124
  if (!bookingId) return
216
- setLoading(true)
217
- setError(null)
125
+ setLoading(true); setError(null)
218
126
  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
- }
127
+ const res = await fetch('/actions/booking/list', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) })
128
+ if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data?.message ?? t('booking.bookingDetail.errorStatus', { status: res.status })) }
228
129
  const all = await res.json()
229
130
  const found = all.find(b => b.id === bookingId)
230
- if (!found) throw new Error('Bokningen hittades inte')
131
+ if (!found) throw new Error(t('booking.bookingDetail.notFound'))
231
132
  setBooking(found)
232
- } catch (err) {
233
- setError(err.message)
234
- } finally {
235
- setLoading(false)
236
- }
237
- }, [bookingId])
133
+ } catch (err) { setError(err.message) } finally { setLoading(false) }
134
+ }, [bookingId, t])
238
135
 
239
- useEffect(() => {
240
- loadBooking()
241
- }, [loadBooking])
136
+ useEffect(() => { loadBooking() }, [loadBooking])
242
137
 
243
- const handleSuccess = () => {
244
- setActionError(null)
245
- loadBooking()
246
- }
247
-
248
- const goBack = () => {
249
- if (window.history.length > 1) window.history.back()
250
- else window.location.href = '/bokningar'
251
- }
138
+ const goBack = () => { if (window.history.length > 1) window.history.back(); else window.location.href = '/bokningar' }
252
139
 
253
140
  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
-
141
+ <View gap="m" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
142
+ <Button variant="link" onClick={goBack}>{t('booking.bookingDetail.back')}</Button>
263
143
  {loading ? (
264
- <Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>
265
- Laddar…
266
- </Text>
144
+ <Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>{t('booking.bookingDetail.loading')}</Text>
267
145
  ) : error ? (
268
146
  <Alert variant="danger">{error}</Alert>
269
147
  ) : booking ? (
@@ -274,18 +152,14 @@ export default function BookingDetailPage ({ bookingId: bookingIdProp }) {
274
152
  <Title variant="secondary">{booking.clientName ?? '—'}</Title>
275
153
  {booking.clientEmail && <Text color="secondary">{booking.clientEmail}</Text>}
276
154
  </View>
277
- <Badge
278
- label={STATUS_LABEL[booking.status] ?? booking.status}
279
- variant={STATUS_VARIANT[booking.status] ?? 'neutral'}
280
- />
155
+ <Badge label={statusLabel[booking.status] ?? booking.status} variant={STATUS_VARIANT[booking.status] ?? 'neutral'} />
281
156
  </View>
282
-
283
157
  <View gap="s">
284
- <DetailRow label="Datum och tid" value={formatDateTime(booking.startsAt)} />
285
- <DetailRow label="Längd" value={formatDuration(booking.duration)} />
158
+ <DetailRow label={t('booking.bookingDetail.label.dateTime')} value={formatDateTime(booking.startsAt, language)} />
159
+ <DetailRow label={t('booking.bookingDetail.label.duration')} value={formatDuration(booking.duration, t)} />
286
160
  {booking.clientMessage && (
287
161
  <View gap="xs">
288
- <Text weight="medium" color="secondary">Meddelande</Text>
162
+ <Text weight="medium" color="secondary">{t('booking.bookingDetail.label.message')}</Text>
289
163
  <View surface="primary" roundness="s" inset="s" style={{ borderLeft: '3px solid var(--separator)' }}>
290
164
  <Text style={{ whiteSpace: 'pre-wrap' }}>{booking.clientMessage}</Text>
291
165
  </View>
@@ -293,36 +167,18 @@ export default function BookingDetailPage ({ bookingId: bookingIdProp }) {
293
167
  )}
294
168
  </View>
295
169
  </View>
296
-
297
170
  {(booking.status === 'pending' || booking.status === 'confirmed') && (
298
171
  <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
-
172
+ <Title variant="tertiary">{booking.status === 'pending' ? t('booking.bookingDetail.manageRequest') : t('booking.bookingDetail.manageBooking')}</Title>
303
173
  {actionError && <Alert variant="danger">{actionError}</Alert>}
304
-
305
174
  {booking.status === 'pending' && (
306
175
  <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
- />
176
+ <ConfirmForm booking={booking} onSuccess={() => { setActionError(null); loadBooking() }} onError={setActionError} t={t} />
177
+ <DeclineForm booking={booking} onSuccess={() => { setActionError(null); loadBooking() }} onError={setActionError} t={t} />
317
178
  </View>
318
179
  )}
319
-
320
180
  {booking.status === 'confirmed' && (
321
- <CancelForm
322
- booking={booking}
323
- onSuccess={handleSuccess}
324
- onError={setActionError}
325
- />
181
+ <CancelForm booking={booking} onSuccess={() => { setActionError(null); loadBooking() }} onError={setActionError} t={t} />
326
182
  )}
327
183
  </View>
328
184
  )}
@@ -1,11 +1,10 @@
1
1
  import React from 'react'
2
- import { View, Title, Text } from '@ossy/design-system'
2
+ import { View, Title, Text, useLocale } from '@ossy/design-system'
3
3
  import { useRouter } from '@ossy/router-react'
4
4
  import { BookingForm } from './BookingForm.jsx'
5
5
 
6
6
  export const metadata = {
7
7
  id: 'booking/book',
8
- // Public-facing — no auth required
9
8
  public: true,
10
9
  path: {
11
10
  sv: '/boka/:workspaceId',
@@ -14,6 +13,7 @@ export const metadata = {
14
13
  }
15
14
 
16
15
  const BookingPage = () => {
16
+ const { t } = useLocale()
17
17
  const router = useRouter()
18
18
  const { workspaceId } = router.params
19
19
 
@@ -24,15 +24,12 @@ const BookingPage = () => {
24
24
  inset="s"
25
25
  style={{ minHeight: '100dvh' }}
26
26
  >
27
- <title>Book a session</title>
28
-
29
27
  <View inset="m" roundness="l" surface="primary" slot="content" gap="m">
30
28
  <View gap="xs">
31
- <Title>Book a session</Title>
32
- <Text>Pick a time that works for you and confirm your appointment.</Text>
29
+ <Title>{t('booking.book.title')}</Title>
30
+ <Text>{t('booking.book.description')}</Text>
33
31
  </View>
34
32
 
35
- {/* TODO: load services for workspaceId, let client pick one */}
36
33
  <BookingForm workspaceId={workspaceId} />
37
34
  </View>
38
35
  </View>