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:
gpt-engineer-app[bot]
2025-07-09 01:20:52 +00:00
committed by PhatPhuckDave
parent 86a9fc4382
commit 0c69b59677
2 changed files with 114 additions and 8 deletions

View File

@@ -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>
);
};

View File

@@ -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>
);
};