import { format } from 'date-fns' import dayjs from 'dayjs' import { ArrowRight, Calendar } from 'lucide-react' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { Button, Calendar as CalendarPicker, Popover, PopoverContent, PopoverSeparator, PopoverTrigger, } from 'ui' import { ButtonProps } from 'ui/src/components/Button/Button' import { TimeSplitInput } from './TimeSplitInput' import type { DatePickerToFrom } from '@/components/interfaces/Settings/Logs/Logs.types' interface DatePickerProps { onChange?: (args: DatePickerToFrom) => void to?: string // ISO string from?: string // ISO string triggerButtonType?: ButtonProps['type'] triggerButtonClassName?: string triggerButtonTitle?: string triggerButtonSize?: 'tiny' | 'small' contentSide?: 'bottom' | 'top' minDate?: Date maxDate?: Date hideTime?: boolean hideClear?: boolean selectsRange?: boolean renderFooter?: (args: DatePickerToFrom) => ReactNode | void children?: ReactNode | ReactNode[] | null } const START_DATE_DEFAULT = new Date() const END_DATE_DEFAULT = new Date() const START_TIME_DEFAULT = { HH: '00', mm: '00', ss: '00' } const END_TIME_DEFAULT = { HH: '23', mm: '59', ss: '59' } const calculateDisabledDays = (minDate?: Date, maxDate?: Date) => { const rules: Array<{ before: Date } | { after: Date }> = [] if (minDate) { rules.push({ before: dayjs(minDate).startOf('day').toDate() }) } if (maxDate) { rules.push({ after: dayjs(maxDate).endOf('day').toDate() }) } if (rules.length === 0) return undefined if (rules.length === 1) return rules[0] return rules } export function DatePicker({ to, from, onChange, triggerButtonType = 'default', triggerButtonClassName = '', triggerButtonTitle, triggerButtonSize, contentSide = 'bottom', minDate, maxDate, hideTime = false, hideClear = false, selectsRange = true, renderFooter = () => null, children, }: DatePickerProps) { const [open, setOpen] = useState(false) const [appliedStartDate, setAppliedStartDate] = useState(null) const [appliedEndDate, setAppliedEndDate] = useState(null) const [startDate, setStartDate] = useState(START_DATE_DEFAULT) const [endDate, setEndDate] = useState(END_DATE_DEFAULT) const [startTime, setStartTime] = useState(START_TIME_DEFAULT) const [endTime, setEndTime] = useState(END_TIME_DEFAULT) const disabledDays = useMemo(() => calculateDisabledDays(minDate, maxDate), [minDate, maxDate]) const clampDateToRange = useCallback( (date: Date | null) => { if (!date) return date let nextDate = date if (minDate && dayjs(nextDate).isBefore(minDate, 'day')) { nextDate = dayjs(minDate).toDate() } if (maxDate && dayjs(nextDate).isAfter(maxDate, 'day')) { nextDate = dayjs(maxDate).toDate() } return nextDate }, [minDate, maxDate] ) useEffect(() => { if (!minDate && !maxDate) return setStartDate((prev) => { if (!prev) return prev const clamped = clampDateToRange(prev) return dayjs(clamped).isSame(prev) ? prev : clamped }) setEndDate((prev) => { if (!prev) return prev const clamped = clampDateToRange(prev) return dayjs(clamped).isSame(prev) ? prev : clamped }) setAppliedStartDate((prev) => { if (!prev) return prev const clamped = clampDateToRange(prev) return dayjs(clamped).isSame(prev) ? prev : clamped }) setAppliedEndDate((prev) => { if (!prev) return prev const clamped = clampDateToRange(prev) return dayjs(clamped).isSame(prev) ? prev : clamped }) }, [minDate, maxDate, clampDateToRange]) useEffect(() => { if (!from) { setAppliedStartDate(null) } else if (from !== appliedStartDate?.toISOString()) { const start = dayjs(from) const startDate = start.toDate() setAppliedStartDate(startDate) setStartDate(startDate) setStartTime({ HH: start.format('HH'), mm: start.format('mm'), ss: start.format('ss'), }) } if (!to) { setAppliedEndDate(null) } else if (to !== appliedEndDate?.toISOString()) { const end = dayjs(to) const endDate = end.toDate() setAppliedEndDate(endDate) setEndDate(endDate) setEndTime({ HH: end.format('HH'), mm: end.format('mm'), ss: end.format('ss'), }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [to, from]) function handleDatePickerChange(dates: Date | [from: Date | null, to: Date | null] | null) { if (!dates) { setStartDate(null) setEndDate(null) } else if (dates instanceof Date) { setStartDate(dates) setEndDate(dates) } else { const [from, to] = dates setStartDate(from) setEndDate(to) } } function handleSubmit() { setOpen(false) setAppliedStartDate(startDate) setAppliedEndDate(endDate) const payload = { from: dayjs(startDate) .second(Number(startTime.ss)) .minute(Number(startTime.mm)) .hour(Number(startTime.HH)) .toISOString(), to: dayjs(endDate || startDate) .second(Number(endTime.ss)) .minute(Number(endTime.mm)) .hour(Number(endTime.HH)) .toISOString(), } if (onChange) onChange(payload) } function handleClear() { setOpen(false) setStartDate(START_DATE_DEFAULT) setEndDate(END_DATE_DEFAULT) setStartTime(START_TIME_DEFAULT) setEndTime(END_TIME_DEFAULT) setAppliedStartDate(null) setAppliedEndDate(null) if (onChange) onChange({ from: null, to: null }) } return ( <> {hideTime ? null : ( <>
{!selectsRange ? null : ( <>
)}
)}
{selectsRange ? ( { handleDatePickerChange([range?.from ?? null, range?.to ?? null]) }} /> ) : ( { handleDatePickerChange(date ?? null) }} /> )}
{renderFooter({ from: startDate?.toISOString() || null, to: endDate?.toISOString() || null, })}
{!hideClear && ( )}
) }