@snapdragonsnursery/react-components 1.15.0 → 1.16.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapdragonsnursery/react-components",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -55,14 +55,14 @@
55
55
  "lucide-react": "^0.526.0",
56
56
  "react": "^18.0.0 || ^19.0.0",
57
57
  "react-day-picker": "^9.8.1",
58
- "react-dom": "^18.3.1",
59
58
  "tailwind-merge": "^3.3.1",
60
59
  "tailwindcss": "^4.1.11",
61
60
  "tailwindcss-animate": "^1.0.7"
62
61
  },
63
62
  "peerDependencies": {
64
63
  "@azure/msal-react": ">=1.0.0",
65
- "react": "^18.0.0 || ^19.0.0"
64
+ "react": "^18.0.0 || ^19.0.0",
65
+ "react-dom": "^18.0.0 || ^19.0.0"
66
66
  },
67
67
  "module": "src/index.js",
68
68
  "types": "src/index.d.ts",
@@ -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] = useState(advancedFilters);
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("full_name", columnLabels.full_name || "Name"),
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">ID: {row.original.employee_id}</div>
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("site_name", columnLabels.site_name || "Site"),
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("role_name", columnLabels.role_name || "Role"),
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("start_date", columnLabels.start_date || "Start Date"),
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("employee_status", columnLabels.employee_status || "Status"),
386
+ header: createSortableHeader(
387
+ "employee_status",
388
+ columnLabels.employee_status || "Status"
389
+ ),
218
390
  cell: ({ row }) => {
219
- if (columnRenderers.employee_status) return columnRenderers.employee_status(row.original);
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: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
223
- Inactive: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
224
- 'On Leave': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
225
- 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",
226
403
  };
227
404
  return (
228
405
  <span
229
406
  className={cn(
230
- 'px-2 py-1 text-xs rounded-full',
231
- statusColors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
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 || 'Unknown'}
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(defaultDataColumns.map((c) => [c.accessorKey || c.id, c]));
278
- dataColumns = visibleColumns.map((key) => mapByKey.get(key)).filter(Boolean);
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(employee => employee.entra_id === selectedEmployee.entra_id);
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((acc, selectedEmployee) => {
306
- const rowIndex = employees.findIndex((employee) => employee.entra_id === selectedEmployee.entra_id);
307
- if (rowIndex !== -1) acc[rowIndex] = true;
308
- return acc;
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 = typeof updater === 'function' ? updater(prevSelection) : updater;
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(selectedEmployeesState.map((e) => [e.entra_id, e]));
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(visibleSelected.map((e) => e.entra_id));
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("site_id", debouncedAdvancedFilters.selectedSiteId.toString());
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 (debouncedAdvancedFilters.status && debouncedAdvancedFilters.status !== "all") {
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("start_date_from", debouncedAdvancedFilters.startDateFrom);
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("on_maternity_leave", debouncedAdvancedFilters.onMaternityLeave);
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("min_hours_per_week", debouncedAdvancedFilters.minHoursPerWeek);
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 = actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0;
613
- 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
+ );
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 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>
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={() => setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)}
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
- {(pagination.totalCount > 0 || employees.length > 0)
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={() => handleEmployeeSelect(row.original)}
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(pagination.totalPages, 1);
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 (index > 0 && array[index - 1] !== page - 1) {
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 && selectedEmployeesState.length > 0 && (
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 = `${total} selected${maxSelections ? ` / ${maxSelections}` : ''}: ` +
877
- preview.map((e) => e.full_name).join(', ') +
878
- (extra > 0 ? ` + ${extra} more` : '');
879
- 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(", ");
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 variant="outline" onClick={handleClearSelection} disabled={selectedEmployeesState.length === 0}>
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}${maxSelections ? `/${maxSelections}` : ''})`}
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;