@snapdragonsnursery/react-components 1.13.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.
@@ -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,6 +75,21 @@ const EmployeeSearchModal = ({
74
75
  showEndDateFilter = true,
75
76
  showDbsFilter = true,
76
77
  showWorkingHoursFilter = true,
78
+ // Profile pictures
79
+ showProfilePics = false,
80
+ // Layout controls
81
+ zIndex = 60,
82
+ maxHeightVh = 85,
83
+ // Auth options: provide token directly or a function to fetch it
84
+ authToken = null,
85
+ getAccessToken = null,
86
+ // Graph auth options specifically for Microsoft Graph calls (e.g., profile photos)
87
+ graphAuthToken = null,
88
+ getGraphAccessToken = null,
89
+ // Column customization
90
+ visibleColumns = null,
91
+ columnLabels = {},
92
+ columnRenderers = {},
77
93
  }) => {
78
94
  const [searchTerm, setSearchTerm] = useState("");
79
95
  const [employees, setEmployees] = useState([]);
@@ -108,18 +124,122 @@ const EmployeeSearchModal = ({
108
124
 
109
125
  // Table sorting state
110
126
  const [sorting, setSorting] = useState([]);
111
- const [debouncedAdvancedFilters, setDebouncedAdvancedFilters] = useState(advancedFilters);
127
+ const [debouncedAdvancedFilters, setDebouncedAdvancedFilters] =
128
+ useState(advancedFilters);
112
129
 
113
130
  // State for multi-select mode
114
131
  const [selectedEmployeesState, setSelectedEmployeesState] = useState(
115
132
  selectedEmployees || []
116
133
  );
117
134
 
135
+ // State for profile pictures
136
+ const [profilePics, setProfilePics] = useState(new Map());
137
+ const [loadingProfilePics, setLoadingProfilePics] = useState(new Set());
138
+
118
139
  const { instance, accounts } = useMsal();
119
140
 
120
141
  // Column helper for TanStack table
121
142
  const columnHelper = createColumnHelper();
122
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
+
123
243
  // Helper function to create sortable header
124
244
  const createSortableHeader = (column, label) => {
125
245
  return ({ column: tableColumn }) => {
@@ -149,94 +269,144 @@ const EmployeeSearchModal = ({
149
269
  };
150
270
  };
151
271
 
152
- // Define table columns
153
- const columns = [
154
- // Checkbox column for multi-select
155
- ...(multiSelect
156
- ? [
157
- columnHelper.display({
158
- id: "select",
159
- header: ({ table }) => (
160
- <input
161
- type="checkbox"
162
- checked={table.getIsAllPageRowsSelected()}
163
- onChange={table.getToggleAllPageRowsSelectedHandler()}
164
- className="rounded border-gray-300"
165
- />
166
- ),
167
- cell: ({ row }) => (
168
- <input
169
- type="checkbox"
170
- checked={row.getIsSelected()}
171
- onChange={row.getToggleSelectedHandler()}
172
- className="rounded border-gray-300"
173
- />
174
- ),
175
- size: 50,
176
- }),
177
- ]
178
- : []),
179
- // Name column - sortable
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
+
317
+ // Define default data columns (excluding optional select)
318
+ const defaultDataColumns = [
180
319
  columnHelper.accessor("full_name", {
181
- header: createSortableHeader("full_name", "Name"),
182
- cell: ({ row }) => (
183
- <div>
184
- <div className="font-medium">{row.original.full_name}</div>
185
- <div className="text-sm text-gray-500">
186
- ID: {row.original.employee_id}
187
- </div>
188
- </div>
320
+ header: createSortableHeader(
321
+ "full_name",
322
+ columnLabels.full_name || "Name"
189
323
  ),
324
+ cell: ({ row }) =>
325
+ columnRenderers.full_name ? (
326
+ columnRenderers.full_name(row.original)
327
+ ) : (
328
+ <div>
329
+ <div className="font-medium">{row.original.full_name}</div>
330
+ <div className="text-sm text-gray-500">
331
+ ID: {row.original.employee_id}
332
+ </div>
333
+ </div>
334
+ ),
190
335
  }),
191
- // Site column - sortable
192
336
  columnHelper.accessor("site_name", {
193
- header: createSortableHeader("site_name", "Site"),
194
- cell: ({ row }) => (
195
- <span>{row.original.site_name}</span>
337
+ header: createSortableHeader(
338
+ "site_name",
339
+ columnLabels.site_name || "Site"
196
340
  ),
341
+ cell: ({ row }) =>
342
+ columnRenderers.site_name ? (
343
+ columnRenderers.site_name(row.original)
344
+ ) : (
345
+ <span>{row.original.site_name}</span>
346
+ ),
197
347
  }),
198
- // Role column - sortable
199
348
  columnHelper.accessor("role_name", {
200
- header: createSortableHeader("role_name", "Role"),
201
- cell: ({ row }) => (
202
- <span>{row.original.role_name}</span>
349
+ header: createSortableHeader(
350
+ "role_name",
351
+ columnLabels.role_name || "Role"
203
352
  ),
353
+ cell: ({ row }) =>
354
+ columnRenderers.role_name ? (
355
+ columnRenderers.role_name(row.original)
356
+ ) : (
357
+ <span>{row.original.role_name}</span>
358
+ ),
204
359
  }),
205
- // Email column
206
360
  columnHelper.accessor("email", {
207
- header: "Email",
208
- cell: ({ row }) => (
209
- <span className="text-sm">{row.original.email}</span>
210
- ),
361
+ header: columnLabels.email || "Email",
362
+ cell: ({ row }) =>
363
+ columnRenderers.email ? (
364
+ columnRenderers.email(row.original)
365
+ ) : (
366
+ <span className="text-sm">{row.original.email}</span>
367
+ ),
211
368
  }),
212
- // Start Date column - sortable
213
369
  columnHelper.accessor("start_date", {
214
- header: createSortableHeader("start_date", "Start Date"),
215
- cell: ({ row }) => (
216
- <span>
217
- {row.original.start_date
218
- ? new Date(row.original.start_date).toLocaleDateString("en-GB")
219
- : "N/A"}
220
- </span>
370
+ header: createSortableHeader(
371
+ "start_date",
372
+ columnLabels.start_date || "Start Date"
221
373
  ),
374
+ cell: ({ row }) =>
375
+ columnRenderers.start_date ? (
376
+ columnRenderers.start_date(row.original)
377
+ ) : (
378
+ <span>
379
+ {row.original.start_date
380
+ ? new Date(row.original.start_date).toLocaleDateString("en-GB")
381
+ : "N/A"}
382
+ </span>
383
+ ),
222
384
  }),
223
- // Status column - sortable
224
385
  columnHelper.accessor("employee_status", {
225
- header: createSortableHeader("employee_status", "Status"),
386
+ header: createSortableHeader(
387
+ "employee_status",
388
+ columnLabels.employee_status || "Status"
389
+ ),
226
390
  cell: ({ row }) => {
391
+ if (columnRenderers.employee_status)
392
+ return columnRenderers.employee_status(row.original);
227
393
  const status = row.original.employee_status;
228
394
  const statusColors = {
229
- 'Active': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
230
- 'Inactive': 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
231
- 'On Leave': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
232
- 'Terminated': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
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",
233
403
  };
234
-
235
404
  return (
236
405
  <span
237
406
  className={cn(
238
407
  "px-2 py-1 text-xs rounded-full",
239
- statusColors[status] || "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"
408
+ statusColors[status] ||
409
+ "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"
240
410
  )}
241
411
  >
242
412
  {status || "Unknown"}
@@ -244,19 +414,59 @@ const EmployeeSearchModal = ({
244
414
  );
245
415
  },
246
416
  }),
247
- // Working Hours column
248
417
  columnHelper.accessor("total_hours_per_week", {
249
- header: "Hours/Week",
250
- cell: ({ row }) => (
251
- <span>
252
- {row.original.total_hours_per_week
253
- ? `${row.original.total_hours_per_week}h`
254
- : "N/A"}
255
- </span>
256
- ),
418
+ header: columnLabels.total_hours_per_week || "Hours/Week",
419
+ cell: ({ row }) =>
420
+ columnRenderers.total_hours_per_week ? (
421
+ columnRenderers.total_hours_per_week(row.original)
422
+ ) : (
423
+ <span>
424
+ {row.original.total_hours_per_week
425
+ ? `${row.original.total_hours_per_week}h`
426
+ : "N/A"}
427
+ </span>
428
+ ),
257
429
  }),
258
430
  ];
259
431
 
432
+ const selectColumn = columnHelper.display({
433
+ id: "select",
434
+ header: ({ table }) => (
435
+ <input
436
+ type="checkbox"
437
+ checked={table.getIsAllPageRowsSelected()}
438
+ onChange={table.getToggleAllPageRowsSelectedHandler()}
439
+ className="rounded border-gray-300"
440
+ />
441
+ ),
442
+ cell: ({ row }) => (
443
+ <input
444
+ type="checkbox"
445
+ checked={row.getIsSelected()}
446
+ onChange={row.getToggleSelectedHandler()}
447
+ className="rounded border-gray-300"
448
+ />
449
+ ),
450
+ size: 50,
451
+ });
452
+
453
+ let dataColumns = defaultDataColumns;
454
+ if (Array.isArray(visibleColumns) && visibleColumns.length > 0) {
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];
466
+ }
467
+
468
+ const columns = multiSelect ? [selectColumn, ...dataColumns] : dataColumns;
469
+
260
470
  // Create table instance
261
471
  const table = useReactTable({
262
472
  data: employees,
@@ -266,7 +476,9 @@ const EmployeeSearchModal = ({
266
476
  state: {
267
477
  sorting,
268
478
  rowSelection: selectedEmployeesState.reduce((acc, selectedEmployee) => {
269
- const rowIndex = employees.findIndex(employee => employee.entra_id === selectedEmployee.entra_id);
479
+ const rowIndex = employees.findIndex(
480
+ (employee) => employee.entra_id === selectedEmployee.entra_id
481
+ );
270
482
  if (rowIndex !== -1) {
271
483
  acc[rowIndex] = true;
272
484
  }
@@ -279,13 +491,19 @@ const EmployeeSearchModal = ({
279
491
  // instead of replacing it, so selection is retained across searches/pages.
280
492
 
281
493
  // Build current selection map (for visible rows only)
282
- const prevSelection = selectedEmployeesState.reduce((acc, selectedEmployee) => {
283
- const rowIndex = employees.findIndex((employee) => employee.entra_id === selectedEmployee.entra_id);
284
- if (rowIndex !== -1) acc[rowIndex] = true;
285
- return acc;
286
- }, {});
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
+ );
287
504
 
288
- const updatedSelection = typeof updater === 'function' ? updater(prevSelection) : updater;
505
+ const updatedSelection =
506
+ typeof updater === "function" ? updater(prevSelection) : updater;
289
507
 
290
508
  // Determine which visible employees are selected after the change
291
509
  const visibleSelected = Object.keys(updatedSelection)
@@ -294,11 +512,15 @@ const EmployeeSearchModal = ({
294
512
  .filter(Boolean);
295
513
 
296
514
  // Start from the previous global selection keyed by entra_id
297
- const prevById = new Map(selectedEmployeesState.map((e) => [e.entra_id, e]));
515
+ const prevById = new Map(
516
+ selectedEmployeesState.map((e) => [e.entra_id, e])
517
+ );
298
518
 
299
519
  // Remove any currently visible employees that are now unselected
300
520
  const visibleIds = new Set(employees.map((e) => e.entra_id));
301
- const visibleSelectedIds = new Set(visibleSelected.map((e) => e.entra_id));
521
+ const visibleSelectedIds = new Set(
522
+ visibleSelected.map((e) => e.entra_id)
523
+ );
302
524
  employees.forEach((e) => {
303
525
  if (visibleIds.has(e.entra_id) && !visibleSelectedIds.has(e.entra_id)) {
304
526
  prevById.delete(e.entra_id);
@@ -348,7 +570,34 @@ const EmployeeSearchModal = ({
348
570
  setError(null);
349
571
 
350
572
  try {
351
- const functionKey = import.meta.env.VITE_COMMON_API_FUNCTION_KEY || "";
573
+ // Resolve access token for APIM
574
+ const apimScope = import.meta.env.VITE_APIM_SCOPE;
575
+
576
+ let accessToken = null;
577
+ if (typeof getAccessToken === "function") {
578
+ accessToken = await getAccessToken();
579
+ } else if (authToken) {
580
+ accessToken = authToken;
581
+ } else if (apimScope) {
582
+ try {
583
+ const response = await instance.acquireTokenSilent({
584
+ account: accounts[0],
585
+ scopes: [apimScope],
586
+ });
587
+ accessToken = response.accessToken;
588
+ } catch (silentErr) {
589
+ const response = await instance.acquireTokenPopup({
590
+ scopes: [apimScope],
591
+ });
592
+ accessToken = response.accessToken;
593
+ }
594
+ }
595
+
596
+ if (!accessToken) {
597
+ throw new Error(
598
+ "Missing access token. Provide authToken/getAccessToken or set VITE_APIM_SCOPE."
599
+ );
600
+ }
352
601
 
353
602
  const params = new URLSearchParams({
354
603
  entra_id: accounts[0].localAccountId,
@@ -363,13 +612,19 @@ const EmployeeSearchModal = ({
363
612
  if (siteIds && siteIds.length > 0) {
364
613
  params.append("site_ids", siteIds.join(","));
365
614
  } else if (debouncedAdvancedFilters.selectedSiteId) {
366
- params.append("site_id", debouncedAdvancedFilters.selectedSiteId.toString());
615
+ params.append(
616
+ "site_id",
617
+ debouncedAdvancedFilters.selectedSiteId.toString()
618
+ );
367
619
  } else if (siteId) {
368
620
  params.append("site_id", siteId.toString());
369
621
  }
370
622
 
371
623
  // Handle status filtering
372
- if (debouncedAdvancedFilters.status && debouncedAdvancedFilters.status !== "all") {
624
+ if (
625
+ debouncedAdvancedFilters.status &&
626
+ debouncedAdvancedFilters.status !== "all"
627
+ ) {
373
628
  params.append("status", debouncedAdvancedFilters.status);
374
629
  } else if (activeOnly) {
375
630
  params.append("active_only", "true");
@@ -387,7 +642,10 @@ const EmployeeSearchModal = ({
387
642
 
388
643
  // Add start date filters
389
644
  if (debouncedAdvancedFilters.startDateFrom) {
390
- params.append("start_date_from", debouncedAdvancedFilters.startDateFrom);
645
+ params.append(
646
+ "start_date_from",
647
+ debouncedAdvancedFilters.startDateFrom
648
+ );
391
649
  }
392
650
  if (debouncedAdvancedFilters.startDateTo) {
393
651
  params.append("start_date_to", debouncedAdvancedFilters.startDateTo);
@@ -408,7 +666,10 @@ const EmployeeSearchModal = ({
408
666
 
409
667
  // Add maternity leave filter
410
668
  if (debouncedAdvancedFilters.onMaternityLeave) {
411
- params.append("on_maternity_leave", debouncedAdvancedFilters.onMaternityLeave);
669
+ params.append(
670
+ "on_maternity_leave",
671
+ debouncedAdvancedFilters.onMaternityLeave
672
+ );
412
673
  }
413
674
 
414
675
  // Add DBS number filter
@@ -418,7 +679,10 @@ const EmployeeSearchModal = ({
418
679
 
419
680
  // Add working hours filter
420
681
  if (debouncedAdvancedFilters.minHoursPerWeek) {
421
- params.append("min_hours_per_week", debouncedAdvancedFilters.minHoursPerWeek);
682
+ params.append(
683
+ "min_hours_per_week",
684
+ debouncedAdvancedFilters.minHoursPerWeek
685
+ );
422
686
  }
423
687
 
424
688
  // Add sorting
@@ -426,14 +690,11 @@ const EmployeeSearchModal = ({
426
690
  params.append("sort_order", debouncedAdvancedFilters.sortOrder);
427
691
 
428
692
  const apiResponse = await fetch(
429
- `${
430
- import.meta.env.VITE_COMMON_API_BASE_URL ||
431
- "https://snaps-common-api.azurewebsites.net"
432
- }/api/search-employees?${params}`,
693
+ `https://snapdragons.azure-api.net/api/employees/search-employees?${params}`,
433
694
  {
434
695
  headers: {
435
696
  "Content-Type": "application/json",
436
- "x-functions-key": functionKey,
697
+ Authorization: `Bearer ${accessToken}`,
437
698
  },
438
699
  }
439
700
  );
@@ -449,9 +710,9 @@ const EmployeeSearchModal = ({
449
710
  employees: data.data.employees.length,
450
711
  pagination: data.data.pagination,
451
712
  totalCount: data.data.pagination.totalCount,
452
- totalPages: data.data.pagination.totalPages
713
+ totalPages: data.data.pagination.totalPages,
453
714
  });
454
-
715
+
455
716
  setEmployees(data.data.employees);
456
717
  setPagination(data.data.pagination);
457
718
  trackEvent("employee_search_modal_success", {
@@ -500,6 +761,17 @@ const EmployeeSearchModal = ({
500
761
  }
501
762
  }, [isOpen, instance, accounts, searchEmployees]);
502
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
+
503
775
  const handlePageChange = (newPage) => {
504
776
  setPagination((prev) => ({ ...prev, page: newPage }));
505
777
  };
@@ -562,17 +834,27 @@ const EmployeeSearchModal = ({
562
834
 
563
835
  // Calculate pagination display values
564
836
  const actualTotalCount = pagination.totalCount || employees.length;
565
- const startItem = actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0;
566
- const endItem = Math.min(pagination.page * pagination.pageSize, actualTotalCount);
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
+ );
567
843
 
568
844
  if (!isOpen) return null;
569
845
 
570
846
  return (
571
- <div className="fixed inset-0 z-50 overflow-y-auto">
847
+ <div className="fixed inset-0 overflow-y-auto" style={{ zIndex }}>
572
848
  <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
573
- <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose}></div>
849
+ <div
850
+ className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
851
+ onClick={onClose}
852
+ ></div>
574
853
 
575
- <div 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">
854
+ <div
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"
856
+ style={{ maxHeight: `${maxHeightVh}vh` }}
857
+ >
576
858
  {/* Header */}
577
859
  <div className="bg-white dark:bg-gray-800 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
578
860
  <div className="flex items-center justify-between">
@@ -590,7 +872,7 @@ const EmployeeSearchModal = ({
590
872
  </div>
591
873
 
592
874
  {/* Content */}
593
- <div className="px-6 py-4 max-h-[70vh] overflow-y-auto">
875
+ <div className="px-6 py-4 overflow-y-auto flex-1">
594
876
  {/* Search Input */}
595
877
  <div className="mb-4">
596
878
  <Input
@@ -612,7 +894,9 @@ const EmployeeSearchModal = ({
612
894
  managers={managers}
613
895
  activeOnly={activeOnly}
614
896
  isAdvancedFiltersOpen={isAdvancedFiltersOpen}
615
- onToggleAdvancedFilters={() => setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)}
897
+ onToggleAdvancedFilters={() =>
898
+ setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)
899
+ }
616
900
  onClearFilters={clearFilters}
617
901
  showRoleFilter={showRoleFilter}
618
902
  showManagerFilter={showManagerFilter}
@@ -645,7 +929,7 @@ const EmployeeSearchModal = ({
645
929
  {/* Results Header */}
646
930
  <div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
647
931
  <div className="text-sm text-gray-600 dark:text-gray-400">
648
- {(pagination.totalCount > 0 || employees.length > 0)
932
+ {pagination.totalCount > 0 || employees.length > 0
649
933
  ? `Showing ${startItem} to ${endItem} of ${actualTotalCount} employees`
650
934
  : "No employees found"}
651
935
  </div>
@@ -677,7 +961,9 @@ const EmployeeSearchModal = ({
677
961
  <TableRow
678
962
  key={row.id}
679
963
  data-state={row.getIsSelected() && "selected"}
680
- onClick={() => handleEmployeeSelect(row.original)}
964
+ onClick={() =>
965
+ handleEmployeeSelect(row.original)
966
+ }
681
967
  className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
682
968
  >
683
969
  {row.getVisibleCells().map((cell) => (
@@ -721,7 +1007,7 @@ const EmployeeSearchModal = ({
721
1007
  <Pagination>
722
1008
  <PaginationContent>
723
1009
  <PaginationItem>
724
- <PaginationPrevious
1010
+ <PaginationPrevious
725
1011
  href="#"
726
1012
  onClick={(e) => {
727
1013
  e.preventDefault();
@@ -743,7 +1029,10 @@ const EmployeeSearchModal = ({
743
1029
  )
744
1030
  .filter((page) => {
745
1031
  const current = pagination.page;
746
- const total = Math.max(pagination.totalPages, 1);
1032
+ const total = Math.max(
1033
+ pagination.totalPages,
1034
+ 1
1035
+ );
747
1036
  return (
748
1037
  page === 1 ||
749
1038
  page === total ||
@@ -751,7 +1040,10 @@ const EmployeeSearchModal = ({
751
1040
  );
752
1041
  })
753
1042
  .map((page, index, array) => {
754
- if (index > 0 && array[index - 1] !== page - 1) {
1043
+ if (
1044
+ index > 0 &&
1045
+ array[index - 1] !== page - 1
1046
+ ) {
755
1047
  return (
756
1048
  <React.Fragment key={`ellipsis-${page}`}>
757
1049
  <PaginationItem>
@@ -789,7 +1081,7 @@ const EmployeeSearchModal = ({
789
1081
  })}
790
1082
 
791
1083
  <PaginationItem>
792
- <PaginationNext
1084
+ <PaginationNext
793
1085
  href="#"
794
1086
  onClick={(e) => {
795
1087
  e.preventDefault();
@@ -818,15 +1110,21 @@ const EmployeeSearchModal = ({
818
1110
  <div className="bg-gray-50 dark:bg-gray-700 px-6 py-3 flex items-center justify-between gap-3">
819
1111
  {/* Selection preview */}
820
1112
  <div className="flex-1 min-w-0">
821
- {multiSelect && selectedEmployeesState.length > 0 && (
1113
+ {multiSelect &&
1114
+ selectedEmployeesState.length > 0 &&
822
1115
  (() => {
823
1116
  const total = selectedEmployeesState.length;
824
1117
  const preview = selectedEmployeesState.slice(0, 5);
825
1118
  const extra = Math.max(total - preview.length, 0);
826
- const previewText = `${total} selected${maxSelections ? ` / ${maxSelections}` : ''}: ` +
827
- preview.map((e) => e.full_name).join(', ') +
828
- (extra > 0 ? ` + ${extra} more` : '');
829
- const fullList = selectedEmployeesState.map((e) => e.full_name).join(', ');
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(", ");
830
1128
  return (
831
1129
  <div
832
1130
  className="text-sm text-gray-600 dark:text-gray-300 truncate"
@@ -835,12 +1133,15 @@ const EmployeeSearchModal = ({
835
1133
  {previewText}
836
1134
  </div>
837
1135
  );
838
- })()
839
- )}
1136
+ })()}
840
1137
  </div>
841
1138
  <div className="flex items-center gap-2">
842
1139
  {multiSelect && (
843
- <Button variant="outline" onClick={handleClearSelection} disabled={selectedEmployeesState.length === 0}>
1140
+ <Button
1141
+ variant="outline"
1142
+ onClick={handleClearSelection}
1143
+ disabled={selectedEmployeesState.length === 0}
1144
+ >
844
1145
  Clear
845
1146
  </Button>
846
1147
  )}
@@ -848,12 +1149,14 @@ const EmployeeSearchModal = ({
848
1149
  Go Back
849
1150
  </Button>
850
1151
  {multiSelect && (
851
- <Button
852
- onClick={handleConfirmSelection}
1152
+ <Button
1153
+ onClick={handleConfirmSelection}
853
1154
  disabled={selectedEmployeesState.length === 0}
854
1155
  aria-label={`Confirm ${selectedEmployeesState.length} selected`}
855
1156
  >
856
- {`Confirm (${selectedEmployeesState.length}${maxSelections ? `/${maxSelections}` : ''})`}
1157
+ {`Confirm (${selectedEmployeesState.length}${
1158
+ maxSelections ? `/${maxSelections}` : ""
1159
+ })`}
857
1160
  </Button>
858
1161
  )}
859
1162
  </div>
@@ -864,4 +1167,4 @@ const EmployeeSearchModal = ({
864
1167
  );
865
1168
  };
866
1169
 
867
- export default EmployeeSearchModal;
1170
+ export default EmployeeSearchModal;