173 lines
5 KiB
TypeScript
173 lines
5 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
import type { DateRange } from "react-day-picker"
|
|
import { IconEraser } from "@tabler/icons-react"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import { Calendar } from "@/components/ui/calendar"
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
|
import { IconCalendar } from "@tabler/icons-react"
|
|
|
|
export type DateRangeValue = {
|
|
from: string | null
|
|
to: string | null
|
|
}
|
|
|
|
type DateRangeButtonProps = {
|
|
from: string | null
|
|
to: string | null
|
|
onChange: (next: DateRangeValue) => void
|
|
className?: string
|
|
clearLabel?: string
|
|
align?: "left" | "center"
|
|
}
|
|
|
|
function strToDate(value?: string | null): Date | undefined {
|
|
if (!value) return undefined
|
|
const [y, m, d] = value.split("-").map(Number)
|
|
if (!y || !m || !d) return undefined
|
|
return new Date(y, m - 1, d)
|
|
}
|
|
|
|
function dateToStr(value?: Date): string | null {
|
|
if (!value) return null
|
|
const y = value.getFullYear()
|
|
const m = String(value.getMonth() + 1).padStart(2, "0")
|
|
const d = String(value.getDate()).padStart(2, "0")
|
|
return `${y}-${m}-${d}`
|
|
}
|
|
|
|
function formatPtBR(value?: Date): string {
|
|
return value ? value.toLocaleDateString("pt-BR") : ""
|
|
}
|
|
|
|
export function DateRangeButton({
|
|
from,
|
|
to,
|
|
onChange,
|
|
className,
|
|
clearLabel = "Limpar período",
|
|
align = "left",
|
|
}: DateRangeButtonProps) {
|
|
const [open, setOpen] = useState(false)
|
|
const range: DateRange | undefined = useMemo(
|
|
() => ({
|
|
from: strToDate(from),
|
|
to: strToDate(to),
|
|
}),
|
|
[from, to],
|
|
)
|
|
const [draftRange, setDraftRange] = useState<DateRange | undefined>(range)
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setDraftRange(range)
|
|
}
|
|
}, [open, range])
|
|
|
|
const displayRange = open ? draftRange ?? range : range
|
|
|
|
const label = (() => {
|
|
if (displayRange?.from && displayRange?.to) {
|
|
return `${formatPtBR(displayRange.from)} - ${formatPtBR(displayRange.to)}`
|
|
}
|
|
if (displayRange?.from && !displayRange?.to) {
|
|
return `${formatPtBR(displayRange.from)} - …`
|
|
}
|
|
return "Período"
|
|
})()
|
|
|
|
const handleSelect = (next?: DateRange) => {
|
|
if (!next?.from && !next?.to) {
|
|
setDraftRange(undefined)
|
|
return
|
|
}
|
|
|
|
if (!draftRange?.from || draftRange?.to) {
|
|
setDraftRange(next?.from ? { from: next.from, to: undefined } : undefined)
|
|
return
|
|
}
|
|
|
|
if (!draftRange.from) {
|
|
setDraftRange(undefined)
|
|
return
|
|
}
|
|
|
|
const start = draftRange.from
|
|
const rawSecond =
|
|
next?.to && next.to.getTime() !== start.getTime()
|
|
? next.to
|
|
: next?.from ?? next?.to ?? start
|
|
|
|
if (!rawSecond) {
|
|
setDraftRange({ from: start, to: undefined })
|
|
return
|
|
}
|
|
|
|
const fromDate = rawSecond < start ? rawSecond : start
|
|
const toDate = rawSecond < start ? start : rawSecond
|
|
const finalRange: DateRange = { from: fromDate, to: toDate }
|
|
setDraftRange(finalRange)
|
|
|
|
const nextFrom = dateToStr(finalRange.from)
|
|
const nextTo = dateToStr(finalRange.to) ?? nextFrom
|
|
onChange({ from: nextFrom, to: nextTo })
|
|
setOpen(false)
|
|
}
|
|
|
|
const clearEnabled = Boolean(from || to || draftRange?.from || draftRange?.to)
|
|
|
|
return (
|
|
<Popover
|
|
open={open}
|
|
onOpenChange={(next) => {
|
|
setOpen(next)
|
|
if (!next) {
|
|
setDraftRange(range)
|
|
} else {
|
|
setDraftRange(range)
|
|
}
|
|
}}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
className={`flex h-10 w-full items-center gap-2 rounded-2xl border-slate-300 bg-white/95 text-sm font-semibold text-neutral-700 ${align === "center" ? "justify-center text-center" : "justify-start text-left"} ${className ?? ""}`}
|
|
>
|
|
<IconCalendar className="size-4 text-neutral-500" />
|
|
<span className="truncate">{label}</span>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto overflow-hidden p-0" align="end">
|
|
<Calendar
|
|
className="w-full"
|
|
mode="range"
|
|
defaultMonth={draftRange?.from ?? range?.from}
|
|
selected={draftRange ?? range}
|
|
onSelect={handleSelect}
|
|
fixedWeeks
|
|
showOutsideDays
|
|
/>
|
|
<div className="flex items-center justify-center gap-3 border-t border-border/70 bg-slate-50/80 px-3 py-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
onChange({ from: null, to: null })
|
|
setDraftRange(undefined)
|
|
setOpen(false)
|
|
}}
|
|
className="inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm font-medium text-neutral-600 transition hover:text-neutral-900 disabled:opacity-40"
|
|
disabled={!clearEnabled}
|
|
>
|
|
<IconEraser className="size-4" />
|
|
<span>{clearLabel}</span>
|
|
</button>
|
|
{!draftRange?.to && draftRange?.from ? (
|
|
<span className="text-xs text-neutral-500">Selecione a data final</span>
|
|
) : null}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|