Files
ember-market-frontend/components/ui/date-picker.tsx
NotII 1b51f29c24 Add flexible date pickers and export options to stock dashboard
Introduces a reusable date picker component with support for single date, date range, and month selection. Updates the stock management page to allow exporting reports by daily, weekly, monthly, or custom date ranges using the new pickers. Refactors promotion form to use the new date picker for start and end dates. Adds more business quotes to the quotes config.
2025-07-30 00:38:25 +02:00

417 lines
12 KiB
TypeScript

"use client"
import * as React from "react"
import { format, addDays, startOfDay, endOfDay, isSameDay, isWithinInterval, getMonth, getYear, setMonth, setYear } from "date-fns"
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react"
import { DateRange } from "react-day-picker"
import { cn } from "@/lib/utils/styles"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
interface DatePickerProps {
date?: Date
onDateChange?: (date: Date | undefined) => void
placeholder?: string
className?: string
}
interface DateRangePickerProps {
dateRange?: DateRange
onDateRangeChange?: (range: DateRange | undefined) => void
placeholder?: string
className?: string
showPresets?: boolean
disabled?: boolean
}
interface MonthPickerProps {
selectedMonth?: Date
onMonthChange?: (date: Date | undefined) => void
placeholder?: string
className?: string
disabled?: boolean
}
// Single Date Picker
export function DatePicker({ date, onDateChange, placeholder = "Pick a date", className }: DatePickerProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : placeholder}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={onDateChange}
initialFocus
/>
</PopoverContent>
</Popover>
)
}
// Month Picker Component
export function MonthPicker({ selectedMonth, onMonthChange, placeholder = "Pick a month", className, disabled = false }: MonthPickerProps) {
const [isOpen, setIsOpen] = React.useState(false)
const [selectedYear, setSelectedYear] = React.useState(selectedMonth ? getYear(selectedMonth) : new Date().getFullYear())
const [selectedMonthIndex, setSelectedMonthIndex] = React.useState(selectedMonth ? getMonth(selectedMonth) : new Date().getMonth())
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
]
const years = Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - 5 + i)
React.useEffect(() => {
if (selectedMonth) {
setSelectedYear(getYear(selectedMonth))
setSelectedMonthIndex(getMonth(selectedMonth))
}
}, [selectedMonth])
const handleMonthSelect = (monthIndex: number) => {
const newDate = new Date(selectedYear, monthIndex, 1)
onMonthChange?.(newDate)
setIsOpen(false)
}
const handleYearChange = (year: string) => {
const newYear = parseInt(year)
setSelectedYear(newYear)
if (selectedMonth) {
const newDate = new Date(newYear, selectedMonthIndex, 1)
onMonthChange?.(newDate)
}
}
const formatSelectedMonth = (date?: Date) => {
if (!date) return placeholder
return format(date, "MMMM yyyy")
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!selectedMonth && "text-muted-foreground",
className
)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{formatSelectedMonth(selectedMonth)}
{selectedMonth && (
<Button
variant="ghost"
size="sm"
className="ml-auto h-6 w-6 p-0 hover:bg-transparent"
onClick={(e) => {
e.stopPropagation()
onMonthChange?.(undefined)
}}
>
<X className="h-3 w-3" />
</Button>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-4" align="start">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-medium">Select Month</h4>
<Select value={selectedYear.toString()} onValueChange={handleYearChange}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{years.map((year) => (
<SelectItem key={year} value={year.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-3 gap-2">
{months.map((month, index) => (
<Button
key={month}
variant={selectedMonthIndex === index ? "default" : "outline"}
size="sm"
onClick={() => handleMonthSelect(index)}
className="text-xs"
>
{month}
</Button>
))}
</div>
</div>
</PopoverContent>
</Popover>
)
}
// Date Range Picker with Presets
export function DateRangePicker({
dateRange,
onDateRangeChange,
placeholder = "Pick a date range",
className,
showPresets = true,
disabled = false
}: DateRangePickerProps) {
const [isOpen, setIsOpen] = React.useState(false)
const presets = [
{
label: "Today",
value: {
from: startOfDay(new Date()),
to: endOfDay(new Date())
}
},
{
label: "Yesterday",
value: {
from: startOfDay(addDays(new Date(), -1)),
to: endOfDay(addDays(new Date(), -1))
}
},
{
label: "Last 7 days",
value: {
from: startOfDay(addDays(new Date(), -6)),
to: endOfDay(new Date())
}
},
{
label: "Last 30 days",
value: {
from: startOfDay(addDays(new Date(), -29)),
to: endOfDay(new Date())
}
},
{
label: "This month",
value: {
from: startOfDay(new Date(new Date().getFullYear(), new Date().getMonth(), 1)),
to: endOfDay(new Date())
}
},
{
label: "Last month",
value: {
from: startOfDay(new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1)),
to: endOfDay(new Date(new Date().getFullYear(), new Date().getMonth(), 0))
}
}
]
const handlePresetClick = (preset: typeof presets[0]) => {
onDateRangeChange?.(preset.value)
setIsOpen(false)
}
const handleClear = () => {
onDateRangeChange?.(undefined)
setIsOpen(false)
}
const formatDateRange = (range: DateRange | undefined) => {
if (!range?.from) return placeholder
if (!range.to) {
return format(range.from, "PPP")
}
if (isSameDay(range.from, range.to)) {
return format(range.from, "PPP")
}
return `${format(range.from, "MMM dd")} - ${format(range.to, "MMM dd, yyyy")}`
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!dateRange?.from && "text-muted-foreground",
className
)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{formatDateRange(dateRange)}
{dateRange?.from && (
<Button
variant="ghost"
size="sm"
className="ml-auto h-6 w-6 p-0 hover:bg-transparent"
onClick={(e) => {
e.stopPropagation()
handleClear()
}}
>
<X className="h-3 w-3" />
</Button>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<div className="p-3">
{showPresets && (
<div className="space-y-2 mb-4">
<Label className="text-sm font-medium">Quick Select</Label>
<div className="grid grid-cols-2 gap-2">
{presets.map((preset) => (
<Button
key={preset.label}
variant="outline"
size="sm"
className="text-xs h-8"
onClick={() => handlePresetClick(preset)}
>
{preset.label}
</Button>
))}
</div>
</div>
)}
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange?.from}
selected={dateRange}
onSelect={onDateRangeChange}
numberOfMonths={2}
className="rounded-md border"
/>
</div>
</PopoverContent>
</Popover>
)
}
// Custom Date Range Input Component
export function CustomDateRangeInput({
dateRange,
onDateRangeChange,
className
}: DateRangePickerProps) {
const [fromDate, setFromDate] = React.useState<string>("")
const [toDate, setToDate] = React.useState<string>("")
React.useEffect(() => {
if (dateRange?.from) {
setFromDate(format(dateRange.from, "yyyy-MM-dd"))
}
if (dateRange?.to) {
setToDate(format(dateRange.to, "yyyy-MM-dd"))
}
}, [dateRange])
const handleFromDateChange = (value: string) => {
setFromDate(value)
const from = value ? new Date(value) : undefined
const to = toDate ? new Date(toDate) : dateRange?.to
if (from && to && from > to) {
// If from date is after to date, adjust to date
onDateRangeChange?.({ from, to: from })
setToDate(value)
} else {
onDateRangeChange?.({ from, to })
}
}
const handleToDateChange = (value: string) => {
setToDate(value)
const from = fromDate ? new Date(fromDate) : dateRange?.from
const to = value ? new Date(value) : undefined
if (from && to && from > to) {
// If to date is before from date, adjust from date
onDateRangeChange?.({ from: to, to })
setFromDate(value)
} else {
onDateRangeChange?.({ from, to })
}
}
return (
<div className={cn("flex items-center gap-2", className)}>
<div className="flex items-center gap-2">
<Label htmlFor="from-date" className="text-sm whitespace-nowrap">From:</Label>
<Input
id="from-date"
type="date"
value={fromDate}
onChange={(e) => handleFromDateChange(e.target.value)}
className="w-32"
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="to-date" className="text-sm whitespace-nowrap">To:</Label>
<Input
id="to-date"
type="date"
value={toDate}
onChange={(e) => handleToDateChange(e.target.value)}
className="w-32"
/>
</div>
</div>
)
}
// Date Range Display Component
export function DateRangeDisplay({ dateRange }: { dateRange?: DateRange }) {
if (!dateRange?.from) return null
const daysDiff = dateRange.to
? Math.ceil((dateRange.to.getTime() - dateRange.from.getTime()) / (1000 * 60 * 60 * 24)) + 1
: 1
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
{daysDiff} day{daysDiff !== 1 ? 's' : ''}
</Badge>
<span className="text-sm text-muted-foreground">
{format(dateRange.from, "MMM dd")}
{dateRange.to && !isSameDay(dateRange.from, dateRange.to) && (
<> - {format(dateRange.to, "MMM dd, yyyy")}</>
)}
</span>
</div>
)
}