@snapdragonsnursery/react-components 1.3.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.
- package/package.json +1 -1
- package/src/EmployeeSearchDemo.jsx +275 -0
- package/src/EmployeeSearchModal.jsx +817 -0
- package/src/EmployeeSearchPage.jsx +804 -0
- package/src/EmployeeSearchPage.test.jsx +240 -0
- package/src/components/EmployeeSearchFilters.jsx +418 -0
- package/src/components/EmployeeSearchFilters.test.jsx +546 -0
- package/src/index.js +7 -0
|
@@ -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 = process.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
|
+
process.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;
|