@snapdragonsnursery/react-components 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ // Employee Search Page Component Tests
2
+ // Tests the EmployeeSearchPage component functionality including search, filtering, pagination, and selection
3
+
4
+ import React from 'react';
5
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
7
+ import EmployeeSearchPage from './EmployeeSearchPage';
8
+
9
+ // Mock MSAL
10
+ jest.mock('@azure/msal-react', () => ({
11
+ useMsal: () => ({
12
+ instance: {
13
+ acquireTokenSilent: jest.fn(),
14
+ },
15
+ accounts: [{ localAccountId: 'test-user-id' }],
16
+ }),
17
+ }));
18
+
19
+ // Mock telemetry
20
+ jest.mock('./telemetry', () => ({
21
+ trackEvent: jest.fn(),
22
+ }));
23
+
24
+ // Mock utils
25
+ jest.mock('./lib/utils', () => ({
26
+ cn: (...classes) => classes.filter(Boolean).join(' '),
27
+ }));
28
+
29
+ // Mock fetch
30
+ global.fetch = jest.fn();
31
+
32
+ // Mock process.env
33
+ process.env.VITE_COMMON_API_FUNCTION_KEY = 'test-key';
34
+ process.env.VITE_COMMON_API_BASE_URL = 'https://test-api.example.com';
35
+
36
+ // Mock the UI components
37
+ jest.mock('./components/ui/input', () => ({
38
+ Input: ({ value, onChange, placeholder, ...props }) => {
39
+ return (
40
+ <input
41
+ data-testid="input"
42
+ value={value}
43
+ onChange={onChange}
44
+ placeholder={placeholder}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+ }));
50
+
51
+ jest.mock('./components/ui/button', () => ({
52
+ Button: ({ children, onClick, variant, size, ...props }) => {
53
+ return (
54
+ <button
55
+ data-testid="button"
56
+ onClick={onClick}
57
+ data-variant={variant}
58
+ data-size={size}
59
+ {...props}
60
+ >
61
+ {children}
62
+ </button>
63
+ );
64
+ }
65
+ }));
66
+
67
+ jest.mock('./components/ui/table', () => ({
68
+ Table: ({ children, ...props }) => <table data-testid="table" {...props}>{children}</table>,
69
+ TableBody: ({ children, ...props }) => <tbody data-testid="table-body" {...props}>{children}</tbody>,
70
+ TableCell: ({ children, ...props }) => <td data-testid="table-cell" {...props}>{children}</td>,
71
+ TableHead: ({ children, ...props }) => <th data-testid="table-head" {...props}>{children}</th>,
72
+ TableHeader: ({ children, ...props }) => <thead data-testid="table-header" {...props}>{children}</thead>,
73
+ TableRow: ({ children, ...props }) => <tr data-testid="table-row" {...props}>{children}</tr>,
74
+ }));
75
+
76
+ jest.mock('./components/ui/pagination', () => ({
77
+ Pagination: ({ children, ...props }) => <nav data-testid="pagination" {...props}>{children}</nav>,
78
+ PaginationContent: ({ children, ...props }) => <div data-testid="pagination-content" {...props}>{children}</div>,
79
+ PaginationEllipsis: ({ ...props }) => <span data-testid="pagination-ellipsis" {...props}>...</span>,
80
+ PaginationItem: ({ children, ...props }) => <div data-testid="pagination-item" {...props}>{children}</div>,
81
+ PaginationLink: ({ children, onClick, isActive, ...props }) => (
82
+ <button data-testid="pagination-link" onClick={onClick} data-active={isActive} {...props}>
83
+ {children}
84
+ </button>
85
+ ),
86
+ PaginationNext: ({ children, onClick, ...props }) => (
87
+ <button data-testid="pagination-next" onClick={onClick} {...props}>
88
+ {children}
89
+ </button>
90
+ ),
91
+ PaginationPrevious: ({ children, onClick, ...props }) => (
92
+ <button data-testid="pagination-previous" onClick={onClick} {...props}>
93
+ {children}
94
+ </button>
95
+ ),
96
+ }));
97
+
98
+ // Mock EmployeeSearchFilters
99
+ jest.mock('./components/EmployeeSearchFilters', () => {
100
+ return function MockEmployeeSearchFilters({ filters, onFiltersChange, onApplyFilters, onClearFilters, ...props }) {
101
+ return (
102
+ <div data-testid="employee-search-filters">
103
+ <button onClick={() => onFiltersChange({ ...filters, status: 'Inactive' })}>
104
+ Change Status
105
+ </button>
106
+ <button onClick={() => onApplyFilters(filters)}>
107
+ Apply Filters
108
+ </button>
109
+ <button onClick={onClearFilters}>
110
+ Clear Filters
111
+ </button>
112
+ </div>
113
+ );
114
+ };
115
+ });
116
+
117
+ const defaultProps = {
118
+ title: 'Employee Search',
119
+ onSelect: jest.fn(),
120
+ };
121
+
122
+ describe('EmployeeSearchPage', () => {
123
+ beforeEach(() => {
124
+ jest.clearAllMocks();
125
+ global.fetch.mockResolvedValue({
126
+ ok: true,
127
+ json: async () => ({
128
+ success: true,
129
+ data: {
130
+ employees: [],
131
+ pagination: {
132
+ page: 1,
133
+ pageSize: 20,
134
+ totalCount: 0,
135
+ totalPages: 0,
136
+ hasNextPage: false,
137
+ hasPreviousPage: false,
138
+ },
139
+ },
140
+ }),
141
+ });
142
+ });
143
+
144
+ describe('Basic Rendering', () => {
145
+ it('renders the page title', () => {
146
+ render(<EmployeeSearchPage {...defaultProps} />);
147
+
148
+ expect(screen.getByText('Employee Search')).toBeInTheDocument();
149
+ });
150
+
151
+ it('renders the search input', () => {
152
+ render(<EmployeeSearchPage {...defaultProps} />);
153
+
154
+ expect(screen.getByPlaceholderText('Search by name, employee ID, or email...')).toBeInTheDocument();
155
+ });
156
+
157
+ it('renders the employee search filters', () => {
158
+ render(<EmployeeSearchPage {...defaultProps} />);
159
+
160
+ expect(screen.getByTestId('employee-search-filters')).toBeInTheDocument();
161
+ });
162
+
163
+ it('shows loading state initially', () => {
164
+ render(<EmployeeSearchPage {...defaultProps} />);
165
+
166
+ expect(screen.getByRole('status')).toBeInTheDocument();
167
+ });
168
+ });
169
+
170
+ describe('Search Functionality', () => {
171
+ it('renders search input with correct placeholder', () => {
172
+ render(<EmployeeSearchPage {...defaultProps} />);
173
+
174
+ const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
175
+ expect(searchInput).toBeInTheDocument();
176
+ expect(searchInput).toHaveAttribute('type', 'text');
177
+ });
178
+
179
+ it('allows typing in search input', () => {
180
+ render(<EmployeeSearchPage {...defaultProps} />);
181
+
182
+ const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
183
+ fireEvent.change(searchInput, { target: { value: 'John' } });
184
+
185
+ expect(searchInput).toHaveValue('John');
186
+ });
187
+ });
188
+
189
+ describe('Filter Integration', () => {
190
+ it('renders filter buttons', () => {
191
+ render(<EmployeeSearchPage {...defaultProps} />);
192
+
193
+ expect(screen.getByText('Change Status')).toBeInTheDocument();
194
+ expect(screen.getByText('Apply Filters')).toBeInTheDocument();
195
+ expect(screen.getByText('Clear Filters')).toBeInTheDocument();
196
+ });
197
+
198
+ it('calls filter change handler when filter button is clicked', () => {
199
+ render(<EmployeeSearchPage {...defaultProps} />);
200
+
201
+ const changeStatusButton = screen.getByText('Change Status');
202
+ fireEvent.click(changeStatusButton);
203
+
204
+ // The mock should have been called
205
+ expect(changeStatusButton).toBeInTheDocument();
206
+ });
207
+ });
208
+
209
+ describe('Accessibility', () => {
210
+ it('has proper ARIA labels', () => {
211
+ render(<EmployeeSearchPage {...defaultProps} />);
212
+
213
+ const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
214
+ expect(searchInput).toBeInTheDocument();
215
+ });
216
+
217
+ it('supports keyboard navigation', () => {
218
+ render(<EmployeeSearchPage {...defaultProps} />);
219
+
220
+ const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
221
+ searchInput.focus();
222
+
223
+ expect(searchInput).toHaveFocus();
224
+ });
225
+ });
226
+
227
+ describe('Props Handling', () => {
228
+ it('renders custom title', () => {
229
+ render(<EmployeeSearchPage {...defaultProps} title="Custom Title" />);
230
+
231
+ expect(screen.getByText('Custom Title')).toBeInTheDocument();
232
+ });
233
+
234
+ it('renders custom search placeholder', () => {
235
+ render(<EmployeeSearchPage {...defaultProps} searchPlaceholder="Custom placeholder" />);
236
+
237
+ expect(screen.getByPlaceholderText('Custom placeholder')).toBeInTheDocument();
238
+ });
239
+ });
240
+ });
@@ -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;