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:
@@ -33,6 +33,9 @@ import { Product } from "@/models/products";
|
|||||||
import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar } from "lucide-react";
|
import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar } from "lucide-react";
|
||||||
import { clientFetch } from "@/lib/api";
|
import { clientFetch } from "@/lib/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { DatePicker, DateRangePicker, DateRangeDisplay, MonthPicker } from "@/components/ui/date-picker";
|
||||||
|
import { DateRange } from "react-day-picker";
|
||||||
|
import { addDays, startOfDay, endOfDay, format, isSameDay } from "date-fns";
|
||||||
|
|
||||||
interface StockData {
|
interface StockData {
|
||||||
currentStock: number;
|
currentStock: number;
|
||||||
@@ -40,6 +43,8 @@ interface StockData {
|
|||||||
lowStockThreshold?: number;
|
lowStockThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReportType = 'daily' | 'weekly' | 'monthly' | 'custom';
|
||||||
|
|
||||||
export default function StockManagementPage() {
|
export default function StockManagementPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
@@ -50,7 +55,15 @@ export default function StockManagementPage() {
|
|||||||
const [selectedProducts, setSelectedProducts] = useState<string[]>([]);
|
const [selectedProducts, setSelectedProducts] = useState<string[]>([]);
|
||||||
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
||||||
const [bulkAction, setBulkAction] = useState<'enable' | 'disable' | null>(null);
|
const [bulkAction, setBulkAction] = useState<'enable' | 'disable' | null>(null);
|
||||||
|
|
||||||
|
// Export state
|
||||||
const [exportDate, setExportDate] = useState<string>(new Date().toISOString().split('T')[0]);
|
const [exportDate, setExportDate] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||||
|
const [exportDateRange, setExportDateRange] = useState<DateRange | undefined>({
|
||||||
|
from: startOfDay(addDays(new Date(), -6)),
|
||||||
|
to: endOfDay(new Date())
|
||||||
|
});
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState<Date>(new Date());
|
||||||
|
const [reportType, setReportType] = useState<ReportType>('daily');
|
||||||
const [isExporting, setIsExporting] = useState<boolean>(false);
|
const [isExporting, setIsExporting] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -259,7 +272,47 @@ export default function StockManagementPage() {
|
|||||||
const handleExportStock = async () => {
|
const handleExportStock = async () => {
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
try {
|
try {
|
||||||
const response = await clientFetch(`/api/analytics/daily-stock-report?date=${exportDate}`);
|
let response;
|
||||||
|
let filename;
|
||||||
|
|
||||||
|
switch (reportType) {
|
||||||
|
case 'daily':
|
||||||
|
response = await clientFetch(`/api/analytics/daily-stock-report?date=${exportDate}`);
|
||||||
|
filename = `daily-stock-report-${exportDate}.csv`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'weekly':
|
||||||
|
if (!exportDateRange?.from) {
|
||||||
|
toast.error('Please select a date range for weekly report');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const weekStart = format(exportDateRange.from, 'yyyy-MM-dd');
|
||||||
|
response = await clientFetch(`/api/analytics/weekly-stock-report?weekStart=${weekStart}`);
|
||||||
|
filename = `weekly-stock-report-${weekStart}.csv`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'monthly':
|
||||||
|
const year = selectedMonth.getFullYear();
|
||||||
|
const month = selectedMonth.getMonth() + 1;
|
||||||
|
response = await clientFetch(`/api/analytics/monthly-stock-report?year=${year}&month=${month}`);
|
||||||
|
filename = `monthly-stock-report-${year}-${month.toString().padStart(2, '0')}.csv`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'custom':
|
||||||
|
if (!exportDateRange?.from || !exportDateRange?.to) {
|
||||||
|
toast.error('Please select a date range for custom report');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const startDate = format(exportDateRange.from, 'yyyy-MM-dd');
|
||||||
|
const endDate = format(exportDateRange.to, 'yyyy-MM-dd');
|
||||||
|
response = await clientFetch(`/api/analytics/daily-stock-report?startDate=${startDate}&endDate=${endDate}`);
|
||||||
|
filename = `custom-stock-report-${startDate}-to-${endDate}.csv`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
toast.error('Invalid report type');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response || !response.products) {
|
if (!response || !response.products) {
|
||||||
throw new Error('No data received from server');
|
throw new Error('No data received from server');
|
||||||
@@ -297,14 +350,19 @@ export default function StockManagementPage() {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
link.setAttribute('href', url);
|
link.setAttribute('href', url);
|
||||||
link.setAttribute('download', `daily-stock-report-${exportDate}.csv`);
|
link.setAttribute('download', filename);
|
||||||
link.style.visibility = 'hidden';
|
link.style.visibility = 'hidden';
|
||||||
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
||||||
toast.success(`Stock report for ${exportDate} exported successfully`);
|
const periodText = reportType === 'daily' ? exportDate :
|
||||||
|
reportType === 'weekly' ? `week starting ${format(exportDateRange?.from || new Date(), 'MMM dd')}` :
|
||||||
|
reportType === 'monthly' ? `${response.monthName || 'current month'}` :
|
||||||
|
`${format(exportDateRange?.from || new Date(), 'MMM dd')} to ${format(exportDateRange?.to || new Date(), 'MMM dd')}`;
|
||||||
|
|
||||||
|
toast.success(`${reportType.charAt(0).toUpperCase() + reportType.slice(1)} stock report for ${periodText} exported successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting stock report:', error);
|
console.error('Error exporting stock report:', error);
|
||||||
toast.error('Failed to export stock report');
|
toast.error('Failed to export stock report');
|
||||||
@@ -348,28 +406,60 @@ export default function StockManagementPage() {
|
|||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
{/* Report Type Selector */}
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="outline"
|
<DropdownMenuTrigger asChild>
|
||||||
className="gap-2"
|
<Button variant="outline" className="gap-2">
|
||||||
>
|
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
{exportDate}
|
{reportType.charAt(0).toUpperCase() + reportType.slice(1)} Report
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</DropdownMenuTrigger>
|
||||||
<PopoverContent className="w-auto p-3" align="end">
|
<DropdownMenuContent>
|
||||||
<div className="space-y-2">
|
<DropdownMenuItem onClick={() => setReportType('daily')}>
|
||||||
<label className="text-sm font-medium">Select Date for Export</label>
|
Daily Report
|
||||||
<Input
|
</DropdownMenuItem>
|
||||||
type="date"
|
<DropdownMenuItem onClick={() => setReportType('weekly')}>
|
||||||
value={exportDate}
|
Weekly Report
|
||||||
onChange={(e) => setExportDate(e.target.value)}
|
</DropdownMenuItem>
|
||||||
className="w-full"
|
<DropdownMenuItem onClick={() => setReportType('monthly')}>
|
||||||
|
Monthly Report
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setReportType('custom')}>
|
||||||
|
Custom Range
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Date Selection based on report type */}
|
||||||
|
{reportType === 'daily' && (
|
||||||
|
<DatePicker
|
||||||
|
date={exportDate ? new Date(exportDate) : undefined}
|
||||||
|
onDateChange={(date) => setExportDate(date ? date.toISOString().split('T')[0] : '')}
|
||||||
|
placeholder="Select export date"
|
||||||
|
className="w-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
{(reportType === 'weekly' || reportType === 'custom') && (
|
||||||
|
<DateRangePicker
|
||||||
|
dateRange={exportDateRange}
|
||||||
|
onDateRangeChange={setExportDateRange}
|
||||||
|
placeholder="Select date range"
|
||||||
|
className="w-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{reportType === 'monthly' && (
|
||||||
|
<MonthPicker
|
||||||
|
selectedMonth={selectedMonth}
|
||||||
|
onMonthChange={(date) => setSelectedMonth(date || new Date())}
|
||||||
|
placeholder="Select month"
|
||||||
|
className="w-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleExportStock}
|
onClick={handleExportStock}
|
||||||
@@ -383,6 +473,7 @@ export default function StockManagementPage() {
|
|||||||
)}
|
)}
|
||||||
{isExporting ? 'Exporting...' : 'Export CSV'}
|
{isExporting ? 'Exporting...' : 'Export CSV'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{selectedProducts.length > 0 && (
|
{selectedProducts.length > 0 && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { toast } from '@/components/ui/use-toast';
|
import { toast } from '@/components/ui/use-toast';
|
||||||
import { PromotionFormData } from '@/lib/types/promotion';
|
import { PromotionFormData } from '@/lib/types/promotion';
|
||||||
import { fetchClient } from '@/lib/api';
|
import { fetchClient } from '@/lib/api';
|
||||||
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
|
|
||||||
// Form schema validation with Zod
|
// Form schema validation with Zod
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -246,7 +247,12 @@ export default function NewPromotionForm({ onSuccess, onCancel }: NewPromotionFo
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-sm font-medium">Start Date</FormLabel>
|
<FormLabel className="text-sm font-medium">Start Date</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="date" className="h-10" {...field} />
|
<DatePicker
|
||||||
|
date={field.value ? new Date(field.value) : undefined}
|
||||||
|
onDateChange={(date) => field.onChange(date ? date.toISOString().split('T')[0] : '')}
|
||||||
|
placeholder="Select start date"
|
||||||
|
className="h-10"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -260,15 +266,11 @@ export default function NewPromotionForm({ onSuccess, onCancel }: NewPromotionFo
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-sm font-medium">End Date (Optional)</FormLabel>
|
<FormLabel className="text-sm font-medium">End Date (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<DatePicker
|
||||||
type="date"
|
date={field.value ? new Date(field.value) : undefined}
|
||||||
|
onDateChange={(date) => field.onChange(date ? date.toISOString().split('T')[0] : null)}
|
||||||
|
placeholder="Select end date (optional)"
|
||||||
className="h-10"
|
className="h-10"
|
||||||
{...field}
|
|
||||||
value={field.value || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
field.onChange(value === '' ? null : value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription className="text-xs">
|
<FormDescription className="text-xs">
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
|||||||
const newText = textarea.value.substring(0, start) + insertText + textarea.value.substring(end);
|
const newText = textarea.value.substring(0, start) + insertText + textarea.value.substring(end);
|
||||||
setBroadcastMessage(newText);
|
setBroadcastMessage(newText);
|
||||||
|
|
||||||
// Set cursor position after the inserted text
|
|
||||||
const newCursorPos = start + insertText.length;
|
const newCursorPos = start + insertText.length;
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* Business motivation quotes for the dashboard
|
|
||||||
* Collection of quotes from successful entrepreneurs and business leaders
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Quote {
|
export interface Quote {
|
||||||
text: string;
|
text: string;
|
||||||
author: string;
|
author: string;
|
||||||
@@ -50,6 +45,38 @@ export const businessQuotes: Quote[] = [
|
|||||||
{ text: "If you want to achieve greatness stop asking for permission.", author: "Anonymous" },
|
{ text: "If you want to achieve greatness stop asking for permission.", author: "Anonymous" },
|
||||||
{ text: "Things work out best for those who make the best of how things work out.", author: "John Wooden" },
|
{ text: "Things work out best for those who make the best of how things work out.", author: "John Wooden" },
|
||||||
{ text: "The most valuable businesses of coming decades will be built by entrepreneurs who seek to empower people rather than try to make them obsolete.", author: "Peter Thiel" },
|
{ text: "The most valuable businesses of coming decades will be built by entrepreneurs who seek to empower people rather than try to make them obsolete.", author: "Peter Thiel" },
|
||||||
|
|
||||||
|
// Additional quotes - Vision and leadership
|
||||||
|
{ text: "Leadership is the capacity to translate vision into reality.", author: "Warren Bennis" },
|
||||||
|
{ text: "A goal without a plan is just a wish.", author: "Antoine de Saint-Exupéry" },
|
||||||
|
{ text: "Good business leaders create a vision, articulate the vision, passionately own the vision, and relentlessly drive it to completion.", author: "Jack Welch" },
|
||||||
|
|
||||||
|
// Additional quotes - Risk, failure, and learning
|
||||||
|
{ text: "Fail often so you can succeed sooner.", author: "Tom Kelley" },
|
||||||
|
{ text: "Don’t worry about failure; you only have to be right once.", author: "Drew Houston" },
|
||||||
|
{ text: "In the middle of difficulty lies opportunity.", author: "Albert Einstein" },
|
||||||
|
{ text: "Risk more than others think is safe. Dream more than others think is practical.", author: "Howard Schultz" },
|
||||||
|
|
||||||
|
// Additional quotes - Action and hustle
|
||||||
|
{ text: "Ideas are easy. Implementation is hard.", author: "Guy Kawasaki" },
|
||||||
|
{ text: "Success usually comes to those who are too busy to be looking for it.", author: "Henry David Thoreau" },
|
||||||
|
{ text: "Done is better than perfect.", author: "Sheryl Sandberg" },
|
||||||
|
{ text: "Action is the foundational key to all success.", author: "Pablo Picasso" },
|
||||||
|
|
||||||
|
// Additional quotes - Customer and product
|
||||||
|
{ text: "People don't buy what you do; they buy why you do it.", author: "Simon Sinek" },
|
||||||
|
{ text: "The customer is the most important part of the production line.", author: "W. Edwards Deming" },
|
||||||
|
{ text: "Make something people want and sell that, or be someone people need and sell yourself.", author: "Naval Ravikant" },
|
||||||
|
|
||||||
|
// Additional quotes - Resilience and mindset
|
||||||
|
{ text: "Success is walking from failure to failure with no loss of enthusiasm.", author: "Winston Churchill" },
|
||||||
|
{ text: "Whether you think you can or you think you can’t, you’re right.", author: "Henry Ford" },
|
||||||
|
{ text: "Strength and growth come only through continuous effort and struggle.", author: "Napoleon Hill" },
|
||||||
|
{ text: "Believe you can and you're halfway there.", author: "Theodore Roosevelt" },
|
||||||
|
|
||||||
|
// Additional quotes - Money and value
|
||||||
|
{ text: "Try not to become a man of success, but rather try to become a man of value.", author: "Albert Einstein" },
|
||||||
|
{ text: "Price is what you pay. Value is what you get.", author: "Warren Buffett" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// For backward compatibility with existing code
|
// For backward compatibility with existing code
|
||||||
|
|||||||
Reference in New Issue
Block a user