@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,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;
|