diff --git a/src/components/JobCardMetrics.tsx b/src/components/JobCardMetrics.tsx index abb4f67..d47a07f 100644 --- a/src/components/JobCardMetrics.tsx +++ b/src/components/JobCardMetrics.tsx @@ -1,9 +1,11 @@ + import { useState } from 'react'; import { formatISK, parseISKAmount } from '@/utils/priceUtils'; import { IndJob } from '@/lib/types'; import { Input } from '@/components/ui/input'; import { useJobs } from '@/hooks/useDataService'; import { useToast } from '@/hooks/use-toast'; +import JobTransactionPopover from './JobTransactionPopover'; interface JobCardMetricsProps { job: IndJob; @@ -28,6 +30,7 @@ const JobCardMetrics: React.FC = ({ job }) => { const margin = totalIncome > 0 ? ((profit / totalIncome) * 100) : 0; const handleFieldClick = (fieldName: string, currentValue: number, e: React.MouseEvent) => { + e.stopPropagation(); setEditingField(fieldName); setTempValues({ ...tempValues, [fieldName]: formatISK(currentValue) }); }; @@ -65,7 +68,11 @@ const JobCardMetrics: React.FC = ({ job }) => {
Costs
-
{formatISK(totalExpenditure)}
+ +
+ {formatISK(totalExpenditure)} +
+
{job.projectedCost > 0 && (
vs {editingField === 'projectedCost' ? ( @@ -96,7 +103,11 @@ const JobCardMetrics: React.FC = ({ job }) => {
Revenue
-
{formatISK(totalIncome)}
+ +
+ {formatISK(totalIncome)} +
+
{job.projectedRevenue > 0 && (
vs {editingField === 'projectedRevenue' ? ( @@ -127,9 +138,11 @@ const JobCardMetrics: React.FC = ({ job }) => {
Profit
-
= 0 ? 'text-green-400' : 'text-red-400'}`}> - {formatISK(profit)} -
+ +
= 0 ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300'}`} data-no-navigate> + {formatISK(profit)} +
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> {margin.toFixed(1)}% margin
diff --git a/src/components/JobTransactionPopover.tsx b/src/components/JobTransactionPopover.tsx new file mode 100644 index 0000000..f6d4620 --- /dev/null +++ b/src/components/JobTransactionPopover.tsx @@ -0,0 +1,133 @@ + +import { useState } from 'react'; +import { IndJob, IndTransaction } from '@/lib/types'; +import { formatISK } from '@/utils/priceUtils'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { ChevronDown, ChevronUp } from 'lucide-react'; + +interface JobTransactionPopoverProps { + job: IndJob; + type: 'costs' | 'revenue' | 'profit'; + children: React.ReactNode; +} + +const JobTransactionPopover: React.FC = ({ + job, + type, + children +}) => { + const [sortDescending, setSortDescending] = useState(true); + + const getTransactions = () => { + switch (type) { + case 'costs': + return job.expenditures || []; + case 'revenue': + return job.income || []; + case 'profit': + return [...(job.expenditures || []), ...(job.income || [])]; + default: + return []; + } + }; + + const getTitle = () => { + switch (type) { + case 'costs': + return 'Cost Breakdown'; + case 'revenue': + return 'Revenue Breakdown'; + case 'profit': + return 'Transaction History'; + default: + return 'Transactions'; + } + }; + + const transactions = getTransactions() + .map(transaction => ({ + ...transaction, + displayValue: type === 'costs' ? transaction.totalPrice : + type === 'revenue' ? transaction.totalPrice : + transaction.totalPrice // For profit, we'll show the actual value + })) + .filter(transaction => transaction.displayValue !== 0) + .sort((a, b) => { + const aValue = Math.abs(a.displayValue); + const bValue = Math.abs(b.displayValue); + return sortDescending ? bValue - aValue : aValue - bValue; + }); + + const toggleSort = () => { + setSortDescending(!sortDescending); + }; + + const getTransactionColor = (transaction: any) => { + if (type === 'profit') { + // For profit view, show costs as red and revenue as green + const isExpenditure = (job.expenditures || []).some(exp => exp.id === transaction.id); + return isExpenditure ? 'text-red-400' : 'text-green-400'; + } + return type === 'costs' ? 'text-red-400' : 'text-green-400'; + }; + + const formatTransactionValue = (transaction: any) => { + if (type === 'profit') { + const isExpenditure = (job.expenditures || []).some(exp => exp.id === transaction.id); + return isExpenditure ? `-${formatISK(transaction.totalPrice)}` : formatISK(transaction.totalPrice); + } + return formatISK(transaction.displayValue); + }; + + return ( + + + {children} + + + + + {getTitle()} + + +
+ {job.outputItem} (ID: {job.id}) +
+
+ + {transactions.length === 0 ? ( +

No transactions to display

+ ) : ( + transactions.map((transaction) => ( +
+
+
+ {transaction.item} +
+
+ Qty: {transaction.quantity.toLocaleString()} • {new Date(transaction.date).toLocaleDateString()} +
+
+
+ {formatTransactionValue(transaction)} +
+
+ )) + )} +
+
+
+ ); +}; + +export default JobTransactionPopover; diff --git a/src/lib/types.ts b/src/lib/types.ts index 1eaa300..f2ff4f2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,3 +1,4 @@ + import { IndJobStatusOptions, IndTransactionRecord } from "./pbtypes" import { IsoDateString } from "./pbtypes" import { IndBillitemRecord } from "./pbtypes" @@ -20,4 +21,6 @@ export type IndJob = { updated?: IsoDateString projectedCost?: number projectedRevenue?: number -} \ No newline at end of file +} + +export type IndTransaction = IndTransactionRecord;