@snapdragonsnursery/react-components 1.17.10 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/EmployeeSelect.jsx +201 -168
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// EmployeeSelect Component
|
|
2
2
|
// A lightweight, searchable employee selector built with shadcn/ui primitives.
|
|
3
|
+
// Redesigned as a combobox with search as the primary input.
|
|
3
4
|
// Use this for simple employee selection with pre-loaded data. For advanced search
|
|
4
5
|
// with server-side filtering, pagination, and complex filters, use EmployeeSearchModal instead.
|
|
5
6
|
//
|
|
@@ -26,13 +27,13 @@
|
|
|
26
27
|
// - showSiteName?: boolean // show site below name (default: true)
|
|
27
28
|
// - showEmployeeId?: boolean // show employee ID below name (default: false)
|
|
28
29
|
// - showEmail?: boolean // show email below name (default: false)
|
|
29
|
-
// - placeholder?: string //
|
|
30
|
-
// - searchPlaceholder?: string //
|
|
30
|
+
// - placeholder?: string // search input placeholder (default: 'Search employees...')
|
|
31
|
+
// - searchPlaceholder?: string // alias for placeholder prop
|
|
31
32
|
// - disabled?: boolean
|
|
32
33
|
// - className?: string
|
|
33
34
|
// - allowAll?: boolean // include "All employees" option
|
|
34
35
|
// - allLabel?: string // custom label for all option
|
|
35
|
-
// - maxHeight?: string // max height for dropdown (default: '
|
|
36
|
+
// - maxHeight?: string // max height for dropdown (default: '300px')
|
|
36
37
|
// - enableServerSearch?: boolean // enable server-side search (default: false)
|
|
37
38
|
// - onSearchChange?: (query: string) => void // callback for search query changes
|
|
38
39
|
// - searchResults?: Array<employee> // search results from server
|
|
@@ -40,17 +41,9 @@
|
|
|
40
41
|
// - requireSearch?: boolean // hide results until user types (default: false)
|
|
41
42
|
|
|
42
43
|
import React from "react";
|
|
43
|
-
import { Users, Search } from "lucide-react";
|
|
44
|
+
import { Users, Search, X } from "lucide-react";
|
|
44
45
|
import { cn } from "../lib/utils";
|
|
45
46
|
|
|
46
|
-
import {
|
|
47
|
-
Select,
|
|
48
|
-
SelectContent,
|
|
49
|
-
SelectItem,
|
|
50
|
-
SelectTrigger,
|
|
51
|
-
SelectValue,
|
|
52
|
-
} from "./ui/radix-select";
|
|
53
|
-
|
|
54
47
|
export const EmployeeSelect = ({
|
|
55
48
|
value,
|
|
56
49
|
onChange,
|
|
@@ -61,14 +54,13 @@ export const EmployeeSelect = ({
|
|
|
61
54
|
showSiteName = true,
|
|
62
55
|
showEmployeeId = false,
|
|
63
56
|
showEmail = false,
|
|
64
|
-
placeholder = "
|
|
65
|
-
searchPlaceholder
|
|
57
|
+
placeholder = "Search employees...",
|
|
58
|
+
searchPlaceholder,
|
|
66
59
|
disabled = false,
|
|
67
60
|
className,
|
|
68
61
|
allowAll = false,
|
|
69
62
|
allLabel = "All employees",
|
|
70
|
-
maxHeight = "
|
|
71
|
-
// New props for server-side search
|
|
63
|
+
maxHeight = "300px",
|
|
72
64
|
enableServerSearch = false,
|
|
73
65
|
onSearchChange = null,
|
|
74
66
|
searchResults = [],
|
|
@@ -77,27 +69,27 @@ export const EmployeeSelect = ({
|
|
|
77
69
|
}) => {
|
|
78
70
|
const [searchTerm, setSearchTerm] = React.useState("");
|
|
79
71
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
80
|
-
const
|
|
81
|
-
|
|
72
|
+
const inputRef = React.useRef(null);
|
|
73
|
+
|
|
74
|
+
// Use searchPlaceholder if provided, otherwise use placeholder
|
|
75
|
+
const effectivePlaceholder = searchPlaceholder || placeholder;
|
|
82
76
|
|
|
83
77
|
// Debounce search term for server-side search
|
|
84
78
|
React.useEffect(() => {
|
|
85
79
|
const timer = setTimeout(() => {
|
|
86
|
-
setDebouncedSearchTerm(searchTerm);
|
|
87
80
|
if (enableServerSearch && onSearchChange) {
|
|
88
81
|
onSearchChange(searchTerm);
|
|
89
82
|
}
|
|
90
|
-
}, 300);
|
|
83
|
+
}, 300);
|
|
91
84
|
|
|
92
85
|
return () => clearTimeout(timer);
|
|
93
86
|
}, [searchTerm, enableServerSearch, onSearchChange]);
|
|
94
87
|
|
|
95
88
|
// Maintain focus on search input when server results update
|
|
96
89
|
React.useEffect(() => {
|
|
97
|
-
if (enableServerSearch && isOpen &&
|
|
98
|
-
// Only refocus if we previously had focus (i.e., user was typing)
|
|
90
|
+
if (enableServerSearch && isOpen && inputRef.current && document.activeElement !== inputRef.current) {
|
|
99
91
|
if (searchTerm && !isSearching) {
|
|
100
|
-
|
|
92
|
+
inputRef.current.focus();
|
|
101
93
|
}
|
|
102
94
|
}
|
|
103
95
|
}, [searchResults, enableServerSearch, isOpen, searchTerm, isSearching]);
|
|
@@ -116,7 +108,6 @@ export const EmployeeSelect = ({
|
|
|
116
108
|
// Helper function to get avatar color based on name
|
|
117
109
|
const getAvatarColor = (name) => {
|
|
118
110
|
if (!name) return "#6b7280";
|
|
119
|
-
// Generate a consistent color based on the name
|
|
120
111
|
let hash = 0;
|
|
121
112
|
for (let i = 0; i < name.length; i++) {
|
|
122
113
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
@@ -126,19 +117,16 @@ export const EmployeeSelect = ({
|
|
|
126
117
|
};
|
|
127
118
|
|
|
128
119
|
const processedItems = React.useMemo(() => {
|
|
129
|
-
// Use server search results if enabled, otherwise use items
|
|
130
120
|
const sourceList = enableServerSearch ? searchResults : items;
|
|
131
121
|
const list = Array.isArray(sourceList) ? [...sourceList] : [];
|
|
132
122
|
|
|
133
|
-
// Filter by employee status
|
|
134
123
|
const filtered = list.filter((e) => {
|
|
135
124
|
if (filter === "active") {
|
|
136
125
|
return e.employee_status === "Active";
|
|
137
126
|
}
|
|
138
|
-
return true;
|
|
127
|
+
return true;
|
|
139
128
|
});
|
|
140
129
|
|
|
141
|
-
// Map to consistent structure
|
|
142
130
|
const mapped = filtered
|
|
143
131
|
.map((e) => ({
|
|
144
132
|
entraId: String(e.entra_id || ""),
|
|
@@ -148,11 +136,10 @@ export const EmployeeSelect = ({
|
|
|
148
136
|
employeeId: String(e.employee_id || ""),
|
|
149
137
|
email: String(e.email || ""),
|
|
150
138
|
}))
|
|
151
|
-
.filter((e) => e.entraId);
|
|
139
|
+
.filter((e) => e.entraId);
|
|
152
140
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
? mapped // Server search already filtered
|
|
141
|
+
const searchFiltered = enableServerSearch
|
|
142
|
+
? mapped
|
|
156
143
|
: mapped.filter((e) => {
|
|
157
144
|
if (!searchTerm) return true;
|
|
158
145
|
const searchLower = searchTerm.toLowerCase();
|
|
@@ -165,7 +152,6 @@ export const EmployeeSelect = ({
|
|
|
165
152
|
);
|
|
166
153
|
});
|
|
167
154
|
|
|
168
|
-
// Sort based on grouping
|
|
169
155
|
searchFiltered.sort((a, b) => {
|
|
170
156
|
if (groupBy === "site") {
|
|
171
157
|
const siteCompare = a.siteName.localeCompare(b.siteName);
|
|
@@ -180,44 +166,44 @@ export const EmployeeSelect = ({
|
|
|
180
166
|
return a.fullName.localeCompare(b.fullName);
|
|
181
167
|
});
|
|
182
168
|
|
|
183
|
-
// If requireSearch is true and no search term, show only selected item (if any)
|
|
184
169
|
if (requireSearch && !searchTerm) {
|
|
185
|
-
// Return only the currently selected item, or empty array if nothing selected
|
|
186
170
|
return searchFiltered.filter((e) => e.entraId === value);
|
|
187
171
|
}
|
|
188
172
|
|
|
189
173
|
return searchFiltered;
|
|
190
174
|
}, [items, filter, groupBy, searchTerm, enableServerSearch, searchResults, requireSearch, value]);
|
|
191
175
|
|
|
192
|
-
const selectedEmployee = processedItems.find((e) => e.entraId === value)
|
|
176
|
+
const selectedEmployee = processedItems.find((e) => e.entraId === value) ||
|
|
177
|
+
(enableServerSearch ? searchResults.find((e) => String(e.entra_id) === value) : items.find((e) => String(e.entra_id) === value));
|
|
178
|
+
|
|
179
|
+
const handleSelect = (employeeId) => {
|
|
180
|
+
onChange?.(employeeId);
|
|
181
|
+
setSearchTerm("");
|
|
182
|
+
setIsOpen(false);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleClear = () => {
|
|
186
|
+
onChange?.(undefined);
|
|
187
|
+
setSearchTerm("");
|
|
188
|
+
};
|
|
193
189
|
|
|
194
|
-
const renderEmployeeItem = (employee
|
|
195
|
-
<div className="flex items-center gap-2">
|
|
196
|
-
{showAvatar &&
|
|
190
|
+
const renderEmployeeItem = (employee) => (
|
|
191
|
+
<div className="flex items-center gap-2 p-2 hover:bg-accent rounded-md cursor-pointer">
|
|
192
|
+
{showAvatar && (
|
|
197
193
|
<div
|
|
198
|
-
className="flex h-
|
|
194
|
+
className="flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium text-white flex-shrink-0"
|
|
199
195
|
style={{ backgroundColor: getAvatarColor(employee.fullName) }}
|
|
200
196
|
>
|
|
201
197
|
{getInitials(employee.fullName)}
|
|
202
198
|
</div>
|
|
203
199
|
)}
|
|
204
200
|
<div className="flex-1 min-w-0">
|
|
205
|
-
<div className="
|
|
206
|
-
|
|
207
|
-
</div>
|
|
208
|
-
{showSiteName && employee.siteName && (
|
|
201
|
+
<div className="text-sm font-medium truncate">{employee.fullName}</div>
|
|
202
|
+
{(showSiteName || showEmail) && (
|
|
209
203
|
<div className="text-xs text-muted-foreground truncate">
|
|
210
|
-
{employee.siteName}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{showEmployeeId && employee.employeeId && (
|
|
214
|
-
<div className="text-xs text-muted-foreground truncate">
|
|
215
|
-
ID: {employee.employeeId}
|
|
216
|
-
</div>
|
|
217
|
-
)}
|
|
218
|
-
{showEmail && employee.email && (
|
|
219
|
-
<div className="text-xs text-muted-foreground truncate">
|
|
220
|
-
{employee.email}
|
|
204
|
+
{showSiteName && employee.siteName && <span>{employee.siteName}</span>}
|
|
205
|
+
{showSiteName && showEmail && employee.siteName && employee.email && <span> • </span>}
|
|
206
|
+
{showEmail && employee.email && <span>{employee.email}</span>}
|
|
221
207
|
</div>
|
|
222
208
|
)}
|
|
223
209
|
</div>
|
|
@@ -225,127 +211,174 @@ export const EmployeeSelect = ({
|
|
|
225
211
|
);
|
|
226
212
|
|
|
227
213
|
return (
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
disabled={disabled}
|
|
240
|
-
open={isOpen}
|
|
241
|
-
onOpenChange={setIsOpen}
|
|
242
|
-
>
|
|
243
|
-
<SelectTrigger className={cn("w-full", className)}>
|
|
244
|
-
<div className="flex items-center gap-2">
|
|
245
|
-
{selectedEmployee && showAvatar ? (
|
|
246
|
-
<div
|
|
247
|
-
className="flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium text-white"
|
|
248
|
-
style={{ backgroundColor: getAvatarColor(selectedEmployee.fullName) }}
|
|
249
|
-
>
|
|
250
|
-
{getInitials(selectedEmployee.fullName)}
|
|
251
|
-
</div>
|
|
252
|
-
) : (
|
|
253
|
-
<Users className="h-4 w-4" />
|
|
254
|
-
)}
|
|
255
|
-
<SelectValue placeholder={placeholder} />
|
|
256
|
-
</div>
|
|
257
|
-
</SelectTrigger>
|
|
258
|
-
<SelectContent
|
|
259
|
-
className="[&_[data-radix-select-viewport]]:max-h-[var(--select-max-height)] [&_[data-radix-select-viewport]]:overflow-y-auto [&>button[data-radix-select-scroll-up-button]]:hidden [&>button[data-radix-select-scroll-down-button]]:hidden"
|
|
260
|
-
style={{ "--select-max-height": maxHeight }}
|
|
261
|
-
>
|
|
262
|
-
{/* Search input */}
|
|
263
|
-
<div className="p-2 border-b">
|
|
264
|
-
<div className="relative">
|
|
265
|
-
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
266
|
-
<input
|
|
267
|
-
ref={searchInputRef}
|
|
268
|
-
type="text"
|
|
269
|
-
placeholder={searchPlaceholder}
|
|
270
|
-
value={searchTerm}
|
|
271
|
-
onChange={(e) => setSearchTerm(e.target.value)}
|
|
272
|
-
className="w-full pl-8 pr-3 py-2 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 bg-background"
|
|
273
|
-
onClick={(e) => e.stopPropagation()}
|
|
274
|
-
/>
|
|
275
|
-
{enableServerSearch && isSearching && (
|
|
276
|
-
<div className="absolute right-2 top-1/2 transform -translate-y-1/2">
|
|
277
|
-
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
|
214
|
+
<div className={cn("relative", className)} disabled={disabled}>
|
|
215
|
+
{/* Selected value display */}
|
|
216
|
+
{selectedEmployee && (
|
|
217
|
+
<div className="mb-2 flex items-center justify-between bg-secondary px-3 py-2 rounded-md">
|
|
218
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
219
|
+
{showAvatar && (
|
|
220
|
+
<div
|
|
221
|
+
className="flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium text-white flex-shrink-0"
|
|
222
|
+
style={{ backgroundColor: getAvatarColor(selectedEmployee.fullName) }}
|
|
223
|
+
>
|
|
224
|
+
{getInitials(selectedEmployee.fullName)}
|
|
278
225
|
</div>
|
|
279
226
|
)}
|
|
227
|
+
<div className="min-w-0">
|
|
228
|
+
<div className="text-sm font-medium truncate">{selectedEmployee.fullName}</div>
|
|
229
|
+
{showSiteName && selectedEmployee.siteName && (
|
|
230
|
+
<div className="text-xs text-muted-foreground truncate">{selectedEmployee.siteName}</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
280
233
|
</div>
|
|
234
|
+
<button
|
|
235
|
+
onClick={handleClear}
|
|
236
|
+
className="ml-2 p-1 hover:bg-background rounded flex-shrink-0"
|
|
237
|
+
aria-label="Clear selection"
|
|
238
|
+
>
|
|
239
|
+
<X className="h-4 w-4" />
|
|
240
|
+
</button>
|
|
281
241
|
</div>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{/* Search input */}
|
|
245
|
+
<div className="relative">
|
|
246
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
|
247
|
+
<input
|
|
248
|
+
ref={inputRef}
|
|
249
|
+
type="text"
|
|
250
|
+
placeholder={effectivePlaceholder}
|
|
251
|
+
value={searchTerm}
|
|
252
|
+
onChange={(e) => {
|
|
253
|
+
setSearchTerm(e.target.value);
|
|
254
|
+
setIsOpen(true);
|
|
255
|
+
}}
|
|
256
|
+
onFocus={() => setIsOpen(true)}
|
|
257
|
+
disabled={disabled}
|
|
258
|
+
className={cn(
|
|
259
|
+
"w-full pl-10 pr-10 py-2 text-sm border rounded-md",
|
|
260
|
+
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
261
|
+
"bg-background transition-colors",
|
|
262
|
+
"placeholder:text-muted-foreground",
|
|
263
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
264
|
+
)}
|
|
265
|
+
/>
|
|
266
|
+
{enableServerSearch && isSearching && (
|
|
267
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
268
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
|
286
269
|
</div>
|
|
287
270
|
)}
|
|
288
|
-
{
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
271
|
+
{searchTerm && !isSearching && (
|
|
272
|
+
<button
|
|
273
|
+
onClick={() => {
|
|
274
|
+
setSearchTerm("");
|
|
275
|
+
inputRef.current?.focus();
|
|
276
|
+
}}
|
|
277
|
+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
278
|
+
aria-label="Clear search"
|
|
279
|
+
>
|
|
280
|
+
<X className="h-4 w-4" />
|
|
281
|
+
</button>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{/* Results dropdown */}
|
|
286
|
+
{isOpen && (
|
|
287
|
+
<>
|
|
288
|
+
{/* Backdrop to close dropdown */}
|
|
289
|
+
<div
|
|
290
|
+
className="fixed inset-0 z-40"
|
|
291
|
+
onClick={() => setIsOpen(false)}
|
|
292
|
+
onKeyDown={(e) => {
|
|
293
|
+
if (e.key === "Escape") setIsOpen(false);
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
|
|
297
|
+
{/* Results popover */}
|
|
298
|
+
<div
|
|
299
|
+
className="absolute top-full left-0 right-0 mt-1 bg-popover border rounded-md shadow-md z-50 overflow-hidden"
|
|
300
|
+
style={{ maxHeight }}
|
|
301
|
+
>
|
|
302
|
+
<div className="overflow-y-auto">
|
|
303
|
+
{requireSearch && !searchTerm && processedItems.length === 0 ? (
|
|
304
|
+
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
305
|
+
Start typing to search for employees...
|
|
302
306
|
</div>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<SelectItem key={employee.entraId} value={employee.entraId}>
|
|
307
|
-
{renderEmployeeItem(employee, isSelected)}
|
|
308
|
-
</SelectItem>
|
|
309
|
-
);
|
|
310
|
-
})}
|
|
311
|
-
</div>
|
|
312
|
-
))
|
|
313
|
-
: groupBy === "role"
|
|
314
|
-
? // Group by role
|
|
315
|
-
Object.entries(
|
|
316
|
-
processedItems.reduce((groups, employee) => {
|
|
317
|
-
const role = employee.roleName || "Other";
|
|
318
|
-
if (!groups[role]) groups[role] = [];
|
|
319
|
-
groups[role].push(employee);
|
|
320
|
-
return groups;
|
|
321
|
-
}, {})
|
|
322
|
-
).map(([roleName, employees]) => (
|
|
323
|
-
<div key={roleName}>
|
|
324
|
-
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
325
|
-
{roleName}
|
|
326
|
-
</div>
|
|
327
|
-
{employees.map((employee) => {
|
|
328
|
-
const isSelected = value === employee.entraId;
|
|
329
|
-
return (
|
|
330
|
-
<SelectItem key={employee.entraId} value={employee.entraId}>
|
|
331
|
-
{renderEmployeeItem(employee, isSelected)}
|
|
332
|
-
</SelectItem>
|
|
333
|
-
);
|
|
334
|
-
})}
|
|
307
|
+
) : processedItems.length === 0 ? (
|
|
308
|
+
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
309
|
+
No employees found
|
|
335
310
|
</div>
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
311
|
+
) : (
|
|
312
|
+
<>
|
|
313
|
+
{allowAll && (
|
|
314
|
+
<div
|
|
315
|
+
onClick={() => handleSelect(undefined)}
|
|
316
|
+
className="px-3 py-2 hover:bg-accent cursor-pointer border-b text-sm"
|
|
317
|
+
>
|
|
318
|
+
{allLabel}
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
{groupBy === "site"
|
|
322
|
+
? Object.entries(
|
|
323
|
+
processedItems.reduce((groups, employee) => {
|
|
324
|
+
const site = employee.siteName || "Other";
|
|
325
|
+
if (!groups[site]) groups[site] = [];
|
|
326
|
+
groups[site].push(employee);
|
|
327
|
+
return groups;
|
|
328
|
+
}, {})
|
|
329
|
+
).map(([siteName, employees]) => (
|
|
330
|
+
<div key={siteName}>
|
|
331
|
+
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground bg-muted">
|
|
332
|
+
{siteName}
|
|
333
|
+
</div>
|
|
334
|
+
{employees.map((employee) => (
|
|
335
|
+
<div
|
|
336
|
+
key={employee.entraId}
|
|
337
|
+
onClick={() => handleSelect(employee.entraId)}
|
|
338
|
+
>
|
|
339
|
+
{renderEmployeeItem(employee)}
|
|
340
|
+
</div>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
))
|
|
344
|
+
: groupBy === "role"
|
|
345
|
+
? Object.entries(
|
|
346
|
+
processedItems.reduce((groups, employee) => {
|
|
347
|
+
const role = employee.roleName || "Other";
|
|
348
|
+
if (!groups[role]) groups[role] = [];
|
|
349
|
+
groups[role].push(employee);
|
|
350
|
+
return groups;
|
|
351
|
+
}, {})
|
|
352
|
+
).map(([roleName, employees]) => (
|
|
353
|
+
<div key={roleName}>
|
|
354
|
+
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground bg-muted">
|
|
355
|
+
{roleName}
|
|
356
|
+
</div>
|
|
357
|
+
{employees.map((employee) => (
|
|
358
|
+
<div
|
|
359
|
+
key={employee.entraId}
|
|
360
|
+
onClick={() => handleSelect(employee.entraId)}
|
|
361
|
+
>
|
|
362
|
+
{renderEmployeeItem(employee)}
|
|
363
|
+
</div>
|
|
364
|
+
))}
|
|
365
|
+
</div>
|
|
366
|
+
))
|
|
367
|
+
: processedItems.map((employee) => (
|
|
368
|
+
<div
|
|
369
|
+
key={employee.entraId}
|
|
370
|
+
onClick={() => handleSelect(employee.entraId)}
|
|
371
|
+
>
|
|
372
|
+
{renderEmployeeItem(employee)}
|
|
373
|
+
</div>
|
|
374
|
+
))}
|
|
375
|
+
</>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
349
382
|
);
|
|
350
383
|
};
|
|
351
384
|
|