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:
parent
db752f55f4
commit
701c805d0c
3 changed files with 259 additions and 24 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue