Introduces an exportOrdersToCSV function in lib/api-client.ts to allow exporting orders by status as a CSV file. Updates various UI components to use the '•' (bullet) symbol instead of '·' (middle dot) and replaces some emoji/unicode characters for improved consistency and compatibility. Also normalizes the 'use client' directive to include a BOM in many files.
414 lines
12 KiB
TypeScript
414 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 && (
|
|
<div
|
|
className="ml-auto h-6 w-6 p-0 flex items-center justify-center rounded-sm hover:bg-accent cursor-pointer"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onMonthChange?.(undefined)
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</div>
|
|
)}
|
|
</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 && (
|
|
<div
|
|
className="ml-auto h-6 w-6 p-0 flex items-center justify-center rounded-sm hover:bg-accent cursor-pointer"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
handleClear()
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</div>
|
|
)}
|
|
</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>
|
|
)
|
|
}
|