@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.
- package/dist/styles/themes/bookedit/_fonts.scss +2 -0
- package/dist/v2/components/ActionMenu/ActionMenu.d.ts.map +1 -1
- package/dist/v2/components/ActionMenu/ActionMenu.js +5 -3
- package/dist/v2/components/AvatarUpload/AvatarUpload.d.ts +12 -0
- package/dist/v2/components/AvatarUpload/AvatarUpload.d.ts.map +1 -0
- package/dist/v2/components/AvatarUpload/index.d.ts +2 -0
- package/dist/v2/components/AvatarUpload/index.d.ts.map +1 -0
- package/dist/v2/components/Button/Button.d.ts +1 -1
- package/dist/v2/components/Button/Button.d.ts.map +1 -1
- package/dist/v2/components/Button/Button.scss.js +1 -1
- package/dist/v2/components/DataTable/DataTable.d.ts +41 -0
- package/dist/v2/components/DataTable/DataTable.d.ts.map +1 -0
- package/dist/v2/components/DataTable/index.d.ts +3 -0
- package/dist/v2/components/DataTable/index.d.ts.map +1 -0
- package/dist/v2/components/EmptyState/EmptyState.d.ts +14 -0
- package/dist/v2/components/EmptyState/EmptyState.d.ts.map +1 -0
- package/dist/v2/components/EmptyState/index.d.ts +3 -0
- package/dist/v2/components/EmptyState/index.d.ts.map +1 -0
- package/dist/v2/components/FormField/FormField.scss.js +1 -1
- package/dist/v2/components/InfoGrid/InfoGrid.d.ts +13 -0
- package/dist/v2/components/InfoGrid/InfoGrid.d.ts.map +1 -0
- package/dist/v2/components/InfoGrid/index.d.ts +2 -0
- package/dist/v2/components/InfoGrid/index.d.ts.map +1 -0
- package/dist/v2/components/NewTable/NewTable.scss.js +1 -1
- package/dist/v2/components/RadioCard/RadioCard.d.ts +17 -0
- package/dist/v2/components/RadioCard/RadioCard.d.ts.map +1 -0
- package/dist/v2/components/RadioCard/index.d.ts +2 -0
- package/dist/v2/components/RadioCard/index.d.ts.map +1 -0
- package/dist/v2/components/StatusBadge/StatusBadge.d.ts +8 -0
- package/dist/v2/components/StatusBadge/StatusBadge.d.ts.map +1 -0
- package/dist/v2/components/StatusBadge/index.d.ts +3 -0
- package/dist/v2/components/StatusBadge/index.d.ts.map +1 -0
- package/dist/v2/components/StepIndicator/StepIndicator.d.ts +9 -0
- package/dist/v2/components/StepIndicator/StepIndicator.d.ts.map +1 -0
- package/dist/v2/components/StepIndicator/index.d.ts +2 -0
- package/dist/v2/components/StepIndicator/index.d.ts.map +1 -0
- package/dist/v2/components/TableControls/TableControls.d.ts +28 -0
- package/dist/v2/components/TableControls/TableControls.d.ts.map +1 -0
- package/dist/v2/components/TableControls/index.d.ts +3 -0
- package/dist/v2/components/TableControls/index.d.ts.map +1 -0
- package/dist/v2/components/Tabs/Tabs.d.ts +15 -0
- package/dist/v2/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/v2/components/Tabs/index.d.ts +3 -0
- package/dist/v2/components/Tabs/index.d.ts.map +1 -0
- package/dist/v2/icons/index.d.ts +42 -0
- package/dist/v2/icons/index.d.ts.map +1 -1
- package/dist/v2/index.d.ts +18 -0
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/pages/CreateUser/CreateUserPage.d.ts +110 -0
- package/dist/v2/pages/CreateUser/CreateUserPage.d.ts.map +1 -0
- package/dist/v2/pages/CreateUser/index.d.ts +3 -0
- package/dist/v2/pages/CreateUser/index.d.ts.map +1 -0
- package/dist/v2/pages/RoleSelection/RoleSelectionPage.d.ts +26 -0
- package/dist/v2/pages/RoleSelection/RoleSelectionPage.d.ts.map +1 -0
- package/dist/v2/pages/RoleSelection/index.d.ts +3 -0
- package/dist/v2/pages/RoleSelection/index.d.ts.map +1 -0
- package/dist/v2/pages/UserDetails/UserDetailsPage.d.ts +37 -0
- package/dist/v2/pages/UserDetails/UserDetailsPage.d.ts.map +1 -0
- package/dist/v2/pages/UserDetails/index.d.ts +3 -0
- package/dist/v2/pages/UserDetails/index.d.ts.map +1 -0
- package/dist/v2/pages/auth/CreatePassword/CreatePasswordPage.d.ts.map +1 -1
- package/dist/v2/pages/auth/Login/LoginPage.d.ts.map +1 -1
- package/dist/v2/pages/auth/ResetPassword/ResetPasswordPage.d.ts.map +1 -1
- package/dist/v2/styles/components/Button.scss +27 -0
- package/package.json +2 -2
- package/src/styles/themes/bookedit/_fonts.scss +2 -0
- package/src/v2/components/ActionMenu/ActionMenu.tsx +4 -2
- package/src/v2/components/AvatarUpload/AvatarUpload.scss +68 -0
- package/src/v2/components/AvatarUpload/AvatarUpload.stories.tsx +83 -0
- package/src/v2/components/AvatarUpload/AvatarUpload.tsx +69 -0
- package/src/v2/components/AvatarUpload/index.ts +1 -0
- package/src/v2/components/Button/Button.tsx +1 -0
- package/src/v2/components/DataTable/DataTable.scss +181 -0
- package/src/v2/components/DataTable/DataTable.tsx +256 -0
- package/src/v2/components/DataTable/index.ts +7 -0
- package/src/v2/components/EmptyState/EmptyState.scss +39 -0
- package/src/v2/components/EmptyState/EmptyState.stories.tsx +45 -0
- package/src/v2/components/EmptyState/EmptyState.tsx +37 -0
- package/src/v2/components/EmptyState/index.ts +2 -0
- package/src/v2/components/FormField/FormField.scss +12 -0
- package/src/v2/components/InfoGrid/InfoGrid.scss +51 -0
- package/src/v2/components/InfoGrid/InfoGrid.stories.tsx +76 -0
- package/src/v2/components/InfoGrid/InfoGrid.tsx +28 -0
- package/src/v2/components/InfoGrid/index.ts +1 -0
- package/src/v2/components/NewTable/NewTable.scss +4 -4
- package/src/v2/components/RadioCard/RadioCard.scss +76 -0
- package/src/v2/components/RadioCard/RadioCard.stories.tsx +115 -0
- package/src/v2/components/RadioCard/RadioCard.tsx +68 -0
- package/src/v2/components/RadioCard/index.ts +1 -0
- package/src/v2/components/StatusBadge/StatusBadge.scss +53 -0
- package/src/v2/components/StatusBadge/StatusBadge.tsx +31 -0
- package/src/v2/components/StatusBadge/index.ts +2 -0
- package/src/v2/components/StepIndicator/StepIndicator.scss +62 -0
- package/src/v2/components/StepIndicator/StepIndicator.stories.tsx +37 -0
- package/src/v2/components/StepIndicator/StepIndicator.tsx +41 -0
- package/src/v2/components/StepIndicator/index.ts +1 -0
- package/src/v2/components/TableControls/TableControls.scss +63 -0
- package/src/v2/components/TableControls/TableControls.tsx +110 -0
- package/src/v2/components/TableControls/index.ts +7 -0
- package/src/v2/components/Tabs/Tabs.scss +36 -0
- package/src/v2/components/Tabs/Tabs.stories.tsx +75 -0
- package/src/v2/components/Tabs/Tabs.tsx +52 -0
- package/src/v2/components/Tabs/index.ts +2 -0
- package/src/v2/icons/index.tsx +219 -0
- package/src/v2/index.ts +98 -0
- package/src/v2/pages/CreateUser/CreateUserPage.scss +760 -0
- package/src/v2/pages/CreateUser/CreateUserPage.stories.tsx +157 -0
- package/src/v2/pages/CreateUser/CreateUserPage.tsx +1062 -0
- package/src/v2/pages/CreateUser/index.ts +13 -0
- package/src/v2/pages/RoleSelection/RoleSelectionPage.scss +193 -0
- package/src/v2/pages/RoleSelection/RoleSelectionPage.stories.tsx +112 -0
- package/src/v2/pages/RoleSelection/RoleSelectionPage.tsx +127 -0
- package/src/v2/pages/RoleSelection/index.ts +2 -0
- package/src/v2/pages/UserDetails/UserDetailsPage.scss +236 -0
- package/src/v2/pages/UserDetails/UserDetailsPage.stories.tsx +84 -0
- package/src/v2/pages/UserDetails/UserDetailsPage.tsx +210 -0
- package/src/v2/pages/UserDetails/index.ts +2 -0
- package/src/v2/pages/auth/AuthLayout/AuthLayout.scss +8 -6
- package/src/v2/pages/auth/CreatePassword/CreatePasswordPage.tsx +1 -3
- package/src/v2/pages/auth/Login/LoginPage.tsx +1 -3
- package/src/v2/pages/auth/ResetPassword/ResetPasswordPage.scss +2 -0
- package/src/v2/pages/auth/ResetPassword/ResetPasswordPage.tsx +1 -2
- 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
|
+
}
|