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 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<TransactionChartProps> = ({
|
||||
});
|
||||
|
||||
// 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<TransactionChartProps> = ({
|
||||
: 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<TransactionChartProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
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<TransactionChartProps> = ({
|
||||
: 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<TransactionChartProps> = ({
|
||||
labelStyle={{ color: '#F3F4F6' }}
|
||||
contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }}
|
||||
/>
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="costs"
|
||||
@@ -206,6 +238,15 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
||||
fillOpacity={0.6}
|
||||
name="Costs"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cumulativeCosts"
|
||||
stroke="#DC2626"
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
name="Cumulative Costs"
|
||||
/>
|
||||
</AreaChart>
|
||||
);
|
||||
}
|
||||
@@ -221,6 +262,7 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
||||
labelStyle={{ color: '#F3F4F6' }}
|
||||
contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151' }}
|
||||
/>
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
@@ -229,6 +271,15 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
||||
fillOpacity={0.6}
|
||||
name="Revenue"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cumulativeRevenue"
|
||||
stroke="#059669"
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
name="Cumulative Revenue"
|
||||
/>
|
||||
</AreaChart>
|
||||
);
|
||||
}
|
||||
@@ -278,6 +329,28 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@@ -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<Record<string, boolean>>(() => {
|
||||
const saved = localStorage.getItem('jobGroupsCollapsed');
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
@@ -220,6 +223,14 @@ const Index = () => {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -239,6 +250,14 @@ const Index = () => {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Briefcase className="w-5 h-5" />
|
||||
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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -321,6 +340,20 @@ const Index = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user