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.
This commit is contained in:
417
components/ui/date-picker.tsx
Normal file
417
components/ui/date-picker.tsx
Normal file
@@ -0,0 +1,417 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user