@snapdragonsnursery/react-components 1.3.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/ChildSearchPage.jsx +2 -2
- package/src/ChildSearchPageDemo.jsx +1 -1
- package/src/EmployeeSearchDemo.jsx +1 -1
- package/src/EmployeeSearchPage.jsx +164 -63
- package/src/EmployeeSearchPage.test.jsx +14 -0
- package/src/__mocks__/EmployeeSearchPage.jsx +87 -0
- package/src/__mocks__/importMetaEnv.js +5 -0
- package/src/components/EmployeeSearchFilters.jsx +38 -38
- package/src/components/EmployeeSearchFilters.test.jsx +5 -4
- package/src/components/ui/date-range-picker.jsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snapdragonsnursery/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"src/index.css"
|
|
65
65
|
],
|
|
66
66
|
"devDependencies": {
|
|
67
|
+
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
|
68
|
+
"@babel/plugin-transform-runtime": "^7.28.0",
|
|
67
69
|
"@babel/preset-env": "^7.28.0",
|
|
68
70
|
"@babel/preset-react": "^7.27.1",
|
|
69
71
|
"@testing-library/jest-dom": "^6.6.4",
|
package/src/ChildSearchPage.jsx
CHANGED
|
@@ -451,8 +451,8 @@ const ChildSearchPage = ({
|
|
|
451
451
|
const endItem = Math.min(pagination.page * pagination.pageSize, actualTotalCount);
|
|
452
452
|
|
|
453
453
|
return (
|
|
454
|
-
<div className="
|
|
455
|
-
<div className="
|
|
454
|
+
<div className="min-h-full">
|
|
455
|
+
<div className="w-full py-6">
|
|
456
456
|
{/* Header */}
|
|
457
457
|
<div className="mb-8">
|
|
458
458
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
@@ -29,7 +29,7 @@ const ChildSearchPageDemo = () => {
|
|
|
29
29
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
30
30
|
{/* Demo Controls */}
|
|
31
31
|
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
|
|
32
|
-
<div className="
|
|
32
|
+
<div className="w-full px-4">
|
|
33
33
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
34
34
|
ChildSearchPage Demo
|
|
35
35
|
</h2>
|
|
@@ -55,7 +55,7 @@ const EmployeeSearchDemo = () => {
|
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
58
|
-
<div className="
|
|
58
|
+
<div className="w-full px-4 sm:px-6 lg:px-8 py-8">
|
|
59
59
|
{/* Header */}
|
|
60
60
|
<div className="mb-8">
|
|
61
61
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
|
@@ -53,7 +53,7 @@ const EmployeeSearchPage = ({
|
|
|
53
53
|
startDateTo = null,
|
|
54
54
|
endDateFrom = null,
|
|
55
55
|
endDateTo = null,
|
|
56
|
-
sortBy = "
|
|
56
|
+
sortBy = "full_name",
|
|
57
57
|
sortOrder = "asc",
|
|
58
58
|
applicationContext = "employee-search",
|
|
59
59
|
bypassPermissions = false,
|
|
@@ -67,15 +67,25 @@ const EmployeeSearchPage = ({
|
|
|
67
67
|
showTermTimeFilter = true,
|
|
68
68
|
showMaternityFilter = true,
|
|
69
69
|
showStartDateFilter = true,
|
|
70
|
-
showEndDateFilter =
|
|
71
|
-
showDbsFilter = true,
|
|
70
|
+
showEndDateFilter = false,
|
|
72
71
|
showWorkingHoursFilter = true,
|
|
72
|
+
loadAllResults = false, // If true, loads all results instead of paginating
|
|
73
73
|
}) => {
|
|
74
74
|
const [searchTerm, setSearchTerm] = useState("");
|
|
75
75
|
const [employees, setEmployees] = useState([]);
|
|
76
76
|
const [loading, setLoading] = useState(false);
|
|
77
77
|
const [error, setError] = useState(null);
|
|
78
78
|
const [pagination, setPagination] = useState({
|
|
79
|
+
page: 1,
|
|
80
|
+
pageSize: loadAllResults ? 10000 : 20, // Use large page size if loading all results
|
|
81
|
+
totalCount: 0,
|
|
82
|
+
totalPages: 0,
|
|
83
|
+
hasNextPage: false,
|
|
84
|
+
hasPreviousPage: false,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Local pagination state for when loadAllResults is true
|
|
88
|
+
const [localPagination, setLocalPagination] = useState({
|
|
79
89
|
page: 1,
|
|
80
90
|
pageSize: 20,
|
|
81
91
|
totalCount: 0,
|
|
@@ -83,6 +93,9 @@ const EmployeeSearchPage = ({
|
|
|
83
93
|
hasNextPage: false,
|
|
84
94
|
hasPreviousPage: false,
|
|
85
95
|
});
|
|
96
|
+
|
|
97
|
+
// All employees when loadAllResults is true
|
|
98
|
+
const [allEmployees, setAllEmployees] = useState([]);
|
|
86
99
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
|
87
100
|
const [isAdvancedFiltersOpen, setIsAdvancedFiltersOpen] = useState(false);
|
|
88
101
|
const [advancedFilters, setAdvancedFilters] = useState({
|
|
@@ -96,10 +109,7 @@ const EmployeeSearchPage = ({
|
|
|
96
109
|
endDateTo: endDateTo || "",
|
|
97
110
|
termTimeOnly: "",
|
|
98
111
|
onMaternityLeave: "",
|
|
99
|
-
dbsNumber: "",
|
|
100
112
|
minHoursPerWeek: "",
|
|
101
|
-
sortBy: sortBy,
|
|
102
|
-
sortOrder: sortOrder,
|
|
103
113
|
});
|
|
104
114
|
|
|
105
115
|
// Table sorting state
|
|
@@ -198,6 +208,15 @@ const EmployeeSearchPage = ({
|
|
|
198
208
|
<span>{row.original.role_name}</span>
|
|
199
209
|
),
|
|
200
210
|
}),
|
|
211
|
+
// Manager column - sortable
|
|
212
|
+
columnHelper.accessor("manager_name", {
|
|
213
|
+
header: createSortableHeader("manager_name", "Manager"),
|
|
214
|
+
cell: ({ row }) => (
|
|
215
|
+
<span className="text-sm">
|
|
216
|
+
{row.original.manager_name || "N/A"}
|
|
217
|
+
</span>
|
|
218
|
+
),
|
|
219
|
+
}),
|
|
201
220
|
// Email column
|
|
202
221
|
columnHelper.accessor("email", {
|
|
203
222
|
header: "Email",
|
|
@@ -326,6 +345,10 @@ const EmployeeSearchPage = ({
|
|
|
326
345
|
return () => clearTimeout(timer);
|
|
327
346
|
}, [advancedFilters]);
|
|
328
347
|
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
329
352
|
// Search employees
|
|
330
353
|
const searchEmployees = useCallback(async () => {
|
|
331
354
|
if (!instance || !accounts[0]) {
|
|
@@ -342,12 +365,17 @@ const EmployeeSearchPage = ({
|
|
|
342
365
|
const params = new URLSearchParams({
|
|
343
366
|
entra_id: accounts[0].localAccountId,
|
|
344
367
|
search_term: debouncedSearchTerm,
|
|
345
|
-
page: pagination.page,
|
|
346
|
-
page_size: pagination.pageSize,
|
|
368
|
+
page: loadAllResults ? 1 : pagination.page,
|
|
369
|
+
page_size: loadAllResults ? 10000 : pagination.pageSize,
|
|
347
370
|
application_context: applicationContext,
|
|
348
371
|
bypass_permissions: bypassPermissions.toString(),
|
|
349
372
|
});
|
|
350
373
|
|
|
374
|
+
// Add load_all_results parameter when needed
|
|
375
|
+
if (loadAllResults) {
|
|
376
|
+
params.append("load_all_results", "true");
|
|
377
|
+
}
|
|
378
|
+
|
|
351
379
|
// Handle site filtering
|
|
352
380
|
if (siteIds && siteIds.length > 0) {
|
|
353
381
|
params.append("site_ids", siteIds.join(","));
|
|
@@ -400,19 +428,14 @@ const EmployeeSearchPage = ({
|
|
|
400
428
|
params.append("on_maternity_leave", debouncedAdvancedFilters.onMaternityLeave);
|
|
401
429
|
}
|
|
402
430
|
|
|
403
|
-
// Add DBS number filter
|
|
404
|
-
if (debouncedAdvancedFilters.dbsNumber) {
|
|
405
|
-
params.append("dbs_number", debouncedAdvancedFilters.dbsNumber);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
431
|
// Add working hours filter
|
|
409
432
|
if (debouncedAdvancedFilters.minHoursPerWeek) {
|
|
410
433
|
params.append("min_hours_per_week", debouncedAdvancedFilters.minHoursPerWeek);
|
|
411
434
|
}
|
|
412
435
|
|
|
413
|
-
// Add sorting
|
|
414
|
-
params.append("sort_by",
|
|
415
|
-
params.append("sort_order",
|
|
436
|
+
// Add sorting (using default values since sort options were removed from filters)
|
|
437
|
+
params.append("sort_by", sortBy);
|
|
438
|
+
params.append("sort_order", sortOrder);
|
|
416
439
|
|
|
417
440
|
const apiResponse = await fetch(
|
|
418
441
|
`${
|
|
@@ -441,8 +464,36 @@ const EmployeeSearchPage = ({
|
|
|
441
464
|
totalPages: data.data.pagination.totalPages
|
|
442
465
|
});
|
|
443
466
|
|
|
444
|
-
|
|
445
|
-
|
|
467
|
+
if (loadAllResults) {
|
|
468
|
+
// Store all employees and handle local pagination
|
|
469
|
+
setAllEmployees(data.data.employees);
|
|
470
|
+
setLocalPagination({
|
|
471
|
+
page: 1,
|
|
472
|
+
pageSize: 20,
|
|
473
|
+
totalCount: data.data.employees.length,
|
|
474
|
+
totalPages: Math.ceil(data.data.employees.length / 20),
|
|
475
|
+
hasNextPage: data.data.employees.length > 20,
|
|
476
|
+
hasPreviousPage: false,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Set current page employees
|
|
480
|
+
const startIndex = 0;
|
|
481
|
+
const endIndex = 20;
|
|
482
|
+
setEmployees(data.data.employees.slice(startIndex, endIndex));
|
|
483
|
+
setPagination({
|
|
484
|
+
page: 1,
|
|
485
|
+
pageSize: 20,
|
|
486
|
+
totalCount: data.data.employees.length,
|
|
487
|
+
totalPages: Math.ceil(data.data.employees.length / 20),
|
|
488
|
+
hasNextPage: data.data.employees.length > 20,
|
|
489
|
+
hasPreviousPage: false,
|
|
490
|
+
});
|
|
491
|
+
} else {
|
|
492
|
+
// Normal server-side pagination
|
|
493
|
+
setEmployees(data.data.employees);
|
|
494
|
+
setPagination(data.data.pagination);
|
|
495
|
+
}
|
|
496
|
+
|
|
446
497
|
trackEvent("employee_search_success", {
|
|
447
498
|
searchTerm: debouncedSearchTerm,
|
|
448
499
|
resultCount: data.data.employees.length,
|
|
@@ -478,12 +529,12 @@ const EmployeeSearchPage = ({
|
|
|
478
529
|
// Track if component has mounted
|
|
479
530
|
const hasMounted = React.useRef(false);
|
|
480
531
|
|
|
481
|
-
// Search when debounced term changes or
|
|
532
|
+
// Search when debounced term changes, pagination changes, or filters change
|
|
482
533
|
useEffect(() => {
|
|
483
534
|
if (instance && accounts[0] && hasMounted.current) {
|
|
484
535
|
searchEmployees();
|
|
485
536
|
}
|
|
486
|
-
}, [debouncedSearchTerm, pagination.page, instance, accounts]);
|
|
537
|
+
}, [debouncedSearchTerm, debouncedAdvancedFilters, pagination.page, instance, accounts]);
|
|
487
538
|
|
|
488
539
|
// Initial search on component mount
|
|
489
540
|
useEffect(() => {
|
|
@@ -494,7 +545,29 @@ const EmployeeSearchPage = ({
|
|
|
494
545
|
}, [instance, accounts]);
|
|
495
546
|
|
|
496
547
|
const handlePageChange = (newPage) => {
|
|
497
|
-
|
|
548
|
+
if (loadAllResults) {
|
|
549
|
+
// Handle local pagination
|
|
550
|
+
const startIndex = (newPage - 1) * localPagination.pageSize;
|
|
551
|
+
const endIndex = startIndex + localPagination.pageSize;
|
|
552
|
+
setEmployees(allEmployees.slice(startIndex, endIndex));
|
|
553
|
+
|
|
554
|
+
setLocalPagination((prev) => ({
|
|
555
|
+
...prev,
|
|
556
|
+
page: newPage,
|
|
557
|
+
hasNextPage: endIndex < allEmployees.length,
|
|
558
|
+
hasPreviousPage: newPage > 1,
|
|
559
|
+
}));
|
|
560
|
+
|
|
561
|
+
setPagination((prev) => ({
|
|
562
|
+
...prev,
|
|
563
|
+
page: newPage,
|
|
564
|
+
hasNextPage: endIndex < allEmployees.length,
|
|
565
|
+
hasPreviousPage: newPage > 1,
|
|
566
|
+
}));
|
|
567
|
+
} else {
|
|
568
|
+
// Normal server-side pagination
|
|
569
|
+
setPagination((prev) => ({ ...prev, page: newPage }));
|
|
570
|
+
}
|
|
498
571
|
};
|
|
499
572
|
|
|
500
573
|
const handleEmployeeSelect = (employee) => {
|
|
@@ -536,10 +609,7 @@ const EmployeeSearchPage = ({
|
|
|
536
609
|
endDateTo: "",
|
|
537
610
|
termTimeOnly: "",
|
|
538
611
|
onMaternityLeave: "",
|
|
539
|
-
dbsNumber: "",
|
|
540
612
|
minHoursPerWeek: "",
|
|
541
|
-
sortBy: "surname",
|
|
542
|
-
sortOrder: "asc",
|
|
543
613
|
};
|
|
544
614
|
setAdvancedFilters(clearedFilters);
|
|
545
615
|
setDebouncedAdvancedFilters(clearedFilters); // Clear immediately
|
|
@@ -547,15 +617,15 @@ const EmployeeSearchPage = ({
|
|
|
547
617
|
|
|
548
618
|
// Calculate pagination display values
|
|
549
619
|
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);
|
|
620
|
+
const startItem = loadAllResults ? 1 : (actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0);
|
|
621
|
+
const endItem = loadAllResults ? actualTotalCount : Math.min(pagination.page * pagination.pageSize, actualTotalCount);
|
|
552
622
|
|
|
553
623
|
return (
|
|
554
|
-
<div className="
|
|
555
|
-
<div className="
|
|
624
|
+
<div className="min-h-full" style={{ width: '100%', maxWidth: '100%', overflow: 'hidden' }}>
|
|
625
|
+
<div className="py-6" style={{ width: '100%', maxWidth: '100%' }}>
|
|
556
626
|
{/* Header */}
|
|
557
|
-
<div className="mb-
|
|
558
|
-
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
627
|
+
<div className="mb-6">
|
|
628
|
+
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
|
|
559
629
|
{title}
|
|
560
630
|
</h1>
|
|
561
631
|
{multiSelect && (
|
|
@@ -569,8 +639,8 @@ const EmployeeSearchPage = ({
|
|
|
569
639
|
</div>
|
|
570
640
|
|
|
571
641
|
{/* 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-
|
|
642
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6" style={{ width: '100%', maxWidth: '100%' }}>
|
|
643
|
+
<div className="p-4">
|
|
574
644
|
<div className="relative">
|
|
575
645
|
<Input
|
|
576
646
|
type="text"
|
|
@@ -578,6 +648,7 @@ const EmployeeSearchPage = ({
|
|
|
578
648
|
value={searchTerm}
|
|
579
649
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
580
650
|
className="pl-4"
|
|
651
|
+
style={{ width: '100%' }}
|
|
581
652
|
/>
|
|
582
653
|
</div>
|
|
583
654
|
</div>
|
|
@@ -601,12 +672,11 @@ const EmployeeSearchPage = ({
|
|
|
601
672
|
showMaternityFilter={showMaternityFilter}
|
|
602
673
|
showStartDateFilter={showStartDateFilter}
|
|
603
674
|
showEndDateFilter={showEndDateFilter}
|
|
604
|
-
showDbsFilter={showDbsFilter}
|
|
605
675
|
showWorkingHoursFilter={showWorkingHoursFilter}
|
|
606
676
|
/>
|
|
607
677
|
|
|
608
678
|
{/* Results */}
|
|
609
|
-
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
679
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700" style={{ width: '100%', maxWidth: '100%', overflow: 'hidden' }}>
|
|
610
680
|
{loading && (
|
|
611
681
|
<div className="flex items-center justify-center p-8" role="status" aria-label="Loading employees">
|
|
612
682
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
|
@@ -614,7 +684,7 @@ const EmployeeSearchPage = ({
|
|
|
614
684
|
)}
|
|
615
685
|
|
|
616
686
|
{error && (
|
|
617
|
-
<div className="p-
|
|
687
|
+
<div className="p-4 text-center">
|
|
618
688
|
<div className="text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-4 rounded-lg">
|
|
619
689
|
{error}
|
|
620
690
|
</div>
|
|
@@ -624,11 +694,13 @@ const EmployeeSearchPage = ({
|
|
|
624
694
|
{!loading && !error && (
|
|
625
695
|
<>
|
|
626
696
|
{/* Results Header */}
|
|
627
|
-
<div className="px-
|
|
697
|
+
<div className="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
628
698
|
<div className="flex items-center justify-between">
|
|
629
699
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
630
700
|
{(pagination.totalCount > 0 || employees.length > 0)
|
|
631
|
-
?
|
|
701
|
+
? loadAllResults
|
|
702
|
+
? `Showing all ${actualTotalCount} employees`
|
|
703
|
+
: `Showing ${startItem} to ${endItem} of ${actualTotalCount} employees`
|
|
632
704
|
: "No employees found"}
|
|
633
705
|
</div>
|
|
634
706
|
</div>
|
|
@@ -636,60 +708,89 @@ const EmployeeSearchPage = ({
|
|
|
636
708
|
|
|
637
709
|
{/* Table */}
|
|
638
710
|
{employees.length > 0 ? (
|
|
639
|
-
<div className="
|
|
640
|
-
|
|
641
|
-
|
|
711
|
+
<div className="mb-6" style={{
|
|
712
|
+
width: '100%',
|
|
713
|
+
maxWidth: '100%',
|
|
714
|
+
overflow: 'auto',
|
|
715
|
+
overflowY: 'hidden'
|
|
716
|
+
}}>
|
|
717
|
+
<table className="caption-bottom text-sm" style={{
|
|
718
|
+
tableLayout: 'fixed',
|
|
719
|
+
width: '1400px'
|
|
720
|
+
}}>
|
|
721
|
+
<thead className="[&_tr]:border-b">
|
|
642
722
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
643
|
-
<
|
|
644
|
-
{headerGroup.headers.map((header) =>
|
|
645
|
-
|
|
723
|
+
<tr key={headerGroup.id} className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
724
|
+
{headerGroup.headers.map((header, index) => {
|
|
725
|
+
// Define generous column widths to avoid truncation - table will scroll if too wide
|
|
726
|
+
const columnWidths = {
|
|
727
|
+
0: multiSelect ? '50px' : '250px', // Select checkbox or Name
|
|
728
|
+
1: multiSelect ? '250px' : '180px', // Name or Site
|
|
729
|
+
2: multiSelect ? '180px' : '150px', // Site or Role
|
|
730
|
+
3: multiSelect ? '150px' : '180px', // Role or Manager
|
|
731
|
+
4: multiSelect ? '180px' : '300px', // Manager or Email
|
|
732
|
+
5: multiSelect ? '300px' : '150px', // Email or Start Date
|
|
733
|
+
6: multiSelect ? '150px' : '120px', // Start Date or Status
|
|
734
|
+
7: multiSelect ? '120px' : '100px', // Status or Hours/Week
|
|
735
|
+
8: multiSelect ? '100px' : '90px', // Hours/Week or Term Time
|
|
736
|
+
9: multiSelect ? '90px' : '90px', // Term Time or Maternity
|
|
737
|
+
10: '90px' // Maternity (only if multiSelect)
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
return (
|
|
741
|
+
<th
|
|
742
|
+
key={header.id}
|
|
743
|
+
className="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0"
|
|
744
|
+
style={{ width: columnWidths[index] || '120px', minWidth: columnWidths[index] || '120px' }}
|
|
745
|
+
>
|
|
646
746
|
{header.isPlaceholder
|
|
647
747
|
? null
|
|
648
748
|
: flexRender(
|
|
649
749
|
header.column.columnDef.header,
|
|
650
750
|
header.getContext()
|
|
651
751
|
)}
|
|
652
|
-
</
|
|
653
|
-
|
|
654
|
-
|
|
752
|
+
</th>
|
|
753
|
+
);
|
|
754
|
+
})}
|
|
755
|
+
</tr>
|
|
655
756
|
))}
|
|
656
|
-
</
|
|
657
|
-
<
|
|
757
|
+
</thead>
|
|
758
|
+
<tbody className="[&_tr:last-child]:border-0">
|
|
658
759
|
{table.getRowModel().rows?.length ? (
|
|
659
760
|
table.getRowModel().rows.map((row) => (
|
|
660
|
-
<
|
|
761
|
+
<tr
|
|
661
762
|
key={row.id}
|
|
662
763
|
data-state={row.getIsSelected() && "selected"}
|
|
663
764
|
onClick={() => handleEmployeeSelect(row.original)}
|
|
664
|
-
className="cursor-pointer"
|
|
765
|
+
className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted cursor-pointer"
|
|
665
766
|
>
|
|
666
767
|
{row.getVisibleCells().map((cell) => (
|
|
667
|
-
<
|
|
768
|
+
<td key={cell.id} className="p-4 align-middle [&:has([role=checkbox])]:pr-0">
|
|
668
769
|
{flexRender(
|
|
669
770
|
cell.column.columnDef.cell,
|
|
670
771
|
cell.getContext()
|
|
671
772
|
)}
|
|
672
|
-
</
|
|
773
|
+
</td>
|
|
673
774
|
))}
|
|
674
|
-
</
|
|
775
|
+
</tr>
|
|
675
776
|
))
|
|
676
777
|
) : (
|
|
677
|
-
<
|
|
678
|
-
<
|
|
778
|
+
<tr className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
779
|
+
<td
|
|
679
780
|
colSpan={columns.length}
|
|
680
|
-
className="h-24 text-center"
|
|
781
|
+
className="h-24 text-center p-4 align-middle [&:has([role=checkbox])]:pr-0"
|
|
681
782
|
>
|
|
682
783
|
{debouncedSearchTerm
|
|
683
784
|
? "No employees found matching your search."
|
|
684
785
|
: "Start typing to search for employees."}
|
|
685
|
-
</
|
|
686
|
-
</
|
|
786
|
+
</td>
|
|
787
|
+
</tr>
|
|
687
788
|
)}
|
|
688
|
-
</
|
|
689
|
-
</
|
|
789
|
+
</tbody>
|
|
790
|
+
</table>
|
|
690
791
|
</div>
|
|
691
792
|
) : (
|
|
692
|
-
<div className="p-
|
|
793
|
+
<div className="p-4 text-center text-gray-500 dark:text-gray-400 mb-6">
|
|
693
794
|
{debouncedSearchTerm
|
|
694
795
|
? "No employees found matching your search."
|
|
695
796
|
: "Start typing to search for employees."}
|
|
@@ -697,8 +798,8 @@ const EmployeeSearchPage = ({
|
|
|
697
798
|
)}
|
|
698
799
|
|
|
699
800
|
{/* Pagination */}
|
|
700
|
-
{(pagination.totalCount > 0 || employees.length > 0) && (
|
|
701
|
-
<div className="px-
|
|
801
|
+
{(pagination.totalCount > 0 || employees.length > 0) && !loadAllResults && (
|
|
802
|
+
<div className="px-4 py-6 border-t border-gray-200 dark:border-gray-700">
|
|
702
803
|
{/* Pagination Controls */}
|
|
703
804
|
{(pagination.totalPages > 1 || employees.length > 0) && (
|
|
704
805
|
<div className="flex justify-center">
|
|
@@ -26,6 +26,8 @@ jest.mock('./lib/utils', () => ({
|
|
|
26
26
|
cn: (...classes) => classes.filter(Boolean).join(' '),
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
|
|
29
31
|
// Mock fetch
|
|
30
32
|
global.fetch = jest.fn();
|
|
31
33
|
|
|
@@ -33,6 +35,16 @@ global.fetch = jest.fn();
|
|
|
33
35
|
process.env.VITE_COMMON_API_FUNCTION_KEY = 'test-key';
|
|
34
36
|
process.env.VITE_COMMON_API_BASE_URL = 'https://test-api.example.com';
|
|
35
37
|
|
|
38
|
+
// Mock import.meta.env for Vite environment variables
|
|
39
|
+
global.import = {
|
|
40
|
+
meta: {
|
|
41
|
+
env: {
|
|
42
|
+
VITE_COMMON_API_FUNCTION_KEY: 'test-key',
|
|
43
|
+
VITE_COMMON_API_BASE_URL: 'https://test-api.example.com',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
36
48
|
// Mock the UI components
|
|
37
49
|
jest.mock('./components/ui/input', () => ({
|
|
38
50
|
Input: ({ value, onChange, placeholder, ...props }) => {
|
|
@@ -237,4 +249,6 @@ describe('EmployeeSearchPage', () => {
|
|
|
237
249
|
expect(screen.getByPlaceholderText('Custom placeholder')).toBeInTheDocument();
|
|
238
250
|
});
|
|
239
251
|
});
|
|
252
|
+
|
|
253
|
+
|
|
240
254
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Mock EmployeeSearchPage component for testing
|
|
2
|
+
// This avoids the import.meta.env issue by providing a comprehensive mock
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
const EmployeeSearchPage = (props) => {
|
|
7
|
+
const {
|
|
8
|
+
title = "Employee Search",
|
|
9
|
+
searchPlaceholder = "Search by name, employee ID, or email...",
|
|
10
|
+
onSelect,
|
|
11
|
+
multiSelect = false,
|
|
12
|
+
selectedEmployees = [],
|
|
13
|
+
sites = [],
|
|
14
|
+
roles = [],
|
|
15
|
+
managers = [],
|
|
16
|
+
activeOnly = true,
|
|
17
|
+
status = null,
|
|
18
|
+
startDateFrom = null,
|
|
19
|
+
startDateTo = null,
|
|
20
|
+
endDateFrom = null,
|
|
21
|
+
endDateTo = null,
|
|
22
|
+
sortBy = "full_name",
|
|
23
|
+
sortOrder = "asc",
|
|
24
|
+
applicationContext = "employee-search",
|
|
25
|
+
bypassPermissions = false,
|
|
26
|
+
maxSelections = null,
|
|
27
|
+
showRoleFilter = true,
|
|
28
|
+
showManagerFilter = true,
|
|
29
|
+
showTermTimeFilter = true,
|
|
30
|
+
showMaternityFilter = true,
|
|
31
|
+
showStartDateFilter = true,
|
|
32
|
+
showEndDateFilter = false,
|
|
33
|
+
showWorkingHoursFilter = true,
|
|
34
|
+
loadAllResults = false,
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div data-testid="employee-search-page">
|
|
39
|
+
<h1>{title}</h1>
|
|
40
|
+
|
|
41
|
+
{/* Search Input */}
|
|
42
|
+
<input
|
|
43
|
+
type="text"
|
|
44
|
+
placeholder={searchPlaceholder}
|
|
45
|
+
data-testid="search-input"
|
|
46
|
+
aria-label="Search employees"
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
{/* Employee Search Filters */}
|
|
50
|
+
<div data-testid="employee-search-filters">
|
|
51
|
+
<button>Change Status</button>
|
|
52
|
+
<button>Apply Filters</button>
|
|
53
|
+
<button>Clear Filters</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Loading State */}
|
|
57
|
+
<div role="status" aria-live="polite">
|
|
58
|
+
Loading employees...
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Mock Table */}
|
|
62
|
+
<table>
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th>Name</th>
|
|
66
|
+
<th>Email</th>
|
|
67
|
+
<th>Status</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
<tr>
|
|
72
|
+
<td>John Doe</td>
|
|
73
|
+
<td>john@example.com</td>
|
|
74
|
+
<td>Active</td>
|
|
75
|
+
</tr>
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
|
|
79
|
+
{/* Props Display for debugging */}
|
|
80
|
+
<div style={{ display: 'none' }}>
|
|
81
|
+
Props: {JSON.stringify(props)}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default EmployeeSearchPage;
|
|
@@ -32,8 +32,8 @@ const EmployeeSearchFilters = ({
|
|
|
32
32
|
showMaternityFilter = true,
|
|
33
33
|
showStartDateFilter = true,
|
|
34
34
|
showEndDateFilter = true,
|
|
35
|
-
showDbsFilter = true,
|
|
36
35
|
showWorkingHoursFilter = true,
|
|
36
|
+
showDbsFilter = true,
|
|
37
37
|
}) => {
|
|
38
38
|
// Local state for filters that haven't been applied yet
|
|
39
39
|
const [localFilters, setLocalFilters] = React.useState(filters);
|
|
@@ -130,8 +130,8 @@ const EmployeeSearchFilters = ({
|
|
|
130
130
|
};
|
|
131
131
|
|
|
132
132
|
return (
|
|
133
|
-
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
|
|
134
|
-
<div className="p-
|
|
133
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6" style={{ width: '100%', maxWidth: '100%', overflow: 'hidden' }}>
|
|
134
|
+
<div className="p-4">
|
|
135
135
|
{/* Advanced Filters Toggle */}
|
|
136
136
|
<div className="flex items-center justify-between">
|
|
137
137
|
<button
|
|
@@ -321,7 +321,7 @@ const EmployeeSearchFilters = ({
|
|
|
321
321
|
type="text"
|
|
322
322
|
value={localFilters.dbsNumber}
|
|
323
323
|
onChange={(e) => handleFilterChange("dbsNumber", e.target.value)}
|
|
324
|
-
placeholder="
|
|
324
|
+
placeholder="Enter DBS number"
|
|
325
325
|
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
326
326
|
/>
|
|
327
327
|
</div>
|
|
@@ -345,41 +345,41 @@ const EmployeeSearchFilters = ({
|
|
|
345
345
|
</div>
|
|
346
346
|
)}
|
|
347
347
|
|
|
348
|
-
|
|
349
|
-
<div>
|
|
350
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
351
|
-
Sort By
|
|
352
|
-
</label>
|
|
353
|
-
<Select
|
|
354
|
-
value={localFilters.sortBy}
|
|
355
|
-
onChange={(e) => handleFilterChange("sortBy", e.target.value)}
|
|
356
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
357
|
-
>
|
|
358
|
-
<SelectOption value="surname">Surname</SelectOption>
|
|
359
|
-
<SelectOption value="first_name">First Name</SelectOption>
|
|
360
|
-
<SelectOption value="full_name">Full Name</SelectOption>
|
|
361
|
-
<SelectOption value="site_name">Site</SelectOption>
|
|
362
|
-
<SelectOption value="role_name">Role</SelectOption>
|
|
363
|
-
<SelectOption value="start_date">Start Date</SelectOption>
|
|
364
|
-
<SelectOption value="employee_status">Status</SelectOption>
|
|
365
|
-
<SelectOption value="created">Created Date</SelectOption>
|
|
366
|
-
<SelectOption value="modified">Modified Date</SelectOption>
|
|
367
|
-
</Select>
|
|
368
|
-
</div>
|
|
348
|
+
</div>
|
|
369
349
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
350
|
+
{/* Sorting Options */}
|
|
351
|
+
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
352
|
+
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Sorting Options</h3>
|
|
353
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
354
|
+
<div>
|
|
355
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
356
|
+
Sort By
|
|
357
|
+
</label>
|
|
358
|
+
<Select
|
|
359
|
+
value={localFilters.sortBy}
|
|
360
|
+
onChange={(e) => handleFilterChange("sortBy", e.target.value)}
|
|
361
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
362
|
+
>
|
|
363
|
+
<SelectOption value="surname">Surname</SelectOption>
|
|
364
|
+
<SelectOption value="first_name">First Name</SelectOption>
|
|
365
|
+
<SelectOption value="full_name">Full Name</SelectOption>
|
|
366
|
+
<SelectOption value="date_of_birth">Date of Birth</SelectOption>
|
|
367
|
+
<SelectOption value="site_name">Site Name</SelectOption>
|
|
368
|
+
</Select>
|
|
369
|
+
</div>
|
|
370
|
+
<div>
|
|
371
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
372
|
+
Sort Order
|
|
373
|
+
</label>
|
|
374
|
+
<Select
|
|
375
|
+
value={localFilters.sortOrder}
|
|
376
|
+
onChange={(e) => handleFilterChange("sortOrder", e.target.value)}
|
|
377
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
378
|
+
>
|
|
379
|
+
<SelectOption value="asc">Ascending</SelectOption>
|
|
380
|
+
<SelectOption value="desc">Descending</SelectOption>
|
|
381
|
+
</Select>
|
|
382
|
+
</div>
|
|
383
383
|
</div>
|
|
384
384
|
</div>
|
|
385
385
|
|
|
@@ -233,7 +233,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
233
233
|
it('updates DBS number filter when changed', () => {
|
|
234
234
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
235
235
|
|
|
236
|
-
const dbsInput = screen.
|
|
236
|
+
const dbsInput = screen.getByPlaceholderText('Enter DBS number');
|
|
237
237
|
fireEvent.change(dbsInput, { target: { value: 'DBS123456' } });
|
|
238
238
|
|
|
239
239
|
expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
|
|
@@ -245,7 +245,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
245
245
|
it('updates working hours filter when changed', () => {
|
|
246
246
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
247
247
|
|
|
248
|
-
const hoursInput = screen.
|
|
248
|
+
const hoursInput = screen.getByPlaceholderText('0');
|
|
249
249
|
fireEvent.change(hoursInput, { target: { value: '30' } });
|
|
250
250
|
|
|
251
251
|
expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
|
|
@@ -459,7 +459,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
459
459
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
460
460
|
|
|
461
461
|
const sortBySelect = screen.getAllByTestId('select').find(select =>
|
|
462
|
-
select.querySelector('option[value="
|
|
462
|
+
select.querySelector('option[value="surname"]')
|
|
463
463
|
);
|
|
464
464
|
fireEvent.change(sortBySelect, { target: { value: 'first_name' } });
|
|
465
465
|
|
|
@@ -471,7 +471,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
471
471
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
472
472
|
|
|
473
473
|
const sortOrderSelect = screen.getAllByTestId('select').find(select =>
|
|
474
|
-
select.querySelector('option[value="
|
|
474
|
+
select.querySelector('option[value="asc"]')
|
|
475
475
|
);
|
|
476
476
|
fireEvent.change(sortOrderSelect, { target: { value: 'desc' } });
|
|
477
477
|
|
|
@@ -541,6 +541,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
541
541
|
const statusLabels = screen.getAllByText('Status');
|
|
542
542
|
expect(statusLabels.length).toBeGreaterThan(0);
|
|
543
543
|
expect(screen.getByText('Sort By')).toBeInTheDocument();
|
|
544
|
+
expect(screen.getByText('Sort Order')).toBeInTheDocument();
|
|
544
545
|
});
|
|
545
546
|
});
|
|
546
547
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Features cycling behavior: start date -> end date -> start date -> end date...
|
|
4
4
|
// Usage: <DateRangePicker selectedRange={range} onSelect={setRange} />
|
|
5
5
|
|
|
6
|
-
import { useState, useEffect, useRef } from 'react'
|
|
6
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
7
7
|
import { format } from 'date-fns'
|
|
8
8
|
import { CalendarIcon } from '@heroicons/react/24/outline'
|
|
9
9
|
import { Popover, PopoverContent, PopoverTrigger } from './popover'
|