feat: Custom date picker (#9247)
* feat: Custom date picker * chore: Calender footer * chore: Minor fix * chore: Reset date picker * chore: Minor fix * feat: Toggle button * chore: Clean up * chore: Use font inter * chore: Cleanup and fix bugs * fix: custom date range reset the calendar * chore: fix logic bug * feat: Add manual date range * fix: styles in rtl * chore: Helper specs * chore: Clean up * chore: Review fixes * chore: remove magic strings * chore: Add comments * chore: Review fixes * chore: Clean up * chore: remove magic strings * fix: Use outline instead of border * chore: Minor style fix * chore: disable pointer events for the disabled dates * chore: Fix code climate --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
parent
a5ab8201c6
commit
28728635c9
@ -24,6 +24,7 @@ import SubmitButton from './buttons/FormSubmitButton';
|
|||||||
import Tabs from './ui/Tabs/Tabs';
|
import Tabs from './ui/Tabs/Tabs';
|
||||||
import TabsItem from './ui/Tabs/TabsItem';
|
import TabsItem from './ui/Tabs/TabsItem';
|
||||||
import Thumbnail from './widgets/Thumbnail.vue';
|
import Thumbnail from './widgets/Thumbnail.vue';
|
||||||
|
import DatePicker from './ui/DatePicker/DatePicker.vue';
|
||||||
|
|
||||||
const WootUIKit = {
|
const WootUIKit = {
|
||||||
AvatarUploader,
|
AvatarUploader,
|
||||||
@ -51,6 +52,7 @@ const WootUIKit = {
|
|||||||
Tabs,
|
Tabs,
|
||||||
TabsItem,
|
TabsItem,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
DatePicker,
|
||||||
install(Vue) {
|
install(Vue) {
|
||||||
const keys = Object.keys(this);
|
const keys = Object.keys(this);
|
||||||
keys.pop(); // remove 'install' from keys
|
keys.pop(); // remove 'install' from keys
|
||||||
|
|||||||
302
app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
Normal file
302
app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
getActiveDateRange,
|
||||||
|
moveCalendarDate,
|
||||||
|
DATE_RANGE_TYPES,
|
||||||
|
CALENDAR_TYPES,
|
||||||
|
CALENDAR_PERIODS,
|
||||||
|
} from './helpers/DatePickerHelper';
|
||||||
|
import {
|
||||||
|
isValid,
|
||||||
|
startOfMonth,
|
||||||
|
subDays,
|
||||||
|
startOfDay,
|
||||||
|
endOfDay,
|
||||||
|
isBefore,
|
||||||
|
subMonths,
|
||||||
|
addMonths,
|
||||||
|
isSameMonth,
|
||||||
|
differenceInCalendarMonths,
|
||||||
|
setMonth,
|
||||||
|
setYear,
|
||||||
|
isAfter,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
|
import DatePickerButton from './components/DatePickerButton.vue';
|
||||||
|
import CalendarDateInput from './components/CalendarDateInput.vue';
|
||||||
|
import CalendarDateRange from './components/CalendarDateRange.vue';
|
||||||
|
import CalendarYear from './components/CalendarYear.vue';
|
||||||
|
import CalendarMonth from './components/CalendarMonth.vue';
|
||||||
|
import CalendarWeek from './components/CalendarWeek.vue';
|
||||||
|
import CalendarFooter from './components/CalendarFooter.vue';
|
||||||
|
|
||||||
|
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));
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
const selectingEndDate = ref(false);
|
||||||
|
const selectedRange = ref(LAST_7_DAYS);
|
||||||
|
const hoveredEndDate = ref(null);
|
||||||
|
|
||||||
|
const manualStartDate = ref(selectedStartDate.value);
|
||||||
|
const manualEndDate = ref(selectedEndDate.value);
|
||||||
|
|
||||||
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
|
// 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, set the start and end dates to the selected start and end dates
|
||||||
|
// If selecting last 7 days, 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 isLast7days = newRange === LAST_7_DAYS;
|
||||||
|
startCurrentDate.value = selectedStartDate.value;
|
||||||
|
endCurrentDate.value =
|
||||||
|
isLast7days && 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
|
||||||
|
watch(
|
||||||
|
[selectedStartDate, selectedEndDate],
|
||||||
|
([newStart, newEnd]) => {
|
||||||
|
if (isValid(newStart)) {
|
||||||
|
manualStartDate.value = newStart;
|
||||||
|
} else {
|
||||||
|
manualStartDate.value = selectedStartDate.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid(newEnd)) {
|
||||||
|
manualEndDate.value = newEnd;
|
||||||
|
} else {
|
||||||
|
manualEndDate.value = selectedEndDate.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 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
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const setDateRange = range => {
|
||||||
|
selectedRange.value = range.value;
|
||||||
|
const { start, end } = getActiveDateRange(range.value, currentDate.value);
|
||||||
|
selectedStartDate.value = start;
|
||||||
|
selectedEndDate.value = end;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveCalendar = (calendar, direction, period = MONTH) => {
|
||||||
|
const { start, end } = moveCalendarDate(
|
||||||
|
calendar,
|
||||||
|
startCurrentDate.value,
|
||||||
|
endCurrentDate.value,
|
||||||
|
direction,
|
||||||
|
period
|
||||||
|
);
|
||||||
|
startCurrentDate.value = start;
|
||||||
|
endCurrentDate.value = end;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectDate = day => {
|
||||||
|
selectedRange.value = CUSTOM_RANGE;
|
||||||
|
if (!selectingEndDate.value || day < selectedStartDate.value) {
|
||||||
|
selectedStartDate.value = day;
|
||||||
|
selectedEndDate.value = null;
|
||||||
|
selectingEndDate.value = true;
|
||||||
|
} else {
|
||||||
|
selectedEndDate.value = day;
|
||||||
|
selectingEndDate.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setViewMode = (calendar, mode) => {
|
||||||
|
selectedRange.value = CUSTOM_RANGE;
|
||||||
|
calendarViews.value[calendar] = mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCalendar = (index, calendarType, period = MONTH) => {
|
||||||
|
const current =
|
||||||
|
calendarType === START_CALENDAR
|
||||||
|
? startCurrentDate.value
|
||||||
|
: endCurrentDate.value;
|
||||||
|
const newDate =
|
||||||
|
period === MONTH
|
||||||
|
? setMonth(startOfMonth(current), index)
|
||||||
|
: setYear(current, index);
|
||||||
|
if (calendarType === START_CALENDAR) {
|
||||||
|
startCurrentDate.value = newDate;
|
||||||
|
} else {
|
||||||
|
endCurrentDate.value = newDate;
|
||||||
|
}
|
||||||
|
setViewMode(calendarType, period === MONTH ? WEEK : MONTH);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateManualInput = (newDate, calendarType) => {
|
||||||
|
if (calendarType === START_CALENDAR) {
|
||||||
|
selectedStartDate.value = newDate;
|
||||||
|
startCurrentDate.value = newDate;
|
||||||
|
} else {
|
||||||
|
selectedEndDate.value = newDate;
|
||||||
|
endCurrentDate.value = newDate;
|
||||||
|
}
|
||||||
|
selectingEndDate.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManualInputError = message => {
|
||||||
|
bus.$emit('newToastMessage', 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);
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitDateRange = () => {
|
||||||
|
if (!isValid(selectedStartDate.value) || !isValid(selectedEndDate.value)) {
|
||||||
|
bus.$emit('newToastMessage', 'Please select a valid time range');
|
||||||
|
} else {
|
||||||
|
showDatePicker.value = false;
|
||||||
|
emit('dateRangeChanged', [selectedStartDate.value, selectedEndDate.value]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative font-inter">
|
||||||
|
<DatePickerButton
|
||||||
|
:selected-start-date="selectedStartDate"
|
||||||
|
:selected-end-date="selectedEndDate"
|
||||||
|
:selected-range="selectedRange"
|
||||||
|
@open="showDatePicker = !showDatePicker"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showDatePicker"
|
||||||
|
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] h-[490px] rounded-2xl border border-slate-50 dark:border-slate-800 bg-white dark:bg-slate-800"
|
||||||
|
>
|
||||||
|
<CalendarDateRange
|
||||||
|
:selected-range="selectedRange"
|
||||||
|
@set-range="setDateRange"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
|
||||||
|
>
|
||||||
|
<div class="flex justify-around h-fit">
|
||||||
|
<!-- Calendars for Start and End Dates -->
|
||||||
|
<div
|
||||||
|
v-for="calendar in [START_CALENDAR, END_CALENDAR]"
|
||||||
|
:key="`${calendar}-calendar`"
|
||||||
|
class="flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<CalendarDateInput
|
||||||
|
:calendar-type="calendar"
|
||||||
|
:date-value="
|
||||||
|
calendar === START_CALENDAR ? manualStartDate : manualEndDate
|
||||||
|
"
|
||||||
|
:compare-date="
|
||||||
|
calendar === START_CALENDAR ? manualEndDate : manualStartDate
|
||||||
|
"
|
||||||
|
:is-disabled="selectedRange !== CUSTOM_RANGE"
|
||||||
|
@update="
|
||||||
|
calendar === START_CALENDAR
|
||||||
|
? (manualStartDate = $event)
|
||||||
|
: (manualEndDate = $event)
|
||||||
|
"
|
||||||
|
@validate="updateManualInput($event, calendar)"
|
||||||
|
@error="handleManualInputError($event)"
|
||||||
|
/>
|
||||||
|
<div class="py-5 border-b border-slate-50 dark:border-slate-700/50">
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center gap-2 px-5 min-w-[340px] max-h-[352px]"
|
||||||
|
:class="
|
||||||
|
calendar === START_CALENDAR &&
|
||||||
|
'ltr:border-r rtl:border-l border-slate-50 dark:border-slate-700/50'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<CalendarYear
|
||||||
|
v-if="calendarViews[calendar] === YEAR"
|
||||||
|
:calendar-type="calendar"
|
||||||
|
:start-current-date="startCurrentDate"
|
||||||
|
:end-current-date="endCurrentDate"
|
||||||
|
@select-year="openCalendar($event, calendar, YEAR)"
|
||||||
|
/>
|
||||||
|
<CalendarMonth
|
||||||
|
v-else-if="calendarViews[calendar] === MONTH"
|
||||||
|
:calendar-type="calendar"
|
||||||
|
:start-current-date="startCurrentDate"
|
||||||
|
:end-current-date="endCurrentDate"
|
||||||
|
@select-month="openCalendar($event, calendar)"
|
||||||
|
@set-view="setViewMode"
|
||||||
|
@prev="moveCalendar(calendar, 'prev', YEAR)"
|
||||||
|
@next="moveCalendar(calendar, 'next', YEAR)"
|
||||||
|
/>
|
||||||
|
<CalendarWeek
|
||||||
|
v-else-if="calendarViews[calendar] === WEEK"
|
||||||
|
:calendar-type="calendar"
|
||||||
|
:current-date="currentDate"
|
||||||
|
:start-current-date="startCurrentDate"
|
||||||
|
:end-current-date="endCurrentDate"
|
||||||
|
:selected-start-date="selectedStartDate"
|
||||||
|
:selected-end-date="selectedEndDate"
|
||||||
|
:selecting-end-date="selectingEndDate"
|
||||||
|
:hovered-end-date="hoveredEndDate"
|
||||||
|
@update-hovered-end-date="hoveredEndDate = $event"
|
||||||
|
@select-date="selectDate"
|
||||||
|
@set-view="setViewMode"
|
||||||
|
@prev="moveCalendar(calendar, 'prev')"
|
||||||
|
@next="moveCalendar(calendar, 'next')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CalendarFooter @change="emitDateRange" @clear="resetDatePicker" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
calendarType: {
|
||||||
|
type: String,
|
||||||
|
default: 'start',
|
||||||
|
},
|
||||||
|
firstButtonLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
viewMode: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['prev', 'next', 'set-view']);
|
||||||
|
|
||||||
|
const { YEAR } = CALENDAR_PERIODS;
|
||||||
|
|
||||||
|
const onClickPrev = type => {
|
||||||
|
emit('prev', type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickNext = type => {
|
||||||
|
emit('next', type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickSetView = (type, mode) => {
|
||||||
|
emit('set-view', type, mode);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-start justify-between w-full h-9">
|
||||||
|
<button
|
||||||
|
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
|
||||||
|
@click="onClickPrev(calendarType)"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevron-left"
|
||||||
|
size="14"
|
||||||
|
class="text-slate-900 dark:text-slate-50"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
v-if="firstButtonLabel"
|
||||||
|
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50 hover:text-woot-600 dark:hover:text-woot-600"
|
||||||
|
@click="onClickSetView(calendarType, viewMode)"
|
||||||
|
>
|
||||||
|
{{ firstButtonLabel }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="buttonLabel"
|
||||||
|
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50"
|
||||||
|
:class="{ 'hover:text-woot-600 dark:hover:text-woot-600': viewMode }"
|
||||||
|
@click="onClickSetView(calendarType, YEAR)"
|
||||||
|
>
|
||||||
|
{{ buttonLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
|
||||||
|
@click="onClickNext(calendarType)"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevron-right"
|
||||||
|
size="14"
|
||||||
|
class="text-slate-900 dark:text-slate-50"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { parse, isValid, isAfter, isBefore } from 'date-fns';
|
||||||
|
import {
|
||||||
|
getIntlDateFormatForLocale,
|
||||||
|
CALENDAR_TYPES,
|
||||||
|
} from '../helpers/DatePickerHelper';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
calendarType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
dateValue: Date,
|
||||||
|
compareDate: Date,
|
||||||
|
isDisabled: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'validate', 'error']);
|
||||||
|
|
||||||
|
const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES;
|
||||||
|
|
||||||
|
const dateFormat = computed(() => getIntlDateFormatForLocale()?.toUpperCase());
|
||||||
|
|
||||||
|
const localDateValue = computed({
|
||||||
|
get: () => props.dateValue?.toLocaleDateString(navigator.language) || '',
|
||||||
|
set: newValue => {
|
||||||
|
const format = getIntlDateFormatForLocale();
|
||||||
|
const parsedDate = parse(newValue, format, new Date());
|
||||||
|
if (isValid(parsedDate)) {
|
||||||
|
emit('update', parsedDate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateDate = () => {
|
||||||
|
if (!isValid(props.dateValue)) {
|
||||||
|
emit('error', `Please enter the date in valid format: ${dateFormat.value}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { calendarType, compareDate, dateValue } = props;
|
||||||
|
const isStartCalendar = calendarType === START_CALENDAR;
|
||||||
|
const isEndCalendar = calendarType === END_CALENDAR;
|
||||||
|
|
||||||
|
if (compareDate && isStartCalendar && isAfter(dateValue, compareDate)) {
|
||||||
|
emit('error', 'Start date must be before the end date.');
|
||||||
|
} else if (compareDate && isEndCalendar && isBefore(dateValue, compareDate)) {
|
||||||
|
emit('error', 'End date must be after the start date.');
|
||||||
|
} else {
|
||||||
|
emit('validate', dateValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-[82px] flex flex-col items-start px-5 gap-1.5 pt-4 w-full">
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-50">
|
||||||
|
{{
|
||||||
|
calendarType === START_CALENDAR
|
||||||
|
? $t('DATE_PICKER.DATE_RANGE_INPUT.START')
|
||||||
|
: $t('DATE_PICKER.DATE_RANGE_INPUT.END')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="localDateValue"
|
||||||
|
type="text"
|
||||||
|
class="reset-base border bg-slate-25 dark:bg-slate-900 ring-offset-ash-900 border-slate-50 dark:border-slate-700/50 w-full disabled:text-slate-200 dark:disabled:text-slate-700 disabled:cursor-not-allowed text-slate-800 dark:text-slate-50 px-1.5 py-1 text-sm rounded-xl h-10"
|
||||||
|
:placeholder="dateFormat"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
@keypress.enter="validateDate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
import { dateRanges } from '../helpers/DatePickerHelper';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
selectedRange: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['set-range']);
|
||||||
|
|
||||||
|
const setDateRange = range => {
|
||||||
|
emit('set-range', range);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-[200px] flex flex-col items-start">
|
||||||
|
<h4
|
||||||
|
class="w-full px-5 py-4 text-sm font-medium capitalize text-start text-slate-600 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
{{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-col items-start w-full">
|
||||||
|
<button
|
||||||
|
v-for="range in dateRanges"
|
||||||
|
:key="range.label"
|
||||||
|
class="w-full px-5 py-3 text-sm font-medium truncate border-none rounded-none text-start hover:bg-slate-50 dark:hover:bg-slate-700"
|
||||||
|
:class="
|
||||||
|
range.value === selectedRange
|
||||||
|
? 'text-slate-800 dark:text-slate-50 bg-slate-50 dark:bg-slate-700'
|
||||||
|
: 'text-slate-600 dark:text-slate-200'
|
||||||
|
"
|
||||||
|
@click="setDateRange(range)"
|
||||||
|
>
|
||||||
|
{{ $t(range.label) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['clear', 'apply']);
|
||||||
|
|
||||||
|
const onClickClear = () => {
|
||||||
|
emit('clear');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickApply = () => {
|
||||||
|
emit('change');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-[56px] flex justify-between px-5 py-3 items-center">
|
||||||
|
<button
|
||||||
|
class="p-1.5 rounded-lg w-fit text-sm font-medium text-slate-600 dark:text-slate-200 hover:text-slate-800 dark:hover:text-slate-100"
|
||||||
|
@click="onClickClear"
|
||||||
|
>
|
||||||
|
{{ $t('DATE_PICKER.CLEAR_BUTTON') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1.5 rounded-lg w-fit text-sm font-medium text-woot-500 dark:text-woot-300 hover:text-woot-700 dark:hover:text-woot-500"
|
||||||
|
@click="onClickApply"
|
||||||
|
>
|
||||||
|
{{ $t('DATE_PICKER.APPLY_BUTTON') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { format, getMonth, setMonth, startOfMonth } from 'date-fns';
|
||||||
|
import {
|
||||||
|
yearName,
|
||||||
|
CALENDAR_TYPES,
|
||||||
|
CALENDAR_PERIODS,
|
||||||
|
} from '../helpers/DatePickerHelper';
|
||||||
|
|
||||||
|
import CalendarAction from './CalendarAction.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
calendarType: {
|
||||||
|
type: String,
|
||||||
|
default: 'start',
|
||||||
|
},
|
||||||
|
startCurrentDate: Date,
|
||||||
|
endCurrentDate: Date,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||||
|
const { MONTH, YEAR } = CALENDAR_PERIODS;
|
||||||
|
|
||||||
|
const months = Array.from({ length: 12 }, (_, index) =>
|
||||||
|
format(setMonth(startOfMonth(new Date()), index), 'MMM')
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeMonthIndex = computed(() => {
|
||||||
|
const date =
|
||||||
|
props.calendarType === START_CALENDAR
|
||||||
|
? props.startCurrentDate
|
||||||
|
: props.endCurrentDate;
|
||||||
|
return getMonth(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['select-month', 'prev', 'next', 'set-view']);
|
||||||
|
|
||||||
|
const setViewMode = (type, mode) => {
|
||||||
|
emit('set-view', type, mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickPrev = () => {
|
||||||
|
emit('prev');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickNext = () => {
|
||||||
|
emit('next');
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectMonth = index => {
|
||||||
|
emit('select-month', index);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||||
|
<CalendarAction
|
||||||
|
:view-mode="YEAR"
|
||||||
|
:calendar-type="calendarType"
|
||||||
|
:button-label="
|
||||||
|
yearName(
|
||||||
|
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate,
|
||||||
|
MONTH
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@set-view="setViewMode"
|
||||||
|
@prev="onClickPrev"
|
||||||
|
@next="onClickNext"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="grid w-full grid-cols-3 gap-x-3 gap-y-2 auto-rows-[61px]">
|
||||||
|
<button
|
||||||
|
v-for="(month, index) in months"
|
||||||
|
:key="index"
|
||||||
|
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[92px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
|
||||||
|
:class="{
|
||||||
|
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:bg-woot-700':
|
||||||
|
index === activeMonthIndex,
|
||||||
|
}"
|
||||||
|
@click="selectMonth(index)"
|
||||||
|
>
|
||||||
|
{{ month }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
monthName,
|
||||||
|
yearName,
|
||||||
|
getWeeksForMonth,
|
||||||
|
isToday,
|
||||||
|
dayIsInRange,
|
||||||
|
isCurrentMonth,
|
||||||
|
isLastDayOfMonth,
|
||||||
|
isHoveringDayInRange,
|
||||||
|
isHoveringNextDayInRange,
|
||||||
|
CALENDAR_TYPES,
|
||||||
|
CALENDAR_PERIODS,
|
||||||
|
} from '../helpers/DatePickerHelper';
|
||||||
|
|
||||||
|
import CalendarWeekLabel from './CalendarWeekLabel.vue';
|
||||||
|
import CalendarAction from './CalendarAction.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
calendarType: {
|
||||||
|
type: String,
|
||||||
|
default: 'start',
|
||||||
|
},
|
||||||
|
currentDate: Date,
|
||||||
|
startCurrentDate: Date,
|
||||||
|
endCurrentDate: Date,
|
||||||
|
selectedStartDate: Date,
|
||||||
|
selectingEndDate: Boolean,
|
||||||
|
selectedEndDate: Date,
|
||||||
|
hoveredEndDate: Date,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update-hovered-end-date',
|
||||||
|
'select-date',
|
||||||
|
'prev',
|
||||||
|
'next',
|
||||||
|
'set-view',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||||
|
const { MONTH } = CALENDAR_PERIODS;
|
||||||
|
|
||||||
|
const emitHoveredEndDate = day => {
|
||||||
|
emit('update-hovered-end-date', day);
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitSelectDate = day => {
|
||||||
|
emit('select-date', day);
|
||||||
|
};
|
||||||
|
const onClickPrev = () => {
|
||||||
|
emit('prev');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickNext = () => {
|
||||||
|
emit('next');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setViewMode = (type, mode) => {
|
||||||
|
emit('set-view', type, mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const weeks = calendarType => {
|
||||||
|
return getWeeksForMonth(
|
||||||
|
calendarType === START_CALENDAR
|
||||||
|
? props.startCurrentDate
|
||||||
|
: props.endCurrentDate
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectedStartOrEndDate = day => {
|
||||||
|
return (
|
||||||
|
dayIsInRange(day, props.selectedStartDate, props.selectedStartDate) ||
|
||||||
|
dayIsInRange(day, props.selectedEndDate, props.selectedEndDate)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInRange = day => {
|
||||||
|
return dayIsInRange(day, props.selectedStartDate, props.selectedEndDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInCurrentMonth = day => {
|
||||||
|
return isCurrentMonth(
|
||||||
|
day,
|
||||||
|
props.calendarType === START_CALENDAR
|
||||||
|
? props.startCurrentDate
|
||||||
|
: props.endCurrentDate
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHoveringInRange = day => {
|
||||||
|
return isHoveringDayInRange(
|
||||||
|
day,
|
||||||
|
props.selectedStartDate,
|
||||||
|
props.selectingEndDate,
|
||||||
|
props.hoveredEndDate
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNextDayInRange = day => {
|
||||||
|
return isHoveringNextDayInRange(
|
||||||
|
day,
|
||||||
|
props.selectedStartDate,
|
||||||
|
props.selectedEndDate,
|
||||||
|
props.hoveredEndDate
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dayClasses = day => ({
|
||||||
|
'text-slate-500 dark:text-slate-400 pointer-events-none':
|
||||||
|
!isInCurrentMonth(day),
|
||||||
|
'text-slate-800 dark:text-slate-50 hover:text-slate-800 dark:hover:text-white hover:bg-woot-100 dark:hover:bg-woot-700':
|
||||||
|
isInCurrentMonth(day),
|
||||||
|
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white':
|
||||||
|
isSelectedStartOrEndDate(day) && isInCurrentMonth(day),
|
||||||
|
'bg-woot-50 dark:bg-woot-800':
|
||||||
|
(isInRange(day) || isHoveringInRange(day)) &&
|
||||||
|
!isSelectedStartOrEndDate(day) &&
|
||||||
|
isInCurrentMonth(day),
|
||||||
|
'outline outline-1 outline-woot-200 -outline-offset-1 dark:outline-woot-700 text-woot-600 dark:text-woot-400':
|
||||||
|
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||||
|
<CalendarAction
|
||||||
|
:view-mode="MONTH"
|
||||||
|
:calendar-type="calendarType"
|
||||||
|
:first-button-label="
|
||||||
|
monthName(
|
||||||
|
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:button-label="
|
||||||
|
yearName(
|
||||||
|
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@prev="onClickPrev"
|
||||||
|
@next="onClickNext"
|
||||||
|
@set-view="setViewMode"
|
||||||
|
/>
|
||||||
|
<CalendarWeekLabel />
|
||||||
|
<div
|
||||||
|
v-for="week in weeks(calendarType)"
|
||||||
|
:key="week[0].getTime()"
|
||||||
|
class="grid max-w-md grid-cols-7 gap-2 mx-auto overflow-hidden rounded-lg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="day in week"
|
||||||
|
:key="day.getTime()"
|
||||||
|
class="flex relative items-center justify-center w-9 h-8 py-1.5 px-2 font-medium text-sm rounded-lg cursor-pointer"
|
||||||
|
:class="dayClasses(day)"
|
||||||
|
@mouseenter="emitHoveredEndDate(day)"
|
||||||
|
@mouseleave="emitHoveredEndDate(null)"
|
||||||
|
@click="emitSelectDate(day)"
|
||||||
|
>
|
||||||
|
{{ day.getDate() }}
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
(isInRange(day) || isHoveringInRange(day)) &&
|
||||||
|
isNextDayInRange(day) &&
|
||||||
|
!isLastDayOfMonth(day) &&
|
||||||
|
isInCurrentMonth(day)
|
||||||
|
"
|
||||||
|
class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-woot-50 dark:bg-woot-800 -z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
import { calendarWeeks } from '../helpers/DatePickerHelper';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="max-w-md mx-auto grid grid-cols-7 gap-2">
|
||||||
|
<div
|
||||||
|
v-for="day in calendarWeeks"
|
||||||
|
:key="day.id"
|
||||||
|
class="flex items-center justify-center font-medium text-sm w-9 h-7 py-1.5 px-2"
|
||||||
|
>
|
||||||
|
{{ day.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { getYear, addYears, subYears } from 'date-fns';
|
||||||
|
import { CALENDAR_TYPES } from '../helpers/DatePickerHelper';
|
||||||
|
|
||||||
|
import CalendarAction from './CalendarAction.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
calendarType: {
|
||||||
|
type: String,
|
||||||
|
default: 'start',
|
||||||
|
},
|
||||||
|
startCurrentDate: Date,
|
||||||
|
endCurrentDate: Date,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||||
|
|
||||||
|
const calculateStartYear = date => {
|
||||||
|
const year = getYear(date);
|
||||||
|
return year - (year % 10); // Align with the beginning of a decade
|
||||||
|
};
|
||||||
|
|
||||||
|
const startYear = ref(
|
||||||
|
calculateStartYear(
|
||||||
|
props.calendarType === START_CALENDAR
|
||||||
|
? props.startCurrentDate
|
||||||
|
: props.endCurrentDate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const years = computed(() =>
|
||||||
|
Array.from({ length: 10 }, (_, i) => startYear.value + i)
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstYear = computed(() => years.value[0]);
|
||||||
|
const lastYear = computed(() => years.value[years.value.length - 1]);
|
||||||
|
|
||||||
|
const activeYear = computed(() => {
|
||||||
|
const date =
|
||||||
|
props.calendarType === START_CALENDAR
|
||||||
|
? props.startCurrentDate
|
||||||
|
: props.endCurrentDate;
|
||||||
|
return getYear(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClickPrev = () => {
|
||||||
|
startYear.value = subYears(new Date(startYear.value, 0, 1), 10).getFullYear();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickNext = () => {
|
||||||
|
startYear.value = addYears(new Date(startYear.value, 0, 1), 10).getFullYear();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['select-year']);
|
||||||
|
|
||||||
|
const selectYear = year => {
|
||||||
|
emit('select-year', year);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||||
|
<CalendarAction
|
||||||
|
:calendar-type="calendarType"
|
||||||
|
:button-label="`${firstYear} - ${lastYear}`"
|
||||||
|
@prev="onClickPrev"
|
||||||
|
@next="onClickNext"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-x-3 gap-y-2 w-full auto-rows-[47px]">
|
||||||
|
<button
|
||||||
|
v-for="year in years"
|
||||||
|
:key="year"
|
||||||
|
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[144px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
|
||||||
|
:class="{
|
||||||
|
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:hover:bg-woot-700':
|
||||||
|
year === activeYear,
|
||||||
|
}"
|
||||||
|
@click="selectYear(year)"
|
||||||
|
>
|
||||||
|
{{ year }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { dateRanges } from '../helpers/DatePickerHelper';
|
||||||
|
import { format, isSameYear, isValid } from 'date-fns';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectedStartDate: Date,
|
||||||
|
selectedEndDate: Date,
|
||||||
|
selectedRange: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDateRange = computed(() => {
|
||||||
|
const startDate = props.selectedStartDate;
|
||||||
|
const endDate = props.selectedEndDate;
|
||||||
|
|
||||||
|
if (!isValid(startDate) || !isValid(endDate)) {
|
||||||
|
return 'Select a date range';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatString = isSameYear(startDate, endDate)
|
||||||
|
? 'MMM d' // Same year: "Apr 1"
|
||||||
|
: 'MMM d yyyy'; // Different years: "Apr 1 2025"
|
||||||
|
|
||||||
|
if (isSameYear(startDate, new Date()) && isSameYear(endDate, new Date())) {
|
||||||
|
// Both dates are in the current year
|
||||||
|
return `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`;
|
||||||
|
}
|
||||||
|
// At least one date is not in the current year
|
||||||
|
return `${format(startDate, formatString)} - ${format(
|
||||||
|
endDate,
|
||||||
|
formatString
|
||||||
|
)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeDateRange = computed(
|
||||||
|
() => dateRanges.find(range => range.value === props.selectedRange).label
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['open']);
|
||||||
|
|
||||||
|
const openDatePicker = () => {
|
||||||
|
emit('open');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-slate-50 dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
|
||||||
|
@click="openDatePicker"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
class="text-slate-800 dark:text-slate-50"
|
||||||
|
icon="calendar"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-50">
|
||||||
|
{{ $t(activeDateRange) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-slate-600 dark:text-slate-200">
|
||||||
|
{{ formatDateRange }}
|
||||||
|
</span>
|
||||||
|
<fluent-icon
|
||||||
|
class="text-slate-800 dark:text-slate-50"
|
||||||
|
icon="chevron-down"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,219 @@
|
|||||||
|
import {
|
||||||
|
startOfDay,
|
||||||
|
subDays,
|
||||||
|
endOfDay,
|
||||||
|
subMonths,
|
||||||
|
addMonths,
|
||||||
|
subYears,
|
||||||
|
addYears,
|
||||||
|
startOfMonth,
|
||||||
|
isSameMonth,
|
||||||
|
format,
|
||||||
|
startOfWeek,
|
||||||
|
addDays,
|
||||||
|
eachDayOfInterval,
|
||||||
|
endOfMonth,
|
||||||
|
isSameDay,
|
||||||
|
isWithinInterval,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
|
// Constants for calendar and date ranges
|
||||||
|
export const calendarWeeks = [
|
||||||
|
{ id: 1, label: 'M' },
|
||||||
|
{ id: 2, label: 'T' },
|
||||||
|
{ id: 3, label: 'W' },
|
||||||
|
{ id: 4, label: 'T' },
|
||||||
|
{ id: 5, label: 'F' },
|
||||||
|
{ id: 6, label: 'S' },
|
||||||
|
{ id: 7, label: 'S' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dateRanges = [
|
||||||
|
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_7_DAYS', value: 'last7days' },
|
||||||
|
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_30_DAYS', value: 'last30days' },
|
||||||
|
{
|
||||||
|
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_3_MONTHS',
|
||||||
|
value: 'last3months',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
|
||||||
|
value: 'last6months',
|
||||||
|
},
|
||||||
|
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_YEAR', value: 'lastYear' },
|
||||||
|
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.CUSTOM_RANGE', value: 'custom' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DATE_RANGE_TYPES = {
|
||||||
|
LAST_7_DAYS: 'last7days',
|
||||||
|
LAST_30_DAYS: 'last30days',
|
||||||
|
LAST_3_MONTHS: 'last3months',
|
||||||
|
LAST_6_MONTHS: 'last6months',
|
||||||
|
LAST_YEAR: 'lastYear',
|
||||||
|
CUSTOM_RANGE: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CALENDAR_TYPES = {
|
||||||
|
START_CALENDAR: 'start',
|
||||||
|
END_CALENDAR: 'end',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CALENDAR_PERIODS = {
|
||||||
|
WEEK: 'week',
|
||||||
|
MONTH: 'month',
|
||||||
|
YEAR: 'year',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility functions for date operations
|
||||||
|
export const monthName = currentDate => format(currentDate, 'MMMM');
|
||||||
|
export const yearName = currentDate => format(currentDate, 'yyyy');
|
||||||
|
|
||||||
|
export const getIntlDateFormatForLocale = () => {
|
||||||
|
const year = 2222;
|
||||||
|
const month = 12;
|
||||||
|
const day = 15;
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
const formattedDate = new Intl.DateTimeFormat(navigator.language).format(
|
||||||
|
date
|
||||||
|
);
|
||||||
|
return formattedDate
|
||||||
|
.replace(`${year}`, 'yyyy')
|
||||||
|
.replace(`${month}`, 'MM')
|
||||||
|
.replace(`${day}`, 'dd');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility functions for calendar operations
|
||||||
|
export const chunk = (array, size) =>
|
||||||
|
Array.from({ length: Math.ceil(array.length / size) }, (_, index) =>
|
||||||
|
array.slice(index * size, index * size + size)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getWeeksForMonth = (date, weekStartsOn = 1) => {
|
||||||
|
const startOfTheMonth = startOfMonth(date);
|
||||||
|
const startOfTheFirstWeek = startOfWeek(startOfTheMonth, { weekStartsOn });
|
||||||
|
const endOfTheLastWeek = addDays(startOfTheFirstWeek, 41); // Covering six weeks to fill the calendar
|
||||||
|
return chunk(
|
||||||
|
eachDayOfInterval({ start: startOfTheFirstWeek, end: endOfTheLastWeek }),
|
||||||
|
7
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveCalendarDate = (
|
||||||
|
calendar,
|
||||||
|
startCurrentDate,
|
||||||
|
endCurrentDate,
|
||||||
|
direction,
|
||||||
|
period
|
||||||
|
) => {
|
||||||
|
const adjustFunctions = {
|
||||||
|
month: { prev: subMonths, next: addMonths },
|
||||||
|
year: { prev: subYears, next: addYears },
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjust = adjustFunctions[period][direction];
|
||||||
|
|
||||||
|
if (calendar === 'start') {
|
||||||
|
const newStart = adjust(startCurrentDate, 1);
|
||||||
|
return { start: newStart, end: endCurrentDate };
|
||||||
|
}
|
||||||
|
const newEnd = adjust(endCurrentDate, 1);
|
||||||
|
return { start: startCurrentDate, end: newEnd };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Date comparison functions
|
||||||
|
export const isToday = (currentDate, date) =>
|
||||||
|
date.getDate() === currentDate.getDate() &&
|
||||||
|
date.getMonth() === currentDate.getMonth() &&
|
||||||
|
date.getFullYear() === currentDate.getFullYear();
|
||||||
|
|
||||||
|
export const isCurrentMonth = (day, referenceDate) =>
|
||||||
|
isSameMonth(day, referenceDate);
|
||||||
|
|
||||||
|
export const isLastDayOfMonth = day => {
|
||||||
|
const lastDay = endOfMonth(day);
|
||||||
|
return day.getDate() === lastDay.getDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dayIsInRange = (date, startDate, endDate) => {
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Normalize dates to ignore time differences
|
||||||
|
let startOfDayStart = startOfDay(startDate);
|
||||||
|
let startOfDayEnd = startOfDay(endDate);
|
||||||
|
// Swap if start is greater than end
|
||||||
|
if (startOfDayStart > startOfDayEnd) {
|
||||||
|
[startOfDayStart, startOfDayEnd] = [startOfDayEnd, startOfDayStart];
|
||||||
|
}
|
||||||
|
// Check if the date is within the interval or is the same as the start date
|
||||||
|
return (
|
||||||
|
isSameDay(date, startOfDayStart) ||
|
||||||
|
isWithinInterval(date, {
|
||||||
|
start: startOfDayStart,
|
||||||
|
end: startOfDayEnd,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handling hovering states in date range pickers
|
||||||
|
export const isHoveringDayInRange = (
|
||||||
|
day,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
hoveredEndDate
|
||||||
|
) => {
|
||||||
|
if (endDate && hoveredEndDate && startDate <= hoveredEndDate) {
|
||||||
|
// Ensure the start date is not after the hovered end date
|
||||||
|
return isWithinInterval(day, { start: startDate, end: hoveredEndDate });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isHoveringNextDayInRange = (
|
||||||
|
day,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
hoveredEndDate
|
||||||
|
) => {
|
||||||
|
if (startDate && !endDate && hoveredEndDate) {
|
||||||
|
// If a start date is selected, and we're hovering (but haven't clicked an end date yet)
|
||||||
|
const nextDay = addDays(day, 1);
|
||||||
|
return isWithinInterval(nextDay, { start: startDate, end: hoveredEndDate });
|
||||||
|
}
|
||||||
|
if (startDate && endDate) {
|
||||||
|
// Normal range checking between selected start and end dates
|
||||||
|
const nextDay = addDays(day, 1);
|
||||||
|
return isWithinInterval(nextDay, { start: startDate, end: endDate });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper func to determine active date ranges based on user selection
|
||||||
|
export const getActiveDateRange = (range, currentDate) => {
|
||||||
|
const ranges = {
|
||||||
|
last7days: () => ({
|
||||||
|
start: startOfDay(subDays(currentDate, 6)),
|
||||||
|
end: endOfDay(currentDate),
|
||||||
|
}),
|
||||||
|
last30days: () => ({
|
||||||
|
start: startOfDay(subDays(currentDate, 29)),
|
||||||
|
end: endOfDay(currentDate),
|
||||||
|
}),
|
||||||
|
last3months: () => ({
|
||||||
|
start: startOfDay(subMonths(currentDate, 3)),
|
||||||
|
end: endOfDay(currentDate),
|
||||||
|
}),
|
||||||
|
last6months: () => ({
|
||||||
|
start: startOfDay(subMonths(currentDate, 6)),
|
||||||
|
end: endOfDay(currentDate),
|
||||||
|
}),
|
||||||
|
lastYear: () => ({
|
||||||
|
start: startOfDay(subMonths(currentDate, 12)),
|
||||||
|
end: endOfDay(currentDate),
|
||||||
|
}),
|
||||||
|
custom: () => ({ start: currentDate, end: currentDate }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
ranges[range] || (() => ({ start: currentDate, end: currentDate }))
|
||||||
|
)();
|
||||||
|
};
|
||||||
@ -0,0 +1,309 @@
|
|||||||
|
import {
|
||||||
|
monthName,
|
||||||
|
yearName,
|
||||||
|
getIntlDateFormatForLocale,
|
||||||
|
getWeeksForMonth,
|
||||||
|
isToday,
|
||||||
|
isCurrentMonth,
|
||||||
|
isLastDayOfMonth,
|
||||||
|
dayIsInRange,
|
||||||
|
getActiveDateRange,
|
||||||
|
isHoveringDayInRange,
|
||||||
|
isHoveringNextDayInRange,
|
||||||
|
moveCalendarDate,
|
||||||
|
chunk,
|
||||||
|
} from '../DatePickerHelper';
|
||||||
|
|
||||||
|
describe('Date formatting functions', () => {
|
||||||
|
const testDate = new Date(2020, 4, 15); // May 15, 2020
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct month name from a date', () => {
|
||||||
|
expect(monthName(testDate)).toBe('May');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct year from a date', () => {
|
||||||
|
expect(yearName(testDate)).toBe('2020');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct date format for the current locale en-US', () => {
|
||||||
|
const expected = 'MM/dd/yyyy';
|
||||||
|
expect(getIntlDateFormatForLocale()).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct date format for the current locale en-IN', () => {
|
||||||
|
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
|
||||||
|
const expected = 'dd/MM/yyyy';
|
||||||
|
expect(getIntlDateFormatForLocale()).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('chunk', () => {
|
||||||
|
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
|
it('correctly chunks an array into smaller arrays of given size', () => {
|
||||||
|
const expected = [
|
||||||
|
[1, 2, 3],
|
||||||
|
[4, 5, 6],
|
||||||
|
[7, 8, 9],
|
||||||
|
];
|
||||||
|
expect(chunk(array, 3)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles arrays that do not divide evenly by the chunk size', () => {
|
||||||
|
const expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9]];
|
||||||
|
expect(chunk(array, 2)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getWeeksForMonth', () => {
|
||||||
|
it('returns the correct weeks array for a month starting on Monday', () => {
|
||||||
|
const date = new Date(2020, 3, 1); // April 2020
|
||||||
|
const weeks = getWeeksForMonth(date, 1);
|
||||||
|
expect(weeks.length).toBe(6);
|
||||||
|
expect(weeks[0][0]).toEqual(new Date(2020, 2, 30)); // Check if first day of the first week is correct
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moveCalendarDate', () => {
|
||||||
|
it('handles the year transition when moving the start date backward by one month from January', () => {
|
||||||
|
const startDate = new Date(2021, 0, 1);
|
||||||
|
const endDate = new Date(2021, 0, 31);
|
||||||
|
const result = moveCalendarDate(
|
||||||
|
'start',
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
'prev',
|
||||||
|
'month'
|
||||||
|
);
|
||||||
|
const expectedStartDate = new Date(2020, 11, 1);
|
||||||
|
expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
|
||||||
|
expect(result.end.toISOString()).toEqual(endDate.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles the year transition when moving the start date forward by one month from January', () => {
|
||||||
|
const startDate = new Date(2020, 0, 1);
|
||||||
|
const endDate = new Date(2020, 1, 31);
|
||||||
|
const result = moveCalendarDate(
|
||||||
|
'start',
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
'next',
|
||||||
|
'month'
|
||||||
|
);
|
||||||
|
const expectedStartDate = new Date(2020, 1, 1);
|
||||||
|
expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
|
||||||
|
expect(result.end.toISOString()).toEqual(endDate.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles the year transition when moving the end date forward by one month from December', () => {
|
||||||
|
const startDate = new Date(2021, 11, 1);
|
||||||
|
const endDate = new Date(2021, 11, 31);
|
||||||
|
const result = moveCalendarDate('end', startDate, endDate, 'next', 'month');
|
||||||
|
const expectedEndDate = new Date(2022, 0, 31);
|
||||||
|
expect(result.start).toEqual(startDate);
|
||||||
|
expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
|
||||||
|
});
|
||||||
|
it('handles the year transition when moving the end date backward by one month from December', () => {
|
||||||
|
const startDate = new Date(2021, 11, 1);
|
||||||
|
const endDate = new Date(2021, 11, 31);
|
||||||
|
const result = moveCalendarDate('end', startDate, endDate, 'prev', 'month');
|
||||||
|
const expectedEndDate = new Date(2021, 10, 30);
|
||||||
|
|
||||||
|
expect(result.start).toEqual(startDate);
|
||||||
|
expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isToday', () => {
|
||||||
|
it('returns true if the dates are the same', () => {
|
||||||
|
const today = new Date();
|
||||||
|
const alsoToday = new Date(today);
|
||||||
|
expect(isToday(today, alsoToday)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the dates are not the same', () => {
|
||||||
|
const today = new Date();
|
||||||
|
const notToday = new Date(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth(),
|
||||||
|
today.getDate() - 1
|
||||||
|
);
|
||||||
|
expect(isToday(today, notToday)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isCurrentMonth', () => {
|
||||||
|
const referenceDate = new Date(2020, 6, 15); // July 15, 2020
|
||||||
|
|
||||||
|
it('returns true if the day is in the same month as the reference date', () => {
|
||||||
|
const testDay = new Date(2020, 6, 1);
|
||||||
|
expect(isCurrentMonth(testDay, referenceDate)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the day is not in the same month as the reference date', () => {
|
||||||
|
const testDay = new Date(2020, 5, 30);
|
||||||
|
expect(isCurrentMonth(testDay, referenceDate)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isLastDayOfMonth', () => {
|
||||||
|
it('returns true if the day is the last day of the month', () => {
|
||||||
|
const testDay = new Date(2020, 6, 31); // July 31, 2020
|
||||||
|
expect(isLastDayOfMonth(testDay)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the day is not the last day of the month', () => {
|
||||||
|
const testDay = new Date(2020, 6, 30); // July 30, 2020
|
||||||
|
expect(isLastDayOfMonth(testDay)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dayIsInRange', () => {
|
||||||
|
it('returns true if the date is within the range', () => {
|
||||||
|
const start = new Date(2020, 1, 10);
|
||||||
|
const end = new Date(2020, 1, 20);
|
||||||
|
const testDate = new Date(2020, 1, 15);
|
||||||
|
expect(dayIsInRange(testDate, start, end)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the date is the same as the start date', () => {
|
||||||
|
const start = new Date(2020, 1, 10);
|
||||||
|
const end = new Date(2020, 1, 20);
|
||||||
|
const testDate = new Date(2020, 1, 10);
|
||||||
|
expect(dayIsInRange(testDate, start, end)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the date is outside the range', () => {
|
||||||
|
const start = new Date(2020, 1, 10);
|
||||||
|
const end = new Date(2020, 1, 20);
|
||||||
|
const testDate = new Date(2020, 1, 9);
|
||||||
|
expect(dayIsInRange(testDate, start, end)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isHoveringDayInRange', () => {
|
||||||
|
const startDate = new Date(2020, 6, 10);
|
||||||
|
const endDate = new Date(2020, 6, 20);
|
||||||
|
const hoveredEndDate = new Date(2020, 6, 15);
|
||||||
|
|
||||||
|
it('returns true if the day is within the start and hovered end dates', () => {
|
||||||
|
const testDay = new Date(2020, 6, 12);
|
||||||
|
expect(
|
||||||
|
isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the day is outside the hovered date range', () => {
|
||||||
|
const testDay = new Date(2020, 6, 16);
|
||||||
|
expect(
|
||||||
|
isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isHoveringNextDayInRange', () => {
|
||||||
|
const startDate = new Date(2020, 6, 10);
|
||||||
|
const hoveredEndDate = new Date(2020, 6, 15);
|
||||||
|
|
||||||
|
it('returns true if the next day after the given day is within the start and hovered end dates', () => {
|
||||||
|
const testDay = new Date(2020, 6, 14);
|
||||||
|
expect(
|
||||||
|
isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the next day is outside the start and hovered end dates', () => {
|
||||||
|
const testDay = new Date(2020, 6, 15);
|
||||||
|
expect(
|
||||||
|
isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getActiveDateRange', () => {
|
||||||
|
const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Mocking the current date to ensure consistency in tests
|
||||||
|
jest.useFakeTimers().setSystemTime(currentDate.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct range for "last7days"', () => {
|
||||||
|
const range = getActiveDateRange('last7days', new Date());
|
||||||
|
const expectedStart = new Date(2020, 5, 9);
|
||||||
|
expectedStart.setHours(0, 0, 0, 0);
|
||||||
|
const expectedEnd = new Date();
|
||||||
|
expectedEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
expect(range.start).toEqual(expectedStart);
|
||||||
|
expect(range.end).toEqual(expectedEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct range for "last30days"', () => {
|
||||||
|
const range = getActiveDateRange('last30days', new Date());
|
||||||
|
const expectedStart = new Date(2020, 4, 17);
|
||||||
|
expectedStart.setHours(0, 0, 0, 0);
|
||||||
|
const expectedEnd = new Date();
|
||||||
|
expectedEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
expect(range.start).toEqual(expectedStart);
|
||||||
|
expect(range.end).toEqual(expectedEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct range for "last3months"', () => {
|
||||||
|
const range = getActiveDateRange('last3months', new Date());
|
||||||
|
const expectedStart = new Date(2020, 2, 15);
|
||||||
|
expectedStart.setHours(0, 0, 0, 0);
|
||||||
|
const expectedEnd = new Date();
|
||||||
|
expectedEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
expect(range.start).toEqual(expectedStart);
|
||||||
|
expect(range.end).toEqual(expectedEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct range for "last6months"', () => {
|
||||||
|
const range = getActiveDateRange('last6months', new Date());
|
||||||
|
const expectedStart = new Date(2019, 11, 15);
|
||||||
|
expectedStart.setHours(0, 0, 0, 0);
|
||||||
|
const expectedEnd = new Date();
|
||||||
|
expectedEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
expect(range.start).toEqual(expectedStart);
|
||||||
|
expect(range.end).toEqual(expectedEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct range for "lastYear"', () => {
|
||||||
|
const range = getActiveDateRange('lastYear', new Date());
|
||||||
|
const expectedStart = new Date(2019, 5, 15);
|
||||||
|
expectedStart.setHours(0, 0, 0, 0);
|
||||||
|
const expectedEnd = new Date();
|
||||||
|
expectedEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
expect(range.start).toEqual(expectedStart);
|
||||||
|
expect(range.end).toEqual(expectedEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct range for "custom date range"', () => {
|
||||||
|
const range = getActiveDateRange('custom', new Date());
|
||||||
|
expect(range.start).toEqual(new Date(currentDate));
|
||||||
|
expect(range.end).toEqual(new Date(currentDate));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles an unknown range label gracefully', () => {
|
||||||
|
const range = getActiveDateRange('unknown', new Date());
|
||||||
|
expect(range.start).toEqual(new Date(currentDate));
|
||||||
|
expect(range.end).toEqual(new Date(currentDate));
|
||||||
|
});
|
||||||
|
});
|
||||||
19
app/javascript/dashboard/i18n/locale/en/datePicker.json
Normal file
19
app/javascript/dashboard/i18n/locale/en/datePicker.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"DATE_PICKER": {
|
||||||
|
"APPLY_BUTTON": "Apply",
|
||||||
|
"CLEAR_BUTTON": "Clear",
|
||||||
|
"DATE_RANGE_INPUT": {
|
||||||
|
"START": "Start Date",
|
||||||
|
"END": "End Date"
|
||||||
|
},
|
||||||
|
"DATE_RANGE_OPTIONS": {
|
||||||
|
"TITLE": "DATE RANGE",
|
||||||
|
"LAST_7_DAYS": "Last 7 days",
|
||||||
|
"LAST_30_DAYS": "Last 30 days",
|
||||||
|
"LAST_3_MONTHS": "Last 3 months",
|
||||||
|
"LAST_6_MONTHS": "Last 6 months",
|
||||||
|
"LAST_YEAR": "Last year",
|
||||||
|
"CUSTOM_RANGE": "Custom date range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ import whatsappTemplates from './whatsappTemplates.json';
|
|||||||
import sla from './sla.json';
|
import sla from './sla.json';
|
||||||
import inbox from './inbox.json';
|
import inbox from './inbox.json';
|
||||||
import general from './general.json';
|
import general from './general.json';
|
||||||
|
import datePicker from './datePicker.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...advancedFilters,
|
...advancedFilters,
|
||||||
@ -68,4 +69,5 @@ export default {
|
|||||||
...whatsappTemplates,
|
...whatsappTemplates,
|
||||||
...inbox,
|
...inbox,
|
||||||
...general,
|
...general,
|
||||||
|
...datePicker,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,24 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-wrap w-full gap-3 md:flex-row">
|
<div class="flex flex-col flex-wrap w-full gap-3 md:flex-row">
|
||||||
<reports-filters-date-range
|
<woot-date-picker @dateRangeChanged="onDateRangeChange" />
|
||||||
class="sm:min-w-[200px] tiny h-8"
|
|
||||||
@on-range-change="onDateRangeChange"
|
|
||||||
/>
|
|
||||||
<woot-date-range-picker
|
|
||||||
v-if="isDateRangeSelected"
|
|
||||||
show-range
|
|
||||||
class="no-margin auto-width sm:min-w-[240px] small h-8"
|
|
||||||
:value="customDateRange"
|
|
||||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
|
||||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
|
||||||
@change="onCustomDateRangeChange"
|
|
||||||
/>
|
|
||||||
<SLA-filter @filter-change="emitFilterChange" />
|
<SLA-filter @filter-change="emitFilterChange" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
|
||||||
import ReportsFiltersDateRange from '../Filters/DateRange.vue';
|
|
||||||
import SLAFilter from '../SLA/SLAFilter.vue';
|
import SLAFilter from '../SLA/SLAFilter.vue';
|
||||||
import subDays from 'date-fns/subDays';
|
import subDays from 'date-fns/subDays';
|
||||||
import { DATE_RANGE_OPTIONS } from '../../constants';
|
import { DATE_RANGE_OPTIONS } from '../../constants';
|
||||||
@ -26,8 +12,6 @@ import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootDateRangePicker,
|
|
||||||
ReportsFiltersDateRange,
|
|
||||||
SLAFilter,
|
SLAFilter,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -39,25 +23,11 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isDateRangeSelected() {
|
|
||||||
return (
|
|
||||||
this.selectedDateRange.id === DATE_RANGE_OPTIONS.CUSTOM_DATE_RANGE.id
|
|
||||||
);
|
|
||||||
},
|
|
||||||
to() {
|
to() {
|
||||||
if (this.isDateRangeSelected) {
|
return getUnixEndOfDay(this.customDateRange[1]);
|
||||||
return getUnixEndOfDay(this.customDateRange[1]);
|
|
||||||
}
|
|
||||||
return getUnixEndOfDay(new Date());
|
|
||||||
},
|
},
|
||||||
from() {
|
from() {
|
||||||
if (this.isDateRangeSelected) {
|
return getUnixStartOfDay(this.customDateRange[0]);
|
||||||
return getUnixStartOfDay(this.customDateRange[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { offset } = this.selectedDateRange;
|
|
||||||
const fromDate = subDays(new Date(), offset);
|
|
||||||
return getUnixStartOfDay(fromDate);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -66,9 +36,20 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.emitChange();
|
this.setInitialRange();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setInitialRange() {
|
||||||
|
const { offset } = this.selectedDateRange;
|
||||||
|
const fromDate = subDays(new Date(), offset);
|
||||||
|
const from = getUnixStartOfDay(fromDate);
|
||||||
|
const to = getUnixEndOfDay(new Date());
|
||||||
|
this.$emit('filter-change', {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
...this.selectedGroupByFilter,
|
||||||
|
});
|
||||||
|
},
|
||||||
emitChange() {
|
emitChange() {
|
||||||
const { from, to } = this;
|
const { from, to } = this;
|
||||||
this.$emit('filter-change', {
|
this.$emit('filter-change', {
|
||||||
@ -81,11 +62,7 @@ export default {
|
|||||||
this.selectedGroupByFilter = params;
|
this.selectedGroupByFilter = params;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
},
|
},
|
||||||
onDateRangeChange(selectedRange) {
|
onDateRangeChange(value) {
|
||||||
this.selectedDateRange = selectedRange;
|
|
||||||
this.emitChange();
|
|
||||||
},
|
|
||||||
onCustomDateRangeChange(value) {
|
|
||||||
this.customDateRange = value;
|
this.customDateRange = value;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -58,6 +58,7 @@
|
|||||||
"M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z",
|
"M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z",
|
||||||
"M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"
|
"M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"
|
||||||
],
|
],
|
||||||
|
"calendar-outline": "M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h11.5Zm1.75 5.5h-15v9.25c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75V8.5Zm-11.75 6a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm4.25 0a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm-4.25-4a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm4.25 0a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm4.25 0a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm1.5-6H6.25A1.75 1.75 0 0 0 4.5 6.25V7h15v-.75a1.75 1.75 0 0 0-1.75-1.75Z",
|
||||||
"call-outline": "m7.056 2.418 1.167-.351a2.75 2.75 0 0 1 3.302 1.505l.902 2.006a2.75 2.75 0 0 1-.633 3.139L10.3 10.11a.25.25 0 0 0-.078.155c-.044.397.225 1.17.845 2.245.451.781.86 1.33 1.207 1.637.242.215.375.261.432.245l2.01-.615a2.75 2.75 0 0 1 3.034 1.02l1.281 1.776a2.75 2.75 0 0 1-.339 3.605l-.886.84a3.75 3.75 0 0 1-3.587.889c-2.754-.769-5.223-3.093-7.435-6.924-2.215-3.836-2.992-7.14-2.276-9.913a3.75 3.75 0 0 1 2.548-2.652Zm.433 1.437a2.25 2.25 0 0 0-1.529 1.59c-.602 2.332.087 5.261 2.123 8.788 2.033 3.522 4.222 5.582 6.54 6.23a2.25 2.25 0 0 0 2.151-.534l.887-.84a1.25 1.25 0 0 0 .154-1.639l-1.28-1.775a1.25 1.25 0 0 0-1.38-.464l-2.015.617c-1.17.348-2.232-.593-3.372-2.568C9 11.93 8.642 10.9 8.731 10.099c.047-.416.24-.8.546-1.086l1.494-1.393a1.25 1.25 0 0 0 .288-1.427l-.902-2.006a1.25 1.25 0 0 0-1.5-.684l-1.168.352Z",
|
"call-outline": "m7.056 2.418 1.167-.351a2.75 2.75 0 0 1 3.302 1.505l.902 2.006a2.75 2.75 0 0 1-.633 3.139L10.3 10.11a.25.25 0 0 0-.078.155c-.044.397.225 1.17.845 2.245.451.781.86 1.33 1.207 1.637.242.215.375.261.432.245l2.01-.615a2.75 2.75 0 0 1 3.034 1.02l1.281 1.776a2.75 2.75 0 0 1-.339 3.605l-.886.84a3.75 3.75 0 0 1-3.587.889c-2.754-.769-5.223-3.093-7.435-6.924-2.215-3.836-2.992-7.14-2.276-9.913a3.75 3.75 0 0 1 2.548-2.652Zm.433 1.437a2.25 2.25 0 0 0-1.529 1.59c-.602 2.332.087 5.261 2.123 8.788 2.033 3.522 4.222 5.582 6.54 6.23a2.25 2.25 0 0 0 2.151-.534l.887-.84a1.25 1.25 0 0 0 .154-1.639l-1.28-1.775a1.25 1.25 0 0 0-1.38-.464l-2.015.617c-1.17.348-2.232-.593-3.372-2.568C9 11.93 8.642 10.9 8.731 10.099c.047-.416.24-.8.546-1.086l1.494-1.393a1.25 1.25 0 0 0 .288-1.427l-.902-2.006a1.25 1.25 0 0 0-1.5-.684l-1.168.352Z",
|
||||||
"chat-help-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5Zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm0-8.75a2.75 2.75 0 0 1 2.75 2.75c0 1.01-.297 1.574-1.051 2.359l-.169.171c-.622.622-.78.886-.78 1.47a.75.75 0 0 1-1.5 0c0-1.01.297-1.574 1.051-2.359l.169-.171c.622-.622.78-.886.78-1.47a1.25 1.25 0 0 0-2.493-.128l-.007.128a.75.75 0 0 1-1.5 0A2.75 2.75 0 0 1 12 6.75Z",
|
"chat-help-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5Zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm0-8.75a2.75 2.75 0 0 1 2.75 2.75c0 1.01-.297 1.574-1.051 2.359l-.169.171c-.622.622-.78.886-.78 1.47a.75.75 0 0 1-1.5 0c0-1.01.297-1.574 1.051-2.359l.169-.171c.622-.622.78-.886.78-1.47a1.25 1.25 0 0 0-2.493-.128l-.007.128a.75.75 0 0 1-1.5 0A2.75 2.75 0 0 1 12 6.75Z",
|
||||||
"chat-multiple-outline": "M9.562 3a7.5 7.5 0 0 0-6.798 10.673l-.724 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.561 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.244-.121-.264.063c-.923.22-1.99.475-2.788.667l.69-2.708.07-.276-.13-.253a5.971 5.971 0 0 1-.664-2.747Zm11 10.5c-1.97 0-3.762-.759-5.1-2h.1c.718 0 1.415-.089 2.08-.257.865.482 1.86.757 2.92.757.96 0 1.866-.225 2.67-.625l.243-.121.264.063c.922.22 1.966.445 2.74.61-.175-.751-.414-1.756-.642-2.651l-.07-.276.13-.253a5.971 5.971 0 0 0 .665-2.747 5.995 5.995 0 0 0-2.747-5.042 8.44 8.44 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.509 2.1.671 2.803a1.244 1.244 0 0 1-1.467 1.5 132.62 132.62 0 0 1-2.913-.64 7.476 7.476 0 0 1-3.088.663Z",
|
"chat-multiple-outline": "M9.562 3a7.5 7.5 0 0 0-6.798 10.673l-.724 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.561 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.244-.121-.264.063c-.923.22-1.99.475-2.788.667l.69-2.708.07-.276-.13-.253a5.971 5.971 0 0 1-.664-2.747Zm11 10.5c-1.97 0-3.762-.759-5.1-2h.1c.718 0 1.415-.089 2.08-.257.865.482 1.86.757 2.92.757.96 0 1.866-.225 2.67-.625l.243-.121.264.063c.922.22 1.966.445 2.74.61-.175-.751-.414-1.756-.642-2.651l-.07-.276.13-.253a5.971 5.971 0 0 0 .665-2.747 5.995 5.995 0 0 0-2.747-5.042 8.44 8.44 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.509 2.1.671 2.803a1.244 1.244 0 0 1-1.467 1.5 132.62 132.62 0 0 1-2.913-.64 7.476 7.476 0 0 1-3.088.663Z",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user