@licklist/design 0.78.5-dev.69 → 0.78.5-dev.70

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 (123) hide show
  1. package/dist/styles/themes/bookedit/_fonts.scss +2 -0
  2. package/dist/v2/components/ActionMenu/ActionMenu.d.ts.map +1 -1
  3. package/dist/v2/components/ActionMenu/ActionMenu.js +5 -3
  4. package/dist/v2/components/AvatarUpload/AvatarUpload.d.ts +12 -0
  5. package/dist/v2/components/AvatarUpload/AvatarUpload.d.ts.map +1 -0
  6. package/dist/v2/components/AvatarUpload/index.d.ts +2 -0
  7. package/dist/v2/components/AvatarUpload/index.d.ts.map +1 -0
  8. package/dist/v2/components/Button/Button.d.ts +1 -1
  9. package/dist/v2/components/Button/Button.d.ts.map +1 -1
  10. package/dist/v2/components/Button/Button.scss.js +1 -1
  11. package/dist/v2/components/DataTable/DataTable.d.ts +41 -0
  12. package/dist/v2/components/DataTable/DataTable.d.ts.map +1 -0
  13. package/dist/v2/components/DataTable/index.d.ts +3 -0
  14. package/dist/v2/components/DataTable/index.d.ts.map +1 -0
  15. package/dist/v2/components/EmptyState/EmptyState.d.ts +14 -0
  16. package/dist/v2/components/EmptyState/EmptyState.d.ts.map +1 -0
  17. package/dist/v2/components/EmptyState/index.d.ts +3 -0
  18. package/dist/v2/components/EmptyState/index.d.ts.map +1 -0
  19. package/dist/v2/components/FormField/FormField.scss.js +1 -1
  20. package/dist/v2/components/InfoGrid/InfoGrid.d.ts +13 -0
  21. package/dist/v2/components/InfoGrid/InfoGrid.d.ts.map +1 -0
  22. package/dist/v2/components/InfoGrid/index.d.ts +2 -0
  23. package/dist/v2/components/InfoGrid/index.d.ts.map +1 -0
  24. package/dist/v2/components/NewTable/NewTable.scss.js +1 -1
  25. package/dist/v2/components/RadioCard/RadioCard.d.ts +17 -0
  26. package/dist/v2/components/RadioCard/RadioCard.d.ts.map +1 -0
  27. package/dist/v2/components/RadioCard/index.d.ts +2 -0
  28. package/dist/v2/components/RadioCard/index.d.ts.map +1 -0
  29. package/dist/v2/components/StatusBadge/StatusBadge.d.ts +8 -0
  30. package/dist/v2/components/StatusBadge/StatusBadge.d.ts.map +1 -0
  31. package/dist/v2/components/StatusBadge/index.d.ts +3 -0
  32. package/dist/v2/components/StatusBadge/index.d.ts.map +1 -0
  33. package/dist/v2/components/StepIndicator/StepIndicator.d.ts +9 -0
  34. package/dist/v2/components/StepIndicator/StepIndicator.d.ts.map +1 -0
  35. package/dist/v2/components/StepIndicator/index.d.ts +2 -0
  36. package/dist/v2/components/StepIndicator/index.d.ts.map +1 -0
  37. package/dist/v2/components/TableControls/TableControls.d.ts +28 -0
  38. package/dist/v2/components/TableControls/TableControls.d.ts.map +1 -0
  39. package/dist/v2/components/TableControls/index.d.ts +3 -0
  40. package/dist/v2/components/TableControls/index.d.ts.map +1 -0
  41. package/dist/v2/components/Tabs/Tabs.d.ts +15 -0
  42. package/dist/v2/components/Tabs/Tabs.d.ts.map +1 -0
  43. package/dist/v2/components/Tabs/index.d.ts +3 -0
  44. package/dist/v2/components/Tabs/index.d.ts.map +1 -0
  45. package/dist/v2/icons/index.d.ts +42 -0
  46. package/dist/v2/icons/index.d.ts.map +1 -1
  47. package/dist/v2/index.d.ts +18 -0
  48. package/dist/v2/index.d.ts.map +1 -1
  49. package/dist/v2/pages/CreateUser/CreateUserPage.d.ts +110 -0
  50. package/dist/v2/pages/CreateUser/CreateUserPage.d.ts.map +1 -0
  51. package/dist/v2/pages/CreateUser/index.d.ts +3 -0
  52. package/dist/v2/pages/CreateUser/index.d.ts.map +1 -0
  53. package/dist/v2/pages/RoleSelection/RoleSelectionPage.d.ts +26 -0
  54. package/dist/v2/pages/RoleSelection/RoleSelectionPage.d.ts.map +1 -0
  55. package/dist/v2/pages/RoleSelection/index.d.ts +3 -0
  56. package/dist/v2/pages/RoleSelection/index.d.ts.map +1 -0
  57. package/dist/v2/pages/UserDetails/UserDetailsPage.d.ts +37 -0
  58. package/dist/v2/pages/UserDetails/UserDetailsPage.d.ts.map +1 -0
  59. package/dist/v2/pages/UserDetails/index.d.ts +3 -0
  60. package/dist/v2/pages/UserDetails/index.d.ts.map +1 -0
  61. package/dist/v2/pages/auth/CreatePassword/CreatePasswordPage.d.ts.map +1 -1
  62. package/dist/v2/pages/auth/Login/LoginPage.d.ts.map +1 -1
  63. package/dist/v2/pages/auth/ResetPassword/ResetPasswordPage.d.ts.map +1 -1
  64. package/dist/v2/styles/components/Button.scss +27 -0
  65. package/package.json +2 -2
  66. package/src/styles/themes/bookedit/_fonts.scss +2 -0
  67. package/src/v2/components/ActionMenu/ActionMenu.tsx +4 -2
  68. package/src/v2/components/AvatarUpload/AvatarUpload.scss +68 -0
  69. package/src/v2/components/AvatarUpload/AvatarUpload.stories.tsx +83 -0
  70. package/src/v2/components/AvatarUpload/AvatarUpload.tsx +69 -0
  71. package/src/v2/components/AvatarUpload/index.ts +1 -0
  72. package/src/v2/components/Button/Button.tsx +1 -0
  73. package/src/v2/components/DataTable/DataTable.scss +181 -0
  74. package/src/v2/components/DataTable/DataTable.tsx +256 -0
  75. package/src/v2/components/DataTable/index.ts +7 -0
  76. package/src/v2/components/EmptyState/EmptyState.scss +39 -0
  77. package/src/v2/components/EmptyState/EmptyState.stories.tsx +45 -0
  78. package/src/v2/components/EmptyState/EmptyState.tsx +37 -0
  79. package/src/v2/components/EmptyState/index.ts +2 -0
  80. package/src/v2/components/FormField/FormField.scss +12 -0
  81. package/src/v2/components/InfoGrid/InfoGrid.scss +51 -0
  82. package/src/v2/components/InfoGrid/InfoGrid.stories.tsx +76 -0
  83. package/src/v2/components/InfoGrid/InfoGrid.tsx +28 -0
  84. package/src/v2/components/InfoGrid/index.ts +1 -0
  85. package/src/v2/components/NewTable/NewTable.scss +4 -4
  86. package/src/v2/components/RadioCard/RadioCard.scss +76 -0
  87. package/src/v2/components/RadioCard/RadioCard.stories.tsx +115 -0
  88. package/src/v2/components/RadioCard/RadioCard.tsx +68 -0
  89. package/src/v2/components/RadioCard/index.ts +1 -0
  90. package/src/v2/components/StatusBadge/StatusBadge.scss +53 -0
  91. package/src/v2/components/StatusBadge/StatusBadge.tsx +31 -0
  92. package/src/v2/components/StatusBadge/index.ts +2 -0
  93. package/src/v2/components/StepIndicator/StepIndicator.scss +62 -0
  94. package/src/v2/components/StepIndicator/StepIndicator.stories.tsx +37 -0
  95. package/src/v2/components/StepIndicator/StepIndicator.tsx +41 -0
  96. package/src/v2/components/StepIndicator/index.ts +1 -0
  97. package/src/v2/components/TableControls/TableControls.scss +63 -0
  98. package/src/v2/components/TableControls/TableControls.tsx +110 -0
  99. package/src/v2/components/TableControls/index.ts +7 -0
  100. package/src/v2/components/Tabs/Tabs.scss +36 -0
  101. package/src/v2/components/Tabs/Tabs.stories.tsx +75 -0
  102. package/src/v2/components/Tabs/Tabs.tsx +52 -0
  103. package/src/v2/components/Tabs/index.ts +2 -0
  104. package/src/v2/icons/index.tsx +219 -0
  105. package/src/v2/index.ts +98 -0
  106. package/src/v2/pages/CreateUser/CreateUserPage.scss +760 -0
  107. package/src/v2/pages/CreateUser/CreateUserPage.stories.tsx +157 -0
  108. package/src/v2/pages/CreateUser/CreateUserPage.tsx +1062 -0
  109. package/src/v2/pages/CreateUser/index.ts +13 -0
  110. package/src/v2/pages/RoleSelection/RoleSelectionPage.scss +193 -0
  111. package/src/v2/pages/RoleSelection/RoleSelectionPage.stories.tsx +112 -0
  112. package/src/v2/pages/RoleSelection/RoleSelectionPage.tsx +127 -0
  113. package/src/v2/pages/RoleSelection/index.ts +2 -0
  114. package/src/v2/pages/UserDetails/UserDetailsPage.scss +236 -0
  115. package/src/v2/pages/UserDetails/UserDetailsPage.stories.tsx +84 -0
  116. package/src/v2/pages/UserDetails/UserDetailsPage.tsx +210 -0
  117. package/src/v2/pages/UserDetails/index.ts +2 -0
  118. package/src/v2/pages/auth/AuthLayout/AuthLayout.scss +8 -6
  119. package/src/v2/pages/auth/CreatePassword/CreatePasswordPage.tsx +1 -3
  120. package/src/v2/pages/auth/Login/LoginPage.tsx +1 -3
  121. package/src/v2/pages/auth/ResetPassword/ResetPasswordPage.scss +2 -0
  122. package/src/v2/pages/auth/ResetPassword/ResetPasswordPage.tsx +1 -2
  123. package/src/v2/styles/components/Button.scss +27 -0
@@ -0,0 +1,1062 @@
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { Button } from '../../components/Button'
3
+ import { FormField } from '../../components/FormField'
4
+ import { Select } from '../../components/Select'
5
+ import { Checkbox } from '../../components/Checkbox'
6
+ import { UserAvatar } from '../../components/UserAvatar'
7
+ import { ArrowLeftIcon, SearchIcon } from '../../icons'
8
+ import './CreateUserPage.scss'
9
+
10
+ // ─── Types ───────────────────────────────────────────────────────────────────
11
+
12
+ export type GlobalRole = 'super_admin' | 'system_admin'
13
+ /** Internal union used for the role-selection step in admin-panel mode */
14
+ type UserTypeSelection = GlobalRole | 'admin'
15
+ export type ProviderRole = 'admin' | 'manager' | 'operations' | 'staff' | 'viewer' | 'desk'
16
+
17
+ export interface ExistingUser {
18
+ id: string
19
+ fullName: string
20
+ email: string
21
+ userNumber?: number
22
+ assignments?: UserAssignment[]
23
+ }
24
+
25
+ export interface UserAssignment {
26
+ id: string
27
+ name: string
28
+ type: 'company' | 'venue' | 'promoter'
29
+ avatarUrl?: string
30
+ role?: string
31
+ friendlyId: string
32
+ }
33
+
34
+ export interface EntityResult {
35
+ id: string
36
+ name: string
37
+ type: 'company' | 'venue' | 'promoter'
38
+ avatarUrl?: string
39
+ friendlyId: string
40
+ }
41
+
42
+ export interface VenueAssignment {
43
+ id: string
44
+ name: string
45
+ type: 'venue' | 'promoter'
46
+ avatarUrl?: string
47
+ friendlyId: string
48
+ role: ProviderRole
49
+ selected: boolean
50
+ }
51
+
52
+ export interface ProviderAssignment {
53
+ id: string
54
+ name: string
55
+ type: 'company' | 'venue' | 'promoter'
56
+ avatarUrl?: string
57
+ friendlyId: string
58
+ role: ProviderRole
59
+ parentCompanyId?: string
60
+ }
61
+
62
+ export interface NestedProvider {
63
+ id: string
64
+ name: string
65
+ type: 'venue' | 'promoter'
66
+ avatarUrl?: string
67
+ friendlyId: string
68
+ selected: boolean
69
+ }
70
+
71
+ export interface InviteData {
72
+ firstName: string
73
+ lastName: string
74
+ email: string
75
+ /** Set when mode is admin-panel and role is super_admin or system_admin */
76
+ globalRole?: GlobalRole
77
+ /** Set for all provider-scoped invitations */
78
+ providerRole?: ProviderRole
79
+ /** Set for admin-panel provider user or company-team modes */
80
+ providers?: Array<{
81
+ id: string
82
+ type: 'company' | 'venue' | 'promoter'
83
+ role: ProviderRole
84
+ parentCompanyId?: string
85
+ }>
86
+ }
87
+
88
+ export interface CreateUserPageProps {
89
+ /**
90
+ * Which context the wizard is opened from:
91
+ * - `admin-panel` → /admin/users/add — global user creation, 5-step flow
92
+ * - `provider-team` → /venue|promoter/{id}/settings/team-settings/add — 3-step flow
93
+ * - `company-team` → /company/{id}/settings/team/add — 4-step flow
94
+ */
95
+ mode: 'admin-panel' | 'provider-team' | 'company-team'
96
+
97
+ /** Required for provider-team and company-team modes */
98
+ entityContext?: {
99
+ id: string
100
+ name: string
101
+ type: 'venue' | 'promoter' | 'company'
102
+ avatarUrl?: string
103
+ }
104
+
105
+ /** Check whether the email belongs to an existing user. Return null if not found. */
106
+ onCheckEmail: (email: string) => Promise<ExistingUser | null>
107
+
108
+ /**
109
+ * admin-panel only: search for companies / venues / promoters.
110
+ * Return filtered results (already-added entities may be excluded by the consumer).
111
+ */
112
+ onSearchEntities?: (query: string) => Promise<EntityResult[]>
113
+
114
+ /** admin-panel only: fetch the nested providers for a selected company */
115
+ onFetchCompanyProviders?: (companyId: string) => Promise<EntityResult[]>
116
+
117
+ /**
118
+ * company-team only: fetch the list of venues/providers belonging to this company.
119
+ * Called once on mount.
120
+ */
121
+ onFetchCompanyVenues?: () => Promise<VenueAssignment[]>
122
+
123
+ /** Called on the final submit with all collected data */
124
+ onInviteUser: (data: InviteData) => Promise<void>
125
+
126
+ /**
127
+ * admin-panel only: navigate to an existing user's detail page.
128
+ * The wizard ends here for existing users in admin-panel mode.
129
+ */
130
+ onViewUser?: (userId: string) => void
131
+
132
+ onCancel: () => void
133
+ isLoading?: boolean
134
+ error?: string
135
+ }
136
+
137
+ // ─── Step type ────────────────────────────────────────────────────────────────
138
+
139
+ type Step =
140
+ | 'email'
141
+ | 'existing-user' // admin-panel: show card → View User (flow ends)
142
+ | 'name' // new user name entry (provider-team / company-team: existing users also land here with name pre-filled)
143
+ | 'role' // admin-panel: global role; provider-team: provider role
144
+ | 'providers' // admin-panel, provider-user path: search + select entities
145
+ | 'set-roles' // admin-panel, provider-user path: assign role per entity
146
+ | 'company-access' // company-team: company toggle + venue selection
147
+ | 'company-roles' // company-team: set role per selected entity
148
+
149
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
150
+
151
+ function formatRole(role: string) {
152
+ return role.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
153
+ }
154
+
155
+ function getInitials(name: string) {
156
+ return name
157
+ .split(' ')
158
+ .map((w) => w[0])
159
+ .join('')
160
+ .slice(0, 2)
161
+ .toUpperCase()
162
+ }
163
+
164
+ const PROVIDER_ROLES: ProviderRole[] = ['admin', 'manager', 'operations', 'staff', 'viewer', 'desk']
165
+
166
+ const PROVIDER_ROLE_DESCRIPTIONS: Record<ProviderRole, string> = {
167
+ admin: 'Full access to provider settings and team management.',
168
+ manager: 'Can manage bookings and view reports.',
169
+ operations: 'Can edit bookings and customer information.',
170
+ staff: 'Can view and process bookings.',
171
+ viewer: 'Read-only access to provider data.',
172
+ desk: 'Front desk access for customer check-in.',
173
+ }
174
+
175
+ // ─── Sub-components ───────────────────────────────────────────────────────────
176
+
177
+ function EntityAvatar({ name, avatarUrl }: { name: string; avatarUrl?: string }) {
178
+ return (
179
+ <div className="cu-entity-avatar">
180
+ {avatarUrl ? (
181
+ <img src={avatarUrl} alt={name} className="cu-entity-avatar__img" />
182
+ ) : (
183
+ <span className="cu-entity-avatar__initials">{name.charAt(0).toUpperCase()}</span>
184
+ )}
185
+ </div>
186
+ )
187
+ }
188
+
189
+ function EntityIdBadge({ type, friendlyId }: { type: string; friendlyId: string }) {
190
+ const label = type === 'company' ? 'Company' : type === 'venue' ? 'Venue' : 'Promoter'
191
+ return (
192
+ <span className="cu-entity-id-badge">
193
+ {label} ID: {friendlyId}
194
+ </span>
195
+ )
196
+ }
197
+
198
+ function UserCard({ user }: { user: ExistingUser }) {
199
+ return (
200
+ <div className="cu-user-card">
201
+ <UserAvatar initials={getInitials(user.fullName)} size="md" />
202
+ <div className="cu-user-card__info">
203
+ <span className="cu-user-card__name">{user.fullName}</span>
204
+ <span className="cu-user-card__email">{user.email}</span>
205
+ </div>
206
+ {user.userNumber && (
207
+ <span className="cu-user-card__number">User #{user.userNumber}</span>
208
+ )}
209
+ </div>
210
+ )
211
+ }
212
+
213
+ function ExistingAssignments({ assignments }: { assignments: UserAssignment[] }) {
214
+ if (!assignments || assignments.length === 0) return null
215
+ return (
216
+ <div className="cu-assignments">
217
+ <span className="cu-assignments__label">Currently assigned to</span>
218
+ <div className="cu-assignments__list">
219
+ {assignments.map((a) => (
220
+ <div key={a.id} className="cu-assignments__item">
221
+ <EntityAvatar name={a.name} avatarUrl={a.avatarUrl} />
222
+ <div className="cu-assignments__item-info">
223
+ <span className="cu-assignments__item-name">{a.name}</span>
224
+ <EntityIdBadge type={a.type} friendlyId={a.friendlyId} />
225
+ </div>
226
+ {a.role && (
227
+ <span className="cu-assignments__item-role">{formatRole(a.role)}</span>
228
+ )}
229
+ </div>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ )
234
+ }
235
+
236
+ // ─── Main Component ───────────────────────────────────────────────────────────
237
+
238
+ export const CreateUserPage: React.FC<CreateUserPageProps> = ({
239
+ mode,
240
+ entityContext,
241
+ onCheckEmail,
242
+ onSearchEntities,
243
+ onFetchCompanyProviders,
244
+ onFetchCompanyVenues,
245
+ onInviteUser,
246
+ onViewUser,
247
+ onCancel,
248
+ isLoading: externalLoading = false,
249
+ error: externalError,
250
+ }) => {
251
+ const [step, setStep] = useState<Step>('email')
252
+ const [isLoading, setIsLoading] = useState(false)
253
+ const [error, setError] = useState<string | undefined>(externalError)
254
+
255
+ // ── Form state ──
256
+ const [email, setEmail] = useState('')
257
+ const [firstName, setFirstName] = useState('')
258
+ const [lastName, setLastName] = useState('')
259
+ const [existingUser, setExistingUser] = useState<ExistingUser | null>(null)
260
+ const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
261
+
262
+ // ── Role state (admin-panel) ──
263
+ const [selectedUserType, setSelectedUserType] = useState<UserTypeSelection>('super_admin')
264
+
265
+ // ── Role state (provider-team) ──
266
+ const [providerRole, setProviderRole] = useState<ProviderRole>('admin')
267
+
268
+ // ── Entity search state (admin-panel, provider-user path) ──
269
+ const [searchQuery, setSearchQuery] = useState('')
270
+ const [searchResults, setSearchResults] = useState<EntityResult[]>([])
271
+ const [isSearching, setIsSearching] = useState(false)
272
+ const [selectedAssignments, setSelectedAssignments] = useState<ProviderAssignment[]>([])
273
+ const [nestedProviders, setNestedProviders] = useState<Record<string, NestedProvider[]>>({})
274
+ const [companyAccessStates, setCompanyAccessStates] = useState<Record<string, boolean>>({})
275
+
276
+ // ── Company-team venue state ──
277
+ const [companyAccessEnabled, setCompanyAccessEnabled] = useState(true)
278
+ const [companyRole, setCompanyRole] = useState<ProviderRole>('admin')
279
+ const [venues, setVenues] = useState<VenueAssignment[]>([])
280
+
281
+ const searchTimeout = useRef<ReturnType<typeof setTimeout>>()
282
+ const loading = isLoading || externalLoading
283
+
284
+ // ── Fetch company venues on mount (company-team mode) ──
285
+ useEffect(() => {
286
+ if (mode !== 'company-team' || !onFetchCompanyVenues) return
287
+ onFetchCompanyVenues().then(setVenues).catch(() => {/* silent */})
288
+ }, [mode])
289
+
290
+ // ── Search debounce (admin-panel providers step) ──
291
+ useEffect(() => {
292
+ if (step !== 'providers' || !searchQuery.trim()) {
293
+ setSearchResults([])
294
+ return undefined
295
+ }
296
+ clearTimeout(searchTimeout.current)
297
+ searchTimeout.current = setTimeout(() => {
298
+ if (!onSearchEntities) return
299
+ setIsSearching(true)
300
+ onSearchEntities(searchQuery)
301
+ .then((results) => {
302
+ const addedCompanyIds = new Set(
303
+ selectedAssignments.filter((a) => a.type === 'company').map((a) => a.id)
304
+ )
305
+ setSearchResults(
306
+ results.filter((r) => {
307
+ if (selectedAssignments.some((a) => a.id === r.id && !a.parentCompanyId)) return false
308
+ if (r.type !== 'company' && addedCompanyIds.has((r as any).parentCompanyId)) return false
309
+ return true
310
+ })
311
+ )
312
+ })
313
+ .catch(() => {/* silent */})
314
+ .finally(() => setIsSearching(false))
315
+ }, 300)
316
+ return () => clearTimeout(searchTimeout.current)
317
+ }, [searchQuery, step])
318
+
319
+ // ── Validation ──
320
+ function validateEmail() {
321
+ if (!email.trim()) { setFieldErrors({ email: 'Email is required.' }); return false }
322
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { setFieldErrors({ email: 'Please enter a valid email address.' }); return false }
323
+ setFieldErrors({})
324
+ return true
325
+ }
326
+
327
+ function validateName() {
328
+ const errs: Record<string, string> = {}
329
+ if (!firstName.trim()) errs.firstName = 'First name is required.'
330
+ if (!lastName.trim()) errs.lastName = 'Last name is required.'
331
+ setFieldErrors(errs)
332
+ return Object.keys(errs).length === 0
333
+ }
334
+
335
+ // ── Step handlers ──
336
+ async function handleEmailNext() {
337
+ if (!validateEmail()) return
338
+ setIsLoading(true)
339
+ setError(undefined)
340
+ try {
341
+ const found = await onCheckEmail(email.trim())
342
+ if (found) {
343
+ setExistingUser(found)
344
+ // In provider-team / company-team: pre-fill name, go to name/role step
345
+ // In admin-panel: go to existing-user card (flow ends with View User)
346
+ if (mode === 'admin-panel') {
347
+ setStep('existing-user')
348
+ } else {
349
+ const nameParts = (found.fullName || '').split(' ')
350
+ setFirstName(nameParts[0] || '')
351
+ setLastName(nameParts.slice(1).join(' ') || '')
352
+ setStep('name')
353
+ }
354
+ } else {
355
+ setExistingUser(null)
356
+ setStep('name')
357
+ }
358
+ } catch (e: any) {
359
+ setError(e?.message || 'Failed to check email.')
360
+ } finally {
361
+ setIsLoading(false)
362
+ }
363
+ }
364
+
365
+ function handleNameNext() {
366
+ if (!validateName()) return
367
+ if (mode === 'company-team') {
368
+ setStep('company-access')
369
+ } else {
370
+ setStep('role')
371
+ }
372
+ }
373
+
374
+ function handleRoleNext() {
375
+ // admin-panel: if Provider User selected, go to entity search
376
+ if (mode === 'admin-panel' && selectedUserType === 'admin') {
377
+ setStep('providers')
378
+ } else {
379
+ handleSubmit()
380
+ }
381
+ }
382
+
383
+ function handleProvidersNext() {
384
+ const topLevel = selectedAssignments.filter((a) => !a.parentCompanyId)
385
+ if (topLevel.length === 0) {
386
+ setFieldErrors({ providers: 'Please add at least one company or provider.' })
387
+ return
388
+ }
389
+ setFieldErrors({})
390
+ setStep('set-roles')
391
+ }
392
+
393
+ function handleCompanyAccessNext() {
394
+ const hasSelection = companyAccessEnabled || venues.some((v) => v.selected)
395
+ if (!hasSelection) {
396
+ setFieldErrors({ venues: 'Please select the company or at least one venue.' })
397
+ return
398
+ }
399
+ setFieldErrors({})
400
+ setStep('company-roles')
401
+ }
402
+
403
+ async function handleSubmit() {
404
+ setIsLoading(true)
405
+ setError(undefined)
406
+ try {
407
+ const data: InviteData = { firstName, lastName, email: email.trim() }
408
+
409
+ if (mode === 'admin-panel') {
410
+ if (selectedUserType === 'admin') {
411
+ data.providerRole = 'admin'
412
+ data.providers = getAdminPermissionItems().map((a) => ({
413
+ id: a.id,
414
+ type: a.type,
415
+ role: a.role,
416
+ parentCompanyId: a.parentCompanyId,
417
+ }))
418
+ } else {
419
+ data.globalRole = selectedUserType as GlobalRole
420
+ }
421
+ } else if (mode === 'provider-team') {
422
+ data.providerRole = providerRole
423
+ if (entityContext) {
424
+ data.providers = [{ id: entityContext.id, type: entityContext.type, role: providerRole }]
425
+ }
426
+ } else {
427
+ // company-team
428
+ data.providerRole = companyRole
429
+ const providers: InviteData['providers'] = []
430
+ if (companyAccessEnabled && entityContext) {
431
+ providers.push({ id: entityContext.id, type: 'company', role: companyRole })
432
+ }
433
+ venues.filter((v) => v.selected).forEach((v) => {
434
+ providers.push({ id: v.id, type: v.type, role: v.role, parentCompanyId: entityContext?.id })
435
+ })
436
+ data.providers = providers
437
+ }
438
+
439
+ await onInviteUser(data)
440
+ } catch (e: any) {
441
+ setError(e?.message || 'Failed to invite user.')
442
+ } finally {
443
+ setIsLoading(false)
444
+ }
445
+ }
446
+
447
+ // ── Entity helpers (admin-panel) ──
448
+ async function handleSelectEntity(entity: EntityResult) {
449
+ if (selectedAssignments.some((a) => a.id === entity.id && !a.parentCompanyId)) return
450
+ setFieldErrors((p) => ({ ...p, providers: '' }))
451
+
452
+ setSelectedAssignments((prev) => [
453
+ ...prev,
454
+ { id: entity.id, name: entity.name, type: entity.type, avatarUrl: entity.avatarUrl, friendlyId: entity.friendlyId, role: 'admin' },
455
+ ])
456
+
457
+ if (entity.type === 'company') {
458
+ setCompanyAccessStates((p) => ({ ...p, [entity.id]: true }))
459
+ if (onFetchCompanyProviders) {
460
+ const providers = await onFetchCompanyProviders(entity.id)
461
+ const nested: NestedProvider[] = providers.map((p) => ({
462
+ id: p.id, name: p.name, type: p.type as 'venue' | 'promoter',
463
+ avatarUrl: p.avatarUrl, friendlyId: p.friendlyId, selected: true,
464
+ }))
465
+ setNestedProviders((p) => ({ ...p, [entity.id]: nested }))
466
+ setSelectedAssignments((prev) => [
467
+ ...prev,
468
+ ...nested.map((p) => ({ id: p.id, name: p.name, type: p.type, avatarUrl: p.avatarUrl, friendlyId: p.friendlyId, role: 'admin' as ProviderRole, parentCompanyId: entity.id })),
469
+ ])
470
+ }
471
+ }
472
+ setSearchQuery('')
473
+ setSearchResults([])
474
+ }
475
+
476
+ function handleRemoveAssignment(id: string) {
477
+ const entity = selectedAssignments.find((a) => a.id === id)
478
+ if (entity?.type === 'company') {
479
+ setSelectedAssignments((p) => p.filter((a) => a.id !== id && a.parentCompanyId !== id))
480
+ setNestedProviders((p) => { const n = { ...p }; delete n[id]; return n })
481
+ setCompanyAccessStates((p) => { const n = { ...p }; delete n[id]; return n })
482
+ } else {
483
+ setSelectedAssignments((p) => p.filter((a) => a.id !== id))
484
+ }
485
+ }
486
+
487
+ function handleToggleNested(companyId: string, providerId: string) {
488
+ setNestedProviders((prev) => ({
489
+ ...prev,
490
+ [companyId]: (prev[companyId] || []).map((p) =>
491
+ p.id === providerId ? { ...p, selected: !p.selected } : p
492
+ ),
493
+ }))
494
+ const existing = selectedAssignments.find((a) => a.id === providerId && a.parentCompanyId === companyId)
495
+ if (existing) {
496
+ setSelectedAssignments((p) => p.filter((a) => !(a.id === providerId && a.parentCompanyId === companyId)))
497
+ } else {
498
+ const nested = nestedProviders[companyId]?.find((p) => p.id === providerId)
499
+ if (nested) {
500
+ setSelectedAssignments((p) => [
501
+ ...p,
502
+ { id: nested.id, name: nested.name, type: nested.type, avatarUrl: nested.avatarUrl, friendlyId: nested.friendlyId, role: 'admin', parentCompanyId: companyId },
503
+ ])
504
+ }
505
+ }
506
+ }
507
+
508
+ function handleSelectAllNested(companyId: string) {
509
+ const nested = nestedProviders[companyId] || []
510
+ setNestedProviders((p) => ({ ...p, [companyId]: nested.map((n) => ({ ...n, selected: true })) }))
511
+ const toAdd = nested.filter((n) => !selectedAssignments.some((a) => a.id === n.id && a.parentCompanyId === companyId))
512
+ if (toAdd.length > 0) {
513
+ setSelectedAssignments((p) => [
514
+ ...p,
515
+ ...toAdd.map((n) => ({ id: n.id, name: n.name, type: n.type, avatarUrl: n.avatarUrl, friendlyId: n.friendlyId, role: 'admin' as ProviderRole, parentCompanyId: companyId })),
516
+ ])
517
+ }
518
+ }
519
+
520
+ function handleClearAllNested(companyId: string) {
521
+ setNestedProviders((p) => ({ ...p, [companyId]: (p[companyId] || []).map((n) => ({ ...n, selected: false })) }))
522
+ setSelectedAssignments((p) => p.filter((a) => a.parentCompanyId !== companyId))
523
+ }
524
+
525
+ function handleAssignmentRoleChange(id: string, role: ProviderRole) {
526
+ setSelectedAssignments((p) => p.map((a) => (a.id === id ? { ...a, role } : a)))
527
+ }
528
+
529
+ function getAdminPermissionItems(): ProviderAssignment[] {
530
+ return selectedAssignments.filter((a) => {
531
+ if (a.type === 'company' && !companyAccessStates[a.id]) return false
532
+ if (a.parentCompanyId) return nestedProviders[a.parentCompanyId]?.find((n) => n.id === a.id)?.selected ?? false
533
+ return true
534
+ })
535
+ }
536
+
537
+ // ── Back map ──
538
+ function getBackStep(): Step {
539
+ const backMap: Record<Step, Step> = {
540
+ email: 'email',
541
+ 'existing-user': 'email',
542
+ name: 'email',
543
+ role: 'name',
544
+ providers: 'role',
545
+ 'set-roles': 'providers',
546
+ 'company-access': 'name',
547
+ 'company-roles': 'company-access',
548
+ }
549
+ return backMap[step]
550
+ }
551
+
552
+ const showBack = step !== 'email'
553
+ const userName = existingUser ? existingUser.fullName.split(' ')[0] : firstName
554
+
555
+ // ── Venue helpers (company-team) ──
556
+ const selectedVenueCount = venues.filter((v) => v.selected).length
557
+ const allVenuesSelected = venues.length > 0 && selectedVenueCount === venues.length
558
+
559
+ return (
560
+ <div className="create-user-page">
561
+ <div className="create-user-page__header">
562
+ <h1 className="create-user-page__title">Add User</h1>
563
+ <Button variant="destructive-soft" onClick={onCancel} disabled={loading}>
564
+ Cancel
565
+ </Button>
566
+ </div>
567
+
568
+ <div className="create-user-page__body">
569
+ {showBack && (
570
+ <button
571
+ className="create-user-page__back"
572
+ onClick={() => setStep(getBackStep())}
573
+ disabled={loading}
574
+ type="button"
575
+ >
576
+ <ArrowLeftIcon />
577
+ Back
578
+ </button>
579
+ )}
580
+
581
+ {(error || externalError) && (
582
+ <div className="create-user-page__error-banner">{error || externalError}</div>
583
+ )}
584
+
585
+ {/* ── Email ──────────────────────────────────────────────────────── */}
586
+ {step === 'email' && (
587
+ <div className="create-user-page__step">
588
+ <h2 className="create-user-page__step-title">Enter Email Address</h2>
589
+ <div className="create-user-page__fields">
590
+ <FormField
591
+ label="Email"
592
+ type="email"
593
+ value={email}
594
+ onChange={(e) => { setEmail(e.target.value); if (fieldErrors.email) setFieldErrors({}) }}
595
+ error={fieldErrors.email}
596
+ autoComplete="email"
597
+ disabled={loading}
598
+ onKeyDown={(e) => e.key === 'Enter' && handleEmailNext()}
599
+ />
600
+ <div className="create-user-page__actions">
601
+ <Button onClick={handleEmailNext} isLoading={loading} disabled={loading}>Next</Button>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ )}
606
+
607
+ {/* ── Existing user (admin-panel only — flow ends here) ──────────── */}
608
+ {step === 'existing-user' && existingUser && (
609
+ <div className="create-user-page__step">
610
+ <div>
611
+ <h2 className="create-user-page__step-title">Existing User Found</h2>
612
+ <p className="create-user-page__step-subtitle">
613
+ A user with this email already exists in the system.
614
+ </p>
615
+ </div>
616
+ <UserCard user={existingUser} />
617
+ {existingUser.assignments && existingUser.assignments.length > 0 && (
618
+ <ExistingAssignments assignments={existingUser.assignments} />
619
+ )}
620
+ <div className="create-user-page__actions">
621
+ <Button onClick={() => onViewUser?.(existingUser.id)}>View User</Button>
622
+ </div>
623
+ </div>
624
+ )}
625
+
626
+ {/* ── Name (new user) / User card (existing, provider-team / company-team) ─ */}
627
+ {step === 'name' && (
628
+ <div className="create-user-page__step">
629
+ {existingUser ? (
630
+ <>
631
+ <div>
632
+ <h2 className="create-user-page__step-title">Existing User Found</h2>
633
+ <p className="create-user-page__step-subtitle">
634
+ This user already exists. You can add them to this{' '}
635
+ {mode === 'company-team' ? 'company' : 'team'}.
636
+ </p>
637
+ </div>
638
+ <UserCard user={existingUser} />
639
+ {existingUser.assignments && existingUser.assignments.length > 0 && (
640
+ <ExistingAssignments assignments={existingUser.assignments} />
641
+ )}
642
+ <div className="create-user-page__actions">
643
+ <Button onClick={handleNameNext} disabled={loading}>Next</Button>
644
+ </div>
645
+ </>
646
+ ) : (
647
+ <>
648
+ <h2 className="create-user-page__step-title">User Details</h2>
649
+ <div className="create-user-page__fields">
650
+ <FormField
651
+ label="First Name"
652
+ value={firstName}
653
+ onChange={(e) => { setFirstName(e.target.value); if (fieldErrors.firstName) setFieldErrors((p) => ({ ...p, firstName: '' })) }}
654
+ error={fieldErrors.firstName}
655
+ disabled={loading}
656
+ />
657
+ <FormField
658
+ label="Last Name"
659
+ value={lastName}
660
+ onChange={(e) => { setLastName(e.target.value); if (fieldErrors.lastName) setFieldErrors((p) => ({ ...p, lastName: '' })) }}
661
+ error={fieldErrors.lastName}
662
+ disabled={loading}
663
+ />
664
+ <div className="create-user-page__actions">
665
+ <Button onClick={handleNameNext} disabled={loading}>Next</Button>
666
+ </div>
667
+ </div>
668
+ </>
669
+ )}
670
+ </div>
671
+ )}
672
+
673
+ {/* ── Role selection ─────────────────────────────────────────────── */}
674
+ {step === 'role' && (
675
+ <div className="create-user-page__step">
676
+ <div>
677
+ <h2 className="create-user-page__step-title">
678
+ {mode === 'admin-panel'
679
+ ? `What kind of user is ${userName}?`
680
+ : `Select a role for ${userName}`}
681
+ </h2>
682
+ {mode === 'provider-team' && (
683
+ <p className="create-user-page__step-subtitle">
684
+ Permissions can be customised after the user is created.
685
+ </p>
686
+ )}
687
+ </div>
688
+
689
+ {mode === 'admin-panel' ? (
690
+ // Global role options
691
+ <div className="create-user-page__role-cards">
692
+ {([
693
+ { value: 'super_admin' as UserTypeSelection, label: 'Super Admin', description: 'Unrestricted permissions across the entire platform.' },
694
+ { value: 'system_admin' as UserTypeSelection, label: 'System Admin', description: 'Admin access and full dashboard access for all providers.' },
695
+ { value: 'admin' as UserTypeSelection, label: 'Provider User', description: 'Permission levels set per company or provider.' },
696
+ ]).map((opt) => (
697
+ <label
698
+ key={opt.value}
699
+ className={`create-user-page__role-card ${selectedUserType === opt.value ? 'create-user-page__role-card--selected' : ''}`}
700
+ >
701
+ <input
702
+ type="radio"
703
+ name="userType"
704
+ value={opt.value}
705
+ checked={selectedUserType === opt.value}
706
+ onChange={() => setSelectedUserType(opt.value)}
707
+ className="create-user-page__radio"
708
+ />
709
+ <div className="create-user-page__role-card-content">
710
+ <span className="create-user-page__role-card-label">{opt.label}</span>
711
+ <span className="create-user-page__role-card-desc">{opt.description}</span>
712
+ </div>
713
+ </label>
714
+ ))}
715
+ </div>
716
+ ) : (
717
+ // Provider role options (provider-team)
718
+ <div className="create-user-page__role-cards">
719
+ {PROVIDER_ROLES.map((role) => (
720
+ <label
721
+ key={role}
722
+ className={`create-user-page__role-card ${providerRole === role ? 'create-user-page__role-card--selected' : ''}`}
723
+ >
724
+ <input
725
+ type="radio"
726
+ name="providerRole"
727
+ value={role}
728
+ checked={providerRole === role}
729
+ onChange={() => setProviderRole(role)}
730
+ className="create-user-page__radio"
731
+ />
732
+ <div className="create-user-page__role-card-content">
733
+ <span className="create-user-page__role-card-label">{formatRole(role)}</span>
734
+ <span className="create-user-page__role-card-desc">{PROVIDER_ROLE_DESCRIPTIONS[role]}</span>
735
+ </div>
736
+ </label>
737
+ ))}
738
+ </div>
739
+ )}
740
+
741
+ <div className="create-user-page__actions">
742
+ <Button onClick={handleRoleNext} isLoading={loading} disabled={loading}>
743
+ {mode === 'admin-panel' && selectedUserType === 'admin'
744
+ ? 'Next'
745
+ : existingUser ? 'Add User' : 'Send Invitation'}
746
+ </Button>
747
+ </div>
748
+ </div>
749
+ )}
750
+
751
+ {/* ── Providers search (admin-panel, provider-user path) ─────────── */}
752
+ {step === 'providers' && (
753
+ <div className="create-user-page__step">
754
+ <h2 className="create-user-page__step-title">
755
+ Add {userName} to an existing team?
756
+ </h2>
757
+
758
+ <div className="create-user-page__search-wrapper">
759
+ <label className="create-user-page__search-label">
760
+ Search for a Company, Venue or Promoter
761
+ </label>
762
+ <div className="create-user-page__search-input-row">
763
+ <SearchIcon />
764
+ <input
765
+ className="create-user-page__search-input"
766
+ value={searchQuery}
767
+ onChange={(e) => setSearchQuery(e.target.value)}
768
+ placeholder="Search..."
769
+ type="text"
770
+ />
771
+ </div>
772
+
773
+ {searchQuery.trim() && !isSearching && searchResults.length === 0 && (
774
+ <div className="create-user-page__search-empty">
775
+ No company or provider found matching that search.
776
+ </div>
777
+ )}
778
+
779
+ {searchResults.length > 0 && (
780
+ <div className="create-user-page__search-results">
781
+ {searchResults.map((r) => (
782
+ <button
783
+ key={r.id}
784
+ type="button"
785
+ className="create-user-page__search-result"
786
+ onClick={() => handleSelectEntity(r)}
787
+ >
788
+ <EntityAvatar name={r.name} avatarUrl={r.avatarUrl} />
789
+ <div>
790
+ <span className="create-user-page__search-result-name">{r.name}</span>
791
+ <EntityIdBadge type={r.type} friendlyId={r.friendlyId} />
792
+ </div>
793
+ </button>
794
+ ))}
795
+ </div>
796
+ )}
797
+
798
+ {fieldErrors.providers && (
799
+ <span className="create-user-page__field-error">{fieldErrors.providers}</span>
800
+ )}
801
+ </div>
802
+
803
+ {selectedAssignments.filter((a) => !a.parentCompanyId).length > 0 && (
804
+ <div className="create-user-page__selected">
805
+ <span className="create-user-page__selected-label">Adding to:</span>
806
+ {selectedAssignments.filter((a) => !a.parentCompanyId).map((assignment) => (
807
+ <div key={assignment.id}>
808
+ <div className="create-user-page__selected-item">
809
+ {assignment.type === 'company' && (
810
+ <Checkbox
811
+ checked={companyAccessStates[assignment.id] ?? true}
812
+ onChange={() => setCompanyAccessStates((p) => ({ ...p, [assignment.id]: !(p[assignment.id] ?? true) }))}
813
+ />
814
+ )}
815
+ <EntityAvatar name={assignment.name} avatarUrl={assignment.avatarUrl} />
816
+ <div className="create-user-page__selected-item-info">
817
+ <span className="create-user-page__selected-item-name">{assignment.name}</span>
818
+ <EntityIdBadge type={assignment.type} friendlyId={assignment.friendlyId} />
819
+ </div>
820
+ <button type="button" className="create-user-page__remove-btn" onClick={() => handleRemoveAssignment(assignment.id)}>
821
+ Remove
822
+ </button>
823
+ </div>
824
+
825
+ {assignment.type === 'company' && nestedProviders[assignment.id] && (
826
+ <div className="create-user-page__nested">
827
+ {(() => {
828
+ const nested = nestedProviders[assignment.id] || []
829
+ const selCount = nested.filter((n) => n.selected).length
830
+ const allSel = nested.length > 0 && selCount === nested.length
831
+ return (
832
+ <>
833
+ <div className="create-user-page__nested-controls">
834
+ <button type="button" className="create-user-page__nested-toggle"
835
+ onClick={() => allSel ? handleClearAllNested(assignment.id) : handleSelectAllNested(assignment.id)}>
836
+ {allSel ? 'Clear All' : 'Select All'}
837
+ </button>
838
+ <span className="create-user-page__nested-count">{selCount} of {nested.length} providers selected</span>
839
+ </div>
840
+ <div className="create-user-page__nested-grid">
841
+ {nested.map((p) => (
842
+ <div key={p.id}
843
+ className={`create-user-page__nested-item ${p.selected ? 'create-user-page__nested-item--selected' : ''}`}
844
+ onClick={() => handleToggleNested(assignment.id, p.id)}
845
+ >
846
+ <Checkbox checked={p.selected} onChange={() => handleToggleNested(assignment.id, p.id)} onClick={(e) => e.stopPropagation()} />
847
+ <EntityAvatar name={p.name} avatarUrl={p.avatarUrl} />
848
+ <div className="create-user-page__nested-item-info">
849
+ <span className="create-user-page__nested-item-name">{p.name}</span>
850
+ <EntityIdBadge type={p.type} friendlyId={p.friendlyId} />
851
+ </div>
852
+ </div>
853
+ ))}
854
+ </div>
855
+ </>
856
+ )
857
+ })()}
858
+ </div>
859
+ )}
860
+ </div>
861
+ ))}
862
+ </div>
863
+ )}
864
+
865
+ <div className="create-user-page__actions">
866
+ <Button onClick={handleProvidersNext} disabled={loading}>Next</Button>
867
+ </div>
868
+ </div>
869
+ )}
870
+
871
+ {/* ── Set roles (admin-panel, provider-user path) ────────────────── */}
872
+ {step === 'set-roles' && (
873
+ <div className="create-user-page__step">
874
+ <h2 className="create-user-page__step-title">Set Roles for {userName}</h2>
875
+
876
+ <div className="create-user-page__role-assignments">
877
+ {(() => {
878
+ const items = getAdminPermissionItems()
879
+ const topLevel = selectedAssignments.filter((a) => !a.parentCompanyId)
880
+
881
+ const renderCard = (a: ProviderAssignment) => (
882
+ <div key={`${a.id}-${a.parentCompanyId || 'standalone'}`} className="create-user-page__role-assignment-card">
883
+ <EntityAvatar name={a.name} avatarUrl={a.avatarUrl} />
884
+ <div className="create-user-page__role-assignment-info">
885
+ <span className="create-user-page__role-assignment-name">{a.name}</span>
886
+ <EntityIdBadge type={a.type} friendlyId={a.friendlyId} />
887
+ </div>
888
+ <div className="create-user-page__role-assignment-select">
889
+ <Select value={a.role} onChange={(e) => handleAssignmentRoleChange(a.id, e.target.value as ProviderRole)} disabled={loading}>
890
+ {PROVIDER_ROLES.map((r) => <option key={r} value={r}>{formatRole(r)}</option>)}
891
+ </Select>
892
+ </div>
893
+ </div>
894
+ )
895
+
896
+ return topLevel.map((entity) => {
897
+ if (entity.type === 'company') {
898
+ const nested = items.filter((i) => i.parentCompanyId === entity.id)
899
+ return (
900
+ <div key={entity.id} className="create-user-page__role-group">
901
+ {(companyAccessStates[entity.id] ?? true) && (
902
+ <div className="create-user-page__role-assignment-card create-user-page__role-assignment-card--company">
903
+ <EntityAvatar name={entity.name} avatarUrl={entity.avatarUrl} />
904
+ <div className="create-user-page__role-assignment-info">
905
+ <span className="create-user-page__role-assignment-name">{entity.name}</span>
906
+ <EntityIdBadge type="company" friendlyId={entity.friendlyId} />
907
+ </div>
908
+ <div className="create-user-page__role-assignment-select">
909
+ <Select value={entity.role} onChange={(e) => handleAssignmentRoleChange(entity.id, e.target.value as ProviderRole)} disabled={loading}>
910
+ {PROVIDER_ROLES.map((r) => <option key={r} value={r}>{formatRole(r)}</option>)}
911
+ </Select>
912
+ </div>
913
+ </div>
914
+ )}
915
+ {nested.map(renderCard)}
916
+ </div>
917
+ )
918
+ }
919
+ return renderCard(entity)
920
+ })
921
+ })()}
922
+ </div>
923
+
924
+ <div className="create-user-page__actions">
925
+ <Button onClick={handleSubmit} isLoading={loading} disabled={loading}>Create User</Button>
926
+ </div>
927
+ </div>
928
+ )}
929
+
930
+ {/* ── Company access + venue selection (company-team) ────────────── */}
931
+ {step === 'company-access' && (
932
+ <div className="create-user-page__step">
933
+ <h2 className="create-user-page__step-title">
934
+ Select access for {userName}
935
+ </h2>
936
+
937
+ {entityContext && (
938
+ <div className="create-user-page__company-access-toggle">
939
+ <div className="create-user-page__company-access-entity">
940
+ <EntityAvatar name={entityContext.name} avatarUrl={entityContext.avatarUrl} />
941
+ <div className="create-user-page__company-access-info">
942
+ <span className="create-user-page__company-access-name">{entityContext.name}</span>
943
+ <EntityIdBadge type={entityContext.type} friendlyId={entityContext.id} />
944
+ </div>
945
+ </div>
946
+ <label className="create-user-page__toggle-label">
947
+ <input
948
+ type="checkbox"
949
+ checked={companyAccessEnabled}
950
+ onChange={(e) => setCompanyAccessEnabled(e.target.checked)}
951
+ className="create-user-page__toggle-input"
952
+ />
953
+ <span className="create-user-page__toggle-track" />
954
+ <span className="create-user-page__toggle-text">Company access</span>
955
+ </label>
956
+ </div>
957
+ )}
958
+
959
+ {venues.length > 0 && (
960
+ <div className="create-user-page__venue-section">
961
+ <div className="create-user-page__nested-controls">
962
+ <span className="create-user-page__selected-label">Venues</span>
963
+ <div className="create-user-page__nested-controls-right">
964
+ <button type="button" className="create-user-page__nested-toggle"
965
+ onClick={() => allVenuesSelected
966
+ ? setVenues((p) => p.map((v) => ({ ...v, selected: false })))
967
+ : setVenues((p) => p.map((v) => ({ ...v, selected: true })))
968
+ }
969
+ >
970
+ {allVenuesSelected ? 'Clear All' : 'Select All'}
971
+ </button>
972
+ <span className="create-user-page__nested-count">
973
+ {selectedVenueCount} of {venues.length} selected
974
+ </span>
975
+ </div>
976
+ </div>
977
+
978
+ <div className="create-user-page__nested-grid">
979
+ {venues.map((venue) => (
980
+ <div
981
+ key={venue.id}
982
+ className={`create-user-page__nested-item ${venue.selected ? 'create-user-page__nested-item--selected' : ''}`}
983
+ onClick={() => setVenues((p) => p.map((v) => v.id === venue.id ? { ...v, selected: !v.selected } : v))}
984
+ >
985
+ <Checkbox
986
+ checked={venue.selected}
987
+ onChange={() => setVenues((p) => p.map((v) => v.id === venue.id ? { ...v, selected: !v.selected } : v))}
988
+ onClick={(e) => e.stopPropagation()}
989
+ />
990
+ <EntityAvatar name={venue.name} avatarUrl={venue.avatarUrl} />
991
+ <div className="create-user-page__nested-item-info">
992
+ <span className="create-user-page__nested-item-name">{venue.name}</span>
993
+ <EntityIdBadge type={venue.type} friendlyId={venue.friendlyId} />
994
+ </div>
995
+ </div>
996
+ ))}
997
+ </div>
998
+
999
+ {fieldErrors.venues && (
1000
+ <span className="create-user-page__field-error">{fieldErrors.venues}</span>
1001
+ )}
1002
+ </div>
1003
+ )}
1004
+
1005
+ <div className="create-user-page__actions">
1006
+ <Button onClick={handleCompanyAccessNext} disabled={loading}>Next</Button>
1007
+ </div>
1008
+ </div>
1009
+ )}
1010
+
1011
+ {/* ── Company roles (company-team) ───────────────────────────────── */}
1012
+ {step === 'company-roles' && (
1013
+ <div className="create-user-page__step">
1014
+ <h2 className="create-user-page__step-title">Set Roles for {userName}</h2>
1015
+
1016
+ <div className="create-user-page__role-assignments">
1017
+ {companyAccessEnabled && entityContext && (
1018
+ <div className="create-user-page__role-assignment-card create-user-page__role-assignment-card--company">
1019
+ <EntityAvatar name={entityContext.name} avatarUrl={entityContext.avatarUrl} />
1020
+ <div className="create-user-page__role-assignment-info">
1021
+ <span className="create-user-page__role-assignment-name">{entityContext.name}</span>
1022
+ <EntityIdBadge type={entityContext.type} friendlyId={entityContext.id} />
1023
+ </div>
1024
+ <div className="create-user-page__role-assignment-select">
1025
+ <Select value={companyRole} onChange={(e) => setCompanyRole(e.target.value as ProviderRole)} disabled={loading}>
1026
+ {PROVIDER_ROLES.map((r) => <option key={r} value={r}>{formatRole(r)}</option>)}
1027
+ </Select>
1028
+ </div>
1029
+ </div>
1030
+ )}
1031
+
1032
+ {venues.filter((v) => v.selected).map((venue) => (
1033
+ <div key={venue.id} className="create-user-page__role-assignment-card">
1034
+ <EntityAvatar name={venue.name} avatarUrl={venue.avatarUrl} />
1035
+ <div className="create-user-page__role-assignment-info">
1036
+ <span className="create-user-page__role-assignment-name">{venue.name}</span>
1037
+ <EntityIdBadge type={venue.type} friendlyId={venue.friendlyId} />
1038
+ </div>
1039
+ <div className="create-user-page__role-assignment-select">
1040
+ <Select
1041
+ value={venue.role}
1042
+ onChange={(e) => setVenues((p) => p.map((v) => v.id === venue.id ? { ...v, role: e.target.value as ProviderRole } : v))}
1043
+ disabled={loading}
1044
+ >
1045
+ {PROVIDER_ROLES.map((r) => <option key={r} value={r}>{formatRole(r)}</option>)}
1046
+ </Select>
1047
+ </div>
1048
+ </div>
1049
+ ))}
1050
+ </div>
1051
+
1052
+ <div className="create-user-page__actions">
1053
+ <Button onClick={handleSubmit} isLoading={loading} disabled={loading}>
1054
+ {existingUser ? 'Add User' : 'Send Invitation'}
1055
+ </Button>
1056
+ </div>
1057
+ </div>
1058
+ )}
1059
+ </div>
1060
+ </div>
1061
+ )
1062
+ }