@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
package/README.md CHANGED
@@ -1,39 +1,46 @@
1
1
  # @ossy/booking
2
2
 
3
- Booking feature package for the Ossy platform. Lets consultants define services, set weekly availability, and accept client bookings with optional Stripe payment.
3
+ Booking feature package for the Ossy platform. Lets consultants define services, set weekly availability, and accept client bookings.
4
4
 
5
5
  ## Pages
6
6
 
7
7
  | Page | Path | Audience |
8
8
  |------|------|----------|
9
- | `booking-page` | `/book/:workspaceId` | Public (clients) |
10
- | `services` | `/services` | Consultant (authenticated) |
11
- | `bookings` | `/bookings` | Consultant (authenticated) |
12
- | `availability` | `/availability` | Consultant (authenticated) |
9
+ | `public-booking` | `/book/:consultantSlug` · `/boka/:consultantSlug` | Public (clients) |
10
+ | `services` | `/services` · `/tjanster` | Consultant (authenticated) |
11
+ | `bookings` | `/bookings` · `/bokningar` | Consultant (authenticated) |
12
+ | `availability-setup` | `/availability` · `/tillganglighet` | Consultant (authenticated) |
13
+ | `booking-detail` | `/bookings/:bookingId` · `/bokningar/:bookingId` | Consultant (authenticated) |
14
+
15
+ ## Public booking flow
16
+
17
+ 1. Client opens `public-booking` (no auth; `metadata.public: true`)
18
+ 2. Picks a service → calendar slot → contact details
19
+ 3. `booking/create` stores a pending booking and sends notification emails
20
+
21
+ ## Service actions
22
+
23
+ | Action | Access |
24
+ |--------|--------|
25
+ | `booking/get-services` | public |
26
+ | `booking/create-service` | workspace |
27
+ | `booking/update-service` | workspace |
28
+ | `booking/delete-service` | workspace |
13
29
 
14
30
  ## Resource templates
15
31
 
16
- Three resource schemas are registered under the `@ossy/booking` category:
32
+ - `@ossy/booking/service` what a consultant sells (`/@ossy/booking/services/`)
33
+ - `@ossy/booking/booking` — an appointment (`/@ossy/booking/bookings/`)
34
+ - `@ossy/booking/availability` — weekly availability config (`/@ossy/booking/availability/`)
17
35
 
18
- - `@ossy/booking/service` what a consultant sells
19
- - `@ossy/booking/booking` — a confirmed appointment
20
- - `@ossy/booking/availability` — recurring weekly availability window
36
+ Import locations from `@ossy/booking` (`services`, `bookings`, `availability`).
21
37
 
22
38
  ## Resource view slots
23
39
 
24
- Register custom UI for resource types via `*.component.jsx` with `metadata.id` matching the slot key (see `@ossy/design-system` [SLOTS.md](../design-system/docs/SLOTS.md)):
25
-
26
40
  | Component | Slot key |
27
41
  |-----------|----------|
28
42
  | `service-detail.component.jsx` | `resource:booking/service/detail` |
29
43
  | `service-card-slot.component.jsx` | `resource:booking/service/card` |
30
44
  | `booking-card.component.jsx` | `resource:booking/booking/card` |
31
45
 
32
- Dedicated pages (`services.page.jsx`, `booking-detail.page.jsx`, etc.) continue to work alongside generic `@ossy/resources` slot hosts.
33
-
34
- ## What still needs implementation
35
-
36
- - **Slot generation** — derive available time slots from `availability` records minus existing `booking` records
37
- - **Stripe integration** — create a PaymentIntent on booking submit, webhook to flip `paymentStatus`
38
- - **Confirmation emails** — `booking.send-confirmation-email` task
39
- - **Public workspace resolution** — resolve `workspaceId` param on the public booking page without auth
46
+ See [SPEC.md](./SPEC.md) for the full product specification.
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.1",
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.1",
18
+ "@ossy/event-store": "^1.8.1",
19
+ "@ossy/observability": "^1.8.1",
20
+ "@ossy/platform": "^1.39.1",
21
+ "@ossy/resources": "^1.12.1"
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": "c0ba5d90749690634e4dc2705178ff8d89dd3070"
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
  }
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,23 +7,19 @@ import {
7
7
  Input,
8
8
  Select,
9
9
  Alert,
10
+ useLocale,
10
11
  } from '@ossy/design-system'
12
+ import { useRouter } from '@ossy/router-react'
13
+ import { useSdk } from '@ossy/sdk-react'
14
+ import { metadata as SaveAvailability } from './save-availability.action.js'
15
+ import { metadata as GetAvailability } from './get-availability.action.js'
16
+ import { invokeErrorMessage } from './invoke-error-message.js'
11
17
 
12
18
  export const metadata = {
13
19
  id: 'booking/availability-setup',
14
20
  path: { sv: '/tillganglighet', en: '/availability' },
15
21
  }
16
22
 
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
23
  const SESSION_DURATIONS = [30, 60, 90]
28
24
  const BUFFER_OPTIONS = [0, 15, 30]
29
25
  const TIMEZONE_OPTIONS = [
@@ -45,15 +41,22 @@ function buildDayState (weeklyWindows = []) {
45
41
  endTime: w.endTime ?? '17:00',
46
42
  }
47
43
  }
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
- )
44
+ const dayOrder = [
45
+ { dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
46
+ { dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
47
+ { dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
48
+ { dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
49
+ { dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
50
+ { dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
51
+ { dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
52
+ ]
53
+ return dayOrder.map(({ dayOfWeek }) => ({
54
+ dayOfWeek,
55
+ ...(byDay[dayOfWeek] ?? { enabled: false, startTime: '09:00', endTime: '17:00' }),
56
+ }))
54
57
  }
55
58
 
56
- function DayRow ({ label, day, onToggle, onTimeChange }) {
59
+ function DayRow ({ label, day, onToggle, onTimeChange, t }) {
57
60
  return (
58
61
  <View
59
62
  layout="row"
@@ -76,26 +79,31 @@ function DayRow ({ label, day, onToggle, onTimeChange }) {
76
79
 
77
80
  {day.enabled ? (
78
81
  <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
- />
82
+ <Input type="time" value={day.startTime} onChange={e => onTimeChange('startTime', e.target.value)} />
84
83
  <Text color="secondary">–</Text>
85
- <Input
86
- type="time"
87
- value={day.endTime}
88
- onChange={e => onTimeChange('endTime', e.target.value)}
89
- />
84
+ <Input type="time" value={day.endTime} onChange={e => onTimeChange('endTime', e.target.value)} />
90
85
  </View>
91
86
  ) : (
92
- <Text color="secondary" size="s">Inte tillgänglig</Text>
87
+ <Text color="secondary" size="s">{t('booking.availabilitySetup.unavailable')}</Text>
93
88
  )}
94
89
  </View>
95
90
  )
96
91
  }
97
92
 
98
93
  export default function AvailabilitySetupPage ({ workspaceId }) {
94
+ const { t } = useLocale()
95
+ const sdk = useSdk()
96
+ const router = useRouter()
97
+ const dayDefs = useMemo(() => [
98
+ { dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
99
+ { dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
100
+ { dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
101
+ { dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
102
+ { dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
103
+ { dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
104
+ { dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
105
+ ], [])
106
+
99
107
  const [dayState, setDayState] = useState(() => buildDayState())
100
108
  const [sessionDurations, setSessionDurations] = useState([60])
101
109
  const [bufferMinutes, setBufferMinutes] = useState(0)
@@ -106,11 +114,15 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
106
114
  const [error, setError] = useState(null)
107
115
  const [copied, setCopied] = useState(false)
108
116
 
109
- const bookingUrl = `https://ossy.se/boka/${workspaceId ?? ''}`
117
+ const bookingPath = workspaceId
118
+ ? router.getHref({ id: 'public-booking', params: { consultantSlug: workspaceId } })
119
+ : ''
120
+ const bookingUrl = typeof window !== 'undefined' && bookingPath
121
+ ? `${window.location.origin}${bookingPath}`
122
+ : bookingPath
110
123
 
111
124
  useEffect(() => {
112
- fetch('/actions/booking/get-availability')
113
- .then(r => (r.ok ? r.json() : null))
125
+ sdk.invoke(GetAvailability, {})
114
126
  .then(data => {
115
127
  if (data) {
116
128
  setDayState(buildDayState(data.weeklyWindows ?? []))
@@ -126,21 +138,17 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
126
138
  })
127
139
  .catch(() => {})
128
140
  .finally(() => setLoading(false))
129
- }, [])
141
+ }, [sdk])
130
142
 
131
143
  const toggleDay = (dayOfWeek) => {
132
- setDayState(prev => ({
133
- ...prev,
134
- [dayOfWeek]: { ...prev[dayOfWeek], enabled: !prev[dayOfWeek].enabled },
135
- }))
144
+ setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek
145
+ ? { ...d, enabled: !d.enabled }
146
+ : d))
136
147
  setSaved(false)
137
148
  }
138
149
 
139
150
  const updateDayTime = (dayOfWeek, field, value) => {
140
- setDayState(prev => ({
141
- ...prev,
142
- [dayOfWeek]: { ...prev[dayOfWeek], [field]: value },
143
- }))
151
+ setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek ? { ...d, [field]: value } : d))
144
152
  setSaved(false)
145
153
  }
146
154
 
@@ -159,27 +167,15 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
159
167
  setSaving(true)
160
168
  setError(null)
161
169
 
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,
170
+ const weeklyWindows = dayState.filter(d => d.enabled).map(({ dayOfWeek, startTime, endTime }) => ({
171
+ dayOfWeek, startTime, endTime,
166
172
  }))
167
173
 
168
174
  try {
169
- const res = await fetch('/actions/booking/save-availability', {
170
- method: 'POST',
171
- headers: { 'Content-Type': 'application/json' },
172
- body: JSON.stringify({ weeklyWindows, sessionDurations, bufferMinutes, timezone }),
173
- })
174
-
175
- if (!res.ok) {
176
- const data = await res.json().catch(() => ({}))
177
- throw new Error(data?.message ?? `Kunde inte spara (${res.status})`)
178
- }
179
-
175
+ await sdk.invoke(SaveAvailability, { weeklyWindows, sessionDurations, bufferMinutes, timezone })
180
176
  setSaved(true)
181
177
  } catch (err) {
182
- setError(err.message)
178
+ setError(await invokeErrorMessage(err, t('booking.availabilitySetup.errorSave', { status: '' })))
183
179
  } finally {
184
180
  setSaving(false)
185
181
  }
@@ -195,7 +191,7 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
195
191
  if (loading) {
196
192
  return (
197
193
  <View surface="primary" inset="l" style={{ height: '100%', overflowY: 'auto' }}>
198
- <Text color="secondary">Laddar…</Text>
194
+ <Text color="secondary">{t('booking.availabilitySetup.loading')}</Text>
199
195
  </View>
200
196
  )
201
197
  }
@@ -207,114 +203,86 @@ export default function AvailabilitySetupPage ({ workspaceId }) {
207
203
  style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto', maxWidth: 620 }}
208
204
  >
209
205
  <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>
206
+ <Title variant="secondary">{t('booking.availabilitySetup.title')}</Title>
207
+ <Text style={{ maxWidth: 480 }}>{t('booking.availabilitySetup.description')}</Text>
214
208
  </View>
215
209
 
216
210
  <View gap="s" surface="secondary" roundness="m" inset="m">
217
- <Title variant="tertiary">Veckoschema</Title>
211
+ <Title variant="tertiary">{t('booking.availabilitySetup.weeklySchedule')}</Title>
218
212
  <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
- ))}
213
+ {dayDefs.map(({ dayOfWeek, key }) => {
214
+ const day = dayState.find(d => d.dayOfWeek === dayOfWeek)
215
+ return (
216
+ <DayRow
217
+ key={dayOfWeek}
218
+ label={t(key)}
219
+ day={day}
220
+ onToggle={() => toggleDay(dayOfWeek)}
221
+ onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
222
+ t={t}
223
+ />
224
+ )
225
+ })}
228
226
  </View>
229
227
  </View>
230
228
 
231
229
  <View gap="s" surface="secondary" roundness="m" inset="m">
232
- <Title variant="tertiary">Sessionslängd</Title>
230
+ <Title variant="tertiary">{t('booking.availabilitySetup.sessionDuration')}</Title>
233
231
  <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
- })}
232
+ {SESSION_DURATIONS.map(dur => (
233
+ <Button
234
+ key={dur}
235
+ variant={sessionDurations.includes(dur) ? 'tab-active' : 'tab'}
236
+ onClick={() => toggleDuration(dur)}
237
+ >
238
+ {t('booking.availabilitySetup.durationMin', { minutes: dur })}
239
+ </Button>
240
+ ))}
246
241
  </View>
247
- <Text size="s" color="secondary">
248
- Välj de sessionslängder klienter kan välja bland.
249
- </Text>
242
+ <Text size="s" color="secondary">{t('booking.availabilitySetup.sessionDurationHint')}</Text>
250
243
  </View>
251
244
 
252
245
  <View gap="s" surface="secondary" roundness="m" inset="m">
253
- <Text weight="medium">Bufferttid mellan bokningar</Text>
246
+ <Text weight="medium">{t('booking.availabilitySetup.bufferLabel')}</Text>
254
247
  <Select
255
248
  id="buffer-select"
256
249
  value={bufferMinutes}
257
- onChange={e => {
258
- setBufferMinutes(Number(e.target.value))
259
- setSaved(false)
260
- }}
250
+ onChange={e => { setBufferMinutes(Number(e.target.value)); setSaved(false) }}
261
251
  >
262
252
  {BUFFER_OPTIONS.map(opt => (
263
253
  <option key={opt} value={opt}>
264
- {opt === 0 ? 'Ingen bufferttid' : `${opt} minuter`}
254
+ {opt === 0 ? t('booking.availabilitySetup.bufferNone') : t('booking.availabilitySetup.bufferMinutes', { minutes: opt })}
265
255
  </option>
266
256
  ))}
267
257
  </Select>
268
- <Text size="s" color="secondary">
269
- Automatisk paustid som läggs till efter varje bokning.
270
- </Text>
258
+ <Text size="s" color="secondary">{t('booking.availabilitySetup.bufferHint')}</Text>
271
259
  </View>
272
260
 
273
261
  <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
- >
262
+ <Text weight="medium">{t('booking.availabilitySetup.timezone')}</Text>
263
+ <Select id="tz-select" value={timezone} onChange={e => { setTimezone(e.target.value); setSaved(false) }}>
283
264
  {TIMEZONE_OPTIONS.map(tz => (
284
- <option key={tz} value={tz}>
285
- {tz}
286
- </option>
265
+ <option key={tz} value={tz}>{tz}</option>
287
266
  ))}
288
267
  </Select>
289
268
  </View>
290
269
 
291
270
  {error && <Alert variant="danger">{error}</Alert>}
292
271
 
293
- <Button variant="primary" disabled={saving} onClick={handleSave}>
294
- {saving ? 'Sparar…' : 'Spara tillgänglighet'}
272
+ <Button data-action={SaveAvailability.id} variant="primary" disabled={saving} onClick={handleSave}>
273
+ {saving ? t('booking.availabilitySetup.saving') : t('booking.availabilitySetup.save')}
295
274
  </Button>
296
275
 
297
276
  {saved && workspaceId && (
298
- <Alert variant="success" title="Tillgänglighet sparad">
277
+ <Alert variant="success" title={t('booking.availabilitySetup.savedTitle')}>
299
278
  <View gap="s">
300
- <Text color="secondary">
301
- Din bokningslänk är redo att delas med klienter.
302
- </Text>
279
+ <Text color="secondary">{t('booking.availabilitySetup.savedBody')}</Text>
303
280
  <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
- >
281
+ <Text as="code" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'ui-monospace, monospace' }}>
314
282
  {bookingUrl}
315
283
  </Text>
316
284
  <Button variant={copied ? 'cta' : 'primary'} onClick={handleCopyLink}>
317
- {copied ? 'Kopierad!' : 'Kopiera länk'}
285
+ {copied ? t('booking.availabilitySetup.copied') : t('booking.availabilitySetup.copyLink')}
318
286
  </Button>
319
287
  </View>
320
288
  </View>