diff --git a/src/components/JobCardHeader.tsx b/src/components/JobCardHeader.tsx index 8a24c2a..214ecb1 100644 --- a/src/components/JobCardHeader.tsx +++ b/src/components/JobCardHeader.tsx @@ -1,12 +1,15 @@ import { CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Copy } from 'lucide-react'; +import { Copy, BarChart3 } from 'lucide-react'; import { IndJob } from '@/lib/types'; import { useClipboard } from '@/hooks/useClipboard'; +import { useJobs } from '@/hooks/useDataService'; +import { useState } from 'react'; import JobStatusDropdown from './JobStatusDropdown'; import BOMActions from './BOMActions'; import EditableProduced from './EditableProduced'; +import TransactionChart from './TransactionChart'; interface JobCardHeaderProps { job: IndJob; @@ -24,6 +27,8 @@ const JobCardHeader: React.FC = ({ onImportBOM }) => { const { copying, copyToClipboard } = useClipboard(); + const { jobs } = useJobs(); + const [overviewChartOpen, setOverviewChartOpen] = useState(false); const sortedIncome = [...job.income].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() @@ -55,8 +60,16 @@ const JobCardHeader: React.FC = ({ Produced: - + Sold: {itemsSold.toLocaleString()} +
setOverviewChartOpen(true)} + data-no-navigate + title="View overview charts" + > + +
@@ -84,6 +97,13 @@ const JobCardHeader: React.FC = ({ + + setOverviewChartOpen(false)} + /> ); }; diff --git a/src/components/JobCardMetrics.tsx b/src/components/JobCardMetrics.tsx index e03afde..4fd3fcc 100644 --- a/src/components/JobCardMetrics.tsx +++ b/src/components/JobCardMetrics.tsx @@ -4,7 +4,9 @@ import { IndJob } from '@/lib/types'; import { Input } from '@/components/ui/input'; import { useJobs } from '@/hooks/useDataService'; import { useToast } from '@/hooks/use-toast'; +import { BarChart3 } from 'lucide-react'; import JobTransactionPopover from './JobTransactionPopover'; +import TransactionChart from './TransactionChart'; interface JobCardMetricsProps { job: IndJob; @@ -13,6 +15,7 @@ interface JobCardMetricsProps { const JobCardMetrics: React.FC = ({ job }) => { const [editingField, setEditingField] = useState(null); const [tempValues, setTempValues] = useState<{ [key: string]: string }>({}); + const [chartModal, setChartModal] = useState<{ type: 'costs' | 'revenue' | 'profit'; isOpen: boolean }>({ type: 'costs', isOpen: false }); const { updateJob } = useJobs(); const { toast } = useToast(); @@ -77,10 +80,25 @@ const JobCardMetrics: React.FC = ({ job }) => { } }; + const openChart = (type: 'costs' | 'revenue' | 'profit') => { + setChartModal({ type, isOpen: true }); + }; + + const closeChart = () => { + setChartModal({ type: 'costs', isOpen: false }); + }; + return (
-
Costs
+
+ Costs + openChart('costs')} + data-no-navigate + /> +
{formatISK(totalExpenditure)} @@ -121,7 +139,14 @@ const JobCardMetrics: React.FC = ({ job }) => { )}
-
Revenue
+
+ Revenue + openChart('revenue')} + data-no-navigate + /> +
{formatISK(totalIncome)} @@ -177,7 +202,14 @@ const JobCardMetrics: React.FC = ({ job }) => { )}
-
Profit
+
+ Profit + openChart('profit')} + data-no-navigate + /> +
= 0 ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300'}`} data-no-navigate> {formatISK(profit)} @@ -192,6 +224,13 @@ const JobCardMetrics: React.FC = ({ job }) => {
)}
+ +
); }; diff --git a/src/components/TransactionChart.tsx b/src/components/TransactionChart.tsx new file mode 100644 index 0000000..35c5c23 --- /dev/null +++ b/src/components/TransactionChart.tsx @@ -0,0 +1,260 @@ +import React, { useState } from 'react'; +import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { formatISK } from '@/utils/priceUtils'; +import { IndJob } from '@/lib/types'; +import { format, parseISO } from 'date-fns'; + +interface TransactionChartProps { + job?: IndJob; + jobs?: IndJob[]; + type: 'costs' | 'revenue' | 'profit' | 'overview'; + isOpen: boolean; + onClose: () => void; +} + +const TransactionChart: React.FC = ({ + job, + jobs, + type, + isOpen, + onClose +}) => { + const [hiddenLines, setHiddenLines] = useState>(new Set()); + + const toggleLine = (dataKey: string) => { + const newHidden = new Set(hiddenLines); + if (newHidden.has(dataKey)) { + newHidden.delete(dataKey); + } else { + newHidden.add(dataKey); + } + setHiddenLines(newHidden); + }; + + const getJobChartData = (job: IndJob) => { + // Combine all transactions and group by date + const allTransactions = [ + ...job.expenditures.map(tx => ({ ...tx, type: 'expenditure' })), + ...job.income.map(tx => ({ ...tx, type: 'income' })) + ]; + + // Group by date + const dateMap = new Map(); + + allTransactions.forEach(tx => { + const dateStr = format(parseISO(tx.date), 'yyyy-MM-dd'); + if (!dateMap.has(dateStr)) { + dateMap.set(dateStr, { costs: 0, revenue: 0, date: dateStr }); + } + const entry = dateMap.get(dateStr)!; + if (tx.type === 'expenditure') { + entry.costs += tx.totalPrice; + } else { + entry.revenue += tx.totalPrice; + } + }); + + // Convert to array and calculate profit + return Array.from(dateMap.values()) + .map(entry => ({ + ...entry, + profit: entry.revenue - entry.costs, + formattedDate: format(new Date(entry.date), 'MMM dd') + })) + .sort((a, b) => a.date.localeCompare(b.date)); + }; + + const getOverviewChartData = (jobs: IndJob[]) => { + const dateMap = new Map(); + + jobs.forEach(job => { + const jobData = getJobChartData(job); + jobData.forEach(entry => { + if (!dateMap.has(entry.date)) { + dateMap.set(entry.date, { revenue: 0, profit: 0, date: entry.date }); + } + const overviewEntry = dateMap.get(entry.date)!; + overviewEntry.revenue += entry.revenue; + overviewEntry.profit += entry.profit; + }); + }); + + return Array.from(dateMap.values()) + .map(entry => ({ + ...entry, + formattedDate: format(new Date(entry.date), 'MMM dd') + })) + .sort((a, b) => a.date.localeCompare(b.date)); + }; + + const formatTooltipValue = (value: number) => formatISK(value); + + const data = type === 'overview' && jobs ? getOverviewChartData(jobs) : job ? getJobChartData(job) : []; + + const getTitle = () => { + if (type === 'overview') return 'Overview - Revenue & Profit Over Time'; + if (job) { + switch (type) { + case 'costs': return `${job.outputItem} - Costs Over Time`; + case 'revenue': return `${job.outputItem} - Revenue Over Time`; + case 'profit': return `${job.outputItem} - Costs, Revenue & Profit Over Time`; + default: return `${job.outputItem} - Transaction History`; + } + } + return 'Transaction History'; + }; + + const renderChart = () => { + if (type === 'overview') { + return ( + + + + + + + + + + ); + } + + if (type === 'costs') { + return ( + + + + + + + + ); + } + + if (type === 'revenue') { + return ( + + + + + + + + ); + } + + // Combined profit chart (costs, revenue, profit) + return ( + + + + + + toggleLine(e.dataKey as string)} + wrapperStyle={{ cursor: 'pointer' }} + /> + {!hiddenLines.has('costs') && ( + + )} + {!hiddenLines.has('revenue') && ( + + )} + {!hiddenLines.has('profit') && ( + + )} + + ); + }; + + return ( + + + + {getTitle()} + +
+ + {renderChart()} + +
+
+ +
+
+
+ ); +}; + +export default TransactionChart; \ No newline at end of file