@snapdragonsnursery/react-components 1.2.0 → 1.3.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.
@@ -0,0 +1,817 @@
1
+ // Employee Search Modal Component
2
+ // Provides a modal interface for employee search with advanced filtering and selection capabilities
3
+ // Usage: <EmployeeSearchModal isOpen={isOpen} onClose={onClose} onSelect={handleSelect} />
4
+
5
+ import React, { useState, useEffect, useCallback } from "react";
6
+ import { useMsal } from "@azure/msal-react";
7
+ import {
8
+ useReactTable,
9
+ getCoreRowModel,
10
+ getSortedRowModel,
11
+ flexRender,
12
+ createColumnHelper,
13
+ } from "@tanstack/react-table";
14
+ import { trackEvent } from "./telemetry";
15
+ import { Button } from "./components/ui/button";
16
+ import { Input } from "./components/ui/input";
17
+ import {
18
+ Table,
19
+ TableBody,
20
+ TableCell,
21
+ TableHead,
22
+ TableHeader,
23
+ TableRow,
24
+ } from "./components/ui/table";
25
+ import {
26
+ Pagination,
27
+ PaginationContent,
28
+ PaginationEllipsis,
29
+ PaginationItem,
30
+ PaginationLink,
31
+ PaginationNext,
32
+ PaginationPrevious,
33
+ } from "./components/ui/pagination";
34
+ import {
35
+ CheckCircleIcon,
36
+ XCircleIcon,
37
+ ChevronUpIcon,
38
+ ChevronDownIcon,
39
+ XMarkIcon,
40
+ } from "@heroicons/react/24/outline";
41
+ import { cn } from "./lib/utils";
42
+ import EmployeeSearchFilters from "./components/EmployeeSearchFilters";
43
+
44
+ const EmployeeSearchModal = ({
45
+ isOpen = false,
46
+ onClose,
47
+ onSelect,
48
+ title = "Select Employee",
49
+ searchPlaceholder = "Search by name, employee ID, or email...",
50
+ siteId = null,
51
+ siteIds = null,
52
+ sites = null,
53
+ roles = null,
54
+ managers = null,
55
+ activeOnly = true,
56
+ status = null,
57
+ startDateFrom = null,
58
+ startDateTo = null,
59
+ endDateFrom = null,
60
+ endDateTo = null,
61
+ sortBy = "surname",
62
+ sortOrder = "asc",
63
+ applicationContext = "employee-search-modal",
64
+ bypassPermissions = false,
65
+ multiSelect = false,
66
+ maxSelections = null,
67
+ selectedEmployees = [],
68
+ // Configurable filter visibility
69
+ showRoleFilter = true,
70
+ showManagerFilter = true,
71
+ showTermTimeFilter = true,
72
+ showMaternityFilter = true,
73
+ showStartDateFilter = true,
74
+ showEndDateFilter = true,
75
+ showDbsFilter = true,
76
+ showWorkingHoursFilter = true,
77
+ }) => {
78
+ const [searchTerm, setSearchTerm] = useState("");
79
+ const [employees, setEmployees] = useState([]);
80
+ const [loading, setLoading] = useState(false);
81
+ const [error, setError] = useState(null);
82
+ const [pagination, setPagination] = useState({
83
+ page: 1,
84
+ pageSize: 20,
85
+ totalCount: 0,
86
+ totalPages: 0,
87
+ hasNextPage: false,
88
+ hasPreviousPage: false,
89
+ });
90
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
91
+ const [isAdvancedFiltersOpen, setIsAdvancedFiltersOpen] = useState(false);
92
+ const [advancedFilters, setAdvancedFilters] = useState({
93
+ status: status || (activeOnly ? "Active" : "all"),
94
+ selectedSiteId: siteId || "",
95
+ roleId: "",
96
+ managerId: "",
97
+ startDateFrom: startDateFrom || "",
98
+ startDateTo: startDateTo || "",
99
+ endDateFrom: endDateFrom || "",
100
+ endDateTo: endDateTo || "",
101
+ termTimeOnly: "",
102
+ onMaternityLeave: "",
103
+ dbsNumber: "",
104
+ minHoursPerWeek: "",
105
+ sortBy: sortBy,
106
+ sortOrder: sortOrder,
107
+ });
108
+
109
+ // Table sorting state
110
+ const [sorting, setSorting] = useState([]);
111
+ const [debouncedAdvancedFilters, setDebouncedAdvancedFilters] = useState(advancedFilters);
112
+
113
+ // State for multi-select mode
114
+ const [selectedEmployeesState, setSelectedEmployeesState] = useState(
115
+ selectedEmployees || []
116
+ );
117
+
118
+ const { instance, accounts } = useMsal();
119
+
120
+ // Column helper for TanStack table
121
+ const columnHelper = createColumnHelper();
122
+
123
+ // Helper function to create sortable header
124
+ const createSortableHeader = (column, label) => {
125
+ return ({ column: tableColumn }) => {
126
+ const isSorted = tableColumn.getIsSorted();
127
+ return (
128
+ <div
129
+ className="flex items-center space-x-1 cursor-pointer select-none"
130
+ onClick={tableColumn.getToggleSortingHandler()}
131
+ >
132
+ <span>{label}</span>
133
+ <div className="flex flex-col">
134
+ <ChevronUpIcon
135
+ className={cn(
136
+ "h-3 w-3 transition-colors",
137
+ isSorted === "asc" ? "text-blue-500" : "text-gray-400"
138
+ )}
139
+ />
140
+ <ChevronDownIcon
141
+ className={cn(
142
+ "h-3 w-3 transition-colors -mt-1",
143
+ isSorted === "desc" ? "text-blue-500" : "text-gray-400"
144
+ )}
145
+ />
146
+ </div>
147
+ </div>
148
+ );
149
+ };
150
+ };
151
+
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
180
+ 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>
189
+ ),
190
+ }),
191
+ // Site column - sortable
192
+ columnHelper.accessor("site_name", {
193
+ header: createSortableHeader("site_name", "Site"),
194
+ cell: ({ row }) => (
195
+ <span>{row.original.site_name}</span>
196
+ ),
197
+ }),
198
+ // Role column - sortable
199
+ columnHelper.accessor("role_name", {
200
+ header: createSortableHeader("role_name", "Role"),
201
+ cell: ({ row }) => (
202
+ <span>{row.original.role_name}</span>
203
+ ),
204
+ }),
205
+ // Email column
206
+ columnHelper.accessor("email", {
207
+ header: "Email",
208
+ cell: ({ row }) => (
209
+ <span className="text-sm">{row.original.email}</span>
210
+ ),
211
+ }),
212
+ // Start Date column - sortable
213
+ 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>
221
+ ),
222
+ }),
223
+ // Status column - sortable
224
+ columnHelper.accessor("employee_status", {
225
+ header: createSortableHeader("employee_status", "Status"),
226
+ cell: ({ row }) => {
227
+ const status = row.original.employee_status;
228
+ 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',
233
+ };
234
+
235
+ return (
236
+ <span
237
+ className={cn(
238
+ "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"
240
+ )}
241
+ >
242
+ {status || "Unknown"}
243
+ </span>
244
+ );
245
+ },
246
+ }),
247
+ // Working Hours column
248
+ 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
+ ),
257
+ }),
258
+ ];
259
+
260
+ // Create table instance
261
+ const table = useReactTable({
262
+ data: employees,
263
+ columns,
264
+ getCoreRowModel: getCoreRowModel(),
265
+ getSortedRowModel: getSortedRowModel(),
266
+ state: {
267
+ sorting,
268
+ rowSelection: selectedEmployeesState.reduce((acc, selectedEmployee) => {
269
+ const rowIndex = employees.findIndex(employee => employee.entra_id === selectedEmployee.entra_id);
270
+ if (rowIndex !== -1) {
271
+ acc[rowIndex] = true;
272
+ }
273
+ return acc;
274
+ }, {}),
275
+ },
276
+ onSortingChange: setSorting,
277
+ onRowSelectionChange: (updater) => {
278
+ const newSelection =
279
+ typeof updater === "function" ? updater({}) : updater;
280
+ const selectedRows = Object.keys(newSelection).map(
281
+ (index) => employees[parseInt(index)]
282
+ );
283
+ setSelectedEmployeesState(selectedRows);
284
+ },
285
+ });
286
+
287
+ // Debounce search term
288
+ useEffect(() => {
289
+ const timer = setTimeout(() => {
290
+ setDebouncedSearchTerm(searchTerm);
291
+ setPagination((prev) => ({ ...prev, page: 1 }));
292
+ }, 300);
293
+
294
+ return () => clearTimeout(timer);
295
+ }, [searchTerm]);
296
+
297
+ // Debounce advanced filters
298
+ useEffect(() => {
299
+ const timer = setTimeout(() => {
300
+ setDebouncedAdvancedFilters(advancedFilters);
301
+ setPagination((prev) => ({ ...prev, page: 1 }));
302
+ }, 500); // Slightly longer delay for filters
303
+
304
+ return () => clearTimeout(timer);
305
+ }, [advancedFilters]);
306
+
307
+ // Search employees
308
+ const searchEmployees = useCallback(async () => {
309
+ if (!instance || !accounts[0]) {
310
+ setError("Authentication required");
311
+ return;
312
+ }
313
+
314
+ setLoading(true);
315
+ setError(null);
316
+
317
+ try {
318
+ const functionKey = process.env.VITE_COMMON_API_FUNCTION_KEY || "";
319
+
320
+ const params = new URLSearchParams({
321
+ entra_id: accounts[0].localAccountId,
322
+ search_term: debouncedSearchTerm,
323
+ page: pagination.page,
324
+ page_size: pagination.pageSize,
325
+ application_context: applicationContext,
326
+ bypass_permissions: bypassPermissions.toString(),
327
+ });
328
+
329
+ // Handle site filtering
330
+ if (siteIds && siteIds.length > 0) {
331
+ params.append("site_ids", siteIds.join(","));
332
+ } else if (debouncedAdvancedFilters.selectedSiteId) {
333
+ params.append("site_id", debouncedAdvancedFilters.selectedSiteId.toString());
334
+ } else if (siteId) {
335
+ params.append("site_id", siteId.toString());
336
+ }
337
+
338
+ // Handle status filtering
339
+ if (debouncedAdvancedFilters.status && debouncedAdvancedFilters.status !== "all") {
340
+ params.append("status", debouncedAdvancedFilters.status);
341
+ } else if (activeOnly) {
342
+ params.append("active_only", "true");
343
+ }
344
+
345
+ // Add role filter
346
+ if (debouncedAdvancedFilters.roleId) {
347
+ params.append("role_id", debouncedAdvancedFilters.roleId);
348
+ }
349
+
350
+ // Add manager filter
351
+ if (debouncedAdvancedFilters.managerId) {
352
+ params.append("manager_id", debouncedAdvancedFilters.managerId);
353
+ }
354
+
355
+ // Add start date filters
356
+ if (debouncedAdvancedFilters.startDateFrom) {
357
+ params.append("start_date_from", debouncedAdvancedFilters.startDateFrom);
358
+ }
359
+ if (debouncedAdvancedFilters.startDateTo) {
360
+ params.append("start_date_to", debouncedAdvancedFilters.startDateTo);
361
+ }
362
+
363
+ // Add end date filters
364
+ if (debouncedAdvancedFilters.endDateFrom) {
365
+ params.append("end_date_from", debouncedAdvancedFilters.endDateFrom);
366
+ }
367
+ if (debouncedAdvancedFilters.endDateTo) {
368
+ params.append("end_date_to", debouncedAdvancedFilters.endDateTo);
369
+ }
370
+
371
+ // Add term time only filter
372
+ if (debouncedAdvancedFilters.termTimeOnly) {
373
+ params.append("term_time_only", debouncedAdvancedFilters.termTimeOnly);
374
+ }
375
+
376
+ // Add maternity leave filter
377
+ if (debouncedAdvancedFilters.onMaternityLeave) {
378
+ params.append("on_maternity_leave", debouncedAdvancedFilters.onMaternityLeave);
379
+ }
380
+
381
+ // Add DBS number filter
382
+ if (debouncedAdvancedFilters.dbsNumber) {
383
+ params.append("dbs_number", debouncedAdvancedFilters.dbsNumber);
384
+ }
385
+
386
+ // Add working hours filter
387
+ if (debouncedAdvancedFilters.minHoursPerWeek) {
388
+ params.append("min_hours_per_week", debouncedAdvancedFilters.minHoursPerWeek);
389
+ }
390
+
391
+ // Add sorting
392
+ params.append("sort_by", debouncedAdvancedFilters.sortBy);
393
+ params.append("sort_order", debouncedAdvancedFilters.sortOrder);
394
+
395
+ const apiResponse = await fetch(
396
+ `${
397
+ process.env.VITE_COMMON_API_BASE_URL ||
398
+ "https://snaps-common-api.azurewebsites.net"
399
+ }/api/search-employees?${params}`,
400
+ {
401
+ headers: {
402
+ "Content-Type": "application/json",
403
+ "x-functions-key": functionKey,
404
+ },
405
+ }
406
+ );
407
+
408
+ if (!apiResponse.ok) {
409
+ throw new Error(`API request failed: ${apiResponse.status}`);
410
+ }
411
+
412
+ const data = await apiResponse.json();
413
+
414
+ if (data.success) {
415
+ console.log("🔍 API Response:", {
416
+ employees: data.data.employees.length,
417
+ pagination: data.data.pagination,
418
+ totalCount: data.data.pagination.totalCount,
419
+ totalPages: data.data.pagination.totalPages
420
+ });
421
+
422
+ setEmployees(data.data.employees);
423
+ setPagination(data.data.pagination);
424
+ trackEvent("employee_search_modal_success", {
425
+ searchTerm: debouncedSearchTerm,
426
+ resultCount: data.data.employees.length,
427
+ totalCount: data.data.pagination.totalCount,
428
+ });
429
+ } else {
430
+ throw new Error(data.error || "Search failed");
431
+ }
432
+ } catch (err) {
433
+ console.error("Error searching employees:", err);
434
+ setError(err.message);
435
+ trackEvent("employee_search_modal_error", {
436
+ error: err.message,
437
+ searchTerm: debouncedSearchTerm,
438
+ });
439
+ } finally {
440
+ setLoading(false);
441
+ }
442
+ }, [
443
+ instance,
444
+ accounts,
445
+ debouncedSearchTerm,
446
+ pagination.page,
447
+ pagination.pageSize,
448
+ siteId,
449
+ siteIds,
450
+ activeOnly,
451
+ debouncedAdvancedFilters,
452
+ applicationContext,
453
+ bypassPermissions,
454
+ ]);
455
+
456
+ // Search when debounced term changes
457
+ useEffect(() => {
458
+ if (isOpen) {
459
+ searchEmployees();
460
+ }
461
+ }, [debouncedSearchTerm, pagination.page, searchEmployees, isOpen]);
462
+
463
+ // Initial search on modal open
464
+ useEffect(() => {
465
+ if (isOpen && instance && accounts[0]) {
466
+ searchEmployees();
467
+ }
468
+ }, [isOpen, instance, accounts, searchEmployees]);
469
+
470
+ const handlePageChange = (newPage) => {
471
+ setPagination((prev) => ({ ...prev, page: newPage }));
472
+ };
473
+
474
+ const handleEmployeeSelect = (employee) => {
475
+ if (multiSelect) {
476
+ const isAlreadySelected = selectedEmployeesState.some(
477
+ (selected) => selected.entra_id === employee.entra_id
478
+ );
479
+
480
+ if (isAlreadySelected) {
481
+ const updatedSelection = selectedEmployeesState.filter(
482
+ (selected) => selected.entra_id !== employee.entra_id
483
+ );
484
+ setSelectedEmployeesState(updatedSelection);
485
+ } else {
486
+ if (maxSelections && selectedEmployeesState.length >= maxSelections) {
487
+ return;
488
+ }
489
+ const updatedSelection = [...selectedEmployeesState, employee];
490
+ setSelectedEmployeesState(updatedSelection);
491
+ }
492
+ } else {
493
+ onSelect(employee);
494
+ onClose();
495
+ }
496
+ };
497
+
498
+ const handleConfirmSelection = () => {
499
+ if (multiSelect && selectedEmployeesState.length > 0) {
500
+ onSelect(selectedEmployeesState);
501
+ onClose();
502
+ }
503
+ };
504
+
505
+ const clearFilters = () => {
506
+ const clearedFilters = {
507
+ status: activeOnly ? "Active" : "all",
508
+ selectedSiteId: "",
509
+ roleId: "",
510
+ managerId: "",
511
+ startDateFrom: "",
512
+ startDateTo: "",
513
+ endDateFrom: "",
514
+ endDateTo: "",
515
+ termTimeOnly: "",
516
+ onMaternityLeave: "",
517
+ dbsNumber: "",
518
+ minHoursPerWeek: "",
519
+ sortBy: "surname",
520
+ sortOrder: "asc",
521
+ };
522
+ setAdvancedFilters(clearedFilters);
523
+ setDebouncedAdvancedFilters(clearedFilters); // Clear immediately
524
+ };
525
+
526
+ // Calculate pagination display values
527
+ const actualTotalCount = pagination.totalCount || employees.length;
528
+ const startItem = actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0;
529
+ const endItem = Math.min(pagination.page * pagination.pageSize, actualTotalCount);
530
+
531
+ if (!isOpen) return null;
532
+
533
+ return (
534
+ <div className="fixed inset-0 z-50 overflow-y-auto">
535
+ <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
536
+ <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose}></div>
537
+
538
+ <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">
539
+ {/* Header */}
540
+ <div className="bg-white dark:bg-gray-800 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
541
+ <div className="flex items-center justify-between">
542
+ <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
543
+ {title}
544
+ </h3>
545
+ <button
546
+ onClick={onClose}
547
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
548
+ >
549
+ <XMarkIcon className="h-6 w-6" />
550
+ </button>
551
+ </div>
552
+ {multiSelect && (
553
+ <div className="mt-2 flex items-center justify-between">
554
+ <span className="text-sm text-gray-600 dark:text-gray-400">
555
+ {selectedEmployeesState.length} selected
556
+ {maxSelections && ` / ${maxSelections}`}
557
+ </span>
558
+ {selectedEmployeesState.length > 0 && (
559
+ <Button
560
+ size="sm"
561
+ onClick={handleConfirmSelection}
562
+ >
563
+ Confirm Selection
564
+ </Button>
565
+ )}
566
+ </div>
567
+ )}
568
+ </div>
569
+
570
+ {/* Content */}
571
+ <div className="px-6 py-4 max-h-[70vh] overflow-y-auto">
572
+ {/* Search Input */}
573
+ <div className="mb-4">
574
+ <Input
575
+ type="text"
576
+ placeholder={searchPlaceholder}
577
+ value={searchTerm}
578
+ onChange={(e) => setSearchTerm(e.target.value)}
579
+ className="w-full"
580
+ />
581
+ </div>
582
+
583
+ {/* Advanced Filters */}
584
+ <EmployeeSearchFilters
585
+ filters={advancedFilters}
586
+ onFiltersChange={setAdvancedFilters}
587
+ onApplyFilters={setAdvancedFilters}
588
+ sites={sites}
589
+ roles={roles}
590
+ managers={managers}
591
+ activeOnly={activeOnly}
592
+ isAdvancedFiltersOpen={isAdvancedFiltersOpen}
593
+ onToggleAdvancedFilters={() => setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)}
594
+ onClearFilters={clearFilters}
595
+ showRoleFilter={showRoleFilter}
596
+ showManagerFilter={showManagerFilter}
597
+ showTermTimeFilter={showTermTimeFilter}
598
+ showMaternityFilter={showMaternityFilter}
599
+ showStartDateFilter={showStartDateFilter}
600
+ showEndDateFilter={showEndDateFilter}
601
+ showDbsFilter={showDbsFilter}
602
+ showWorkingHoursFilter={showWorkingHoursFilter}
603
+ />
604
+
605
+ {/* Results */}
606
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
607
+ {loading && (
608
+ <div className="flex items-center justify-center p-8">
609
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
610
+ </div>
611
+ )}
612
+
613
+ {error && (
614
+ <div className="p-6 text-center">
615
+ <div className="text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-4 rounded-lg">
616
+ {error}
617
+ </div>
618
+ </div>
619
+ )}
620
+
621
+ {!loading && !error && (
622
+ <>
623
+ {/* Results Header */}
624
+ <div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
625
+ <div className="text-sm text-gray-600 dark:text-gray-400">
626
+ {(pagination.totalCount > 0 || employees.length > 0)
627
+ ? `Showing ${startItem} to ${endItem} of ${actualTotalCount} employees`
628
+ : "No employees found"}
629
+ </div>
630
+ </div>
631
+
632
+ {/* Table */}
633
+ {employees.length > 0 ? (
634
+ <div className="overflow-x-auto">
635
+ <Table>
636
+ <TableHeader>
637
+ {table.getHeaderGroups().map((headerGroup) => (
638
+ <TableRow key={headerGroup.id}>
639
+ {headerGroup.headers.map((header) => (
640
+ <TableHead key={header.id}>
641
+ {header.isPlaceholder
642
+ ? null
643
+ : flexRender(
644
+ header.column.columnDef.header,
645
+ header.getContext()
646
+ )}
647
+ </TableHead>
648
+ ))}
649
+ </TableRow>
650
+ ))}
651
+ </TableHeader>
652
+ <TableBody>
653
+ {table.getRowModel().rows?.length ? (
654
+ table.getRowModel().rows.map((row) => (
655
+ <TableRow
656
+ key={row.id}
657
+ data-state={row.getIsSelected() && "selected"}
658
+ onClick={() => handleEmployeeSelect(row.original)}
659
+ className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
660
+ >
661
+ {row.getVisibleCells().map((cell) => (
662
+ <TableCell key={cell.id}>
663
+ {flexRender(
664
+ cell.column.columnDef.cell,
665
+ cell.getContext()
666
+ )}
667
+ </TableCell>
668
+ ))}
669
+ </TableRow>
670
+ ))
671
+ ) : (
672
+ <TableRow>
673
+ <TableCell
674
+ colSpan={columns.length}
675
+ className="h-24 text-center"
676
+ >
677
+ {debouncedSearchTerm
678
+ ? "No employees found matching your search."
679
+ : "Start typing to search for employees."}
680
+ </TableCell>
681
+ </TableRow>
682
+ )}
683
+ </TableBody>
684
+ </Table>
685
+ </div>
686
+ ) : (
687
+ <div className="p-6 text-center text-gray-500 dark:text-gray-400">
688
+ {debouncedSearchTerm
689
+ ? "No employees found matching your search."
690
+ : "Start typing to search for employees."}
691
+ </div>
692
+ )}
693
+
694
+ {/* Pagination */}
695
+ {(pagination.totalCount > 0 || employees.length > 0) && (
696
+ <div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
697
+ {(pagination.totalPages > 1 || employees.length > 0) && (
698
+ <div className="flex justify-center">
699
+ <Pagination>
700
+ <PaginationContent>
701
+ <PaginationItem>
702
+ <PaginationPrevious
703
+ href="#"
704
+ onClick={(e) => {
705
+ e.preventDefault();
706
+ if (pagination.hasPreviousPage) {
707
+ handlePageChange(pagination.page - 1);
708
+ }
709
+ }}
710
+ className={cn(
711
+ !pagination.hasPreviousPage &&
712
+ "pointer-events-none opacity-50"
713
+ )}
714
+ />
715
+ </PaginationItem>
716
+
717
+ {/* Page numbers */}
718
+ {Array.from(
719
+ { length: Math.max(pagination.totalPages, 1) },
720
+ (_, i) => i + 1
721
+ )
722
+ .filter((page) => {
723
+ const current = pagination.page;
724
+ const total = Math.max(pagination.totalPages, 1);
725
+ return (
726
+ page === 1 ||
727
+ page === total ||
728
+ (page >= current - 1 && page <= current + 1)
729
+ );
730
+ })
731
+ .map((page, index, array) => {
732
+ if (index > 0 && array[index - 1] !== page - 1) {
733
+ return (
734
+ <React.Fragment key={`ellipsis-${page}`}>
735
+ <PaginationItem>
736
+ <PaginationEllipsis />
737
+ </PaginationItem>
738
+ <PaginationItem>
739
+ <PaginationLink
740
+ href="#"
741
+ onClick={(e) => {
742
+ e.preventDefault();
743
+ handlePageChange(page);
744
+ }}
745
+ isActive={page === pagination.page}
746
+ >
747
+ {page}
748
+ </PaginationLink>
749
+ </PaginationItem>
750
+ </React.Fragment>
751
+ );
752
+ }
753
+ return (
754
+ <PaginationItem key={page}>
755
+ <PaginationLink
756
+ href="#"
757
+ onClick={(e) => {
758
+ e.preventDefault();
759
+ handlePageChange(page);
760
+ }}
761
+ isActive={page === pagination.page}
762
+ >
763
+ {page}
764
+ </PaginationLink>
765
+ </PaginationItem>
766
+ );
767
+ })}
768
+
769
+ <PaginationItem>
770
+ <PaginationNext
771
+ href="#"
772
+ onClick={(e) => {
773
+ e.preventDefault();
774
+ if (pagination.hasNextPage) {
775
+ handlePageChange(pagination.page + 1);
776
+ }
777
+ }}
778
+ className={cn(
779
+ !pagination.hasNextPage &&
780
+ "pointer-events-none opacity-50"
781
+ )}
782
+ />
783
+ </PaginationItem>
784
+ </PaginationContent>
785
+ </Pagination>
786
+ </div>
787
+ )}
788
+ </div>
789
+ )}
790
+ </>
791
+ )}
792
+ </div>
793
+ </div>
794
+
795
+ {/* Footer */}
796
+ <div className="bg-gray-50 dark:bg-gray-700 px-6 py-3 flex justify-end space-x-3">
797
+ <Button
798
+ variant="outline"
799
+ onClick={onClose}
800
+ >
801
+ Cancel
802
+ </Button>
803
+ {multiSelect && selectedEmployeesState.length > 0 && (
804
+ <Button
805
+ onClick={handleConfirmSelection}
806
+ >
807
+ Select {selectedEmployeesState.length} Employee{selectedEmployeesState.length !== 1 ? 's' : ''}
808
+ </Button>
809
+ )}
810
+ </div>
811
+ </div>
812
+ </div>
813
+ </div>
814
+ );
815
+ };
816
+
817
+ export default EmployeeSearchModal;