diff --git a/src/components/TransactionChart.tsx b/src/components/TransactionChart.tsx index 56e3745..0e61c36 100644 --- a/src/components/TransactionChart.tsx +++ b/src/components/TransactionChart.tsx @@ -40,13 +40,13 @@ const TransactionChart: React.FC = ({ const dates = transactions.map(tx => new Date(tx.date)); const minDate = new Date(Math.min(...dates.map(d => d.getTime()))); const maxDate = new Date(Math.max(...dates.map(d => d.getTime()))); - const timeSpanDays = (maxDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24); + const timeSpanDays = Math.max((maxDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24), 1); // Calculate transaction density per day - const transactionsPerDay = transactions.length / Math.max(timeSpanDays, 1); + const transactionsPerDay = transactions.length / timeSpanDays; - // Smart scoping: if many transactions per day, use hourly; otherwise daily - if (transactionsPerDay > 10 && timeSpanDays < 7) { + // Smart scoping: if many transactions per day or short timespan with multiple transactions, use hourly + if ((transactionsPerDay > 5 && timeSpanDays < 7) || (transactions.length > 3 && timeSpanDays <= 1)) { return 'hour'; } return 'day'; @@ -81,7 +81,7 @@ const TransactionChart: React.FC = ({ }); // Convert to array and calculate profit - return Array.from(dateMap.values()) + const sortedData = Array.from(dateMap.values()) .map(entry => ({ ...entry, profit: entry.revenue - entry.costs, @@ -90,6 +90,23 @@ const TransactionChart: React.FC = ({ : format(new Date(entry.date), 'MMM dd') })) .sort((a, b) => a.date.localeCompare(b.date)); + + // Add cumulative values + let cumulativeCosts = 0; + let cumulativeRevenue = 0; + let cumulativeProfit = 0; + + return sortedData.map(entry => { + cumulativeCosts += entry.costs; + cumulativeRevenue += entry.revenue; + cumulativeProfit += entry.profit; + return { + ...entry, + cumulativeCosts, + cumulativeRevenue, + cumulativeProfit + }; + }); }; const getOverviewChartData = (jobs: IndJob[]) => { @@ -119,7 +136,7 @@ const TransactionChart: React.FC = ({ } }); - return Array.from(dateMap.values()) + const sortedData = Array.from(dateMap.values()) .map(entry => ({ ...entry, formattedDate: timeFormat === 'hour' @@ -127,11 +144,25 @@ const TransactionChart: React.FC = ({ : format(new Date(entry.date), 'MMM dd') })) .sort((a, b) => a.date.localeCompare(b.date)); + + // Add cumulative values + let cumulativeRevenue = 0; + let cumulativeProfit = 0; + + return sortedData.map(entry => { + cumulativeRevenue += entry.revenue; + cumulativeProfit += entry.profit; + return { + ...entry, + cumulativeRevenue, + cumulativeProfit + }; + }); }; const formatTooltipValue = (value: number) => formatISK(value); - const data = type === 'overview' && jobs ? getOverviewChartData(jobs) : job ? getJobChartData(job) : []; + const data = (type === 'overview' || type === 'total-revenue' || type === 'total-profit') && jobs ? getOverviewChartData(jobs) : job ? getJobChartData(job) : []; const getTitle = () => { if (type === 'overview') return 'Overview - Revenue & Profit Over Time'; @@ -198,6 +229,7 @@ const TransactionChart: React.FC = ({ labelStyle={{ color: '#F3F4F6' }} contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }} /> + = ({ fillOpacity={0.6} name="Costs" /> + ); } @@ -221,6 +262,7 @@ const TransactionChart: React.FC = ({ labelStyle={{ color: '#F3F4F6' }} contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }} /> + = ({ fillOpacity={0.6} name="Revenue" /> + ); } @@ -278,6 +329,28 @@ const TransactionChart: React.FC = ({ dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }} /> )} + {!hiddenLines.has('cumulativeRevenue') && ( + + )} + {!hiddenLines.has('cumulativeProfit') && ( + + )} ); }; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 5bfde08..22308ea 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { Plus, Factory, TrendingUp, Briefcase, FileText, Settings } from 'lucide-react'; +import { Plus, Factory, TrendingUp, Briefcase, FileText, Settings, BarChart3 } from 'lucide-react'; import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes'; import { formatISK } from '@/utils/priceUtils'; import { getStatusPriority } from '@/utils/jobStatusUtils'; @@ -16,6 +16,7 @@ import { useJobs } from '@/hooks/useDataService'; import { useJobMetrics } from '@/hooks/useJobMetrics'; import SearchOverlay from '@/components/SearchOverlay'; import RecapPopover from '@/components/RecapPopover'; +import TransactionChart from '@/components/TransactionChart'; const Index = () => { const { @@ -35,6 +36,8 @@ const Index = () => { const [showBatchForm, setShowBatchForm] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); + const [totalRevenueChartOpen, setTotalRevenueChartOpen] = useState(false); + const [totalProfitChartOpen, setTotalProfitChartOpen] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState>(() => { const saved = localStorage.getItem('jobGroupsCollapsed'); return saved ? JSON.parse(saved) : {}; @@ -220,6 +223,14 @@ const Index = () => { Total Revenue + @@ -239,6 +250,14 @@ const Index = () => { Total Profit + @@ -321,6 +340,20 @@ const Index = () => { onTransactionsAssigned={handleBatchTransactionsAssigned} /> )} + + setTotalRevenueChartOpen(false)} + /> + + setTotalProfitChartOpen(false)} + /> ); };