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

View File

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