@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.
- 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/ChildSearchFilters.test.jsx +2 -30
- package/src/components/EmployeeSearchFilters.jsx +418 -0
- package/src/components/EmployeeSearchFilters.test.jsx +546 -0
- package/src/index.js +7 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// Employee Search Filters Component
|
|
2
|
+
// Provides advanced filtering capabilities for employee search including status, site, start date, role, manager, and other employee-specific filters
|
|
3
|
+
// Usage: <EmployeeSearchFilters filters={filters} onFiltersChange={setFilters} sites={sites} roles={roles} managers={managers} />
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Input } from "./ui/input";
|
|
7
|
+
import { Select, SelectOption } from "./ui/select";
|
|
8
|
+
import { DateRangePicker } from "./ui/date-range-picker";
|
|
9
|
+
import { Button } from "./ui/button";
|
|
10
|
+
import {
|
|
11
|
+
FunnelIcon,
|
|
12
|
+
ChevronDownIcon,
|
|
13
|
+
ChevronUpIcon,
|
|
14
|
+
} from "@heroicons/react/24/outline";
|
|
15
|
+
|
|
16
|
+
const EmployeeSearchFilters = ({
|
|
17
|
+
filters,
|
|
18
|
+
onFiltersChange,
|
|
19
|
+
sites = null,
|
|
20
|
+
roles = null,
|
|
21
|
+
managers = null,
|
|
22
|
+
activeOnly = true,
|
|
23
|
+
isAdvancedFiltersOpen,
|
|
24
|
+
onToggleAdvancedFilters,
|
|
25
|
+
onClearFilters,
|
|
26
|
+
onApplyFilters,
|
|
27
|
+
onCancelChanges,
|
|
28
|
+
// Configurable filter visibility
|
|
29
|
+
showRoleFilter = true,
|
|
30
|
+
showManagerFilter = true,
|
|
31
|
+
showTermTimeFilter = true,
|
|
32
|
+
showMaternityFilter = true,
|
|
33
|
+
showStartDateFilter = true,
|
|
34
|
+
showEndDateFilter = true,
|
|
35
|
+
showDbsFilter = true,
|
|
36
|
+
showWorkingHoursFilter = true,
|
|
37
|
+
}) => {
|
|
38
|
+
// Local state for filters that haven't been applied yet
|
|
39
|
+
const [localFilters, setLocalFilters] = React.useState(filters);
|
|
40
|
+
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
|
|
41
|
+
|
|
42
|
+
// Update local filters when props change
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
setLocalFilters(filters);
|
|
45
|
+
setHasUnsavedChanges(false);
|
|
46
|
+
}, [filters]);
|
|
47
|
+
|
|
48
|
+
// Convert existing date strings to DateRangePicker format
|
|
49
|
+
const getStartDateRange = () => {
|
|
50
|
+
if (localFilters.startDateFrom || localFilters.startDateTo) {
|
|
51
|
+
return {
|
|
52
|
+
from: localFilters.startDateFrom ? new Date(localFilters.startDateFrom) : null,
|
|
53
|
+
to: localFilters.startDateTo ? new Date(localFilters.startDateTo) : null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getEndDateRange = () => {
|
|
60
|
+
if (localFilters.endDateFrom || localFilters.endDateTo) {
|
|
61
|
+
return {
|
|
62
|
+
from: localFilters.endDateFrom ? new Date(localFilters.endDateFrom) : null,
|
|
63
|
+
to: localFilters.endDateTo ? new Date(localFilters.endDateTo) : null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleStartDateRangeChange = (range) => {
|
|
70
|
+
if (range?.from && range?.to) {
|
|
71
|
+
setLocalFilters(prev => ({
|
|
72
|
+
...prev,
|
|
73
|
+
startDateFrom: range.from.toISOString().split('T')[0],
|
|
74
|
+
startDateTo: range.to.toISOString().split('T')[0],
|
|
75
|
+
}));
|
|
76
|
+
setHasUnsavedChanges(true);
|
|
77
|
+
} else if (!range?.from && !range?.to) {
|
|
78
|
+
setLocalFilters(prev => ({
|
|
79
|
+
...prev,
|
|
80
|
+
startDateFrom: '',
|
|
81
|
+
startDateTo: '',
|
|
82
|
+
}));
|
|
83
|
+
setHasUnsavedChanges(true);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleEndDateRangeChange = (range) => {
|
|
88
|
+
if (range?.from && range?.to) {
|
|
89
|
+
setLocalFilters(prev => ({
|
|
90
|
+
...prev,
|
|
91
|
+
endDateFrom: range.from.toISOString().split('T')[0],
|
|
92
|
+
endDateTo: range.to.toISOString().split('T')[0],
|
|
93
|
+
}));
|
|
94
|
+
setHasUnsavedChanges(true);
|
|
95
|
+
} else if (!range?.from && !range?.to) {
|
|
96
|
+
setLocalFilters(prev => ({
|
|
97
|
+
...prev,
|
|
98
|
+
endDateFrom: '',
|
|
99
|
+
endDateTo: '',
|
|
100
|
+
}));
|
|
101
|
+
setHasUnsavedChanges(true);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleFilterChange = (key, value) => {
|
|
106
|
+
const updatedFilters = {
|
|
107
|
+
...localFilters,
|
|
108
|
+
[key]: value,
|
|
109
|
+
};
|
|
110
|
+
setLocalFilters(updatedFilters);
|
|
111
|
+
setHasUnsavedChanges(true);
|
|
112
|
+
onFiltersChange(updatedFilters);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const clearFilters = () => {
|
|
116
|
+
onClearFilters();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleApplyFilters = () => {
|
|
120
|
+
onApplyFilters(localFilters);
|
|
121
|
+
setHasUnsavedChanges(false);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleCancelChanges = () => {
|
|
125
|
+
setLocalFilters(filters);
|
|
126
|
+
setHasUnsavedChanges(false);
|
|
127
|
+
if (onCancelChanges) {
|
|
128
|
+
onCancelChanges();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
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-6">
|
|
135
|
+
{/* Advanced Filters Toggle */}
|
|
136
|
+
<div className="flex items-center justify-between">
|
|
137
|
+
<button
|
|
138
|
+
onClick={onToggleAdvancedFilters}
|
|
139
|
+
className="flex items-center space-x-2 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
|
|
140
|
+
>
|
|
141
|
+
<FunnelIcon className="h-4 w-4" />
|
|
142
|
+
<span>Advanced Filters</span>
|
|
143
|
+
{isAdvancedFiltersOpen ? (
|
|
144
|
+
<ChevronUpIcon className="h-4 w-4" />
|
|
145
|
+
) : (
|
|
146
|
+
<ChevronDownIcon className="h-4 w-4" />
|
|
147
|
+
)}
|
|
148
|
+
</button>
|
|
149
|
+
<button
|
|
150
|
+
onClick={clearFilters}
|
|
151
|
+
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
|
152
|
+
>
|
|
153
|
+
Clear Filters
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Advanced Filters */}
|
|
158
|
+
{isAdvancedFiltersOpen && (
|
|
159
|
+
<div
|
|
160
|
+
className="mt-4 p-4 rounded-lg"
|
|
161
|
+
style={{
|
|
162
|
+
backgroundColor: 'hsl(var(--card))',
|
|
163
|
+
border: '1px solid hsl(var(--border))'
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
167
|
+
{/* Status Filter */}
|
|
168
|
+
<div>
|
|
169
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
170
|
+
Status
|
|
171
|
+
</label>
|
|
172
|
+
<Select
|
|
173
|
+
value={localFilters.status}
|
|
174
|
+
onChange={(e) => handleFilterChange("status", e.target.value)}
|
|
175
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
176
|
+
>
|
|
177
|
+
<SelectOption value="all">All Employees</SelectOption>
|
|
178
|
+
<SelectOption value="Active">Active Only</SelectOption>
|
|
179
|
+
<SelectOption value="Inactive">Inactive Only</SelectOption>
|
|
180
|
+
<SelectOption value="On Leave">On Leave Only</SelectOption>
|
|
181
|
+
<SelectOption value="Terminated">Terminated Only</SelectOption>
|
|
182
|
+
</Select>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Site Filter */}
|
|
186
|
+
{sites && sites.length > 0 && (
|
|
187
|
+
<div>
|
|
188
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
189
|
+
Site
|
|
190
|
+
</label>
|
|
191
|
+
<Select
|
|
192
|
+
value={localFilters.selectedSiteId}
|
|
193
|
+
onChange={(e) => handleFilterChange("selectedSiteId", e.target.value)}
|
|
194
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
195
|
+
>
|
|
196
|
+
<SelectOption value="">All Sites</SelectOption>
|
|
197
|
+
{sites.map((site) => (
|
|
198
|
+
<SelectOption key={site.site_id} value={site.site_id}>
|
|
199
|
+
{site.site_name}
|
|
200
|
+
</SelectOption>
|
|
201
|
+
))}
|
|
202
|
+
</Select>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Role Filter */}
|
|
207
|
+
{showRoleFilter && roles && roles.length > 0 && (
|
|
208
|
+
<div>
|
|
209
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
210
|
+
Role
|
|
211
|
+
</label>
|
|
212
|
+
<Select
|
|
213
|
+
value={localFilters.roleId}
|
|
214
|
+
onChange={(e) => handleFilterChange("roleId", e.target.value)}
|
|
215
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
216
|
+
>
|
|
217
|
+
<SelectOption value="">All Roles</SelectOption>
|
|
218
|
+
{roles.map((role) => (
|
|
219
|
+
<SelectOption key={role.role_id} value={role.role_id}>
|
|
220
|
+
{role.role_name}
|
|
221
|
+
</SelectOption>
|
|
222
|
+
))}
|
|
223
|
+
</Select>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{/* Manager Filter */}
|
|
228
|
+
{showManagerFilter && managers && managers.length > 0 && (
|
|
229
|
+
<div>
|
|
230
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
231
|
+
Manager
|
|
232
|
+
</label>
|
|
233
|
+
<Select
|
|
234
|
+
value={localFilters.managerId}
|
|
235
|
+
onChange={(e) => handleFilterChange("managerId", e.target.value)}
|
|
236
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
237
|
+
>
|
|
238
|
+
<SelectOption value="">All Managers</SelectOption>
|
|
239
|
+
{managers.map((manager) => (
|
|
240
|
+
<SelectOption key={manager.entra_id} value={manager.entra_id}>
|
|
241
|
+
{manager.full_name}
|
|
242
|
+
</SelectOption>
|
|
243
|
+
))}
|
|
244
|
+
</Select>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{/* Start Date Range */}
|
|
249
|
+
{showStartDateFilter && (
|
|
250
|
+
<div>
|
|
251
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
252
|
+
Start Date Range
|
|
253
|
+
</label>
|
|
254
|
+
<DateRangePicker
|
|
255
|
+
selectedRange={getStartDateRange()}
|
|
256
|
+
onSelect={handleStartDateRangeChange}
|
|
257
|
+
placeholder="Select start date range"
|
|
258
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* End Date Range */}
|
|
264
|
+
{showEndDateFilter && (
|
|
265
|
+
<div>
|
|
266
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
267
|
+
End Date Range
|
|
268
|
+
</label>
|
|
269
|
+
<DateRangePicker
|
|
270
|
+
selectedRange={getEndDateRange()}
|
|
271
|
+
onSelect={handleEndDateRangeChange}
|
|
272
|
+
placeholder="Select end date range"
|
|
273
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* Term Time Only Filter */}
|
|
279
|
+
{showTermTimeFilter && (
|
|
280
|
+
<div>
|
|
281
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
282
|
+
Term Time Only
|
|
283
|
+
</label>
|
|
284
|
+
<Select
|
|
285
|
+
value={localFilters.termTimeOnly}
|
|
286
|
+
onChange={(e) => handleFilterChange("termTimeOnly", e.target.value)}
|
|
287
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
288
|
+
>
|
|
289
|
+
<SelectOption value="">All</SelectOption>
|
|
290
|
+
<SelectOption value="true">Term Time Only</SelectOption>
|
|
291
|
+
<SelectOption value="false">Not Term Time Only</SelectOption>
|
|
292
|
+
</Select>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{/* Maternity Leave Filter */}
|
|
297
|
+
{showMaternityFilter && (
|
|
298
|
+
<div>
|
|
299
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
300
|
+
Maternity Leave
|
|
301
|
+
</label>
|
|
302
|
+
<Select
|
|
303
|
+
value={localFilters.onMaternityLeave}
|
|
304
|
+
onChange={(e) => handleFilterChange("onMaternityLeave", e.target.value)}
|
|
305
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
306
|
+
>
|
|
307
|
+
<SelectOption value="">All</SelectOption>
|
|
308
|
+
<SelectOption value="true">On Maternity Leave</SelectOption>
|
|
309
|
+
<SelectOption value="false">Not On Maternity Leave</SelectOption>
|
|
310
|
+
</Select>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{/* DBS Number Filter */}
|
|
315
|
+
{showDbsFilter && (
|
|
316
|
+
<div>
|
|
317
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
318
|
+
DBS Number
|
|
319
|
+
</label>
|
|
320
|
+
<Input
|
|
321
|
+
type="text"
|
|
322
|
+
value={localFilters.dbsNumber}
|
|
323
|
+
onChange={(e) => handleFilterChange("dbsNumber", e.target.value)}
|
|
324
|
+
placeholder="Search by DBS number"
|
|
325
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{/* Working Hours Filter */}
|
|
331
|
+
{showWorkingHoursFilter && (
|
|
332
|
+
<div>
|
|
333
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
334
|
+
Min Hours Per Week
|
|
335
|
+
</label>
|
|
336
|
+
<Input
|
|
337
|
+
type="number"
|
|
338
|
+
min="0"
|
|
339
|
+
max="168"
|
|
340
|
+
value={localFilters.minHoursPerWeek}
|
|
341
|
+
onChange={(e) => handleFilterChange("minHoursPerWeek", e.target.value)}
|
|
342
|
+
placeholder="0"
|
|
343
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
{/* Sort By */}
|
|
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>
|
|
369
|
+
|
|
370
|
+
{/* Sort Order */}
|
|
371
|
+
<div>
|
|
372
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
373
|
+
Sort Order
|
|
374
|
+
</label>
|
|
375
|
+
<Select
|
|
376
|
+
value={localFilters.sortOrder}
|
|
377
|
+
onChange={(e) => handleFilterChange("sortOrder", e.target.value)}
|
|
378
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
379
|
+
>
|
|
380
|
+
<SelectOption value="asc">Ascending</SelectOption>
|
|
381
|
+
<SelectOption value="desc">Descending</SelectOption>
|
|
382
|
+
</Select>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
{/* Apply/Cancel Buttons */}
|
|
387
|
+
{hasUnsavedChanges && (
|
|
388
|
+
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
389
|
+
<div className="flex items-center justify-between">
|
|
390
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
391
|
+
You have unsaved filter changes
|
|
392
|
+
</span>
|
|
393
|
+
<div className="flex items-center space-x-2">
|
|
394
|
+
<Button
|
|
395
|
+
variant="outline"
|
|
396
|
+
size="sm"
|
|
397
|
+
onClick={handleCancelChanges}
|
|
398
|
+
>
|
|
399
|
+
Cancel
|
|
400
|
+
</Button>
|
|
401
|
+
<Button
|
|
402
|
+
size="sm"
|
|
403
|
+
onClick={handleApplyFilters}
|
|
404
|
+
>
|
|
405
|
+
Apply Filters
|
|
406
|
+
</Button>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export default EmployeeSearchFilters;
|