@snapdragonsnursery/react-components 1.13.0 → 1.16.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 +4 -4
- package/package.json +1 -1
- package/src/ChildSearchModal.jsx +41 -11
- package/src/ChildSearchPage.jsx +33 -6
- package/src/EmployeeSearchDemo.jsx +37 -1
- package/src/EmployeeSearchModal.jsx +425 -122
- package/src/EmployeeSearchPage.jsx +193 -138
- package/src/__mocks__/importMetaEnv.js +2 -3
- package/src/components/ui/date-range-picker.jsx +68 -76
- package/src/components/ui/popover.jsx +1 -1
|
@@ -72,6 +72,13 @@ const EmployeeSearchPage = ({
|
|
|
72
72
|
showEndDateFilter = false,
|
|
73
73
|
showWorkingHoursFilter = true,
|
|
74
74
|
loadAllResults = false, // If true, loads all results instead of paginating
|
|
75
|
+
// Auth options: provide token directly or a function to fetch it
|
|
76
|
+
authToken = null,
|
|
77
|
+
getAccessToken = null,
|
|
78
|
+
// Column customization
|
|
79
|
+
visibleColumns = null, // e.g. ['full_name','site_name','role_name','email','start_date','employee_status','total_hours_per_week']
|
|
80
|
+
columnLabels = {}, // e.g. { full_name: 'Employee', site_name: 'Location' }
|
|
81
|
+
columnRenderers = {}, // e.g. { email: (row) => <a>{row.email}</a> }
|
|
75
82
|
}) => {
|
|
76
83
|
const [searchTerm, setSearchTerm] = useState("");
|
|
77
84
|
const [employees, setEmployees] = useState([]);
|
|
@@ -157,98 +164,80 @@ const EmployeeSearchPage = ({
|
|
|
157
164
|
};
|
|
158
165
|
};
|
|
159
166
|
|
|
160
|
-
// Define
|
|
161
|
-
const
|
|
162
|
-
// Checkbox column for multi-select
|
|
163
|
-
...(multiSelect
|
|
164
|
-
? [
|
|
165
|
-
columnHelper.display({
|
|
166
|
-
id: "select",
|
|
167
|
-
header: ({ table }) => (
|
|
168
|
-
<input
|
|
169
|
-
type="checkbox"
|
|
170
|
-
checked={table.getIsAllPageRowsSelected()}
|
|
171
|
-
onChange={table.getToggleAllPageRowsSelectedHandler()}
|
|
172
|
-
className="rounded border-gray-300"
|
|
173
|
-
/>
|
|
174
|
-
),
|
|
175
|
-
cell: ({ row }) => (
|
|
176
|
-
<input
|
|
177
|
-
type="checkbox"
|
|
178
|
-
checked={row.getIsSelected()}
|
|
179
|
-
onChange={row.getToggleSelectedHandler()}
|
|
180
|
-
className="rounded border-gray-300"
|
|
181
|
-
/>
|
|
182
|
-
),
|
|
183
|
-
size: 50,
|
|
184
|
-
}),
|
|
185
|
-
]
|
|
186
|
-
: []),
|
|
187
|
-
// Name column - sortable
|
|
167
|
+
// Define default data columns (excluding the optional select column)
|
|
168
|
+
const defaultDataColumns = [
|
|
188
169
|
columnHelper.accessor("full_name", {
|
|
189
|
-
header: createSortableHeader("full_name", "Name"),
|
|
190
|
-
cell: ({ row }) =>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
170
|
+
header: createSortableHeader("full_name", columnLabels.full_name || "Name"),
|
|
171
|
+
cell: ({ row }) =>
|
|
172
|
+
columnRenderers.full_name ? (
|
|
173
|
+
columnRenderers.full_name(row.original)
|
|
174
|
+
) : (
|
|
175
|
+
<div>
|
|
176
|
+
<div className="font-medium">{row.original.full_name}</div>
|
|
177
|
+
<div className="text-sm text-gray-500">ID: {row.original.employee_id}</div>
|
|
195
178
|
</div>
|
|
196
|
-
|
|
197
|
-
),
|
|
179
|
+
),
|
|
198
180
|
}),
|
|
199
|
-
// Site column - sortable
|
|
200
181
|
columnHelper.accessor("site_name", {
|
|
201
|
-
header: createSortableHeader("site_name", "Site"),
|
|
202
|
-
cell: ({ row }) =>
|
|
203
|
-
|
|
204
|
-
|
|
182
|
+
header: createSortableHeader("site_name", columnLabels.site_name || "Site"),
|
|
183
|
+
cell: ({ row }) =>
|
|
184
|
+
columnRenderers.site_name ? (
|
|
185
|
+
columnRenderers.site_name(row.original)
|
|
186
|
+
) : (
|
|
187
|
+
<span>{row.original.site_name}</span>
|
|
188
|
+
),
|
|
205
189
|
}),
|
|
206
|
-
// Role column - sortable
|
|
207
190
|
columnHelper.accessor("role_name", {
|
|
208
|
-
header: createSortableHeader("role_name", "Role"),
|
|
209
|
-
cell: ({ row }) =>
|
|
210
|
-
|
|
211
|
-
|
|
191
|
+
header: createSortableHeader("role_name", columnLabels.role_name || "Role"),
|
|
192
|
+
cell: ({ row }) =>
|
|
193
|
+
columnRenderers.role_name ? (
|
|
194
|
+
columnRenderers.role_name(row.original)
|
|
195
|
+
) : (
|
|
196
|
+
<span>{row.original.role_name}</span>
|
|
197
|
+
),
|
|
212
198
|
}),
|
|
213
|
-
// Manager column - sortable
|
|
214
199
|
columnHelper.accessor("manager_name", {
|
|
215
|
-
header: createSortableHeader("manager_name", "Manager"),
|
|
216
|
-
cell: ({ row }) =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
200
|
+
header: createSortableHeader("manager_name", columnLabels.manager_name || "Manager"),
|
|
201
|
+
cell: ({ row }) =>
|
|
202
|
+
columnRenderers.manager_name ? (
|
|
203
|
+
columnRenderers.manager_name(row.original)
|
|
204
|
+
) : (
|
|
205
|
+
<span className="text-sm">{row.original.manager_name || "N/A"}</span>
|
|
206
|
+
),
|
|
221
207
|
}),
|
|
222
|
-
// Email column
|
|
223
208
|
columnHelper.accessor("email", {
|
|
224
|
-
header: "Email",
|
|
225
|
-
cell: ({ row }) =>
|
|
226
|
-
|
|
227
|
-
|
|
209
|
+
header: columnLabels.email || "Email",
|
|
210
|
+
cell: ({ row }) =>
|
|
211
|
+
columnRenderers.email ? (
|
|
212
|
+
columnRenderers.email(row.original)
|
|
213
|
+
) : (
|
|
214
|
+
<span className="text-sm">{row.original.email}</span>
|
|
215
|
+
),
|
|
228
216
|
}),
|
|
229
|
-
// Start Date column - sortable
|
|
230
217
|
columnHelper.accessor("start_date", {
|
|
231
|
-
header: createSortableHeader("start_date", "Start Date"),
|
|
232
|
-
cell: ({ row }) =>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
218
|
+
header: createSortableHeader("start_date", columnLabels.start_date || "Start Date"),
|
|
219
|
+
cell: ({ row }) =>
|
|
220
|
+
columnRenderers.start_date ? (
|
|
221
|
+
columnRenderers.start_date(row.original)
|
|
222
|
+
) : (
|
|
223
|
+
<span>
|
|
224
|
+
{row.original.start_date
|
|
225
|
+
? new Date(row.original.start_date).toLocaleDateString("en-GB")
|
|
226
|
+
: "N/A"}
|
|
227
|
+
</span>
|
|
228
|
+
),
|
|
239
229
|
}),
|
|
240
|
-
// Status column - sortable
|
|
241
230
|
columnHelper.accessor("employee_status", {
|
|
242
|
-
header: createSortableHeader("employee_status", "Status"),
|
|
231
|
+
header: createSortableHeader("employee_status", columnLabels.employee_status || "Status"),
|
|
243
232
|
cell: ({ row }) => {
|
|
233
|
+
if (columnRenderers.employee_status) return columnRenderers.employee_status(row.original);
|
|
244
234
|
const status = row.original.employee_status;
|
|
245
235
|
const statusColors = {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
236
|
+
Active: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
237
|
+
Inactive: "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200",
|
|
238
|
+
"On Leave": "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
|
239
|
+
Terminated: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
|
250
240
|
};
|
|
251
|
-
|
|
252
241
|
return (
|
|
253
242
|
<span
|
|
254
243
|
className={cn(
|
|
@@ -261,45 +250,86 @@ const EmployeeSearchPage = ({
|
|
|
261
250
|
);
|
|
262
251
|
},
|
|
263
252
|
}),
|
|
264
|
-
// Working Hours column
|
|
265
253
|
columnHelper.accessor("total_hours_per_week", {
|
|
266
|
-
header: "Hours/Week",
|
|
267
|
-
cell: ({ row }) =>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
254
|
+
header: columnLabels.total_hours_per_week || "Hours/Week",
|
|
255
|
+
cell: ({ row }) =>
|
|
256
|
+
columnRenderers.total_hours_per_week ? (
|
|
257
|
+
columnRenderers.total_hours_per_week(row.original)
|
|
258
|
+
) : (
|
|
259
|
+
<span>
|
|
260
|
+
{row.original.total_hours_per_week
|
|
261
|
+
? `${row.original.total_hours_per_week}h`
|
|
262
|
+
: "N/A"}
|
|
263
|
+
</span>
|
|
264
|
+
),
|
|
274
265
|
}),
|
|
275
|
-
// Term Time Only column
|
|
276
266
|
columnHelper.accessor("term_time_only", {
|
|
277
|
-
header: "Term Time",
|
|
278
|
-
cell: ({ row }) =>
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
267
|
+
header: columnLabels.term_time_only || "Term Time",
|
|
268
|
+
cell: ({ row }) =>
|
|
269
|
+
columnRenderers.term_time_only ? (
|
|
270
|
+
columnRenderers.term_time_only(row.original)
|
|
271
|
+
) : (
|
|
272
|
+
<span>
|
|
273
|
+
{row.original.term_time_only ? (
|
|
274
|
+
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
|
275
|
+
) : (
|
|
276
|
+
<XCircleIcon className="h-4 w-4 text-gray-400" />
|
|
277
|
+
)}
|
|
278
|
+
</span>
|
|
279
|
+
),
|
|
287
280
|
}),
|
|
288
|
-
// Maternity Leave column
|
|
289
281
|
columnHelper.accessor("on_maternity_leave", {
|
|
290
|
-
header: "Maternity",
|
|
291
|
-
cell: ({ row }) =>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
282
|
+
header: columnLabels.on_maternity_leave || "Maternity",
|
|
283
|
+
cell: ({ row }) =>
|
|
284
|
+
columnRenderers.on_maternity_leave ? (
|
|
285
|
+
columnRenderers.on_maternity_leave(row.original)
|
|
286
|
+
) : (
|
|
287
|
+
<span>
|
|
288
|
+
{row.original.on_maternity_leave ? (
|
|
289
|
+
<CheckCircleIcon className="h-4 w-4 text-blue-500" />
|
|
290
|
+
) : (
|
|
291
|
+
<XCircleIcon className="h-4 w-4 text-gray-400" />
|
|
292
|
+
)}
|
|
293
|
+
</span>
|
|
294
|
+
),
|
|
300
295
|
}),
|
|
301
296
|
];
|
|
302
297
|
|
|
298
|
+
// Build final columns order
|
|
299
|
+
const selectColumn = columnHelper.display({
|
|
300
|
+
id: "select",
|
|
301
|
+
header: ({ table }) => (
|
|
302
|
+
<input
|
|
303
|
+
type="checkbox"
|
|
304
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
305
|
+
onChange={table.getToggleAllPageRowsSelectedHandler()}
|
|
306
|
+
className="rounded border-gray-300"
|
|
307
|
+
/>
|
|
308
|
+
),
|
|
309
|
+
cell: ({ row }) => (
|
|
310
|
+
<input
|
|
311
|
+
type="checkbox"
|
|
312
|
+
checked={row.getIsSelected()}
|
|
313
|
+
onChange={row.getToggleSelectedHandler()}
|
|
314
|
+
className="rounded border-gray-300"
|
|
315
|
+
/>
|
|
316
|
+
),
|
|
317
|
+
size: 50,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Filter/reorder if visibleColumns provided
|
|
321
|
+
let dataColumns = defaultDataColumns;
|
|
322
|
+
if (Array.isArray(visibleColumns) && visibleColumns.length > 0) {
|
|
323
|
+
const mapByKey = new Map(
|
|
324
|
+
defaultDataColumns.map((c) => [c.accessorKey || c.id, c])
|
|
325
|
+
);
|
|
326
|
+
dataColumns = visibleColumns
|
|
327
|
+
.map((key) => mapByKey.get(key))
|
|
328
|
+
.filter(Boolean);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const columns = multiSelect ? [selectColumn, ...dataColumns] : dataColumns;
|
|
332
|
+
|
|
303
333
|
// Create table instance
|
|
304
334
|
const table = useReactTable({
|
|
305
335
|
data: employees,
|
|
@@ -367,7 +397,34 @@ const EmployeeSearchPage = ({
|
|
|
367
397
|
setError(null);
|
|
368
398
|
|
|
369
399
|
try {
|
|
370
|
-
|
|
400
|
+
// Resolve access token for APIM
|
|
401
|
+
const apimScope = import.meta.env.VITE_APIM_SCOPE;
|
|
402
|
+
|
|
403
|
+
let accessToken = null;
|
|
404
|
+
if (typeof getAccessToken === "function") {
|
|
405
|
+
accessToken = await getAccessToken();
|
|
406
|
+
} else if (authToken) {
|
|
407
|
+
accessToken = authToken;
|
|
408
|
+
} else if (apimScope) {
|
|
409
|
+
try {
|
|
410
|
+
const response = await instance.acquireTokenSilent({
|
|
411
|
+
account: accounts[0],
|
|
412
|
+
scopes: [apimScope],
|
|
413
|
+
});
|
|
414
|
+
accessToken = response.accessToken;
|
|
415
|
+
} catch (silentErr) {
|
|
416
|
+
const response = await instance.acquireTokenPopup({
|
|
417
|
+
scopes: [apimScope],
|
|
418
|
+
});
|
|
419
|
+
accessToken = response.accessToken;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!accessToken) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
"Missing access token. Provide authToken/getAccessToken or set VITE_APIM_SCOPE."
|
|
426
|
+
);
|
|
427
|
+
}
|
|
371
428
|
|
|
372
429
|
const params = new URLSearchParams({
|
|
373
430
|
entra_id: accounts[0].localAccountId,
|
|
@@ -445,14 +502,11 @@ const EmployeeSearchPage = ({
|
|
|
445
502
|
params.append("sort_order", sortOrder);
|
|
446
503
|
|
|
447
504
|
const apiResponse = await fetch(
|
|
448
|
-
|
|
449
|
-
import.meta.env.VITE_COMMON_API_BASE_URL ||
|
|
450
|
-
"https://snaps-common-api.azurewebsites.net"
|
|
451
|
-
}/api/search-employees?${params}`,
|
|
505
|
+
`https://snapdragons.azure-api.net/api/employees/search-employees?${params}`,
|
|
452
506
|
{
|
|
453
507
|
headers: {
|
|
454
508
|
"Content-Type": "application/json",
|
|
455
|
-
|
|
509
|
+
Authorization: `Bearer ${accessToken}`,
|
|
456
510
|
},
|
|
457
511
|
}
|
|
458
512
|
);
|
|
@@ -752,35 +806,36 @@ const EmployeeSearchPage = ({
|
|
|
752
806
|
<thead className="[&_tr]:border-b">
|
|
753
807
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
754
808
|
<tr key={headerGroup.id} className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
755
|
-
{headerGroup.headers.map((header
|
|
756
|
-
//
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
809
|
+
{headerGroup.headers.map((header) => {
|
|
810
|
+
// Widths keyed by column id/accessor for stability when columns are customized
|
|
811
|
+
const defaultWidthByKey = {
|
|
812
|
+
select: '50px',
|
|
813
|
+
full_name: multiSelect ? '250px' : '250px',
|
|
814
|
+
site_name: '180px',
|
|
815
|
+
role_name: '150px',
|
|
816
|
+
manager_name: '180px',
|
|
817
|
+
email: '300px',
|
|
818
|
+
start_date: '150px',
|
|
819
|
+
employee_status: '120px',
|
|
820
|
+
total_hours_per_week: '100px',
|
|
821
|
+
term_time_only: '90px',
|
|
822
|
+
on_maternity_leave: '90px',
|
|
769
823
|
};
|
|
770
|
-
|
|
824
|
+
const key = header.column.columnDef.accessorKey || header.column.id;
|
|
825
|
+
const width = defaultWidthByKey[key] || '120px';
|
|
771
826
|
return (
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
827
|
+
<th
|
|
828
|
+
key={header.id}
|
|
829
|
+
className="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0"
|
|
830
|
+
style={{ width, minWidth: width }}
|
|
831
|
+
>
|
|
832
|
+
{header.isPlaceholder
|
|
833
|
+
? null
|
|
834
|
+
: flexRender(
|
|
835
|
+
header.column.columnDef.header,
|
|
836
|
+
header.getContext()
|
|
837
|
+
)}
|
|
838
|
+
</th>
|
|
784
839
|
);
|
|
785
840
|
})}
|
|
786
841
|
</tr>
|
|
@@ -32,6 +32,14 @@ export function DateRangePicker({
|
|
|
32
32
|
const [internalRange, setInternalRange] = useState(selectedRange)
|
|
33
33
|
const isSelectingRange = useRef(false)
|
|
34
34
|
|
|
35
|
+
const normalizeDate = (date) =>
|
|
36
|
+
date
|
|
37
|
+
? new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
|
38
|
+
: null
|
|
39
|
+
|
|
40
|
+
const datesEqual = (a, b) =>
|
|
41
|
+
a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
|
|
42
|
+
|
|
35
43
|
const handleOpenChange = (open) => {
|
|
36
44
|
// If we're in the middle of selecting a range and trying to close, prevent it
|
|
37
45
|
if (!open && isSelectingRange.current) {
|
|
@@ -59,90 +67,75 @@ export function DateRangePicker({
|
|
|
59
67
|
setInternalRange(selectedRange)
|
|
60
68
|
}, [selectedRange])
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const normalizedRange =
|
|
68
|
-
from:
|
|
69
|
-
to:
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
const handleSelect = (range, selectedDay) => {
|
|
71
|
+
if (!range || (!range.from && !range.to)) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedRange = {
|
|
76
|
+
from: normalizeDate(range.from),
|
|
77
|
+
to: normalizeDate(range.to),
|
|
78
|
+
}
|
|
79
|
+
const normalizedSelectedDay = normalizeDate(selectedDay)
|
|
80
|
+
|
|
81
|
+
const hasCompleteCurrentRange = Boolean(
|
|
82
|
+
internalRange?.from && internalRange?.to
|
|
83
|
+
)
|
|
84
|
+
|
|
75
85
|
let newRange = normalizedRange
|
|
76
|
-
|
|
77
|
-
if (normalizedRange
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
} else {
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
|
|
87
|
+
if (normalizedRange.from && normalizedRange.to) {
|
|
88
|
+
if (hasCompleteCurrentRange && !isSelectingRange.current) {
|
|
89
|
+
const sameStart = datesEqual(normalizedRange.from, internalRange.from)
|
|
90
|
+
|
|
91
|
+
if (sameStart) {
|
|
92
|
+
// Adjusting end date on an existing range
|
|
93
|
+
newRange = {
|
|
94
|
+
from: internalRange.from,
|
|
95
|
+
to: normalizedRange.to,
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Starting a new range selection cycle
|
|
99
|
+
newRange = {
|
|
100
|
+
from: normalizedSelectedDay ?? normalizedRange.from,
|
|
101
|
+
to: null,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else if (internalRange?.from && !internalRange?.to) {
|
|
105
|
+
// Completing an in-progress range
|
|
106
|
+
newRange = {
|
|
107
|
+
from: internalRange.from,
|
|
108
|
+
to: normalizedRange.to,
|
|
109
|
+
}
|
|
98
110
|
}
|
|
99
|
-
} else if (normalizedRange
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (currentFrom && currentTo) {
|
|
108
|
-
// We have a complete range, so the next click should start a new cycle
|
|
109
|
-
console.log('🔍 Complete range detected, starting new cycle (single date)')
|
|
110
|
-
newRange = { from: normalizedRange.from, to: null }
|
|
111
|
-
} else if (currentFrom && !currentTo) {
|
|
112
|
-
// We have a start date but no end date, so this should complete the range
|
|
113
|
-
console.log('🔍 Completing range with end date (single date)')
|
|
114
|
-
newRange = { from: currentFrom, to: normalizedRange.from }
|
|
111
|
+
} else if (normalizedRange.from && !normalizedRange.to) {
|
|
112
|
+
if (hasCompleteCurrentRange && !isSelectingRange.current) {
|
|
113
|
+
// New cycle starting from the selected day
|
|
114
|
+
newRange = {
|
|
115
|
+
from: normalizedSelectedDay ?? normalizedRange.from,
|
|
116
|
+
to: null,
|
|
117
|
+
}
|
|
115
118
|
} else {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
newRange = {
|
|
120
|
+
from: normalizedRange.from,
|
|
121
|
+
to: null,
|
|
122
|
+
}
|
|
119
123
|
}
|
|
124
|
+
} else {
|
|
125
|
+
newRange = null
|
|
120
126
|
}
|
|
121
|
-
|
|
122
|
-
console.log('🔍 Final newRange:', newRange)
|
|
123
|
-
|
|
124
|
-
// Update internal state immediately for UI responsiveness
|
|
127
|
+
|
|
125
128
|
setInternalRange(newRange)
|
|
126
|
-
|
|
127
|
-
// Update the selecting range flag
|
|
129
|
+
|
|
128
130
|
if (newRange?.from && !newRange?.to) {
|
|
129
131
|
isSelectingRange.current = true
|
|
130
|
-
} else if (newRange?.from && newRange?.to) {
|
|
131
|
-
isSelectingRange.current = false
|
|
132
132
|
} else {
|
|
133
133
|
isSelectingRange.current = false
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (newRange?.from && newRange?.to) {
|
|
138
|
-
onSelect(newRange)
|
|
139
|
-
// Don't close the popover automatically - let user click outside to close
|
|
140
|
-
} else if (!newRange?.from && !newRange?.to) {
|
|
141
|
-
// Range cleared - call onSelect immediately
|
|
135
|
+
|
|
136
|
+
if (!newRange?.from || newRange?.to) {
|
|
142
137
|
onSelect(newRange)
|
|
143
138
|
}
|
|
144
|
-
// Don't call onSelect when only first date is selected
|
|
145
|
-
// Popover stays open until both dates are selected or user clicks outside
|
|
146
139
|
}
|
|
147
140
|
|
|
148
141
|
return (
|
|
@@ -200,10 +193,9 @@ export function DateRangePicker({
|
|
|
200
193
|
mode="range"
|
|
201
194
|
defaultMonth={internalRange?.from}
|
|
202
195
|
selected={internalRange}
|
|
203
|
-
onSelect={(range) => {
|
|
204
|
-
// Only handle the selection if we have a valid range
|
|
196
|
+
onSelect={(range, selectedDay) => {
|
|
205
197
|
if (range && (range.from || range.to)) {
|
|
206
|
-
handleSelect(range)
|
|
198
|
+
handleSelect(range, selectedDay)
|
|
207
199
|
}
|
|
208
200
|
}}
|
|
209
201
|
numberOfMonths={numberOfMonths}
|
|
@@ -289,7 +281,7 @@ export function DatePicker({
|
|
|
289
281
|
}
|
|
290
282
|
|
|
291
283
|
return (
|
|
292
|
-
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
284
|
+
<Popover open={isOpen} onOpenChange={setIsOpen} modal={false}>
|
|
293
285
|
<PopoverTrigger asChild>
|
|
294
286
|
<Button
|
|
295
287
|
variant="outline"
|
|
@@ -320,4 +312,4 @@ export function DatePicker({
|
|
|
320
312
|
</PopoverContent>
|
|
321
313
|
</Popover>
|
|
322
314
|
)
|
|
323
|
-
}
|
|
315
|
+
}
|
|
@@ -29,7 +29,7 @@ function PopoverContent({
|
|
|
29
29
|
align={align}
|
|
30
30
|
sideOffset={sideOffset}
|
|
31
31
|
className={cn(
|
|
32
|
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-
|
|
32
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
33
33
|
className
|
|
34
34
|
)}
|
|
35
35
|
{...props} />
|