import moment from 'moment' import * as React from 'react' import { type DateRange } from 'react-day-picker' import Button from '../Button/Button' import styles from './_datePicker.module.scss' export type RangeButtonType = | 'last7Days' | 'next7Days' | 'thisWeekToDate' | 'thisWeek' | 'lastWeek' | 'nextWeek' | 'lastMonth' | 'thisMonth' | 'thisMonthToDate' | 'nextMonth' | 'thisQuarter' | 'thisQuarterToDate' | 'lastQuarter' | 'nextQuarter' | 'thisYear' | 'thisYearToDate' | 'lastYear' | 'nextYear' export type SingleButtonType = 'today' | 'yesterday' | 'tomorrow' export const trimWhitespace = (str: string): string => { return str.trim() } /** * Checks if the given string is either empty or contains only whitespace characters. * * @param {string} str - The string to check. * @returns {boolean} - Returns true if the string is empty or contains only whitespace, otherwise false. */ export const isEmptyOrWhitespace = (str: string): boolean => { return !str || trimWhitespace(str).length === 0 } /** * Returns a date range object (`from` and `to` dates) based on the specified button type. * * @param {RangeButtonType} buttonType - The type of action button representing a specific date range. * @returns {DateRange} - An object containing `from` and `to` dates for the specified button type. */ export const getDateRangeForButtonType = ( buttonType: RangeButtonType, ): DateRange => { const today = moment().startOf('day') let fromDate: Date let toDate: Date switch (buttonType) { case 'last7Days': fromDate = today.clone().subtract(7, 'days').toDate() toDate = today.clone().subtract(1, 'day').toDate() break case 'next7Days': fromDate = today.clone().add(1, 'day').toDate() toDate = today.clone().add(7, 'days').toDate() break case 'thisWeekToDate': { const sunday = today.clone().startOf('week') fromDate = sunday.toDate() toDate = today.toDate() break } case 'thisWeek': { const sunday = today.clone().startOf('week') const saturday = today.clone().endOf('week') fromDate = sunday.toDate() toDate = saturday.toDate() break } case 'lastWeek': { const lastWeekSunday = today.clone().subtract(1, 'week').startOf('week') const lastWeekSaturday = today.clone().subtract(1, 'week').endOf('week') fromDate = lastWeekSunday.toDate() toDate = lastWeekSaturday.toDate() break } case 'nextWeek': { const nextWeekSunday = today.clone().add(1, 'week').startOf('week') const nextWeekSaturday = today.clone().add(1, 'week').endOf('week') fromDate = nextWeekSunday.toDate() toDate = nextWeekSaturday.toDate() break } case 'thisMonth': fromDate = today.clone().startOf('month').toDate() toDate = today.clone().endOf('month').toDate() break case 'lastMonth': fromDate = today.clone().subtract(1, 'month').startOf('month').toDate() toDate = today.clone().subtract(1, 'month').endOf('month').toDate() break case 'thisMonthToDate': fromDate = today.clone().startOf('month').toDate() toDate = today.toDate() break case 'nextMonth': fromDate = today.clone().add(1, 'month').startOf('month').toDate() toDate = today.clone().add(1, 'month').endOf('month').toDate() break case 'thisQuarter': fromDate = today.clone().startOf('quarter').toDate() toDate = today.clone().endOf('quarter').toDate() break case 'thisQuarterToDate': fromDate = today.clone().startOf('quarter').toDate() toDate = today.toDate() break case 'lastQuarter': fromDate = today .clone() .subtract(1, 'quarter') .startOf('quarter') .toDate() toDate = today.clone().subtract(1, 'quarter').endOf('quarter').toDate() break case 'nextQuarter': fromDate = today.clone().add(1, 'quarter').startOf('quarter').toDate() toDate = today .clone() .add(1, 'quarter') .endOf('quarter') .startOf('day') .toDate() break case 'thisYearToDate': fromDate = today.clone().startOf('year').toDate() toDate = today.toDate() break case 'thisYear': fromDate = today.clone().startOf('year').toDate() toDate = today.clone().endOf('year').startOf('day').toDate() break case 'lastYear': fromDate = today.clone().subtract(1, 'year').startOf('year').toDate() toDate = today .clone() .subtract(1, 'year') .endOf('year') .startOf('day') .toDate() break case 'nextYear': fromDate = today.clone().add(1, 'year').startOf('year').toDate() toDate = today .clone() .add(1, 'year') .endOf('year') .startOf('day') .toDate() break default: fromDate = today.toDate() toDate = today.toDate() } return { from: fromDate, to: toDate, } } /** * Validates whether a given string is a valid date in the format MM/DD/YYYY. * * @param {string} dateString - The date string to validate. * @returns {boolean} - Returns true if the string is empty, contains only whitespace, or is a valid date in the specified format. Otherwise, returns false. */ export const isValidDateString = (dateString: string): boolean => { if (isEmptyOrWhitespace(dateString)) { return true } const trimmedDate = trimWhitespace(dateString) return moment(trimmedDate, 'MM/DD/YYYY', true).isValid() } /** * Parses a date range string in the format "MM/DD/YYYY - MM/DD/YYYY" into a DateRange object. * * @param {string} dateRangeString - The date range string to parse. * @returns {DateRange | null} - Returns a DateRange object with `from` and `to` dates if valid, otherwise null. */ export const parseDateRangeString = ( dateRangeString: string, ): DateRange | null => { if (isEmptyOrWhitespace(dateRangeString)) { return null } const trimmedRange = trimWhitespace(dateRangeString) const dates = trimmedRange .split(/\s*-\s*/) .map((part) => trimWhitespace(part)) if (dates.length !== 2) { return null } const fromDate = moment(dates[0], 'MM/DD/YYYY', true) const toDate = moment(dates[1], 'MM/DD/YYYY', true) if (!fromDate.isValid() || !toDate.isValid()) { return null } if (fromDate.isAfter(toDate)) { return { from: toDate.toDate(), to: fromDate.toDate(), } } return { from: fromDate.toDate(), to: toDate.toDate(), } } export const isDateRangeValidWithRestrictions = ( dateRange: DateRange, dateRestrictions?: { beforeDate?: Date afterDate?: Date }, ): boolean => { if (!dateRange.from || !dateRange.to) { return false } if ( !dateRestrictions || (!dateRestrictions.beforeDate && !dateRestrictions.afterDate) ) { return true } const fromDate = moment(dateRange.from).startOf('day') const toDate = moment(dateRange.to).startOf('day') if (dateRestrictions.beforeDate) { const beforeDate = moment(dateRestrictions.beforeDate).startOf('day') if (fromDate.isBefore(beforeDate) || toDate.isBefore(beforeDate)) { return false } } if (dateRestrictions.afterDate) { const afterDate = moment(dateRestrictions.afterDate).startOf('day') if (fromDate.isAfter(afterDate) || toDate.isAfter(afterDate)) { return false } } return true } /** * Adjusts a date range to comply with specified date restrictions. * If the date range falls outside the restrictions, it will be adjusted to the nearest valid dates. * * @param {DateRange} dateRange - The date range to be adjusted, containing `from` and `to` dates. * @param {Object} [dateRestrictions] - Optional restrictions for the date range. * @param {Date} [dateRestrictions.beforeDate] - The date before which the range cannot start. * @param {Date} [dateRestrictions.afterDate] - The date after which the range cannot end. * @returns {DateRange} - The adjusted date range that complies with the restrictions. */ export const adjustDateRangeWithRestrictions = ( dateRange: DateRange, dateRestrictions?: { beforeDate?: Date afterDate?: Date }, ): DateRange => { // Case 1: No restrictions or no dates if (!dateRange.from || !dateRange.to) { return dateRange } if ( !dateRestrictions || (!dateRestrictions.beforeDate && !dateRestrictions.afterDate) ) { return dateRange } const fromDate = moment(dateRange.from).startOf('day') const toDate = moment(dateRange.to).startOf('day') let adjustedFromDate = fromDate.clone() let adjustedToDate = toDate.clone() // Case 2: Check if entire range is in restricted area if (dateRestrictions.beforeDate) { const beforeDate = moment(dateRestrictions.beforeDate).startOf('day') if (fromDate.isBefore(beforeDate) && toDate.isBefore(beforeDate)) { return { from: undefined, to: undefined } } } if (dateRestrictions.afterDate) { const afterDate = moment(dateRestrictions.afterDate).startOf('day') if (fromDate.isAfter(afterDate) && toDate.isAfter(afterDate)) { return { from: undefined, to: undefined } } } // Case 3 & 4: Adjust dates to stay within restrictions if (dateRestrictions.beforeDate) { const beforeDate = moment(dateRestrictions.beforeDate).startOf('day') if (fromDate.isBefore(beforeDate)) { adjustedFromDate = beforeDate.clone() } if (toDate.isBefore(beforeDate)) { adjustedToDate = beforeDate.clone() } } if (dateRestrictions.afterDate) { const afterDate = moment(dateRestrictions.afterDate).startOf('day') if (fromDate.isAfter(afterDate)) { adjustedFromDate = afterDate.clone() } if (toDate.isAfter(afterDate)) { adjustedToDate = afterDate.clone() } } // If after adjustments, the range is invalid (from after to), return undefined if (adjustedFromDate.isAfter(adjustedToDate)) { return { from: undefined, to: undefined } } // Return the adjusted range return { from: adjustedFromDate.toDate(), to: adjustedToDate.toDate(), } } /** * Returns a date based on the specified single date button type. * * @param {SingleButtonType} buttonType - The type of action button representing a specific date * @returns {Date} - The date for the specified button type */ export const getDateForButtonType = (buttonType: SingleButtonType): Date => { switch (buttonType) { case 'today': return moment().startOf('day').toDate() case 'yesterday': return moment().subtract(1, 'day').startOf('day').toDate() case 'tomorrow': return moment().add(1, 'day').startOf('day').toDate() } } /** * Checks if a single date is valid considering restrictions like not showing past dates * and specific before/after date restrictions. * * @param {Date} date - The date to validate. * @param {boolean} showPastDates - Whether past dates are allowed. * @param {object} [dateRestrictions] - Optional restrictions for the date. * @param {Date} [dateRestrictions.beforeDate] - The date before which the input date is invalid. * @param {Date} [dateRestrictions.afterDate] - The date after which the input date is invalid. * @returns {boolean} - True if the date is valid within the restrictions, false otherwise. */ export const isSingleDateValidWithRestrictions = ( date: Date, dateRestrictions?: { beforeDate?: Date afterDate?: Date }, ): boolean => { const day = moment(date).startOf('day') if ( dateRestrictions?.beforeDate && day.isBefore(moment(dateRestrictions.beforeDate).startOf('day')) ) { return false } if ( dateRestrictions?.afterDate && day.isAfter(moment(dateRestrictions.afterDate).startOf('day')) ) { return false } return true } export type ButtonConfig = { show: boolean onClick: () => void label: string groupName: string } // Utility: Returns true if any date in the range is valid under the given restrictions; otherwise, returns false. Used to determine button visibility. function isAnyDateInRangeValid( dateRange: DateRange, dateRestrictions?: { beforeDate?: Date; afterDate?: Date }, ): boolean { if (!dateRange.from || !dateRange.to) return false const from = moment(dateRange.from).startOf('day') const to = moment(dateRange.to).startOf('day') const days = to.diff(from, 'days') for (let i = 0; i <= days; i++) { const d = from.clone().add(i, 'days').toDate() if (isSingleDateValidWithRestrictions(d, dateRestrictions)) { return true } } return false } // Utility: Returns true if the date range contains any restricted dates; otherwise, returns false. Used to hide buttons when their ranges contain restricted dates. function doesDateRangeContainRestrictedDates( dateRange: DateRange, dateRestrictions?: { beforeDate?: Date; afterDate?: Date }, ): boolean { if (!dateRange.from || !dateRange.to || !dateRestrictions) return false const from = moment(dateRange.from).startOf('day') const to = moment(dateRange.to).startOf('day') const days = to.diff(from, 'days') for (let i = 0; i <= days; i++) { const d = from.clone().add(i, 'days').toDate() if (!isSingleDateValidWithRestrictions(d, dateRestrictions)) { return true } } return false } export const getSingleDateButtonConfigs = ({ showTodaySelector, showYesterdaySelector, showTomorrowSelector, selectToday, selectYesterday, selectTomorrow, dateRestrictions, }: { showTodaySelector?: boolean showYesterdaySelector?: boolean showTomorrowSelector?: boolean selectToday: () => void selectYesterday: () => void selectTomorrow: () => void dateRestrictions?: { beforeDate?: Date; afterDate?: Date } }): ButtonConfig[] => { return [ { show: (showTodaySelector ?? true) && isSingleDateValidWithRestrictions( getDateForButtonType('today'), dateRestrictions, ), onClick: selectToday, label: 'today', groupName: 'today', }, { show: (showYesterdaySelector ?? true) && isSingleDateValidWithRestrictions( getDateForButtonType('yesterday'), dateRestrictions, ), onClick: selectYesterday, label: 'yesterday', groupName: 'yesterday', }, { show: (showTomorrowSelector ?? true) && isSingleDateValidWithRestrictions( getDateForButtonType('tomorrow'), dateRestrictions, ), onClick: selectTomorrow, label: 'tomorrow', groupName: 'tomorrow', }, ] } export const getRangeDateButtonConfigs = ({ last7Days, next7Days, thisWeekToDate, thisWeek, lastWeek, nextWeek, thisMonth, lastMonth, thisMonthToDate, nextMonth, thisQuarter, thisQuarterToDate, lastQuarter, nextQuarter, thisYear, thisYearToDate, lastYear, nextYear, selectLast7Days, selectNext7Days, selectThisWeekToDate, selectThisWeek, selectLastWeek, selectNextWeek, selectThisMonth, selectLastMonth, selectThisMonthToDate, selectNextMonth, selectThisQuarter, selectThisQuarterToDate, selectLastQuarter, selectNextQuarter, selectThisYear, selectThisYearToDate, selectLastYear, selectNextYear, dateRestrictions, }: { last7Days?: boolean next7Days?: boolean thisWeekToDate?: boolean thisWeek?: boolean lastWeek?: boolean nextWeek?: boolean thisMonth?: boolean lastMonth?: boolean thisMonthToDate?: boolean nextMonth?: boolean thisQuarter?: boolean thisQuarterToDate?: boolean lastQuarter?: boolean nextQuarter?: boolean thisYear?: boolean thisYearToDate?: boolean lastYear?: boolean nextYear?: boolean selectLast7Days: () => void selectNext7Days: () => void selectThisWeekToDate: () => void selectThisWeek: () => void selectLastWeek: () => void selectNextWeek: () => void selectThisMonth: () => void selectLastMonth: () => void selectThisMonthToDate: () => void selectNextMonth: () => void selectThisQuarter: () => void selectThisQuarterToDate: () => void selectLastQuarter: () => void selectNextQuarter: () => void selectThisYear: () => void selectThisYearToDate: () => void selectLastYear: () => void selectNextYear: () => void dateRestrictions?: { beforeDate?: Date; afterDate?: Date } }): ButtonConfig[] => { return [ { show: (last7Days ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('last7Days'), dateRestrictions, ), onClick: selectLast7Days, label: 'last7Days', groupName: 'days', }, { show: (next7Days ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('next7Days'), dateRestrictions, ), onClick: selectNext7Days, label: 'next7Days', groupName: 'days', }, { show: (thisWeekToDate ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('thisWeekToDate'), dateRestrictions, ), onClick: selectThisWeekToDate, label: 'thisWeekToDate', groupName: 'weeks', }, { show: (thisWeek ?? true) && !doesDateRangeContainRestrictedDates( getDateRangeForButtonType('thisWeek'), dateRestrictions, ), onClick: selectThisWeek, label: 'thisWeek', groupName: 'weeks', }, { show: (lastWeek ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('lastWeek'), dateRestrictions, ), onClick: selectLastWeek, label: 'lastWeek', groupName: 'weeks', }, { show: (nextWeek ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('nextWeek'), dateRestrictions, ), onClick: selectNextWeek, label: 'nextWeek', groupName: 'weeks', }, { show: (thisMonthToDate ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('thisMonthToDate'), dateRestrictions, ), onClick: selectThisMonthToDate, label: 'thisMonthToDate', groupName: 'months', }, { show: (thisMonth ?? true) && !doesDateRangeContainRestrictedDates( getDateRangeForButtonType('thisMonth'), dateRestrictions, ), onClick: selectThisMonth, label: 'thisMonth', groupName: 'months', }, { show: (lastMonth ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('lastMonth'), dateRestrictions, ), onClick: selectLastMonth, label: 'lastMonth', groupName: 'months', }, { show: (nextMonth ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('nextMonth'), dateRestrictions, ), onClick: selectNextMonth, label: 'nextMonth', groupName: 'months', }, { show: (thisQuarterToDate ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('thisQuarterToDate'), dateRestrictions, ), onClick: selectThisQuarterToDate, label: 'thisQuarterToDate', groupName: 'quarters', }, { show: (thisQuarter ?? true) && !doesDateRangeContainRestrictedDates( getDateRangeForButtonType('thisQuarter'), dateRestrictions, ), onClick: selectThisQuarter, label: 'thisQuarter', groupName: 'quarters', }, { show: (lastQuarter ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('lastQuarter'), dateRestrictions, ), onClick: selectLastQuarter, label: 'lastQuarter', groupName: 'quarters', }, { show: (nextQuarter ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('nextQuarter'), dateRestrictions, ), onClick: selectNextQuarter, label: 'nextQuarter', groupName: 'quarters', }, { show: (thisYearToDate ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('thisYearToDate'), dateRestrictions, ), onClick: selectThisYearToDate, label: 'thisYearToDate', groupName: 'years', }, { show: (thisYear ?? true) && !doesDateRangeContainRestrictedDates( getDateRangeForButtonType('thisYear'), dateRestrictions, ), onClick: selectThisYear, label: 'thisYear', groupName: 'years', }, { show: (lastYear ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('lastYear'), dateRestrictions, ), onClick: selectLastYear, label: 'lastYear', groupName: 'years', }, { show: (nextYear ?? true) && isAnyDateInRangeValid( getDateRangeForButtonType('nextYear'), dateRestrictions, ), onClick: selectNextYear, label: 'nextYear', groupName: 'years', }, ] } export type MainButtonType = 'day' | 'week' | 'month' | 'quarter' | 'year' export const renderButtons = ( buttonConfigs: ButtonConfig[], selectedBtnType: SingleButtonType | RangeButtonType | null, c: (key: string) => string, type: 'single' | 'range', selectedGroup: string | null, onGroupSelect: (group: string | null) => void, reset: boolean, handleReset: () => void, ) => { const mainButtons: { type: MainButtonType; group: string }[] = [ { type: 'day', group: 'days' }, { type: 'week', group: 'weeks' }, { type: 'month', group: 'months' }, { type: 'quarter', group: 'quarters' }, { type: 'year', group: 'years' }, ] const renderBackButton = () => ( ) const hasAvailableButtonsInGroup = (group: string): boolean => { return buttonConfigs.some( ({ show, groupName }) => show && groupName === group, ) } const renderMainButtons = () => ( <> {mainButtons .filter(({ group }) => hasAvailableButtonsInGroup(group)) .map(({ type, group }) => ( ))} {reset ? renderResetButton() : null} ) const renderGroupButtons = () => ( <> {buttonConfigs .filter(({ show, groupName }) => show && groupName === selectedGroup) .map(({ onClick, label }) => ( ))} ) const renderSingleDateButtons = () => ( <> {buttonConfigs .filter(({ show }) => show) .map(({ onClick, label }) => ( ))} {reset ? renderResetButton() : null} ) return (
{type === 'range' && selectedGroup ? (
{renderBackButton()}
) : ( <> )}
{type === 'single' ? renderSingleDateButtons() : selectedGroup ? renderGroupButtons() : renderMainButtons()}
) } interface RenderActionButtonsParams { type: 'single' | 'range' actionButtons: any selectedBtnType: RangeButtonType | SingleButtonType | null c: (key: string) => string selectedGroup: string | null setSelectedGroup: (group: string | null) => void handlers: any reset: boolean handleReset: () => void dateRestrictions?: { beforeDate?: Date; afterDate?: Date } } export function renderActionButtons({ type, actionButtons, selectedBtnType, c, selectedGroup, setSelectedGroup, handlers, reset, handleReset, dateRestrictions, }: RenderActionButtonsParams) { if (type === 'single') { return renderButtons( getSingleDateButtonConfigs({ showTodaySelector: actionButtons.today, showYesterdaySelector: actionButtons.yesterday, showTomorrowSelector: actionButtons.tomorrow, selectToday: handlers.selectToday!, selectYesterday: handlers.selectYesterday!, selectTomorrow: handlers.selectTomorrow!, dateRestrictions, }), selectedBtnType, c, type, selectedGroup, setSelectedGroup, reset, handleReset, ) } else { return renderButtons( getRangeDateButtonConfigs({ last7Days: actionButtons.last7Days, next7Days: actionButtons.next7Days, thisWeekToDate: actionButtons.thisWeekToDate, thisWeek: actionButtons.thisWeek, lastWeek: actionButtons.lastWeek, nextWeek: actionButtons.nextWeek, thisMonth: actionButtons.thisMonth, lastMonth: actionButtons.lastMonth, thisMonthToDate: actionButtons.thisMonthToDate, nextMonth: actionButtons.nextMonth, thisQuarter: actionButtons.thisQuarter, thisQuarterToDate: actionButtons.thisQuarterToDate, lastQuarter: actionButtons.lastQuarter, nextQuarter: actionButtons.nextQuarter, thisYear: actionButtons.thisYear, thisYearToDate: actionButtons.thisYearToDate, lastYear: actionButtons.lastYear, nextYear: actionButtons.nextYear, selectLast7Days: handlers.selectLast7Days!, selectNext7Days: handlers.selectNext7Days!, selectThisWeekToDate: handlers.selectThisWeekToDate!, selectThisWeek: handlers.selectThisWeek!, selectLastWeek: handlers.selectLastWeek!, selectNextWeek: handlers.selectNextWeek!, selectThisMonth: handlers.selectThisMonth!, selectLastMonth: handlers.selectLastMonth!, selectThisMonthToDate: handlers.selectThisMonthToDate!, selectNextMonth: handlers.selectNextMonth!, selectThisQuarter: handlers.selectThisQuarter!, selectThisQuarterToDate: handlers.selectThisQuarterToDate!, selectLastQuarter: handlers.selectLastQuarter!, selectNextQuarter: handlers.selectNextQuarter!, selectThisYear: handlers.selectThisYear!, selectThisYearToDate: handlers.selectThisYearToDate!, selectLastYear: handlers.selectLastYear!, selectNextYear: handlers.selectNextYear!, dateRestrictions, }), selectedBtnType, c, type, selectedGroup, setSelectedGroup, reset, handleReset, ) } } // To check if the default date passed is valid or not with the restrictions export const getSelectedDate = ( selectedDate: Date | undefined, dateRestrictions?: { beforeDate?: Date afterDate?: Date }, ) => { if (dateRestrictions && selectedDate) { return isSingleDateValidWithRestrictions(selectedDate, dateRestrictions) ? selectedDate : undefined } return selectedDate } // To check if the default date range passed is valid or not with the restrictions export const getSelectedDateRange = ( selectedDateRange: DateRange | undefined, dateRestrictions?: { beforeDate?: Date afterDate?: Date }, ) => { if (dateRestrictions && selectedDateRange) { return adjustDateRangeWithRestrictions(selectedDateRange, dateRestrictions) } return selectedDateRange }