| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- 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<boolean>(false)
- const [appliedStartDate, setAppliedStartDate] = useState<null | Date>(null)
- const [appliedEndDate, setAppliedEndDate] = useState<null | Date>(null)
- const [startDate, setStartDate] = useState<Date | null>(START_DATE_DEFAULT)
- const [endDate, setEndDate] = useState<Date | null>(END_DATE_DEFAULT)
- const [startTime, setStartTime] = useState<any>(START_TIME_DEFAULT)
- const [endTime, setEndTime] = useState<any>(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 (
- <Popover modal open={open} onOpenChange={setOpen}>
- <PopoverTrigger asChild>
- <Button
- title={triggerButtonTitle}
- type={triggerButtonType}
- icon={<Calendar />}
- size={triggerButtonSize}
- className={triggerButtonClassName}
- >
- {children !== undefined ? (
- children
- ) : (
- <>
- {/* Custom */}
- {selectsRange &&
- appliedStartDate &&
- appliedEndDate &&
- appliedStartDate !== appliedEndDate ? (
- <>
- {format(new Date(appliedStartDate), 'dd MMM')} -{' '}
- {format(new Date(appliedEndDate), 'dd MMM')}
- </>
- ) : appliedStartDate || appliedEndDate ? (
- format(new Date((appliedStartDate || appliedEndDate)!), 'dd MMM')
- ) : (
- 'Custom'
- )}
- </>
- )}
- </Button>
- </PopoverTrigger>
- <PopoverContent align="center" side={contentSide} className="p-0">
- <>
- {hideTime ? null : (
- <>
- <div className="flex items-stretch justify-between p-2">
- {!selectsRange ? null : (
- <>
- <div className="flex grow flex-col gap-1">
- <TimeSplitInput
- type="start"
- startTime={startTime}
- endTime={endTime}
- time={startTime}
- setTime={setStartTime}
- setStartTime={setStartTime}
- setEndTime={setEndTime}
- startDate={startDate}
- endDate={endDate}
- />
- </div>
- <div className="flex items-center justify-center w-12 text-foreground-lighter">
- <ArrowRight strokeWidth={1.5} size={14} />
- </div>
- </>
- )}
- <div className="flex grow flex-col gap-1">
- <TimeSplitInput
- type="end"
- startTime={startTime}
- endTime={endTime}
- time={endTime}
- setTime={setEndTime}
- setStartTime={setStartTime}
- setEndTime={setEndTime}
- startDate={startDate}
- endDate={endDate}
- />
- </div>
- </div>
- </>
- )}
- <div className="p-2">
- {selectsRange ? (
- <CalendarPicker
- mode="range"
- disabled={disabledDays}
- selected={{ from: startDate ?? undefined, to: endDate ?? undefined }}
- onSelect={(range) => {
- handleDatePickerChange([range?.from ?? null, range?.to ?? null])
- }}
- />
- ) : (
- <CalendarPicker
- mode="single"
- disabled={disabledDays}
- selected={endDate ?? undefined}
- onSelect={(date) => {
- handleDatePickerChange(date ?? null)
- }}
- />
- )}
- </div>
- {renderFooter({
- from: startDate?.toISOString() || null,
- to: endDate?.toISOString() || null,
- })}
- <PopoverSeparator />
- <div className="flex items-center justify-end gap-2 py-2 px-3 pb-4">
- {!hideClear && (
- <Button type="default" onClick={() => handleClear()}>
- Clear
- </Button>
- )}
- <Button onClick={() => handleSubmit()}>Apply</Button>
- </div>
- </>
- </PopoverContent>
- </Popover>
- )
- }
|