feat: implement dynamic date selection with custom range support

- Add dynamic date range generation based on actual order data
- Implement custom date range picker with start/end date inputs
- Unify date filtering logic between Analytics and ProfitAnalysis pages
- Support multiple date formats (YYYY, YYYY-MM, YYYY-QX, custom ranges)
- Remove hardcoded years for future-proof date selection
- Enhance ProfitAnalysisService with custom date range support
This commit is contained in:
dlawler489 2026-05-05 22:04:52 +10:00
parent db752f55f4
commit 701c805d0c
3 changed files with 259 additions and 24 deletions

View file

@ -11,7 +11,92 @@ const Analytics = () => {
const { customers } = useSelector((state: RootState) => state.customers);
const { expenses } = useSelector((state: RootState) => state.expenses);
const [dateRange, setDateRange] = useState('2026');
const [dateRange, setDateRange] = useState(() => {
// Default to current year
return new Date().getFullYear().toString();
});
const [customStartDate, setCustomStartDate] = useState('');
const [customEndDate, setCustomEndDate] = useState('');
// Generate dynamic date range options based on actual order data
const dateRangeOptions = useMemo(() => {
const presets = [
{ value: 'all', label: 'All Time', type: 'preset' },
{ value: 'week', label: 'Last 7 Days', type: 'preset' },
{ value: 'month', label: 'Last 30 Days', type: 'preset' },
{ value: 'quarter', label: 'Last 90 Days', type: 'preset' },
{ value: 'year', label: 'This Year (Calendar)', type: 'preset' },
{ value: 'custom', label: 'Custom Date Range', type: 'preset' }
];
if (!orders || orders.length === 0) {
return {
presets,
years: [],
quarters: [],
months: []
};
}
// Get date ranges from actual order data
const years = new Set<number>();
const months = new Set<string>();
const quarters = new Set<string>();
orders.forEach(order => {
if (order.dateOrdered) {
const date = new Date(order.dateOrdered);
const year = date.getFullYear();
const month = date.getMonth();
const quarter = Math.floor(month / 3) + 1;
years.add(year);
months.add(`${year}-${String(month + 1).padStart(2, '0')}`);
quarters.add(`${year}-Q${quarter}`);
}
});
// Add year options (sorted newest first)
const yearOptions = Array.from(years)
.sort((a, b) => b - a)
.map(year => ({
value: year.toString(),
label: year.toString(),
type: 'year'
}));
// Add quarter options (last 8 quarters)
const quarterOptions = Array.from(quarters)
.sort((a, b) => b.localeCompare(a))
.slice(0, 8)
.map(quarter => ({
value: quarter,
label: quarter.replace('-', ' '),
type: 'quarter'
}));
// Add month options (last 24 months)
const monthOptions = Array.from(months)
.sort((a, b) => b.localeCompare(a))
.slice(0, 24)
.map(monthKey => {
const [year, month] = monthKey.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1);
const label = date.toLocaleDateString('en-AU', { year: 'numeric', month: 'long' });
return {
value: monthKey,
label,
type: 'month'
};
});
return {
presets,
years: yearOptions,
quarters: quarterOptions,
months: monthOptions
};
}, [orders]);
// Helper function to get updated printing cost using current product costs
const getUpdatedPrintingCost = (order: any) => {
@ -45,6 +130,15 @@ const Analytics = () => {
return orderYear === targetYear && orderQuarter === targetQuarter;
}
// Handle custom date range format (e.g., "2025-01-01_2025-12-31")
if (dateRange.includes('_')) {
const [startDate, endDate] = dateRange.split('_');
const start = new Date(startDate);
const end = new Date(endDate);
end.setHours(23, 59, 59, 999); // Include the end date
return orderDate >= start && orderDate <= end;
}
// Handle year format (e.g., "2026") - calendar year, not rolling 365 days
if (dateRange.match(/^\d{4}$/)) {
return orderDate.getFullYear() === parseInt(dateRange);
@ -64,6 +158,15 @@ const Analytics = () => {
case 'year':
// For "This Year" option, show current calendar year
return orderDate.getFullYear() === now.getFullYear();
case 'custom':
// Handle custom date range using state variables
if (customStartDate && customEndDate) {
const start = new Date(customStartDate);
const end = new Date(customEndDate);
end.setHours(23, 59, 59, 999); // Include the end date
return orderDate >= start && orderDate <= end;
}
return true;
default:
return true;
}
@ -95,6 +198,15 @@ const Analytics = () => {
return expenseYear === targetYear && expenseQuarter === targetQuarter;
}
// Handle custom date range format (e.g., "2025-01-01_2025-12-31")
if (dateRange.includes('_')) {
const [startDate, endDate] = dateRange.split('_');
const start = new Date(startDate);
const end = new Date(endDate);
end.setHours(23, 59, 59, 999); // Include the end date
return expenseDate >= start && expenseDate <= end;
}
// Handle year format (e.g., "2026") - calendar year, not rolling 365 days
if (dateRange.match(/^\d{4}$/)) {
return expenseDate.getFullYear() === parseInt(dateRange);
@ -114,6 +226,15 @@ const Analytics = () => {
case 'year':
// For "This Year" option, show current calendar year
return expenseDate.getFullYear() === now.getFullYear();
case 'custom':
// Handle custom date range using state variables
if (customStartDate && customEndDate) {
const start = new Date(customStartDate);
const end = new Date(customEndDate);
end.setHours(23, 59, 59, 999); // Include the end date
return expenseDate >= start && expenseDate <= end;
}
return true;
default:
return true;
}
@ -304,27 +425,45 @@ const Analytics = () => {
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
<option value="quarter">Last Quarter</option>
<option value="year">This Year (2026)</option>
<option value="2026">2026 Full Year</option>
<option value="2025">2025 Full Year</option>
<optgroup label="2026 Months">
<option value="2026-05">May 2026</option>
<option value="2026-04">April 2026</option>
<option value="2026-03">March 2026</option>
<option value="2026-02">February 2026</option>
<option value="2026-01">January 2026</option>
{/* Preset Ranges */}
{dateRangeOptions.presets.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
{/* Years */}
{dateRangeOptions.years.length > 0 && (
<optgroup label="Years">
{dateRangeOptions.years.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
<optgroup label="2025 Months">
<option value="2025-12">December 2025</option>
<option value="2025-11">November 2025</option>
<option value="2025-10">October 2025</option>
<option value="2025-09">September 2025</option>
<option value="2025-08">August 2025</option>
<option value="2025-07">July 2025</option>
)}
{/* Quarters */}
{dateRangeOptions.quarters.length > 0 && (
<optgroup label="Quarters">
{dateRangeOptions.quarters.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
)}
{/* Months */}
{dateRangeOptions.months.length > 0 && (
<optgroup label="Months">
{dateRangeOptions.months.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
)}
</select>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
<Download className="w-4 h-4" />
@ -333,6 +472,51 @@ const Analytics = () => {
</div>
</div>
{/* Custom Date Range Inputs */}
{dateRange === 'custom' && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-4">
<div>
<label htmlFor="startDate" className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="date"
id="startDate"
value={customStartDate}
onChange={(e) => setCustomStartDate(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="endDate" className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="date"
id="endDate"
value={customEndDate}
onChange={(e) => setCustomEndDate(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="pt-6">
<button
onClick={() => {
if (customStartDate && customEndDate) {
setDateRange(`${customStartDate}_${customEndDate}`);
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
disabled={!customStartDate || !customEndDate}
>
Apply Range
</button>
</div>
</div>
</div>
)}
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">

View file

@ -8,7 +8,12 @@ const ProfitAnalysis = () => {
const { orders } = useSelector((state: RootState) => state.orders);
const { products } = useSelector((state: RootState) => state.products);
const { expenses } = useSelector((state: RootState) => state.expenses);
const [dateRange, setDateRange] = useState('all');
const [dateRange, setDateRange] = useState(() => {
// Default to current year
return new Date().getFullYear().toString();
});
const [customStartDate, setCustomStartDate] = useState('');
const [customEndDate, setCustomEndDate] = useState('');
const [selectedView, setSelectedView] = useState<'overview' | 'trends' | 'products' | 'orders'>('overview');
const [expandedOrder, setExpandedOrder] = useState<string | null>(null);
const [orderSortBy, setOrderSortBy] = useState<'date' | 'profit' | 'margin' | 'revenue'>('date');
@ -146,6 +151,51 @@ const ProfitAnalysis = () => {
</div>
</div>
{/* Custom Date Range Inputs */}
{dateRange === 'custom' && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-4">
<div>
<label htmlFor="startDate" className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="date"
id="startDate"
value={customStartDate}
onChange={(e) => setCustomStartDate(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="endDate" className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="date"
id="endDate"
value={customEndDate}
onChange={(e) => setCustomEndDate(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="pt-6">
<button
onClick={() => {
if (customStartDate && customEndDate) {
setDateRange(`${customStartDate}_${customEndDate}`);
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
disabled={!customStartDate || !customEndDate}
>
Apply Range
</button>
</div>
</div>
</div>
)}
{/* View Selector */}
<div className="flex gap-2 mb-6">
<button

View file

@ -351,7 +351,8 @@ export class ProfitAnalysisService {
{ value: 'week', label: 'Last 7 Days', type: 'preset' },
{ value: 'month', label: 'Last 30 Days', type: 'preset' },
{ value: 'quarter', label: 'Last 90 Days', type: 'preset' },
{ value: 'year', label: 'Last 365 Days', type: 'preset' }
{ value: 'year', label: 'Last 365 Days', type: 'preset' },
{ value: 'custom', label: 'Custom Date Range', type: 'custom' }
];
// Get unique months from orders