Skip to content

Commit 72d4d54

Browse files
committed
feat: add custom date range picker with floating tabbed interface
1 parent 2c627b7 commit 72d4d54

4 files changed

Lines changed: 447 additions & 64 deletions

File tree

src/lib/helpers/types/conversationTypes.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ IRichContent.prototype.language;
324324
* @property {UserStateDetailModel[]} states
325325
* @property {string[]} tags
326326
* @property {string?} [timeRange]
327-
* @property {string} [specificDate] - When timeRange is SpecificDay, date in YYYY-MM-DD (e.g. 2026-01-25)
327+
* @property {string} [startDate] - When timeRange is SpecificDay, start date in YYYY-MM-DD format (e.g. 2026-01-25)
328+
* @property {string} [endDate] - When timeRange is SpecificDay, end date in YYYY-MM-DD format (e.g. 2026-01-30). Defaults to startDate if not provided
328329
*/
329330

330331
/**

src/lib/helpers/utils/common.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,11 @@ export function getCleanUrl(url) {
192192

193193
/**
194194
* @param {string} timeRange
195-
* @param {string} [specificDate] - When timeRange is SpecificDay, date in YYYY-MM-DD format (e.g. 2026-01-25)
195+
* @param {string} [startDate] - When timeRange is SpecificDay, start date in YYYY-MM-DD format (e.g. 2026-01-25)
196+
* @param {string} [endDate] - When timeRange is SpecificDay, end date in YYYY-MM-DD format (e.g. 2026-01-30). If not provided, uses startDate
196197
* @returns {{ startTime: string | null, endTime: string | null }}
197198
*/
198-
export function convertTimeRange(timeRange, specificDate) {
199+
export function convertTimeRange(timeRange, startDate, endDate) {
199200
let ret = { startTime: null, endTime: null };
200201

201202
if (!timeRange) {
@@ -242,13 +243,14 @@ export function convertTimeRange(timeRange, specificDate) {
242243
};
243244
break;
244245
case TimeRange.SpecificDay:
245-
if (specificDate && moment(specificDate).isValid()) {
246+
if (startDate && moment(startDate).isValid()) {
247+
const endDateToUse = endDate && moment(endDate).isValid() ? endDate : startDate;
246248
ret = {
247249
...ret,
248250
// @ts-ignore
249-
startTime: moment(specificDate).startOf('day').utc().format(),
251+
startTime: moment(startDate).startOf('day').utc().format(),
250252
// @ts-ignore
251-
endTime: moment(specificDate).endOf('day').utc().format()
253+
endTime: moment(endDateToUse).endOf('day').utc().format()
252254
};
253255
}
254256
break;

src/routes/page/conversation/+page.svelte

Lines changed: 221 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import { utcToLocal } from '$lib/helpers/datetime';
2424
import { ConversationChannel, TimeRange } from '$lib/helpers/enums';
2525
import { TIME_RANGE_OPTIONS } from '$lib/helpers/constants';
26+
import { clickoutsideDirective } from '$lib/helpers/directives';
2627
import {
2728
getConversations,
2829
deleteConversation,
@@ -46,12 +47,71 @@
4647
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
4748
};
4849
50+
// Get yesterday's date in YYYY-MM-DD format
51+
const getYesterdayStr = () => {
52+
const d = new Date();
53+
d.setDate(d.getDate() - 1);
54+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
55+
};
56+
57+
// Format date for display (YYYY-MM-DD -> MM/DD/YYYY)
58+
const formatDateForDisplay = (/** @type {string} */ dateStr) => {
59+
if (!dateStr) return '';
60+
const [year, month, day] = dateStr.split('-');
61+
return `${month}/${day}/${year}`;
62+
};
63+
64+
// Get time range display text
65+
const getTimeRangeDisplayText = () => {
66+
if (searchOption.timeRange === TimeRange.SpecificDay) {
67+
if (searchOption.startDate && searchOption.endDate) {
68+
const start = formatDateForDisplay(searchOption.startDate);
69+
const end = formatDateForDisplay(searchOption.endDate);
70+
if (start === end) {
71+
return start;
72+
}
73+
return `${start} - ${end}`;
74+
}
75+
return 'Custom';
76+
}
77+
// Find the label for the selected time range
78+
const selected = presetTimeRangeOptions.find(x => x.value === searchOption.timeRange);
79+
return selected ? selected.label : '';
80+
};
81+
4982
/** @type {boolean} */
5083
let isLoading = false;
5184
let isComplete = false;
5285
let showStateSearch = false;
5386
let isPageMounted = false;
5487
88+
/** @type {boolean} */
89+
let showDatePicker = false;
90+
91+
/** @type {HTMLDivElement | null} */
92+
let datePickerRef = null;
93+
94+
/** @type {string} */
95+
let timeRangeDisplayText = '';
96+
97+
// Update time range display text reactively
98+
$: {
99+
if (searchOption.timeRange === TimeRange.SpecificDay && searchOption.startDate && searchOption.endDate) {
100+
const start = formatDateForDisplay(searchOption.startDate);
101+
const end = formatDateForDisplay(searchOption.endDate);
102+
if (start === end) {
103+
timeRangeDisplayText = start;
104+
} else {
105+
timeRangeDisplayText = `${start} - ${end}`;
106+
}
107+
} else if (searchOption.timeRange === TimeRange.SpecificDay) {
108+
timeRangeDisplayText = 'Custom';
109+
} else {
110+
const selected = presetTimeRangeOptions.find(x => x.value === searchOption.timeRange);
111+
timeRangeDisplayText = selected ? selected.label : '';
112+
}
113+
}
114+
55115
/** @type {import('$commonTypes').PagedItems<import('$conversationTypes').ConversationModel>} */
56116
let conversations = { count: 0, items: [] };
57117
@@ -83,10 +143,16 @@
83143
{ value: k.toLowerCase(), label: v }
84144
));
85145
86-
const timeRangeOptions = TIME_RANGE_OPTIONS.map(x => ({
87-
label: x.label,
88-
value: x.value
89-
}));
146+
// Preset time range options (excluding SpecificDay)
147+
const presetTimeRangeOptions = TIME_RANGE_OPTIONS
148+
.filter(x => x.value !== TimeRange.SpecificDay)
149+
.map(x => ({
150+
label: x.label,
151+
value: x.value
152+
}));
153+
154+
/** @type {string} */
155+
let datePickerTab = 'relative';
90156
91157
/** @type {{ startTime: string | null, endTime: string | null }} */
92158
let innerTimeRange = {
@@ -102,7 +168,8 @@
102168
status: null,
103169
taskId: null,
104170
timeRange: TimeRange.Last12Hours,
105-
specificDate: '',
171+
startDate: '',
172+
endDate: '',
106173
states: [],
107174
tags: []
108175
};
@@ -118,7 +185,7 @@
118185
page: $page.url.searchParams.get("page"),
119186
pageSize: $page.url.searchParams.get("pageSize")
120187
}, { defaultPageSize: pageSize });
121-
innerTimeRange = convertTimeRange(searchOption.timeRange || '', searchOption.specificDate);
188+
innerTimeRange = convertTimeRange(searchOption.timeRange || '', searchOption.startDate, searchOption.endDate);
122189
123190
filter = {
124191
...filter,
@@ -305,7 +372,7 @@
305372
306373
function refreshFilter() {
307374
const searchStates = getSearchStates();
308-
innerTimeRange = convertTimeRange(searchOption.timeRange || '', searchOption.specificDate);
375+
innerTimeRange = convertTimeRange(searchOption.timeRange || '', searchOption.startDate, searchOption.endDate);
309376
310377
filter = {
311378
...filter,
@@ -405,13 +472,7 @@
405472
tags: e.target.value?.length > 0 ? [e.target.value] : []
406473
};
407474
} else if (type === 'timeRange') {
408-
const timeRange = selectedValues.length > 0 ? selectedValues[0] : null;
409-
const isSpecificDay = timeRange === TimeRange.SpecificDay;
410-
searchOption = {
411-
...searchOption,
412-
timeRange,
413-
specificDate: isSpecificDay ? (searchOption.specificDate || getTodayStr()) : ''
414-
};
475+
// This handler is no longer used, but kept for compatibility
415476
}
416477
}
417478
@@ -518,21 +579,152 @@
518579
on:input={e => changeOption(e, "tags")}
519580
/>
520581
</Col>
521-
<Col lg="2">
522-
<Select
523-
tag={'conversation-datetime-select'}
524-
placeholder={'Select time range'}
525-
selectedText={''}
526-
selectedValues={searchOption.timeRange ? [searchOption.timeRange] : []}
527-
options={timeRangeOptions}
528-
on:select={e => changeOption(e, 'timeRange')}
529-
/>
530-
{#if searchOption.timeRange === TimeRange.SpecificDay}
531-
<Input
532-
type="date"
533-
bind:value={searchOption.specificDate}
534-
class="mt-2"
535-
/>
582+
<Col lg="2" class="position-relative">
583+
<button
584+
type="button"
585+
class="form-control text-start d-flex align-items-center justify-content-between"
586+
on:click={() => {
587+
showDatePicker = !showDatePicker;
588+
if (showDatePicker) {
589+
datePickerTab = 'relative';
590+
}
591+
}}
592+
style="cursor: pointer;"
593+
>
594+
<span>{timeRangeDisplayText || 'Select time range'}</span>
595+
<i class="bx bx-chevron-down"></i>
596+
</button>
597+
{#if showDatePicker}
598+
<div
599+
bind:this={datePickerRef}
600+
use:clickoutsideDirective
601+
on:clickoutside={(/** @type {any} */ e) => {
602+
if (e.detail && e.detail.targetNode && datePickerRef) {
603+
if (!datePickerRef.contains(e.detail.targetNode)) {
604+
showDatePicker = false;
605+
}
606+
}
607+
}}
608+
class="position-absolute top-100 start-0 mt-1 bg-white border rounded shadow-lg"
609+
style="z-index: 1050; min-width: 320px; max-width: 350px;"
610+
>
611+
<ul class="nav nav-tabs border-bottom mb-0 px-2 pt-2" role="tablist">
612+
<li class="nav-item flex-fill" role="presentation">
613+
<button
614+
class="nav-link fw-semibold {datePickerTab === 'relative' ? 'active text-primary' : 'text-muted'}"
615+
type="button"
616+
role="tab"
617+
style="padding: 0.5rem 0.75rem; {datePickerTab === 'relative' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
618+
on:click={() => datePickerTab = 'relative'}
619+
>
620+
Relative
621+
</button>
622+
</li>
623+
<li class="nav-item flex-fill" role="presentation">
624+
<button
625+
class="nav-link fw-semibold {datePickerTab === 'custom' ? 'active text-primary' : 'text-muted'}"
626+
type="button"
627+
role="tab"
628+
style="padding: 0.5rem 0.75rem; {datePickerTab === 'custom' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
629+
on:click={() => {
630+
datePickerTab = 'custom';
631+
// Set default dates to yesterday and today if not already set
632+
if (!searchOption.startDate && !searchOption.endDate) {
633+
searchOption.startDate = getYesterdayStr();
634+
searchOption.endDate = getTodayStr();
635+
}
636+
}}
637+
>
638+
Custom
639+
</button>
640+
</li>
641+
</ul>
642+
643+
<div class="p-4">
644+
{#if datePickerTab === 'relative'}
645+
<div class="d-flex flex-column gap-2" style="max-height: 300px; overflow-y: auto;">
646+
{#each presetTimeRangeOptions as option}
647+
<button
648+
type="button"
649+
class="btn btn-sm btn-outline-secondary text-start {searchOption.timeRange === option.value ? 'active' : ''}"
650+
on:click={(e) => {
651+
e.preventDefault();
652+
e.stopPropagation();
653+
searchOption.timeRange = option.value;
654+
searchOption.startDate = '';
655+
searchOption.endDate = '';
656+
showDatePicker = false;
657+
refreshFilter();
658+
initFilterPager();
659+
getPagedConversations();
660+
}}
661+
>
662+
{option.label}
663+
</button>
664+
{/each}
665+
</div>
666+
{:else if datePickerTab === 'custom'}
667+
<div class="mb-3">
668+
<label for="start-date-picker" class="form-label small mb-2">From:</label>
669+
<Input
670+
id="start-date-picker"
671+
type="date"
672+
bind:value={searchOption.startDate}
673+
class="form-control form-control-sm"
674+
/>
675+
</div>
676+
<div class="mb-4">
677+
<label for="end-date-picker" class="form-label small mb-2">To:</label>
678+
<Input
679+
id="end-date-picker"
680+
type="date"
681+
bind:value={searchOption.endDate}
682+
class="form-control form-control-sm"
683+
/>
684+
</div>
685+
<div class="d-flex justify-content-end gap-2 mt-3">
686+
<Button
687+
color="secondary"
688+
size="sm"
689+
type="button"
690+
on:click={(e) => {
691+
e.preventDefault();
692+
e.stopPropagation();
693+
showDatePicker = false;
694+
}}
695+
>
696+
Cancel
697+
</Button>
698+
<Button
699+
color="primary"
700+
size="sm"
701+
type="button"
702+
on:click={(e) => {
703+
e.preventDefault();
704+
e.stopPropagation();
705+
if (searchOption.startDate) {
706+
// If endDate is not provided, default to startDate
707+
if (!searchOption.endDate) {
708+
searchOption.endDate = searchOption.startDate;
709+
}
710+
// Force reactivity by reassigning the object
711+
searchOption = {
712+
...searchOption,
713+
timeRange: TimeRange.SpecificDay
714+
};
715+
}
716+
showDatePicker = false;
717+
refreshFilter();
718+
initFilterPager();
719+
getPagedConversations();
720+
}}
721+
>
722+
Confirm
723+
</Button>
724+
</div>
725+
{/if}
726+
</div>
727+
</div>
536728
{/if}
537729
</Col>
538730
<Col lg="1">

0 commit comments

Comments
 (0)