@snapdragonsnursery/react-components 1.3.0 → 1.3.2

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