feat: Add chart icons to total revenue/profit
Adds chart icons to the total revenue and profit displays on the main page. Implements cumulative lines on all charts. Addresses chart scoping issues.
This commit is contained in:
@@ -40,13 +40,13 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
const dates = transactions.map(tx => new Date(tx.date));
|
const dates = transactions.map(tx => new Date(tx.date));
|
||||||
const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
|
const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
|
||||||
const maxDate = new Date(Math.max(...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
|
// 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
|
// Smart scoping: if many transactions per day or short timespan with multiple transactions, use hourly
|
||||||
if (transactionsPerDay > 10 && timeSpanDays < 7) {
|
if ((transactionsPerDay > 5 && timeSpanDays < 7) || (transactions.length > 3 && timeSpanDays <= 1)) {
|
||||||
return 'hour';
|
return 'hour';
|
||||||
}
|
}
|
||||||
return 'day';
|
return 'day';
|
||||||
@@ -81,7 +81,7 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Convert to array and calculate profit
|
// Convert to array and calculate profit
|
||||||
return Array.from(dateMap.values())
|
const sortedData = Array.from(dateMap.values())
|
||||||
.map(entry => ({
|
.map(entry => ({
|
||||||
...entry,
|
...entry,
|
||||||
profit: entry.revenue - entry.costs,
|
profit: entry.revenue - entry.costs,
|
||||||
@@ -90,6 +90,23 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
: format(new Date(entry.date), 'MMM dd')
|
: format(new Date(entry.date), 'MMM dd')
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.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[]) => {
|
const getOverviewChartData = (jobs: IndJob[]) => {
|
||||||
@@ -119,7 +136,7 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(dateMap.values())
|
const sortedData = Array.from(dateMap.values())
|
||||||
.map(entry => ({
|
.map(entry => ({
|
||||||
...entry,
|
...entry,
|
||||||
formattedDate: timeFormat === 'hour'
|
formattedDate: timeFormat === 'hour'
|
||||||
@@ -127,11 +144,25 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
: format(new Date(entry.date), 'MMM dd')
|
: format(new Date(entry.date), 'MMM dd')
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.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 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 = () => {
|
const getTitle = () => {
|
||||||
if (type === 'overview') return 'Overview - Revenue & Profit Over Time';
|
if (type === 'overview') return 'Overview - Revenue & Profit Over Time';
|
||||||
@@ -198,6 +229,7 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
labelStyle={{ color: '#F3F4F6' }}
|
labelStyle={{ color: '#F3F4F6' }}
|
||||||
contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }}
|
contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }}
|
||||||
/>
|
/>
|
||||||
|
<Legend />
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="costs"
|
dataKey="costs"
|
||||||
@@ -206,6 +238,15 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
fillOpacity={0.6}
|
fillOpacity={0.6}
|
||||||
name="Costs"
|
name="Costs"
|
||||||
/>
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="cumulativeCosts"
|
||||||
|
stroke="#DC2626"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="5 5"
|
||||||
|
name="Cumulative Costs"
|
||||||
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -221,6 +262,7 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
labelStyle={{ color: '#F3F4F6' }}
|
labelStyle={{ color: '#F3F4F6' }}
|
||||||
contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }}
|
contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }}
|
||||||
/>
|
/>
|
||||||
|
<Legend />
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="revenue"
|
dataKey="revenue"
|
||||||
@@ -229,6 +271,15 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
fillOpacity={0.6}
|
fillOpacity={0.6}
|
||||||
name="Revenue"
|
name="Revenue"
|
||||||
/>
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="cumulativeRevenue"
|
||||||
|
stroke="#059669"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="5 5"
|
||||||
|
name="Cumulative Revenue"
|
||||||
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -278,6 +329,28 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }}
|
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!hiddenLines.has('cumulativeRevenue') && (
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="cumulativeRevenue"
|
||||||
|
stroke="#059669"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="5 5"
|
||||||
|
name="Cumulative Revenue"
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!hiddenLines.has('cumulativeProfit') && (
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="cumulativeProfit"
|
||||||
|
stroke="#1E40AF"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="5 5"
|
||||||
|
name="Cumulative Profit"
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
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 { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes';
|
||||||
import { formatISK } from '@/utils/priceUtils';
|
import { formatISK } from '@/utils/priceUtils';
|
||||||
import { getStatusPriority } from '@/utils/jobStatusUtils';
|
import { getStatusPriority } from '@/utils/jobStatusUtils';
|
||||||
@@ -16,6 +16,7 @@ import { useJobs } from '@/hooks/useDataService';
|
|||||||
import { useJobMetrics } from '@/hooks/useJobMetrics';
|
import { useJobMetrics } from '@/hooks/useJobMetrics';
|
||||||
import SearchOverlay from '@/components/SearchOverlay';
|
import SearchOverlay from '@/components/SearchOverlay';
|
||||||
import RecapPopover from '@/components/RecapPopover';
|
import RecapPopover from '@/components/RecapPopover';
|
||||||
|
import TransactionChart from '@/components/TransactionChart';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const {
|
const {
|
||||||
@@ -35,6 +36,8 @@ const Index = () => {
|
|||||||
const [showBatchForm, setShowBatchForm] = useState(false);
|
const [showBatchForm, setShowBatchForm] = useState(false);
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [totalRevenueChartOpen, setTotalRevenueChartOpen] = useState(false);
|
||||||
|
const [totalProfitChartOpen, setTotalProfitChartOpen] = useState(false);
|
||||||
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>(() => {
|
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>(() => {
|
||||||
const saved = localStorage.getItem('jobGroupsCollapsed');
|
const saved = localStorage.getItem('jobGroupsCollapsed');
|
||||||
return saved ? JSON.parse(saved) : {};
|
return saved ? JSON.parse(saved) : {};
|
||||||
@@ -220,6 +223,14 @@ const Index = () => {
|
|||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-5 h-5" />
|
<TrendingUp className="w-5 h-5" />
|
||||||
Total Revenue
|
Total Revenue
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 ml-auto"
|
||||||
|
onClick={() => setTotalRevenueChartOpen(true)}
|
||||||
|
>
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -239,6 +250,14 @@ const Index = () => {
|
|||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Briefcase className="w-5 h-5" />
|
<Briefcase className="w-5 h-5" />
|
||||||
Total Profit
|
Total Profit
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 ml-auto"
|
||||||
|
onClick={() => setTotalProfitChartOpen(true)}
|
||||||
|
>
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -321,6 +340,20 @@ const Index = () => {
|
|||||||
onTransactionsAssigned={handleBatchTransactionsAssigned}
|
onTransactionsAssigned={handleBatchTransactionsAssigned}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TransactionChart
|
||||||
|
jobs={regularJobs}
|
||||||
|
type="total-revenue"
|
||||||
|
isOpen={totalRevenueChartOpen}
|
||||||
|
onClose={() => setTotalRevenueChartOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TransactionChart
|
||||||
|
jobs={regularJobs}
|
||||||
|
type="total-profit"
|
||||||
|
isOpen={totalProfitChartOpen}
|
||||||
|
onClose={() => setTotalProfitChartOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user