@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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/booking",
3
3
  "description": "Booking feature package — services, availability, and appointment management for Ossy consultants",
4
- "version": "1.15.7",
4
+ "version": "1.16.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "module": "./src/index.js",
@@ -14,11 +14,11 @@
14
14
  "src": "./src"
15
15
  },
16
16
  "dependencies": {
17
- "@ossy/email": "^1.5.7",
18
- "@ossy/event-store": "^1.7.7",
19
- "@ossy/observability": "^1.7.7",
20
- "@ossy/platform": "^1.38.7",
21
- "@ossy/resources": "^1.11.7"
17
+ "@ossy/email": "^1.6.0",
18
+ "@ossy/event-store": "^1.8.0",
19
+ "@ossy/observability": "^1.8.0",
20
+ "@ossy/platform": "^1.39.0",
21
+ "@ossy/resources": "^1.12.0"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "@ossy/design-system": ">=1.0.0",
@@ -34,5 +34,5 @@
34
34
  "/src",
35
35
  "README.md"
36
36
  ],
37
- "gitHead": "f65df0cd6da864c314c4f424112e05e8e2dec0ff"
37
+ "gitHead": "d9d31182be64a448d575da432af2830d909ad4b9"
38
38
  }
@@ -1,12 +1,10 @@
1
- import React, { useState } from 'react'
2
- import { View, Title, Text, Button, Input } from '@ossy/design-system'
3
-
4
- const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
1
+ import React, { useState, useMemo } from 'react'
2
+ import { View, Text, Button, Input, useLocale } from '@ossy/design-system'
5
3
 
6
4
  const DEFAULT_WINDOW = { enabled: false, startTime: '09:00', endTime: '17:00', bufferMinutes: 0 }
7
5
 
8
6
  const buildInitialState = (availability = []) => {
9
- const state = DAY_LABELS.map(() => ({ ...DEFAULT_WINDOW }))
7
+ const state = Array.from({ length: 7 }, () => ({ ...DEFAULT_WINDOW }))
10
8
  for (const window of availability) {
11
9
  if (window.dayOfWeek >= 0 && window.dayOfWeek <= 6) {
12
10
  state[window.dayOfWeek] = {
@@ -21,8 +19,19 @@ const buildInitialState = (availability = []) => {
21
19
  }
22
20
 
23
21
  export const AvailabilityEditor = ({ availability = [], onSave }) => {
22
+ const { t } = useLocale()
24
23
  const [days, setDays] = useState(() => buildInitialState(availability))
25
24
 
25
+ const dayKeys = useMemo(() => [
26
+ 'booking.availabilityEditor.day.sun',
27
+ 'booking.availabilityEditor.day.mon',
28
+ 'booking.availabilityEditor.day.tue',
29
+ 'booking.availabilityEditor.day.wed',
30
+ 'booking.availabilityEditor.day.thu',
31
+ 'booking.availabilityEditor.day.fri',
32
+ 'booking.availabilityEditor.day.sat',
33
+ ], [])
34
+
26
35
  const update = (idx, patch) =>
27
36
  setDays((prev) => prev.map((d, i) => (i === idx ? { ...d, ...patch } : d)))
28
37
 
@@ -37,16 +46,15 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
37
46
  bufferMinutes: Number(bufferMinutes),
38
47
  }))
39
48
 
40
- // TODO: dispatch booking.set-availability action with `windows`
41
49
  onSave?.(windows)
42
50
  }
43
51
 
44
52
  return (
45
53
  <View gap="m">
46
54
  <View gap="s">
47
- {DAY_LABELS.map((label, idx) => (
55
+ {dayKeys.map((key, idx) => (
48
56
  <View
49
- key={label}
57
+ key={key}
50
58
  layout="row"
51
59
  gap="m"
52
60
  alignItems="center"
@@ -62,7 +70,7 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
62
70
  style={{ width: 16, height: 16, accentColor: 'var(--color-accent)' }}
63
71
  />
64
72
  </label>
65
- <Text weight="medium" style={{ width: '2.5rem', flexShrink: 0 }}>{label}</Text>
73
+ <Text weight="medium" style={{ width: '2.5rem', flexShrink: 0 }}>{t(key)}</Text>
66
74
 
67
75
  {days[idx].enabled ? (
68
76
  <View layout="row" gap="s" alignItems="center" style={{ flex: 1 }}>
@@ -72,14 +80,14 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
72
80
  onChange={(v) => update(idx, { startTime: v })}
73
81
  size="s"
74
82
  />
75
- <Text color="secondary">to</Text>
83
+ <Text color="secondary">{t('booking.availabilityEditor.to')}</Text>
76
84
  <Input
77
85
  type="time"
78
86
  value={days[idx].endTime}
79
87
  onChange={(v) => update(idx, { endTime: v })}
80
88
  size="s"
81
89
  />
82
- <Text color="secondary" size="s">Buffer</Text>
90
+ <Text color="secondary" size="s">{t('booking.availabilityEditor.buffer')}</Text>
83
91
  <Input
84
92
  type="number"
85
93
  value={String(days[idx].bufferMinutes)}
@@ -87,17 +95,16 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
87
95
  size="s"
88
96
  style={{ width: '4rem' }}
89
97
  />
90
- <Text color="secondary" size="s">min</Text>
98
+ <Text color="secondary" size="s">{t('booking.availabilityEditor.min')}</Text>
91
99
  </View>
92
100
  ) : (
93
- <Text color="secondary" size="s">Unavailable</Text>
101
+ <Text color="secondary" size="s">{t('booking.availabilityEditor.unavailable')}</Text>
94
102
  )}
95
103
  </View>
96
104
  ))}
97
105
  </View>
98
106
 
99
- {/* TODO: wire to booking.set-availability action */}
100
- <Button variant="primary" onClick={handleSave}>Save availability</Button>
107
+ <Button variant="primary" onClick={handleSave}>{t('booking.availabilityEditor.save')}</Button>
101
108
  </View>
102
109
  )
103
110
  }
@@ -1,15 +1,8 @@
1
1
  import React, { useState } from 'react'
2
- import { View, Title, Text, Button, Input, Select } from '@ossy/design-system'
2
+ import { View, Title, Text, Button, Input, useLocale } from '@ossy/design-system'
3
3
 
4
- /**
5
- * Public client-facing form.
6
- *
7
- * TODO:
8
- * - Load services for workspaceId via booking.get-services
9
- * - Load available slots for selected service via booking.get-available-slots
10
- * - On submit: call booking.create-booking action, then redirect to Stripe Checkout
11
- */
12
4
  export const BookingForm = ({ workspaceId }) => {
5
+ const { t } = useLocale()
13
6
  const [step, setStep] = useState('pick-service')
14
7
  const [selectedService, setSelectedService] = useState(null)
15
8
  const [selectedSlot, setSelectedSlot] = useState(null)
@@ -19,11 +12,10 @@ export const BookingForm = ({ workspaceId }) => {
19
12
  if (step === 'pick-service') {
20
13
  return (
21
14
  <View gap="m">
22
- <Title variant="secondary">Choose a service</Title>
23
- {/* TODO: replace with real services loaded from API */}
24
- <Text color="secondary">Loading services for workspace {workspaceId}…</Text>
15
+ <Title variant="secondary">{t('booking.book.form.chooseService')}</Title>
16
+ <Text color="secondary">{t('booking.book.form.loadingServices', { workspaceId })}</Text>
25
17
  <Button variant="secondary" onClick={() => setStep('pick-slot')}>
26
- Continue (placeholder)
18
+ {t('booking.book.form.continuePlaceholder')}
27
19
  </Button>
28
20
  </View>
29
21
  )
@@ -32,13 +24,12 @@ export const BookingForm = ({ workspaceId }) => {
32
24
  if (step === 'pick-slot') {
33
25
  return (
34
26
  <View gap="m">
35
- <Title variant="secondary">Pick a time</Title>
36
- {/* TODO: render a slot grid derived from availability minus existing bookings */}
37
- <Text color="secondary">Available slots will appear here.</Text>
27
+ <Title variant="secondary">{t('booking.book.form.pickTime')}</Title>
28
+ <Text color="secondary">{t('booking.book.form.slotsPlaceholder')}</Text>
38
29
  <View layout="row" gap="s">
39
- <Button variant="ghost" onClick={() => setStep('pick-service')}>Back</Button>
30
+ <Button variant="ghost" onClick={() => setStep('pick-service')}>{t('booking.book.form.back')}</Button>
40
31
  <Button variant="secondary" onClick={() => setStep('enter-details')}>
41
- Continue (placeholder)
32
+ {t('booking.book.form.continuePlaceholder')}
42
33
  </Button>
43
34
  </View>
44
35
  </View>
@@ -48,25 +39,24 @@ export const BookingForm = ({ workspaceId }) => {
48
39
  if (step === 'enter-details') {
49
40
  return (
50
41
  <View gap="m">
51
- <Title variant="secondary">Your details</Title>
42
+ <Title variant="secondary">{t('booking.book.form.yourDetails')}</Title>
52
43
  <Input
53
- label="Name"
44
+ label={t('booking.book.form.nameLabel')}
54
45
  value={clientName}
55
46
  onChange={setClientName}
56
- placeholder="Jane Smith"
47
+ placeholder={t('booking.book.form.namePlaceholder')}
57
48
  />
58
49
  <Input
59
- label="Email"
50
+ label={t('booking.book.form.emailLabel')}
60
51
  type="email"
61
52
  value={clientEmail}
62
53
  onChange={setClientEmail}
63
- placeholder="jane@example.com"
54
+ placeholder={t('booking.book.form.emailPlaceholder')}
64
55
  />
65
56
  <View layout="row" gap="s">
66
- <Button variant="ghost" onClick={() => setStep('pick-slot')}>Back</Button>
67
- {/* TODO: submit booking action then redirect to Stripe */}
57
+ <Button variant="ghost" onClick={() => setStep('pick-slot')}>{t('booking.book.form.back')}</Button>
68
58
  <Button variant="primary" onClick={() => setStep('confirm')}>
69
- Confirm &amp; Pay
59
+ {t('booking.book.form.confirmPay')}
70
60
  </Button>
71
61
  </View>
72
62
  </View>
@@ -75,8 +65,8 @@ export const BookingForm = ({ workspaceId }) => {
75
65
 
76
66
  return (
77
67
  <View gap="m">
78
- <Title variant="secondary">Booking confirmed!</Title>
79
- <Text>Check your email for a confirmation.</Text>
68
+ <Title variant="secondary">{t('booking.book.form.confirmed')}</Title>
69
+ <Text>{t('booking.book.form.checkEmail')}</Text>
80
70
  </View>
81
71
  )
82
72
  }
package/src/Definition.js CHANGED
@@ -2,28 +2,6 @@ export const Definition = {
2
2
  id: 'booking',
3
3
  title: 'Booking',
4
4
  description: 'Services, availability windows, and client appointment management for consultants.',
5
- module: {
6
- id: 'booking',
7
- enabled: true,
8
- },
5
+ icon: 'calendar',
9
6
  statuses: ['beta'],
10
- actions: [
11
- 'booking.create-service',
12
- 'booking.update-service',
13
- 'booking.delete-service',
14
- 'booking.create-booking',
15
- 'booking.confirm-booking',
16
- 'booking.cancel-booking',
17
- 'booking.set-availability',
18
- ],
19
- views: [
20
- 'booking.get-services',
21
- 'booking.get-bookings',
22
- 'booking.get-availability',
23
- 'booking.get-available-slots',
24
- ],
25
- tasks: [
26
- 'booking.send-confirmation-email',
27
- 'booking.sync-stripe-payment-status',
28
- ],
29
7
  }
@@ -6,20 +6,24 @@ import {
6
6
  Icon,
7
7
  PageSection,
8
8
  MarkdownViewer,
9
+ useLocale,
9
10
  } from '@ossy/design-system'
10
11
  import { HeroCover } from './HeroCover.jsx'
11
12
 
12
13
  export default ({
13
14
  cover,
14
15
  features,
15
- }) => (
16
+ }) => {
17
+ const { t } = useLocale()
18
+
19
+ return (
16
20
  <PageSection maxWidth="xl" gap="l">
17
21
  <View gap="l" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
18
22
  <HeroCover {...cover} />
19
23
 
20
24
  <View gap="m" inset="s">
21
25
  <Title variant="secondary">
22
- Översikt
26
+ {t('booking.sales.overview')}
23
27
  </Title>
24
28
 
25
29
  <View gap="m" layout="row-wrap">
@@ -47,7 +51,7 @@ export default ({
47
51
 
48
52
  <View gap="m" inset="s">
49
53
  <Title variant="secondary">
50
- Funktioner
54
+ {t('booking.sales.features')}
51
55
  </Title>
52
56
 
53
57
  {features.map(x => (
@@ -69,4 +73,5 @@ export default ({
69
73
  </View>
70
74
  </View>
71
75
  </PageSection>
72
- )
76
+ )
77
+ }
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react'
1
+ import React, { useState, useEffect, useMemo } from 'react'
2
2
  import {
3
3
  View,
4
4
  Title,
@@ -7,6 +7,7 @@ import {
7
7
  Input,
8
8
  Select,
9
9
  Alert,
10
+ useLocale,
10
11
  } from '@ossy/design-system'
11
12
 
12
13
  export const metadata = {
@@ -14,16 +15,6 @@ export const metadata = {
14
15
  path: { sv: '/tillganglighet', en: '/availability' },
15
16
  }
16
17
 
17
- const DAYS = [
18
- { label: 'Måndag', dayOfWeek: 1 },
19
- { label: 'Tisdag', dayOfWeek: 2 },
20
- { label: 'Onsdag', dayOfWeek: 3 },
21
- { label: 'Torsdag', dayOfWeek: 4 },
22
- { label: 'Fredag', dayOfWeek: 5 },
23
- { label: 'Lördag', dayOfWeek: 6 },
24
- { label: 'Söndag', dayOfWeek: 0 },
25
- ]
26
-
27
18
  const SESSION_DURATIONS = [30, 60, 90]
28
19
  const BUFFER_OPTIONS = [0, 15, 30]
29
20
  const TIMEZONE_OPTIONS = [
@@ -45,15 +36,22 @@ function buildDayState (weeklyWindows = []) {
45
36
  endTime: w.endTime ?? '17:00',
46
37
  }
47
38
  }
48
- return Object.fromEntries(
49
- DAYS.map(d => [
50
- d.dayOfWeek,
51
- byDay[d.dayOfWeek] ?? { enabled: false, startTime: '09:00', endTime: '17:00' },
52
- ]),
53
- )
39
+ const dayOrder = [
40
+ { dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
41
+ { dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
42
+ { dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
43
+ { dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
44
+ { dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
45
+ { dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
46
+ { dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
47
+ ]
48
+ return dayOrder.map(({ dayOfWeek }) => ({
49
+ dayOfWeek,
50
+ ...(byDay[dayOfWeek] ?? { enabled: false, startTime: '09:00', endTime: '17:00' }),
51
+ }))
54
52
  }
55
53
 
56
- function DayRow ({ label, day, onToggle, onTimeChange }) {
54
+ function DayRow ({ label, day, onToggle, onTimeChange, t }) {
57
55
  return (
58
56
  <View
59
57
  layout="row"
@@ -76,26 +74,29 @@ function DayRow ({ label, day, onToggle, onTimeChange }) {
76
74
 
77
75
  {day.enabled ? (
78
76
  <View layout="row" gap="s" alignItems="center" style={{ flex: 1, flexWrap: 'wrap' }}>
79
- <Input
80
- type="time"
81
- value={day.startTime}
82
- onChange={e => onTimeChange('startTime', e.target.value)}
83
- />
77
+ <Input type="time" value={day.startTime} onChange={e => onTimeChange('startTime', e.target.value)} />
84
78
  <Text color="secondary">–</Text>
85
- <Input
86
- type="time"
87
- value={day.endTime}
88
- onChange={e => onTimeChange('endTime', e.target.value)}
89
- />
79
+ <Input type="time" value={day.endTime} onChange={e => onTimeChange('endTime', e.target.value)} />
90
80
  </View>
91
81
  ) : (
92
- <Text color="secondary" size="s">Inte tillgänglig</Text>
82
+ <Text color="secondary" size="s">{t('booking.availabilitySetup.unavailable')}</Text>
93
83
  )}
94
84
  </View>
95
85
  )
96
86
  }
97
87
 
98
88
  export default function AvailabilitySetupPage ({ workspaceId }) {
89
+ const { t } = useLocale()
90
+ const dayDefs = useMemo(() => [
91
+ { dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
92
+ { dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
93
+ { dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
94
+ { dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
95
+ { dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
96
+ { dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
97
+ { dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
98
+ ], [])
99
+
99
100
  const [dayState, setDayState] = useState(() => buildDayState())
100
101
  const [sessionDurations, setSessionDurations] = useState([60])
101
102
  const [bufferMinutes, setBufferMinutes] = useState(0)
@@ -129,18 +130,14 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
129
130
  }, [])
130
131
 
131
132
  const toggleDay = (dayOfWeek) => {
132
- setDayState(prev => ({
133
- ...prev,
134
- [dayOfWeek]: { ...prev[dayOfWeek], enabled: !prev[dayOfWeek].enabled },
135
- }))
133
+ setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek
134
+ ? { ...d, enabled: !d.enabled }
135
+ : d))
136
136
  setSaved(false)
137
137
  }
138
138
 
139
139
  const updateDayTime = (dayOfWeek, field, value) => {
140
- setDayState(prev => ({
141
- ...prev,
142
- [dayOfWeek]: { ...prev[dayOfWeek], [field]: value },
143
- }))
140
+ setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek ? { ...d, [field]: value } : d))
144
141
  setSaved(false)
145
142
  }
146
143
 
@@ -159,10 +156,8 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
159
156
  setSaving(true)
160
157
  setError(null)
161
158
 
162
- const weeklyWindows = DAYS.filter(d => dayState[d.dayOfWeek]?.enabled).map(d => ({
163
- dayOfWeek: d.dayOfWeek,
164
- startTime: dayState[d.dayOfWeek].startTime,
165
- endTime: dayState[d.dayOfWeek].endTime,
159
+ const weeklyWindows = dayState.filter(d => d.enabled).map(({ dayOfWeek, startTime, endTime }) => ({
160
+ dayOfWeek, startTime, endTime,
166
161
  }))
167
162
 
168
163
  try {
@@ -174,7 +169,7 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
174
169
 
175
170
  if (!res.ok) {
176
171
  const data = await res.json().catch(() => ({}))
177
- throw new Error(data?.message ?? `Kunde inte spara (${res.status})`)
172
+ throw new Error(data?.message ?? t('booking.availabilitySetup.errorSave', { status: res.status }))
178
173
  }
179
174
 
180
175
  setSaved(true)
@@ -195,7 +190,7 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
195
190
  if (loading) {
196
191
  return (
197
192
  <View surface="primary" inset="l" style={{ height: '100%', overflowY: 'auto' }}>
198
- <Text color="secondary">Laddar…</Text>
193
+ <Text color="secondary">{t('booking.availabilitySetup.loading')}</Text>
199
194
  </View>
200
195
  )
201
196
  }
@@ -207,83 +202,66 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
207
202
  style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto', maxWidth: 620 }}
208
203
  >
209
204
  <View inset="s" gap="xs">
210
- <Title variant="secondary">Tillgänglighet</Title>
211
- <Text style={{ maxWidth: 480 }}>
212
- Ange när du är tillgänglig för bokningar. Klienter kan bara boka tider inom dessa fönster.
213
- </Text>
205
+ <Title variant="secondary">{t('booking.availabilitySetup.title')}</Title>
206
+ <Text style={{ maxWidth: 480 }}>{t('booking.availabilitySetup.description')}</Text>
214
207
  </View>
215
208
 
216
209
  <View gap="s" surface="secondary" roundness="m" inset="m">
217
- <Title variant="tertiary">Veckoschema</Title>
210
+ <Title variant="tertiary">{t('booking.availabilitySetup.weeklySchedule')}</Title>
218
211
  <View gap="xs">
219
- {DAYS.map(({ label, dayOfWeek }) => (
220
- <DayRow
221
- key={dayOfWeek}
222
- label={label}
223
- day={dayState[dayOfWeek]}
224
- onToggle={() => toggleDay(dayOfWeek)}
225
- onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
226
- />
227
- ))}
212
+ {dayDefs.map(({ dayOfWeek, key }) => {
213
+ const day = dayState.find(d => d.dayOfWeek === dayOfWeek)
214
+ return (
215
+ <DayRow
216
+ key={dayOfWeek}
217
+ label={t(key)}
218
+ day={day}
219
+ onToggle={() => toggleDay(dayOfWeek)}
220
+ onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
221
+ t={t}
222
+ />
223
+ )
224
+ })}
228
225
  </View>
229
226
  </View>
230
227
 
231
228
  <View gap="s" surface="secondary" roundness="m" inset="m">
232
- <Title variant="tertiary">Sessionslängd</Title>
229
+ <Title variant="tertiary">{t('booking.availabilitySetup.sessionDuration')}</Title>
233
230
  <View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
234
- {SESSION_DURATIONS.map(dur => {
235
- const active = sessionDurations.includes(dur)
236
- return (
237
- <Button
238
- key={dur}
239
- variant={active ? 'tab-active' : 'tab'}
240
- onClick={() => toggleDuration(dur)}
241
- >
242
- {dur} min
243
- </Button>
244
- )
245
- })}
231
+ {SESSION_DURATIONS.map(dur => (
232
+ <Button
233
+ key={dur}
234
+ variant={sessionDurations.includes(dur) ? 'tab-active' : 'tab'}
235
+ onClick={() => toggleDuration(dur)}
236
+ >
237
+ {t('booking.availabilitySetup.durationMin', { minutes: dur })}
238
+ </Button>
239
+ ))}
246
240
  </View>
247
- <Text size="s" color="secondary">
248
- Välj de sessionslängder klienter kan välja bland.
249
- </Text>
241
+ <Text size="s" color="secondary">{t('booking.availabilitySetup.sessionDurationHint')}</Text>
250
242
  </View>
251
243
 
252
244
  <View gap="s" surface="secondary" roundness="m" inset="m">
253
- <Text weight="medium">Bufferttid mellan bokningar</Text>
245
+ <Text weight="medium">{t('booking.availabilitySetup.bufferLabel')}</Text>
254
246
  <Select
255
247
  id="buffer-select"
256
248
  value={bufferMinutes}
257
- onChange={e => {
258
- setBufferMinutes(Number(e.target.value))
259
- setSaved(false)
260
- }}
249
+ onChange={e => { setBufferMinutes(Number(e.target.value)); setSaved(false) }}
261
250
  >
262
251
  {BUFFER_OPTIONS.map(opt => (
263
252
  <option key={opt} value={opt}>
264
- {opt === 0 ? 'Ingen bufferttid' : `${opt} minuter`}
253
+ {opt === 0 ? t('booking.availabilitySetup.bufferNone') : t('booking.availabilitySetup.bufferMinutes', { minutes: opt })}
265
254
  </option>
266
255
  ))}
267
256
  </Select>
268
- <Text size="s" color="secondary">
269
- Automatisk paustid som läggs till efter varje bokning.
270
- </Text>
257
+ <Text size="s" color="secondary">{t('booking.availabilitySetup.bufferHint')}</Text>
271
258
  </View>
272
259
 
273
260
  <View gap="s" surface="secondary" roundness="m" inset="m">
274
- <Text weight="medium">Tidszon</Text>
275
- <Select
276
- id="tz-select"
277
- value={timezone}
278
- onChange={e => {
279
- setTimezone(e.target.value)
280
- setSaved(false)
281
- }}
282
- >
261
+ <Text weight="medium">{t('booking.availabilitySetup.timezone')}</Text>
262
+ <Select id="tz-select" value={timezone} onChange={e => { setTimezone(e.target.value); setSaved(false) }}>
283
263
  {TIMEZONE_OPTIONS.map(tz => (
284
- <option key={tz} value={tz}>
285
- {tz}
286
- </option>
264
+ <option key={tz} value={tz}>{tz}</option>
287
265
  ))}
288
266
  </Select>
289
267
  </View>
@@ -291,30 +269,19 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
291
269
  {error && <Alert variant="danger">{error}</Alert>}
292
270
 
293
271
  <Button variant="primary" disabled={saving} onClick={handleSave}>
294
- {saving ? 'Sparar…' : 'Spara tillgänglighet'}
272
+ {saving ? t('booking.availabilitySetup.saving') : t('booking.availabilitySetup.save')}
295
273
  </Button>
296
274
 
297
275
  {saved && workspaceId && (
298
- <Alert variant="success" title="Tillgänglighet sparad">
276
+ <Alert variant="success" title={t('booking.availabilitySetup.savedTitle')}>
299
277
  <View gap="s">
300
- <Text color="secondary">
301
- Din bokningslänk är redo att delas med klienter.
302
- </Text>
278
+ <Text color="secondary">{t('booking.availabilitySetup.savedBody')}</Text>
303
279
  <View layout="row" gap="s" alignItems="center">
304
- <Text
305
- as="code"
306
- style={{
307
- flex: 1,
308
- overflow: 'hidden',
309
- textOverflow: 'ellipsis',
310
- whiteSpace: 'nowrap',
311
- fontFamily: 'ui-monospace, monospace',
312
- }}
313
- >
280
+ <Text as="code" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'ui-monospace, monospace' }}>
314
281
  {bookingUrl}
315
282
  </Text>
316
283
  <Button variant={copied ? 'cta' : 'primary'} onClick={handleCopyLink}>
317
- {copied ? 'Kopierad!' : 'Kopiera länk'}
284
+ {copied ? t('booking.availabilitySetup.copied') : t('booking.availabilitySetup.copyLink')}
318
285
  </Button>
319
286
  </View>
320
287
  </View>