diff --git a/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue b/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue index 7d9e6f9ea..e5dc011b0 100644 --- a/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue +++ b/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue @@ -13,14 +13,12 @@ import { subDays, startOfDay, endOfDay, - isBefore, subMonths, addMonths, isSameMonth, differenceInCalendarMonths, setMonth, setYear, - isAfter, } from 'date-fns'; import { useAlert } from 'dashboard/composables'; import DatePickerButton from './components/DatePickerButton.vue'; @@ -32,91 +30,94 @@ import CalendarWeek from './components/CalendarWeek.vue'; import CalendarFooter from './components/CalendarFooter.vue'; const emit = defineEmits(['dateRangeChanged']); -const { LAST_7_DAYS, LAST_30_DAYS, CUSTOM_RANGE } = DATE_RANGE_TYPES; + +const dateRange = defineModel('dateRange', { + type: Array, + default: undefined, +}); + +const rangeType = defineModel('rangeType', { + type: String, + default: undefined, +}); +const { LAST_7_DAYS, CUSTOM_RANGE } = DATE_RANGE_TYPES; const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES; const { WEEK, MONTH, YEAR } = CALENDAR_PERIODS; const showDatePicker = ref(false); const calendarViews = ref({ start: WEEK, end: WEEK }); const currentDate = ref(new Date()); -const selectedStartDate = ref(startOfDay(subDays(currentDate.value, 6))); // LAST_7_DAYS -const selectedEndDate = ref(endOfDay(currentDate.value)); -// Setting the start and end calendar -const startCurrentDate = ref(startOfDay(selectedStartDate.value)); + +// Use dates from v-model if provided, otherwise default to last 7 days +const selectedStartDate = ref( + dateRange.value?.[0] + ? startOfDay(dateRange.value[0]) + : startOfDay(subDays(currentDate.value, 6)) // LAST_7_DAYS +); +const selectedEndDate = ref( + dateRange.value?.[1] + ? endOfDay(dateRange.value[1]) + : endOfDay(currentDate.value) +); +// Calendar month positioning (left and right calendars) +// These control which months are displayed in the dual calendar view +const startCurrentDate = ref(startOfMonth(selectedStartDate.value)); const endCurrentDate = ref( isSameMonth(selectedStartDate.value, selectedEndDate.value) - ? startOfMonth(addMonths(selectedEndDate.value, 1)) // Moves to the start of the next month if dates are in the same month (Mounted case LAST_7_DAYS) - : startOfMonth(selectedEndDate.value) // Always shows the month of the end date starting from the first (Mounted case LAST_7_DAYS) + ? startOfMonth(addMonths(selectedEndDate.value, 1)) // Same month: show next month on right (e.g., Jan 25-31 shows Jan + Feb) + : startOfMonth(selectedEndDate.value) // Different months: show end month on right (e.g., Dec 5 - Jan 3 shows Dec + Jan) ); const selectingEndDate = ref(false); -const selectedRange = ref(LAST_7_DAYS); +const selectedRange = ref(rangeType.value || LAST_7_DAYS); const hoveredEndDate = ref(null); const manualStartDate = ref(selectedStartDate.value); const manualEndDate = ref(selectedEndDate.value); -// Watcher will set the start and end dates based on the selected range -watch(selectedRange, newRange => { - if (newRange !== CUSTOM_RANGE) { - // If selecting a range other than last 7 days or last 30 days, set the start and end dates to the selected start and end dates - // If selecting last 7 days or last 30 days is, set the start date to the selected start date - // and the end date to one month ahead of the start date if the start date and end date are in the same month - // Otherwise set the end date to the selected end date - const isLastSevenOrThirtyDays = - newRange === LAST_7_DAYS || newRange === LAST_30_DAYS; - startCurrentDate.value = selectedStartDate.value; - endCurrentDate.value = - isLastSevenOrThirtyDays && - isSameMonth(selectedStartDate.value, selectedEndDate.value) - ? startOfMonth(addMonths(selectedStartDate.value, 1)) - : selectedEndDate.value; - selectingEndDate.value = false; - } else if (!selectingEndDate.value) { - // If selecting a custom range and not selecting an end date, set the start date to the selected start date - startCurrentDate.value = startOfDay(currentDate.value); - } -}); - -// Watcher will set the input values based on the selected start and end dates +// Watcher 1: Sync v-model props from parent component +// Handles: URL params, parent component updates, rangeType changes watch( - [selectedStartDate, selectedEndDate], - ([newStart, newEnd]) => { - if (isValid(newStart)) { - manualStartDate.value = newStart; - } else { - manualStartDate.value = selectedStartDate.value; + [rangeType, dateRange], + ([newRangeType, newDateRange]) => { + if (newRangeType && newRangeType !== selectedRange.value) { + selectedRange.value = newRangeType; + + // If rangeType changes without dateRange, recompute dates from the range + if (!newDateRange && newRangeType !== CUSTOM_RANGE) { + const activeDates = getActiveDateRange(newRangeType); + if (activeDates) { + selectedStartDate.value = startOfDay(activeDates.startDate); + selectedEndDate.value = endOfDay(activeDates.endDate); + } + } } - if (isValid(newEnd)) { - manualEndDate.value = newEnd; - } else { - manualEndDate.value = selectedEndDate.value; + // When parent provides new dateRange (e.g., from URL params) + if (newDateRange?.[0] && newDateRange?.[1]) { + selectedStartDate.value = startOfDay(newDateRange[0]); + selectedEndDate.value = endOfDay(newDateRange[1]); + + // Update calendar to show the months of the new date range + startCurrentDate.value = startOfMonth(newDateRange[0]); + endCurrentDate.value = isSameMonth(newDateRange[0], newDateRange[1]) + ? startOfMonth(addMonths(newDateRange[1], 1)) + : startOfMonth(newDateRange[1]); } }, { immediate: true } ); -// Watcher to ensure dates are always in logical order -// This watch is will ensure that the start date is always before the end date +// Watcher 2: Keep manual input fields in sync with selected dates +// Updates the input field values when dates change programmatically watch( - [startCurrentDate, endCurrentDate], - ([newStart, newEnd], [oldStart, oldEnd]) => { - const monthDifference = differenceInCalendarMonths(newEnd, newStart); - - if (newStart !== oldStart) { - if (isAfter(newStart, newEnd) || monthDifference === 0) { - // Adjust the end date forward if the start date is adjusted and is after the end date or in the same month - endCurrentDate.value = addMonths(newStart, 1); - } - } - if (newEnd !== oldEnd) { - if (isBefore(newEnd, newStart) || monthDifference === 0) { - // Adjust the start date backward if the end date is adjusted and is before the start date or in the same month - startCurrentDate.value = subMonths(newEnd, 1); - } - } + [selectedStartDate, selectedEndDate], + ([newStart, newEnd]) => { + manualStartDate.value = isValid(newStart) + ? newStart + : selectedStartDate.value; + manualEndDate.value = isValid(newEnd) ? newEnd : selectedEndDate.value; }, - { immediate: true, deep: true } + { immediate: true } ); const setDateRange = range => { @@ -124,6 +125,12 @@ const setDateRange = range => { const { start, end } = getActiveDateRange(range.value, currentDate.value); selectedStartDate.value = start; selectedEndDate.value = end; + + // Position calendar to show the months of the selected range + startCurrentDate.value = startOfMonth(start); + endCurrentDate.value = isSameMonth(start, end) + ? startOfMonth(addMonths(start, 1)) + : startOfMonth(end); }; const moveCalendar = (calendar, direction, period = MONTH) => { @@ -134,8 +141,22 @@ const moveCalendar = (calendar, direction, period = MONTH) => { direction, period ); - startCurrentDate.value = start; - endCurrentDate.value = end; + + // Prevent calendar months from overlapping + const monthDiff = differenceInCalendarMonths(end, start); + if (monthDiff === 0) { + // If they would be the same month, adjust the other calendar + if (calendar === START_CALENDAR) { + endCurrentDate.value = addMonths(start, 1); + startCurrentDate.value = start; + } else { + startCurrentDate.value = subMonths(end, 1); + endCurrentDate.value = end; + } + } else { + startCurrentDate.value = start; + endCurrentDate.value = end; + } }; const selectDate = day => { @@ -175,10 +196,10 @@ const openCalendar = (index, calendarType, period = MONTH) => { const updateManualInput = (newDate, calendarType) => { if (calendarType === START_CALENDAR) { selectedStartDate.value = newDate; - startCurrentDate.value = newDate; + startCurrentDate.value = startOfMonth(newDate); } else { selectedEndDate.value = newDate; - endCurrentDate.value = newDate; + endCurrentDate.value = startOfMonth(newDate); } selectingEndDate.value = false; }; @@ -188,13 +209,21 @@ const handleManualInputError = message => { }; const resetDatePicker = () => { - startCurrentDate.value = startOfDay(currentDate.value); // Resets to today at start of the day - endCurrentDate.value = addMonths(startOfDay(currentDate.value), 1); // Resets to one month ahead - selectedStartDate.value = startOfDay(subDays(currentDate.value, 6)); - selectedEndDate.value = endOfDay(currentDate.value); + // Calculate Last 7 days from today + const startDate = startOfDay(subDays(currentDate.value, 6)); + const endDate = endOfDay(currentDate.value); + + selectedStartDate.value = startDate; + selectedEndDate.value = endDate; + + // Position calendar to show the months of Last 7 days + // Example: If today is Feb 5, Last 7 days = Jan 30 - Feb 5, so show Jan + Feb + startCurrentDate.value = startOfMonth(startDate); + endCurrentDate.value = isSameMonth(startDate, endDate) + ? startOfMonth(addMonths(startDate, 1)) + : startOfMonth(endDate); selectingEndDate.value = false; selectedRange.value = LAST_7_DAYS; - // Reset view modes if they are being used to toggle between different calendar views calendarViews.value = { start: WEEK, end: WEEK }; }; @@ -203,10 +232,33 @@ const emitDateRange = () => { useAlert('Please select a valid time range'); } else { showDatePicker.value = false; - emit('dateRangeChanged', [selectedStartDate.value, selectedEndDate.value]); + emit('dateRangeChanged', [ + selectedStartDate.value, + selectedEndDate.value, + selectedRange.value, + ]); } }; +// Called when picker opens - positions calendar to show selected date range +// Fixes issue where calendar showed wrong months when loaded from URL params +const initializeCalendarMonths = () => { + if (selectedStartDate.value && selectedEndDate.value) { + startCurrentDate.value = startOfMonth(selectedStartDate.value); + endCurrentDate.value = isSameMonth( + selectedStartDate.value, + selectedEndDate.value + ) + ? startOfMonth(addMonths(selectedEndDate.value, 1)) + : startOfMonth(selectedEndDate.value); + } +}; + +const toggleDatePicker = () => { + showDatePicker.value = !showDatePicker.value; + if (showDatePicker.value) initializeCalendarMonths(); +}; + const closeDatePicker = () => { showDatePicker.value = false; }; @@ -218,7 +270,7 @@ const closeDatePicker = () => { :selected-start-date="selectedStartDate" :selected-end-date="selectedEndDate" :selected-range="selectedRange" - @open="showDatePicker = !showDatePicker" + @open="toggleDatePicker" />
import { ref, onMounted } from 'vue'; +import { useRoute, useRouter } from 'vue-router'; import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper'; import subDays from 'date-fns/subDays'; import WootDatePicker from 'dashboard/components/ui/DatePicker/DatePicker.vue'; import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue'; +import { + generateReportURLParams, + parseReportURLParams, +} from '../helpers/reportFilterHelper'; +import { DATE_RANGE_TYPES } from 'dashboard/components/ui/DatePicker/helpers/DatePickerHelper'; const emit = defineEmits(['filterChange']); +const route = useRoute(); +const router = useRouter(); + const customDateRange = ref([subDays(new Date(), 6), new Date()]); +const selectedDateRange = ref(DATE_RANGE_TYPES.LAST_7_DAYS); const businessHoursSelected = ref(false); +const updateURLParams = () => { + const params = generateReportURLParams({ + from: getUnixStartOfDay(customDateRange.value[0]), + to: getUnixEndOfDay(customDateRange.value[1]), + businessHours: businessHoursSelected.value, + range: selectedDateRange.value, + }); + + router.replace({ query: { ...params } }); +}; + const emitChange = () => { + updateURLParams(); emit('filterChange', { from: getUnixStartOfDay(customDateRange.value[0]), to: getUnixEndOfDay(customDateRange.value[1]), @@ -19,7 +41,9 @@ const emitChange = () => { }; const onDateRangeChange = value => { - customDateRange.value = value; + const [startDate, endDate, rangeType] = value; + customDateRange.value = [startDate, endDate]; + selectedDateRange.value = rangeType || DATE_RANGE_TYPES.CUSTOM_RANGE; emitChange(); }; @@ -27,7 +51,29 @@ const onBusinessHoursToggle = () => { emitChange(); }; +const initializeFromURL = () => { + const urlParams = parseReportURLParams(route.query); + + // Set the range type first + if (urlParams.range) { + selectedDateRange.value = urlParams.range; + } + + // Restore dates from URL if available + if (urlParams.from && urlParams.to) { + customDateRange.value = [ + new Date(urlParams.from * 1000), + new Date(urlParams.to * 1000), + ]; + } + + if (urlParams.businessHours) { + businessHoursSelected.value = urlParams.businessHours; + } +}; + onMounted(() => { + initializeFromURL(); emitChange(); }); @@ -35,7 +81,11 @@ onMounted(() => {