@snapdragonsnursery/react-components 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/EmployeeSearchModal.jsx +308 -55
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { trackEvent } from "./telemetry";
|
|
15
15
|
import { Button } from "./components/ui/button";
|
|
16
16
|
import { Input } from "./components/ui/input";
|
|
17
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./components/ui/avatar";
|
|
17
18
|
import {
|
|
18
19
|
Table,
|
|
19
20
|
TableBody,
|
|
@@ -74,12 +75,17 @@ const EmployeeSearchModal = ({
|
|
|
74
75
|
showEndDateFilter = true,
|
|
75
76
|
showDbsFilter = true,
|
|
76
77
|
showWorkingHoursFilter = true,
|
|
78
|
+
// Profile pictures
|
|
79
|
+
showProfilePics = false,
|
|
77
80
|
// Layout controls
|
|
78
81
|
zIndex = 60,
|
|
79
82
|
maxHeightVh = 85,
|
|
80
83
|
// Auth options: provide token directly or a function to fetch it
|
|
81
84
|
authToken = null,
|
|
82
85
|
getAccessToken = null,
|
|
86
|
+
// Graph auth options specifically for Microsoft Graph calls (e.g., profile photos)
|
|
87
|
+
graphAuthToken = null,
|
|
88
|
+
getGraphAccessToken = null,
|
|
83
89
|
// Column customization
|
|
84
90
|
visibleColumns = null,
|
|
85
91
|
columnLabels = {},
|
|
@@ -118,18 +124,122 @@ const EmployeeSearchModal = ({
|
|
|
118
124
|
|
|
119
125
|
// Table sorting state
|
|
120
126
|
const [sorting, setSorting] = useState([]);
|
|
121
|
-
const [debouncedAdvancedFilters, setDebouncedAdvancedFilters] =
|
|
127
|
+
const [debouncedAdvancedFilters, setDebouncedAdvancedFilters] =
|
|
128
|
+
useState(advancedFilters);
|
|
122
129
|
|
|
123
130
|
// State for multi-select mode
|
|
124
131
|
const [selectedEmployeesState, setSelectedEmployeesState] = useState(
|
|
125
132
|
selectedEmployees || []
|
|
126
133
|
);
|
|
127
134
|
|
|
135
|
+
// State for profile pictures
|
|
136
|
+
const [profilePics, setProfilePics] = useState(new Map());
|
|
137
|
+
const [loadingProfilePics, setLoadingProfilePics] = useState(new Set());
|
|
138
|
+
|
|
128
139
|
const { instance, accounts } = useMsal();
|
|
129
140
|
|
|
130
141
|
// Column helper for TanStack table
|
|
131
142
|
const columnHelper = createColumnHelper();
|
|
132
143
|
|
|
144
|
+
// Function to fetch profile picture from Microsoft Graph
|
|
145
|
+
const fetchProfilePicture = useCallback(
|
|
146
|
+
async (entraId) => {
|
|
147
|
+
if (
|
|
148
|
+
!entraId ||
|
|
149
|
+
profilePics.has(entraId) ||
|
|
150
|
+
loadingProfilePics.has(entraId)
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setLoadingProfilePics((prev) => new Set(prev).add(entraId));
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Get Graph API token (prefer explicit Graph token accessors to avoid APIM token misuse)
|
|
159
|
+
let graphToken = null;
|
|
160
|
+
|
|
161
|
+
if (typeof getGraphAccessToken === "function") {
|
|
162
|
+
graphToken = await getGraphAccessToken();
|
|
163
|
+
} else if (graphAuthToken) {
|
|
164
|
+
graphToken = graphAuthToken;
|
|
165
|
+
} else if (instance && accounts[0]) {
|
|
166
|
+
// Fallback: acquire Graph token via MSAL
|
|
167
|
+
// Prefer broader scopes that allow reading other users' profiles/photos when consented
|
|
168
|
+
const graphScopes = ["User.Read", "Directory.Read.All"]; // host app should consent these
|
|
169
|
+
try {
|
|
170
|
+
const response = await instance.acquireTokenSilent({
|
|
171
|
+
account: accounts[0],
|
|
172
|
+
scopes: graphScopes,
|
|
173
|
+
});
|
|
174
|
+
graphToken = response.accessToken;
|
|
175
|
+
} catch (silentErr) {
|
|
176
|
+
try {
|
|
177
|
+
const response = await instance.acquireTokenPopup({
|
|
178
|
+
scopes: graphScopes,
|
|
179
|
+
});
|
|
180
|
+
graphToken = response.accessToken;
|
|
181
|
+
} catch (popupErr) {
|
|
182
|
+
console.warn(
|
|
183
|
+
"Failed to get Graph token for profile picture:",
|
|
184
|
+
popupErr
|
|
185
|
+
);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!graphToken) {
|
|
192
|
+
console.warn("No Graph token available for profile picture");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Try different Graph photo endpoints (small size first)
|
|
197
|
+
const tryUrls = [
|
|
198
|
+
`https://graph.microsoft.com/v1.0/users/${entraId}/photos/48x48/$value`,
|
|
199
|
+
`https://graph.microsoft.com/v1.0/users/${entraId}/photo/$value`,
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
for (const url of tryUrls) {
|
|
203
|
+
const response = await fetch(url, {
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: `Bearer ${graphToken}`,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (response.ok) {
|
|
210
|
+
const blob = await response.blob();
|
|
211
|
+
const imageUrl = URL.createObjectURL(blob);
|
|
212
|
+
setProfilePics((prev) => new Map(prev).set(entraId, imageUrl));
|
|
213
|
+
return; // success
|
|
214
|
+
}
|
|
215
|
+
if (response.status === 404) {
|
|
216
|
+
// No profile picture available - store null to avoid retrying
|
|
217
|
+
setProfilePics((prev) => new Map(prev).set(entraId, null));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.warn(`Failed to fetch profile picture for ${entraId}: Non-200 response`);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.warn(`Error fetching profile picture for ${entraId}:`, error);
|
|
225
|
+
} finally {
|
|
226
|
+
setLoadingProfilePics((prev) => {
|
|
227
|
+
const newSet = new Set(prev);
|
|
228
|
+
newSet.delete(entraId);
|
|
229
|
+
return newSet;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[
|
|
234
|
+
instance,
|
|
235
|
+
accounts,
|
|
236
|
+
getGraphAccessToken,
|
|
237
|
+
graphAuthToken,
|
|
238
|
+
profilePics,
|
|
239
|
+
loadingProfilePics,
|
|
240
|
+
]
|
|
241
|
+
);
|
|
242
|
+
|
|
133
243
|
// Helper function to create sortable header
|
|
134
244
|
const createSortableHeader = (column, label) => {
|
|
135
245
|
return ({ column: tableColumn }) => {
|
|
@@ -159,22 +269,75 @@ const EmployeeSearchModal = ({
|
|
|
159
269
|
};
|
|
160
270
|
};
|
|
161
271
|
|
|
272
|
+
// Profile picture column
|
|
273
|
+
const profilePicColumn = columnHelper.display({
|
|
274
|
+
id: "profile_pic",
|
|
275
|
+
header: columnLabels.profile_pic || "Photo",
|
|
276
|
+
cell: ({ row }) => {
|
|
277
|
+
const entraId = row.original.entra_id;
|
|
278
|
+
const profilePic = profilePics.get(entraId);
|
|
279
|
+
const isLoading = loadingProfilePics.has(entraId);
|
|
280
|
+
|
|
281
|
+
// Generate initials for fallback
|
|
282
|
+
const initials =
|
|
283
|
+
row.original.full_name
|
|
284
|
+
?.split(" ")
|
|
285
|
+
.map((name) => name.charAt(0))
|
|
286
|
+
.join("")
|
|
287
|
+
.toUpperCase()
|
|
288
|
+
.slice(0, 2) || "??";
|
|
289
|
+
|
|
290
|
+
if (isLoading) {
|
|
291
|
+
return (
|
|
292
|
+
<Avatar className="w-8 h-8">
|
|
293
|
+
<AvatarFallback className="bg-gray-200 dark:bg-gray-700">
|
|
294
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
|
|
295
|
+
</AvatarFallback>
|
|
296
|
+
</Avatar>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<Avatar className="w-8 h-8">
|
|
302
|
+
{profilePic && (
|
|
303
|
+
<AvatarImage
|
|
304
|
+
src={profilePic}
|
|
305
|
+
alt={`${row.original.full_name} profile`}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
<AvatarFallback className="bg-blue-500 text-white text-sm font-medium">
|
|
309
|
+
{initials}
|
|
310
|
+
</AvatarFallback>
|
|
311
|
+
</Avatar>
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
size: 60,
|
|
315
|
+
});
|
|
316
|
+
|
|
162
317
|
// Define default data columns (excluding optional select)
|
|
163
318
|
const defaultDataColumns = [
|
|
164
319
|
columnHelper.accessor("full_name", {
|
|
165
|
-
header: createSortableHeader(
|
|
320
|
+
header: createSortableHeader(
|
|
321
|
+
"full_name",
|
|
322
|
+
columnLabels.full_name || "Name"
|
|
323
|
+
),
|
|
166
324
|
cell: ({ row }) =>
|
|
167
325
|
columnRenderers.full_name ? (
|
|
168
326
|
columnRenderers.full_name(row.original)
|
|
169
327
|
) : (
|
|
170
328
|
<div>
|
|
171
329
|
<div className="font-medium">{row.original.full_name}</div>
|
|
172
|
-
<div className="text-sm text-gray-500">
|
|
330
|
+
<div className="text-sm text-gray-500">
|
|
331
|
+
ID: {row.original.employee_id}
|
|
332
|
+
</div>
|
|
173
333
|
</div>
|
|
174
334
|
),
|
|
175
335
|
}),
|
|
176
336
|
columnHelper.accessor("site_name", {
|
|
177
|
-
header: createSortableHeader(
|
|
337
|
+
header: createSortableHeader(
|
|
338
|
+
"site_name",
|
|
339
|
+
columnLabels.site_name || "Site"
|
|
340
|
+
),
|
|
178
341
|
cell: ({ row }) =>
|
|
179
342
|
columnRenderers.site_name ? (
|
|
180
343
|
columnRenderers.site_name(row.original)
|
|
@@ -183,7 +346,10 @@ const EmployeeSearchModal = ({
|
|
|
183
346
|
),
|
|
184
347
|
}),
|
|
185
348
|
columnHelper.accessor("role_name", {
|
|
186
|
-
header: createSortableHeader(
|
|
349
|
+
header: createSortableHeader(
|
|
350
|
+
"role_name",
|
|
351
|
+
columnLabels.role_name || "Role"
|
|
352
|
+
),
|
|
187
353
|
cell: ({ row }) =>
|
|
188
354
|
columnRenderers.role_name ? (
|
|
189
355
|
columnRenderers.role_name(row.original)
|
|
@@ -201,7 +367,10 @@ const EmployeeSearchModal = ({
|
|
|
201
367
|
),
|
|
202
368
|
}),
|
|
203
369
|
columnHelper.accessor("start_date", {
|
|
204
|
-
header: createSortableHeader(
|
|
370
|
+
header: createSortableHeader(
|
|
371
|
+
"start_date",
|
|
372
|
+
columnLabels.start_date || "Start Date"
|
|
373
|
+
),
|
|
205
374
|
cell: ({ row }) =>
|
|
206
375
|
columnRenderers.start_date ? (
|
|
207
376
|
columnRenderers.start_date(row.original)
|
|
@@ -214,24 +383,33 @@ const EmployeeSearchModal = ({
|
|
|
214
383
|
),
|
|
215
384
|
}),
|
|
216
385
|
columnHelper.accessor("employee_status", {
|
|
217
|
-
header: createSortableHeader(
|
|
386
|
+
header: createSortableHeader(
|
|
387
|
+
"employee_status",
|
|
388
|
+
columnLabels.employee_status || "Status"
|
|
389
|
+
),
|
|
218
390
|
cell: ({ row }) => {
|
|
219
|
-
if (columnRenderers.employee_status)
|
|
391
|
+
if (columnRenderers.employee_status)
|
|
392
|
+
return columnRenderers.employee_status(row.original);
|
|
220
393
|
const status = row.original.employee_status;
|
|
221
394
|
const statusColors = {
|
|
222
|
-
Active:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
395
|
+
Active:
|
|
396
|
+
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
397
|
+
Inactive:
|
|
398
|
+
"bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200",
|
|
399
|
+
"On Leave":
|
|
400
|
+
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
|
401
|
+
Terminated:
|
|
402
|
+
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
|
226
403
|
};
|
|
227
404
|
return (
|
|
228
405
|
<span
|
|
229
406
|
className={cn(
|
|
230
|
-
|
|
231
|
-
statusColors[status] ||
|
|
407
|
+
"px-2 py-1 text-xs rounded-full",
|
|
408
|
+
statusColors[status] ||
|
|
409
|
+
"bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"
|
|
232
410
|
)}
|
|
233
411
|
>
|
|
234
|
-
{status ||
|
|
412
|
+
{status || "Unknown"}
|
|
235
413
|
</span>
|
|
236
414
|
);
|
|
237
415
|
},
|
|
@@ -274,8 +452,17 @@ const EmployeeSearchModal = ({
|
|
|
274
452
|
|
|
275
453
|
let dataColumns = defaultDataColumns;
|
|
276
454
|
if (Array.isArray(visibleColumns) && visibleColumns.length > 0) {
|
|
277
|
-
const mapByKey = new Map(
|
|
278
|
-
|
|
455
|
+
const mapByKey = new Map(
|
|
456
|
+
defaultDataColumns.map((c) => [c.accessorKey || c.id, c])
|
|
457
|
+
);
|
|
458
|
+
dataColumns = visibleColumns
|
|
459
|
+
.map((key) => mapByKey.get(key))
|
|
460
|
+
.filter(Boolean);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Add profile picture column if enabled
|
|
464
|
+
if (showProfilePics) {
|
|
465
|
+
dataColumns = [profilePicColumn, ...dataColumns];
|
|
279
466
|
}
|
|
280
467
|
|
|
281
468
|
const columns = multiSelect ? [selectColumn, ...dataColumns] : dataColumns;
|
|
@@ -289,7 +476,9 @@ const EmployeeSearchModal = ({
|
|
|
289
476
|
state: {
|
|
290
477
|
sorting,
|
|
291
478
|
rowSelection: selectedEmployeesState.reduce((acc, selectedEmployee) => {
|
|
292
|
-
const rowIndex = employees.findIndex(
|
|
479
|
+
const rowIndex = employees.findIndex(
|
|
480
|
+
(employee) => employee.entra_id === selectedEmployee.entra_id
|
|
481
|
+
);
|
|
293
482
|
if (rowIndex !== -1) {
|
|
294
483
|
acc[rowIndex] = true;
|
|
295
484
|
}
|
|
@@ -302,13 +491,19 @@ const EmployeeSearchModal = ({
|
|
|
302
491
|
// instead of replacing it, so selection is retained across searches/pages.
|
|
303
492
|
|
|
304
493
|
// Build current selection map (for visible rows only)
|
|
305
|
-
const prevSelection = selectedEmployeesState.reduce(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
494
|
+
const prevSelection = selectedEmployeesState.reduce(
|
|
495
|
+
(acc, selectedEmployee) => {
|
|
496
|
+
const rowIndex = employees.findIndex(
|
|
497
|
+
(employee) => employee.entra_id === selectedEmployee.entra_id
|
|
498
|
+
);
|
|
499
|
+
if (rowIndex !== -1) acc[rowIndex] = true;
|
|
500
|
+
return acc;
|
|
501
|
+
},
|
|
502
|
+
{}
|
|
503
|
+
);
|
|
310
504
|
|
|
311
|
-
const updatedSelection =
|
|
505
|
+
const updatedSelection =
|
|
506
|
+
typeof updater === "function" ? updater(prevSelection) : updater;
|
|
312
507
|
|
|
313
508
|
// Determine which visible employees are selected after the change
|
|
314
509
|
const visibleSelected = Object.keys(updatedSelection)
|
|
@@ -317,11 +512,15 @@ const EmployeeSearchModal = ({
|
|
|
317
512
|
.filter(Boolean);
|
|
318
513
|
|
|
319
514
|
// Start from the previous global selection keyed by entra_id
|
|
320
|
-
const prevById = new Map(
|
|
515
|
+
const prevById = new Map(
|
|
516
|
+
selectedEmployeesState.map((e) => [e.entra_id, e])
|
|
517
|
+
);
|
|
321
518
|
|
|
322
519
|
// Remove any currently visible employees that are now unselected
|
|
323
520
|
const visibleIds = new Set(employees.map((e) => e.entra_id));
|
|
324
|
-
const visibleSelectedIds = new Set(
|
|
521
|
+
const visibleSelectedIds = new Set(
|
|
522
|
+
visibleSelected.map((e) => e.entra_id)
|
|
523
|
+
);
|
|
325
524
|
employees.forEach((e) => {
|
|
326
525
|
if (visibleIds.has(e.entra_id) && !visibleSelectedIds.has(e.entra_id)) {
|
|
327
526
|
prevById.delete(e.entra_id);
|
|
@@ -413,13 +612,19 @@ const EmployeeSearchModal = ({
|
|
|
413
612
|
if (siteIds && siteIds.length > 0) {
|
|
414
613
|
params.append("site_ids", siteIds.join(","));
|
|
415
614
|
} else if (debouncedAdvancedFilters.selectedSiteId) {
|
|
416
|
-
params.append(
|
|
615
|
+
params.append(
|
|
616
|
+
"site_id",
|
|
617
|
+
debouncedAdvancedFilters.selectedSiteId.toString()
|
|
618
|
+
);
|
|
417
619
|
} else if (siteId) {
|
|
418
620
|
params.append("site_id", siteId.toString());
|
|
419
621
|
}
|
|
420
622
|
|
|
421
623
|
// Handle status filtering
|
|
422
|
-
if (
|
|
624
|
+
if (
|
|
625
|
+
debouncedAdvancedFilters.status &&
|
|
626
|
+
debouncedAdvancedFilters.status !== "all"
|
|
627
|
+
) {
|
|
423
628
|
params.append("status", debouncedAdvancedFilters.status);
|
|
424
629
|
} else if (activeOnly) {
|
|
425
630
|
params.append("active_only", "true");
|
|
@@ -437,7 +642,10 @@ const EmployeeSearchModal = ({
|
|
|
437
642
|
|
|
438
643
|
// Add start date filters
|
|
439
644
|
if (debouncedAdvancedFilters.startDateFrom) {
|
|
440
|
-
params.append(
|
|
645
|
+
params.append(
|
|
646
|
+
"start_date_from",
|
|
647
|
+
debouncedAdvancedFilters.startDateFrom
|
|
648
|
+
);
|
|
441
649
|
}
|
|
442
650
|
if (debouncedAdvancedFilters.startDateTo) {
|
|
443
651
|
params.append("start_date_to", debouncedAdvancedFilters.startDateTo);
|
|
@@ -458,7 +666,10 @@ const EmployeeSearchModal = ({
|
|
|
458
666
|
|
|
459
667
|
// Add maternity leave filter
|
|
460
668
|
if (debouncedAdvancedFilters.onMaternityLeave) {
|
|
461
|
-
params.append(
|
|
669
|
+
params.append(
|
|
670
|
+
"on_maternity_leave",
|
|
671
|
+
debouncedAdvancedFilters.onMaternityLeave
|
|
672
|
+
);
|
|
462
673
|
}
|
|
463
674
|
|
|
464
675
|
// Add DBS number filter
|
|
@@ -468,7 +679,10 @@ const EmployeeSearchModal = ({
|
|
|
468
679
|
|
|
469
680
|
// Add working hours filter
|
|
470
681
|
if (debouncedAdvancedFilters.minHoursPerWeek) {
|
|
471
|
-
params.append(
|
|
682
|
+
params.append(
|
|
683
|
+
"min_hours_per_week",
|
|
684
|
+
debouncedAdvancedFilters.minHoursPerWeek
|
|
685
|
+
);
|
|
472
686
|
}
|
|
473
687
|
|
|
474
688
|
// Add sorting
|
|
@@ -496,9 +710,9 @@ const EmployeeSearchModal = ({
|
|
|
496
710
|
employees: data.data.employees.length,
|
|
497
711
|
pagination: data.data.pagination,
|
|
498
712
|
totalCount: data.data.pagination.totalCount,
|
|
499
|
-
totalPages: data.data.pagination.totalPages
|
|
713
|
+
totalPages: data.data.pagination.totalPages,
|
|
500
714
|
});
|
|
501
|
-
|
|
715
|
+
|
|
502
716
|
setEmployees(data.data.employees);
|
|
503
717
|
setPagination(data.data.pagination);
|
|
504
718
|
trackEvent("employee_search_modal_success", {
|
|
@@ -547,6 +761,17 @@ const EmployeeSearchModal = ({
|
|
|
547
761
|
}
|
|
548
762
|
}, [isOpen, instance, accounts, searchEmployees]);
|
|
549
763
|
|
|
764
|
+
// Fetch profile pictures when employees change and showProfilePics is enabled
|
|
765
|
+
useEffect(() => {
|
|
766
|
+
if (showProfilePics && employees.length > 0) {
|
|
767
|
+
employees.forEach((employee) => {
|
|
768
|
+
if (employee.entra_id) {
|
|
769
|
+
fetchProfilePicture(employee.entra_id);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}, [employees, showProfilePics, fetchProfilePicture]);
|
|
774
|
+
|
|
550
775
|
const handlePageChange = (newPage) => {
|
|
551
776
|
setPagination((prev) => ({ ...prev, page: newPage }));
|
|
552
777
|
};
|
|
@@ -609,15 +834,22 @@ const EmployeeSearchModal = ({
|
|
|
609
834
|
|
|
610
835
|
// Calculate pagination display values
|
|
611
836
|
const actualTotalCount = pagination.totalCount || employees.length;
|
|
612
|
-
const startItem =
|
|
613
|
-
|
|
837
|
+
const startItem =
|
|
838
|
+
actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0;
|
|
839
|
+
const endItem = Math.min(
|
|
840
|
+
pagination.page * pagination.pageSize,
|
|
841
|
+
actualTotalCount
|
|
842
|
+
);
|
|
614
843
|
|
|
615
844
|
if (!isOpen) return null;
|
|
616
845
|
|
|
617
846
|
return (
|
|
618
847
|
<div className="fixed inset-0 overflow-y-auto" style={{ zIndex }}>
|
|
619
848
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
620
|
-
<div
|
|
849
|
+
<div
|
|
850
|
+
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
|
851
|
+
onClick={onClose}
|
|
852
|
+
></div>
|
|
621
853
|
|
|
622
854
|
<div
|
|
623
855
|
className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-7xl flex flex-col"
|
|
@@ -662,7 +894,9 @@ const EmployeeSearchModal = ({
|
|
|
662
894
|
managers={managers}
|
|
663
895
|
activeOnly={activeOnly}
|
|
664
896
|
isAdvancedFiltersOpen={isAdvancedFiltersOpen}
|
|
665
|
-
onToggleAdvancedFilters={() =>
|
|
897
|
+
onToggleAdvancedFilters={() =>
|
|
898
|
+
setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)
|
|
899
|
+
}
|
|
666
900
|
onClearFilters={clearFilters}
|
|
667
901
|
showRoleFilter={showRoleFilter}
|
|
668
902
|
showManagerFilter={showManagerFilter}
|
|
@@ -695,7 +929,7 @@ const EmployeeSearchModal = ({
|
|
|
695
929
|
{/* Results Header */}
|
|
696
930
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
697
931
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
698
|
-
{
|
|
932
|
+
{pagination.totalCount > 0 || employees.length > 0
|
|
699
933
|
? `Showing ${startItem} to ${endItem} of ${actualTotalCount} employees`
|
|
700
934
|
: "No employees found"}
|
|
701
935
|
</div>
|
|
@@ -727,7 +961,9 @@ const EmployeeSearchModal = ({
|
|
|
727
961
|
<TableRow
|
|
728
962
|
key={row.id}
|
|
729
963
|
data-state={row.getIsSelected() && "selected"}
|
|
730
|
-
onClick={() =>
|
|
964
|
+
onClick={() =>
|
|
965
|
+
handleEmployeeSelect(row.original)
|
|
966
|
+
}
|
|
731
967
|
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
732
968
|
>
|
|
733
969
|
{row.getVisibleCells().map((cell) => (
|
|
@@ -771,7 +1007,7 @@ const EmployeeSearchModal = ({
|
|
|
771
1007
|
<Pagination>
|
|
772
1008
|
<PaginationContent>
|
|
773
1009
|
<PaginationItem>
|
|
774
|
-
<PaginationPrevious
|
|
1010
|
+
<PaginationPrevious
|
|
775
1011
|
href="#"
|
|
776
1012
|
onClick={(e) => {
|
|
777
1013
|
e.preventDefault();
|
|
@@ -793,7 +1029,10 @@ const EmployeeSearchModal = ({
|
|
|
793
1029
|
)
|
|
794
1030
|
.filter((page) => {
|
|
795
1031
|
const current = pagination.page;
|
|
796
|
-
const total = Math.max(
|
|
1032
|
+
const total = Math.max(
|
|
1033
|
+
pagination.totalPages,
|
|
1034
|
+
1
|
|
1035
|
+
);
|
|
797
1036
|
return (
|
|
798
1037
|
page === 1 ||
|
|
799
1038
|
page === total ||
|
|
@@ -801,7 +1040,10 @@ const EmployeeSearchModal = ({
|
|
|
801
1040
|
);
|
|
802
1041
|
})
|
|
803
1042
|
.map((page, index, array) => {
|
|
804
|
-
if (
|
|
1043
|
+
if (
|
|
1044
|
+
index > 0 &&
|
|
1045
|
+
array[index - 1] !== page - 1
|
|
1046
|
+
) {
|
|
805
1047
|
return (
|
|
806
1048
|
<React.Fragment key={`ellipsis-${page}`}>
|
|
807
1049
|
<PaginationItem>
|
|
@@ -839,7 +1081,7 @@ const EmployeeSearchModal = ({
|
|
|
839
1081
|
})}
|
|
840
1082
|
|
|
841
1083
|
<PaginationItem>
|
|
842
|
-
<PaginationNext
|
|
1084
|
+
<PaginationNext
|
|
843
1085
|
href="#"
|
|
844
1086
|
onClick={(e) => {
|
|
845
1087
|
e.preventDefault();
|
|
@@ -868,15 +1110,21 @@ const EmployeeSearchModal = ({
|
|
|
868
1110
|
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-3 flex items-center justify-between gap-3">
|
|
869
1111
|
{/* Selection preview */}
|
|
870
1112
|
<div className="flex-1 min-w-0">
|
|
871
|
-
{multiSelect &&
|
|
1113
|
+
{multiSelect &&
|
|
1114
|
+
selectedEmployeesState.length > 0 &&
|
|
872
1115
|
(() => {
|
|
873
1116
|
const total = selectedEmployeesState.length;
|
|
874
1117
|
const preview = selectedEmployeesState.slice(0, 5);
|
|
875
1118
|
const extra = Math.max(total - preview.length, 0);
|
|
876
|
-
const previewText =
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1119
|
+
const previewText =
|
|
1120
|
+
`${total} selected${
|
|
1121
|
+
maxSelections ? ` / ${maxSelections}` : ""
|
|
1122
|
+
}: ` +
|
|
1123
|
+
preview.map((e) => e.full_name).join(", ") +
|
|
1124
|
+
(extra > 0 ? ` + ${extra} more` : "");
|
|
1125
|
+
const fullList = selectedEmployeesState
|
|
1126
|
+
.map((e) => e.full_name)
|
|
1127
|
+
.join(", ");
|
|
880
1128
|
return (
|
|
881
1129
|
<div
|
|
882
1130
|
className="text-sm text-gray-600 dark:text-gray-300 truncate"
|
|
@@ -885,12 +1133,15 @@ const EmployeeSearchModal = ({
|
|
|
885
1133
|
{previewText}
|
|
886
1134
|
</div>
|
|
887
1135
|
);
|
|
888
|
-
})()
|
|
889
|
-
)}
|
|
1136
|
+
})()}
|
|
890
1137
|
</div>
|
|
891
1138
|
<div className="flex items-center gap-2">
|
|
892
1139
|
{multiSelect && (
|
|
893
|
-
<Button
|
|
1140
|
+
<Button
|
|
1141
|
+
variant="outline"
|
|
1142
|
+
onClick={handleClearSelection}
|
|
1143
|
+
disabled={selectedEmployeesState.length === 0}
|
|
1144
|
+
>
|
|
894
1145
|
Clear
|
|
895
1146
|
</Button>
|
|
896
1147
|
)}
|
|
@@ -898,12 +1149,14 @@ const EmployeeSearchModal = ({
|
|
|
898
1149
|
Go Back
|
|
899
1150
|
</Button>
|
|
900
1151
|
{multiSelect && (
|
|
901
|
-
<Button
|
|
902
|
-
onClick={handleConfirmSelection}
|
|
1152
|
+
<Button
|
|
1153
|
+
onClick={handleConfirmSelection}
|
|
903
1154
|
disabled={selectedEmployeesState.length === 0}
|
|
904
1155
|
aria-label={`Confirm ${selectedEmployeesState.length} selected`}
|
|
905
1156
|
>
|
|
906
|
-
{`Confirm (${selectedEmployeesState.length}${
|
|
1157
|
+
{`Confirm (${selectedEmployeesState.length}${
|
|
1158
|
+
maxSelections ? `/${maxSelections}` : ""
|
|
1159
|
+
})`}
|
|
907
1160
|
</Button>
|
|
908
1161
|
)}
|
|
909
1162
|
</div>
|
|
@@ -914,4 +1167,4 @@ const EmployeeSearchModal = ({
|
|
|
914
1167
|
);
|
|
915
1168
|
};
|
|
916
1169
|
|
|
917
|
-
export default EmployeeSearchModal;
|
|
1170
|
+
export default EmployeeSearchModal;
|