@snapdragonsnursery/react-components 1.1.38 → 1.2.0
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/README.md +115 -0
- package/package.json +23 -6
- package/src/ApplyButtonDemo.jsx +94 -0
- package/src/CalendarDemo.jsx +39 -0
- package/src/ChildSearchPage.jsx +100 -256
- package/src/DateRangePickerDebug.jsx +1 -0
- package/src/DateRangePickerDemo.jsx +58 -0
- package/src/DateRangePickerTest.jsx +71 -0
- package/src/components/ChildSearchFilters.jsx +237 -0
- package/src/components/ChildSearchFilters.test.jsx +308 -0
- package/src/components/ui/calendar.jsx +173 -0
- package/src/components/ui/date-range-picker.jsx +277 -0
- package/src/components/ui/date-range-picker.test.jsx +95 -0
- package/src/components/ui/popover.jsx +46 -0
- package/src/components/ui/simple-calendar.jsx +65 -0
- package/src/index.css +59 -0
- package/src/index.js +11 -0
package/src/ChildSearchPage.jsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import { trackEvent } from "./telemetry";
|
|
14
14
|
import { Button } from "./components/ui/button";
|
|
15
15
|
import { Input } from "./components/ui/input";
|
|
16
|
-
import { Select, SelectOption } from "./components/ui/select";
|
|
17
16
|
import {
|
|
18
17
|
Table,
|
|
19
18
|
TableBody,
|
|
@@ -32,17 +31,13 @@ import {
|
|
|
32
31
|
PaginationPrevious,
|
|
33
32
|
} from "./components/ui/pagination";
|
|
34
33
|
import {
|
|
35
|
-
MagnifyingGlassIcon,
|
|
36
|
-
FunnelIcon,
|
|
37
|
-
ChevronDownIcon,
|
|
38
|
-
ChevronUpIcon,
|
|
39
|
-
UserIcon,
|
|
40
|
-
MapPinIcon,
|
|
41
|
-
CalendarIcon,
|
|
42
34
|
CheckCircleIcon,
|
|
43
35
|
XCircleIcon,
|
|
36
|
+
ChevronUpIcon,
|
|
37
|
+
ChevronDownIcon,
|
|
44
38
|
} from "@heroicons/react/24/outline";
|
|
45
39
|
import { cn } from "./lib/utils";
|
|
40
|
+
import ChildSearchFilters from "./components/ChildSearchFilters";
|
|
46
41
|
|
|
47
42
|
const ChildSearchPage = ({
|
|
48
43
|
title = "Child Search",
|
|
@@ -90,6 +85,10 @@ const ChildSearchPage = ({
|
|
|
90
85
|
sortOrder: sortOrder,
|
|
91
86
|
});
|
|
92
87
|
|
|
88
|
+
// Table sorting state
|
|
89
|
+
const [sorting, setSorting] = useState([]);
|
|
90
|
+
const [debouncedAdvancedFilters, setDebouncedAdvancedFilters] = useState(advancedFilters);
|
|
91
|
+
|
|
93
92
|
// State for multi-select mode
|
|
94
93
|
const [selectedChildrenState, setSelectedChildrenState] = useState(
|
|
95
94
|
selectedChildren || []
|
|
@@ -100,6 +99,35 @@ const ChildSearchPage = ({
|
|
|
100
99
|
// Column helper for TanStack table
|
|
101
100
|
const columnHelper = createColumnHelper();
|
|
102
101
|
|
|
102
|
+
// Helper function to create sortable header
|
|
103
|
+
const createSortableHeader = (column, label) => {
|
|
104
|
+
return ({ column: tableColumn }) => {
|
|
105
|
+
const isSorted = tableColumn.getIsSorted();
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
className="flex items-center space-x-1 cursor-pointer select-none"
|
|
109
|
+
onClick={tableColumn.getToggleSortingHandler()}
|
|
110
|
+
>
|
|
111
|
+
<span>{label}</span>
|
|
112
|
+
<div className="flex flex-col">
|
|
113
|
+
<ChevronUpIcon
|
|
114
|
+
className={cn(
|
|
115
|
+
"h-3 w-3 transition-colors",
|
|
116
|
+
isSorted === "asc" ? "text-blue-500" : "text-gray-400"
|
|
117
|
+
)}
|
|
118
|
+
/>
|
|
119
|
+
<ChevronDownIcon
|
|
120
|
+
className={cn(
|
|
121
|
+
"h-3 w-3 transition-colors -mt-1",
|
|
122
|
+
isSorted === "desc" ? "text-blue-500" : "text-gray-400"
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
103
131
|
// Define table columns
|
|
104
132
|
const columns = [
|
|
105
133
|
// Checkbox column for multi-select
|
|
@@ -127,9 +155,9 @@ const ChildSearchPage = ({
|
|
|
127
155
|
}),
|
|
128
156
|
]
|
|
129
157
|
: []),
|
|
130
|
-
// Name column
|
|
158
|
+
// Name column - sortable
|
|
131
159
|
columnHelper.accessor("full_name", {
|
|
132
|
-
header: "Name",
|
|
160
|
+
header: createSortableHeader("full_name", "Name"),
|
|
133
161
|
cell: ({ row }) => (
|
|
134
162
|
<div>
|
|
135
163
|
<div className="font-medium">{row.original.full_name}</div>
|
|
@@ -139,16 +167,16 @@ const ChildSearchPage = ({
|
|
|
139
167
|
</div>
|
|
140
168
|
),
|
|
141
169
|
}),
|
|
142
|
-
// Site column
|
|
170
|
+
// Site column - sortable
|
|
143
171
|
columnHelper.accessor("site_name", {
|
|
144
|
-
header: "Site",
|
|
172
|
+
header: createSortableHeader("site_name", "Site"),
|
|
145
173
|
cell: ({ row }) => (
|
|
146
174
|
<span>{row.original.site_name}</span>
|
|
147
175
|
),
|
|
148
176
|
}),
|
|
149
|
-
// Date of Birth column
|
|
177
|
+
// Date of Birth column - sortable
|
|
150
178
|
columnHelper.accessor("date_of_birth", {
|
|
151
|
-
header: "Date of Birth",
|
|
179
|
+
header: createSortableHeader("date_of_birth", "Date of Birth"),
|
|
152
180
|
cell: ({ row }) => (
|
|
153
181
|
<span>
|
|
154
182
|
{row.original.date_of_birth
|
|
@@ -157,9 +185,9 @@ const ChildSearchPage = ({
|
|
|
157
185
|
</span>
|
|
158
186
|
),
|
|
159
187
|
}),
|
|
160
|
-
// Age column
|
|
188
|
+
// Age column - sortable (using age_years for sorting)
|
|
161
189
|
columnHelper.accessor("age_years", {
|
|
162
|
-
header: "Age",
|
|
190
|
+
header: createSortableHeader("age_years", "Age"),
|
|
163
191
|
cell: ({ row }) => {
|
|
164
192
|
const { age_years, age_months } = row.original;
|
|
165
193
|
|
|
@@ -175,27 +203,20 @@ const ChildSearchPage = ({
|
|
|
175
203
|
return <span>{ageText}</span>;
|
|
176
204
|
},
|
|
177
205
|
}),
|
|
178
|
-
// Status column
|
|
206
|
+
// Status column - sortable
|
|
179
207
|
columnHelper.accessor("is_active", {
|
|
180
|
-
header: "Status",
|
|
208
|
+
header: createSortableHeader("is_active", "Status"),
|
|
181
209
|
cell: ({ row }) => (
|
|
182
|
-
<
|
|
183
|
-
{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
<span
|
|
211
|
+
className={cn(
|
|
212
|
+
"px-2 py-1 text-xs rounded-full",
|
|
213
|
+
row.original.is_active
|
|
214
|
+
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
|
215
|
+
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
|
187
216
|
)}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
row.original.is_active
|
|
192
|
-
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
|
193
|
-
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
|
194
|
-
)}
|
|
195
|
-
>
|
|
196
|
-
{row.original.is_active ? "Active" : "Inactive"}
|
|
197
|
-
</span>
|
|
198
|
-
</div>
|
|
217
|
+
>
|
|
218
|
+
{row.original.is_active ? "Active" : "Inactive"}
|
|
219
|
+
</span>
|
|
199
220
|
),
|
|
200
221
|
}),
|
|
201
222
|
];
|
|
@@ -207,6 +228,7 @@ const ChildSearchPage = ({
|
|
|
207
228
|
getCoreRowModel: getCoreRowModel(),
|
|
208
229
|
getSortedRowModel: getSortedRowModel(),
|
|
209
230
|
state: {
|
|
231
|
+
sorting,
|
|
210
232
|
rowSelection: selectedChildrenState.reduce((acc, selectedChild) => {
|
|
211
233
|
const rowIndex = children.findIndex(child => child.child_id === selectedChild.child_id);
|
|
212
234
|
if (rowIndex !== -1) {
|
|
@@ -215,6 +237,7 @@ const ChildSearchPage = ({
|
|
|
215
237
|
return acc;
|
|
216
238
|
}, {}),
|
|
217
239
|
},
|
|
240
|
+
onSortingChange: setSorting,
|
|
218
241
|
onRowSelectionChange: (updater) => {
|
|
219
242
|
const newSelection =
|
|
220
243
|
typeof updater === "function" ? updater({}) : updater;
|
|
@@ -235,6 +258,16 @@ const ChildSearchPage = ({
|
|
|
235
258
|
return () => clearTimeout(timer);
|
|
236
259
|
}, [searchTerm]);
|
|
237
260
|
|
|
261
|
+
// Debounce advanced filters
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
const timer = setTimeout(() => {
|
|
264
|
+
setDebouncedAdvancedFilters(advancedFilters);
|
|
265
|
+
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
266
|
+
}, 500); // Slightly longer delay for filters
|
|
267
|
+
|
|
268
|
+
return () => clearTimeout(timer);
|
|
269
|
+
}, [advancedFilters]);
|
|
270
|
+
|
|
238
271
|
// Search children
|
|
239
272
|
const searchChildren = useCallback(async () => {
|
|
240
273
|
if (!instance || !accounts[0]) {
|
|
@@ -260,38 +293,38 @@ const ChildSearchPage = ({
|
|
|
260
293
|
// Handle site filtering
|
|
261
294
|
if (siteIds && siteIds.length > 0) {
|
|
262
295
|
params.append("site_ids", siteIds.join(","));
|
|
263
|
-
} else if (
|
|
264
|
-
params.append("site_id",
|
|
296
|
+
} else if (debouncedAdvancedFilters.selectedSiteId) {
|
|
297
|
+
params.append("site_id", debouncedAdvancedFilters.selectedSiteId.toString());
|
|
265
298
|
} else if (siteId) {
|
|
266
299
|
params.append("site_id", siteId.toString());
|
|
267
300
|
}
|
|
268
301
|
|
|
269
302
|
// Handle status filtering
|
|
270
|
-
if (
|
|
271
|
-
params.append("status",
|
|
303
|
+
if (debouncedAdvancedFilters.status && debouncedAdvancedFilters.status !== "all") {
|
|
304
|
+
params.append("status", debouncedAdvancedFilters.status);
|
|
272
305
|
} else if (activeOnly) {
|
|
273
306
|
params.append("active_only", "true");
|
|
274
307
|
}
|
|
275
308
|
|
|
276
309
|
// Add date of birth filters
|
|
277
|
-
if (
|
|
278
|
-
params.append("dob_from",
|
|
310
|
+
if (debouncedAdvancedFilters.dobFrom) {
|
|
311
|
+
params.append("dob_from", debouncedAdvancedFilters.dobFrom);
|
|
279
312
|
}
|
|
280
|
-
if (
|
|
281
|
-
params.append("dob_to",
|
|
313
|
+
if (debouncedAdvancedFilters.dobTo) {
|
|
314
|
+
params.append("dob_to", debouncedAdvancedFilters.dobTo);
|
|
282
315
|
}
|
|
283
316
|
|
|
284
317
|
// Add age filters
|
|
285
|
-
if (
|
|
286
|
-
params.append("age_from",
|
|
318
|
+
if (debouncedAdvancedFilters.ageFrom) {
|
|
319
|
+
params.append("age_from", debouncedAdvancedFilters.ageFrom);
|
|
287
320
|
}
|
|
288
|
-
if (
|
|
289
|
-
params.append("age_to",
|
|
321
|
+
if (debouncedAdvancedFilters.ageTo) {
|
|
322
|
+
params.append("age_to", debouncedAdvancedFilters.ageTo);
|
|
290
323
|
}
|
|
291
324
|
|
|
292
325
|
// Add sorting
|
|
293
|
-
params.append("sort_by",
|
|
294
|
-
params.append("sort_order",
|
|
326
|
+
params.append("sort_by", debouncedAdvancedFilters.sortBy);
|
|
327
|
+
params.append("sort_order", debouncedAdvancedFilters.sortOrder);
|
|
295
328
|
|
|
296
329
|
const apiResponse = await fetch(
|
|
297
330
|
`${
|
|
@@ -349,7 +382,7 @@ const ChildSearchPage = ({
|
|
|
349
382
|
siteId,
|
|
350
383
|
siteIds,
|
|
351
384
|
activeOnly,
|
|
352
|
-
|
|
385
|
+
debouncedAdvancedFilters,
|
|
353
386
|
applicationContext,
|
|
354
387
|
bypassPermissions,
|
|
355
388
|
]);
|
|
@@ -398,7 +431,7 @@ const ChildSearchPage = ({
|
|
|
398
431
|
};
|
|
399
432
|
|
|
400
433
|
const clearFilters = () => {
|
|
401
|
-
|
|
434
|
+
const clearedFilters = {
|
|
402
435
|
status: activeOnly ? "active" : "all",
|
|
403
436
|
selectedSiteId: "",
|
|
404
437
|
dobFrom: "",
|
|
@@ -407,7 +440,9 @@ const ChildSearchPage = ({
|
|
|
407
440
|
ageTo: "",
|
|
408
441
|
sortBy: "last_name",
|
|
409
442
|
sortOrder: "asc",
|
|
410
|
-
}
|
|
443
|
+
};
|
|
444
|
+
setAdvancedFilters(clearedFilters);
|
|
445
|
+
setDebouncedAdvancedFilters(clearedFilters); // Clear immediately
|
|
411
446
|
};
|
|
412
447
|
|
|
413
448
|
// Calculate pagination display values
|
|
@@ -433,11 +468,10 @@ const ChildSearchPage = ({
|
|
|
433
468
|
)}
|
|
434
469
|
</div>
|
|
435
470
|
|
|
436
|
-
{/* Search
|
|
471
|
+
{/* Search Input */}
|
|
437
472
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
|
|
438
473
|
<div className="p-6">
|
|
439
|
-
|
|
440
|
-
<div className="relative mb-4">
|
|
474
|
+
<div className="relative">
|
|
441
475
|
<Input
|
|
442
476
|
type="text"
|
|
443
477
|
placeholder={searchPlaceholder}
|
|
@@ -446,211 +480,21 @@ const ChildSearchPage = ({
|
|
|
446
480
|
className="pl-4"
|
|
447
481
|
/>
|
|
448
482
|
</div>
|
|
449
|
-
|
|
450
|
-
{/* Advanced Filters Toggle */}
|
|
451
|
-
<div className="flex items-center justify-between">
|
|
452
|
-
<button
|
|
453
|
-
onClick={() => setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)}
|
|
454
|
-
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"
|
|
455
|
-
>
|
|
456
|
-
<FunnelIcon className="h-4 w-4" />
|
|
457
|
-
<span>Advanced Filters</span>
|
|
458
|
-
{isAdvancedFiltersOpen ? (
|
|
459
|
-
<ChevronUpIcon className="h-4 w-4" />
|
|
460
|
-
) : (
|
|
461
|
-
<ChevronDownIcon className="h-4 w-4" />
|
|
462
|
-
)}
|
|
463
|
-
</button>
|
|
464
|
-
<button
|
|
465
|
-
onClick={clearFilters}
|
|
466
|
-
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
|
467
|
-
>
|
|
468
|
-
Clear Filters
|
|
469
|
-
</button>
|
|
470
|
-
</div>
|
|
471
|
-
|
|
472
|
-
{/* Advanced Filters */}
|
|
473
|
-
{isAdvancedFiltersOpen && (
|
|
474
|
-
<div
|
|
475
|
-
className="mt-4 p-4 rounded-lg"
|
|
476
|
-
style={{
|
|
477
|
-
backgroundColor: 'hsl(var(--card))',
|
|
478
|
-
border: '1px solid hsl(var(--border))'
|
|
479
|
-
}}
|
|
480
|
-
>
|
|
481
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
482
|
-
{/* Status Filter */}
|
|
483
|
-
<div>
|
|
484
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
485
|
-
Status
|
|
486
|
-
</label>
|
|
487
|
-
<Select
|
|
488
|
-
value={advancedFilters.status}
|
|
489
|
-
onChange={(e) =>
|
|
490
|
-
setAdvancedFilters((prev) => ({
|
|
491
|
-
...prev,
|
|
492
|
-
status: e.target.value,
|
|
493
|
-
}))
|
|
494
|
-
}
|
|
495
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
496
|
-
>
|
|
497
|
-
<SelectOption value="all">All Children</SelectOption>
|
|
498
|
-
<SelectOption value="active">Active Only</SelectOption>
|
|
499
|
-
<SelectOption value="inactive">
|
|
500
|
-
Inactive Only
|
|
501
|
-
</SelectOption>
|
|
502
|
-
</Select>
|
|
503
|
-
</div>
|
|
504
|
-
|
|
505
|
-
{/* Site Filter */}
|
|
506
|
-
{sites && sites.length > 0 && (
|
|
507
|
-
<div>
|
|
508
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
509
|
-
Site
|
|
510
|
-
</label>
|
|
511
|
-
<Select
|
|
512
|
-
value={advancedFilters.selectedSiteId}
|
|
513
|
-
onChange={(e) =>
|
|
514
|
-
setAdvancedFilters((prev) => ({
|
|
515
|
-
...prev,
|
|
516
|
-
selectedSiteId: e.target.value,
|
|
517
|
-
}))
|
|
518
|
-
}
|
|
519
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
520
|
-
>
|
|
521
|
-
<SelectOption value="">All Sites</SelectOption>
|
|
522
|
-
{sites.map((site) => (
|
|
523
|
-
<SelectOption key={site.site_id} value={site.site_id}>
|
|
524
|
-
{site.site_name}
|
|
525
|
-
</SelectOption>
|
|
526
|
-
))}
|
|
527
|
-
</Select>
|
|
528
|
-
</div>
|
|
529
|
-
)}
|
|
530
|
-
|
|
531
|
-
{/* Date of Birth Range */}
|
|
532
|
-
<div>
|
|
533
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
534
|
-
Date of Birth From
|
|
535
|
-
</label>
|
|
536
|
-
<Input
|
|
537
|
-
type="date"
|
|
538
|
-
value={advancedFilters.dobFrom}
|
|
539
|
-
onChange={(e) =>
|
|
540
|
-
setAdvancedFilters((prev) => ({
|
|
541
|
-
...prev,
|
|
542
|
-
dobFrom: e.target.value,
|
|
543
|
-
}))
|
|
544
|
-
}
|
|
545
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
546
|
-
/>
|
|
547
|
-
</div>
|
|
548
|
-
|
|
549
|
-
<div>
|
|
550
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
551
|
-
Date of Birth To
|
|
552
|
-
</label>
|
|
553
|
-
<Input
|
|
554
|
-
type="date"
|
|
555
|
-
value={advancedFilters.dobTo}
|
|
556
|
-
onChange={(e) =>
|
|
557
|
-
setAdvancedFilters((prev) => ({
|
|
558
|
-
...prev,
|
|
559
|
-
dobTo: e.target.value,
|
|
560
|
-
}))
|
|
561
|
-
}
|
|
562
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
563
|
-
/>
|
|
564
|
-
</div>
|
|
565
|
-
|
|
566
|
-
{/* Age Range */}
|
|
567
|
-
<div>
|
|
568
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
569
|
-
Age From (months)
|
|
570
|
-
</label>
|
|
571
|
-
<Input
|
|
572
|
-
type="number"
|
|
573
|
-
min="0"
|
|
574
|
-
value={advancedFilters.ageFrom}
|
|
575
|
-
onChange={(e) =>
|
|
576
|
-
setAdvancedFilters((prev) => ({
|
|
577
|
-
...prev,
|
|
578
|
-
ageFrom: e.target.value,
|
|
579
|
-
}))
|
|
580
|
-
}
|
|
581
|
-
placeholder="0"
|
|
582
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
583
|
-
/>
|
|
584
|
-
</div>
|
|
585
|
-
|
|
586
|
-
<div>
|
|
587
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
588
|
-
Age To (months)
|
|
589
|
-
</label>
|
|
590
|
-
<Input
|
|
591
|
-
type="number"
|
|
592
|
-
min="0"
|
|
593
|
-
value={advancedFilters.ageTo}
|
|
594
|
-
onChange={(e) =>
|
|
595
|
-
setAdvancedFilters((prev) => ({
|
|
596
|
-
...prev,
|
|
597
|
-
ageTo: e.target.value,
|
|
598
|
-
}))
|
|
599
|
-
}
|
|
600
|
-
placeholder="60"
|
|
601
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
602
|
-
/>
|
|
603
|
-
</div>
|
|
604
|
-
|
|
605
|
-
{/* Sort Options */}
|
|
606
|
-
<div>
|
|
607
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
608
|
-
Sort By
|
|
609
|
-
</label>
|
|
610
|
-
<Select
|
|
611
|
-
value={advancedFilters.sortBy}
|
|
612
|
-
onChange={(e) =>
|
|
613
|
-
setAdvancedFilters((prev) => ({
|
|
614
|
-
...prev,
|
|
615
|
-
sortBy: e.target.value,
|
|
616
|
-
}))
|
|
617
|
-
}
|
|
618
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
619
|
-
>
|
|
620
|
-
<SelectOption value="last_name">Last Name</SelectOption>
|
|
621
|
-
<SelectOption value="first_name">First Name</SelectOption>
|
|
622
|
-
<SelectOption value="full_name">Full Name</SelectOption>
|
|
623
|
-
<SelectOption value="date_of_birth">
|
|
624
|
-
Date of Birth
|
|
625
|
-
</SelectOption>
|
|
626
|
-
<SelectOption value="site_name">Site Name</SelectOption>
|
|
627
|
-
</Select>
|
|
628
|
-
</div>
|
|
629
|
-
|
|
630
|
-
<div>
|
|
631
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
632
|
-
Sort Order
|
|
633
|
-
</label>
|
|
634
|
-
<Select
|
|
635
|
-
value={advancedFilters.sortOrder}
|
|
636
|
-
onChange={(e) =>
|
|
637
|
-
setAdvancedFilters((prev) => ({
|
|
638
|
-
...prev,
|
|
639
|
-
sortOrder: e.target.value,
|
|
640
|
-
}))
|
|
641
|
-
}
|
|
642
|
-
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
643
|
-
>
|
|
644
|
-
<SelectOption value="asc">Ascending</SelectOption>
|
|
645
|
-
<SelectOption value="desc">Descending</SelectOption>
|
|
646
|
-
</Select>
|
|
647
|
-
</div>
|
|
648
|
-
</div>
|
|
649
|
-
</div>
|
|
650
|
-
)}
|
|
651
483
|
</div>
|
|
652
484
|
</div>
|
|
653
485
|
|
|
486
|
+
{/* Advanced Filters */}
|
|
487
|
+
<ChildSearchFilters
|
|
488
|
+
filters={advancedFilters}
|
|
489
|
+
onFiltersChange={setAdvancedFilters}
|
|
490
|
+
onApplyFilters={setAdvancedFilters}
|
|
491
|
+
sites={sites}
|
|
492
|
+
activeOnly={activeOnly}
|
|
493
|
+
isAdvancedFiltersOpen={isAdvancedFiltersOpen}
|
|
494
|
+
onToggleAdvancedFilters={() => setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)}
|
|
495
|
+
onClearFilters={clearFilters}
|
|
496
|
+
/>
|
|
497
|
+
|
|
654
498
|
{/* Results */}
|
|
655
499
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
656
500
|
{loading && (
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Demo component showing how to use DateRangePicker and DatePicker components
|
|
2
|
+
// Demonstrates both single date and date range selection functionality
|
|
3
|
+
|
|
4
|
+
import React, { useState } from 'react'
|
|
5
|
+
import { DateRangePicker, DatePicker } from './components/ui/date-range-picker'
|
|
6
|
+
|
|
7
|
+
export default function DateRangePickerDemo() {
|
|
8
|
+
const [selectedRange, setSelectedRange] = useState(null)
|
|
9
|
+
const [selectedDate, setSelectedDate] = useState(null)
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="p-6 space-y-6 max-w-md mx-auto">
|
|
13
|
+
<h2 className="text-2xl font-bold mb-4">Date Picker Components Demo</h2>
|
|
14
|
+
|
|
15
|
+
<div className="space-y-4">
|
|
16
|
+
<div>
|
|
17
|
+
<h3 className="text-lg font-semibold mb-2">Date Range Picker</h3>
|
|
18
|
+
<DateRangePicker
|
|
19
|
+
selectedRange={selectedRange}
|
|
20
|
+
onSelect={setSelectedRange}
|
|
21
|
+
placeholder="Select a date range"
|
|
22
|
+
/>
|
|
23
|
+
{selectedRange?.from && selectedRange?.to && (
|
|
24
|
+
<p className="text-sm text-gray-600 mt-2">
|
|
25
|
+
Selected: {selectedRange.from.toLocaleDateString()} - {selectedRange.to.toLocaleDateString()}
|
|
26
|
+
</p>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div>
|
|
31
|
+
<h3 className="text-lg font-semibold mb-2">Single Date Picker</h3>
|
|
32
|
+
<DatePicker
|
|
33
|
+
selectedDate={selectedDate}
|
|
34
|
+
onSelect={setSelectedDate}
|
|
35
|
+
placeholder="Select a date"
|
|
36
|
+
/>
|
|
37
|
+
{selectedDate && (
|
|
38
|
+
<p className="text-sm text-gray-600 mt-2">
|
|
39
|
+
Selected: {selectedDate.toLocaleDateString()}
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="pt-4">
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => {
|
|
47
|
+
setSelectedRange(null)
|
|
48
|
+
setSelectedDate(null)
|
|
49
|
+
}}
|
|
50
|
+
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded"
|
|
51
|
+
>
|
|
52
|
+
Clear All
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Test component to verify DateRangePicker behavior
|
|
2
|
+
// This can be used to test if the date range picker stays open when selecting first date
|
|
3
|
+
|
|
4
|
+
import React, { useState } from 'react'
|
|
5
|
+
import { DateRangePicker } from './components/ui/date-range-picker'
|
|
6
|
+
|
|
7
|
+
export default function DateRangePickerTest() {
|
|
8
|
+
const [selectedRange, setSelectedRange] = useState(null)
|
|
9
|
+
const [filters, setFilters] = useState({
|
|
10
|
+
dobFrom: '',
|
|
11
|
+
dobTo: '',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const handleDateRangeChange = (range) => {
|
|
15
|
+
console.log('DateRangePickerTest handleDateRangeChange called with:', range)
|
|
16
|
+
|
|
17
|
+
// DateRangePicker now only calls onSelect when both dates are selected or when cleared
|
|
18
|
+
if (range?.from && range?.to) {
|
|
19
|
+
console.log('Both dates selected - updating filters')
|
|
20
|
+
setFilters({
|
|
21
|
+
dobFrom: range.from.toISOString().split('T')[0],
|
|
22
|
+
dobTo: range.to.toISOString().split('T')[0],
|
|
23
|
+
})
|
|
24
|
+
} else if (!range?.from && !range?.to) {
|
|
25
|
+
console.log('Range cleared - clearing filters')
|
|
26
|
+
setFilters({
|
|
27
|
+
dobFrom: '',
|
|
28
|
+
dobTo: '',
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
// No need to handle single date selection as DateRangePicker handles it internally
|
|
32
|
+
|
|
33
|
+
setSelectedRange(range)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="p-6 space-y-4 max-w-md mx-auto">
|
|
38
|
+
<h2 className="text-2xl font-bold">Date Range Picker Test</h2>
|
|
39
|
+
|
|
40
|
+
<div className="space-y-2">
|
|
41
|
+
<label className="block text-sm font-medium">Date of Birth Range</label>
|
|
42
|
+
<DateRangePicker
|
|
43
|
+
selectedRange={selectedRange}
|
|
44
|
+
onSelect={handleDateRangeChange}
|
|
45
|
+
placeholder="Select date of birth range"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="space-y-2">
|
|
50
|
+
<h3 className="text-lg font-semibold">Current State:</h3>
|
|
51
|
+
<div className="text-sm space-y-1">
|
|
52
|
+
<div>Selected Range: {selectedRange ? JSON.stringify(selectedRange, null, 2) : 'null'}</div>
|
|
53
|
+
<div>Filters dobFrom: {filters.dobFrom || 'empty'}</div>
|
|
54
|
+
<div>Filters dobTo: {filters.dobTo || 'empty'}</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="pt-4">
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => {
|
|
61
|
+
setSelectedRange(null)
|
|
62
|
+
setFilters({ dobFrom: '', dobTo: '' })
|
|
63
|
+
}}
|
|
64
|
+
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded"
|
|
65
|
+
>
|
|
66
|
+
Clear All
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|