From 1b51f29c24458ec356f5c239c13b548c21425d9e Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:38:25 +0200 Subject: [PATCH] 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. --- app/dashboard/stock/page.tsx | 137 +++++- .../dashboard/promotions/NewPromotionForm.tsx | 20 +- components/modals/broadcast-dialog.tsx | 1 - components/ui/date-picker.tsx | 417 ++++++++++++++++++ config/quotes.ts | 39 +- 5 files changed, 575 insertions(+), 39 deletions(-) create mode 100644 components/ui/date-picker.tsx diff --git a/app/dashboard/stock/page.tsx b/app/dashboard/stock/page.tsx index 5cbb8d8..9eb0075 100644 --- a/app/dashboard/stock/page.tsx +++ b/app/dashboard/stock/page.tsx @@ -33,6 +33,9 @@ import { Product } from "@/models/products"; import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar } from "lucide-react"; import { clientFetch } from "@/lib/api"; 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 { currentStock: number; @@ -40,6 +43,8 @@ interface StockData { lowStockThreshold?: number; } +type ReportType = 'daily' | 'weekly' | 'monthly' | 'custom'; + export default function StockManagementPage() { const router = useRouter(); const [products, setProducts] = useState([]); @@ -50,7 +55,15 @@ export default function StockManagementPage() { const [selectedProducts, setSelectedProducts] = useState([]); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [bulkAction, setBulkAction] = useState<'enable' | 'disable' | null>(null); + + // Export state const [exportDate, setExportDate] = useState(new Date().toISOString().split('T')[0]); + const [exportDateRange, setExportDateRange] = useState({ + from: startOfDay(addDays(new Date(), -6)), + to: endOfDay(new Date()) + }); + const [selectedMonth, setSelectedMonth] = useState(new Date()); + const [reportType, setReportType] = useState('daily'); const [isExporting, setIsExporting] = useState(false); useEffect(() => { @@ -259,7 +272,47 @@ export default function StockManagementPage() { const handleExportStock = async () => { setIsExporting(true); 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) { throw new Error('No data received from server'); @@ -297,14 +350,19 @@ export default function StockManagementPage() { const url = URL.createObjectURL(blob); link.setAttribute('href', url); - link.setAttribute('download', `daily-stock-report-${exportDate}.csv`); + link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); 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) { console.error('Error exporting stock report:', error); toast.error('Failed to export stock report'); @@ -348,28 +406,60 @@ export default function StockManagementPage() { value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> - - - - - -
- - setExportDate(e.target.value)} - className="w-full" - /> -
-
-
+ + + setReportType('daily')}> + Daily Report + + setReportType('weekly')}> + Weekly Report + + setReportType('monthly')}> + Monthly Report + + setReportType('custom')}> + Custom Range + + + + + {/* Date Selection based on report type */} + {reportType === 'daily' && ( + setExportDate(date ? date.toISOString().split('T')[0] : '')} + placeholder="Select export date" + className="w-auto" + /> + )} + + {(reportType === 'weekly' || reportType === 'custom') && ( + + )} + + {reportType === 'monthly' && ( + setSelectedMonth(date || new Date())} + placeholder="Select month" + className="w-auto" + /> + )} + + + + + + + ) +} + +// 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 ( + + + + )} + + + +
+
+

Select Month

+ +
+ +
+ {months.map((month, index) => ( + + ))} +
+
+
+
+ ) +} + +// 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 ( + + + + )} + + + +
+ {showPresets && ( +
+ +
+ {presets.map((preset) => ( + + ))} +
+
+ )} + +
+
+
+ ) +} + +// Custom Date Range Input Component +export function CustomDateRangeInput({ + dateRange, + onDateRangeChange, + className +}: DateRangePickerProps) { + const [fromDate, setFromDate] = React.useState("") + const [toDate, setToDate] = React.useState("") + + 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 ( +
+
+ + handleFromDateChange(e.target.value)} + className="w-32" + /> +
+
+ + handleToDateChange(e.target.value)} + className="w-32" + /> +
+
+ ) +} + +// 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 ( +
+ + {daysDiff} day{daysDiff !== 1 ? 's' : ''} + + + {format(dateRange.from, "MMM dd")} + {dateRange.to && !isSameDay(dateRange.from, dateRange.to) && ( + <> - {format(dateRange.to, "MMM dd, yyyy")} + )} + +
+ ) +} \ No newline at end of file diff --git a/config/quotes.ts b/config/quotes.ts index 9268b9f..85ab63c 100644 --- a/config/quotes.ts +++ b/config/quotes.ts @@ -1,8 +1,3 @@ -/** - * Business motivation quotes for the dashboard - * Collection of quotes from successful entrepreneurs and business leaders - */ - export interface Quote { text: 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: "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" }, + + // 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 @@ -88,4 +115,4 @@ export function getQuotesByTheme(keyword: string): Quote[] { return businessQuotes.filter(quote => quote.text.toLowerCase().includes(keyword.toLowerCase()) ); -} \ No newline at end of file +}