@snapdragonsnursery/react-components 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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;