diff --git a/src/components/BatchTransactionForm.tsx b/src/components/BatchTransactionForm.tsx index d331da8..e52db95 100644 --- a/src/components/BatchTransactionForm.tsx +++ b/src/components/BatchTransactionForm.tsx @@ -107,90 +107,68 @@ const BatchTransactionForm: React.FC = ({ onClose, on const lines = value.trim().split('\n'); const transactions: ParsedTransaction[] = []; const seenTransactions = new Set(); + const pasteTransactionMap = new Map(); // Pre-populate seenTransactions with existing transactions from jobs - console.log('Starting to check existing transactions from jobs...'); eligibleJobs.forEach(job => { - console.log(`Checking job ${job.id} (${job.outputItem}) with ${job.income.length} income transactions`); job.income.forEach(tx => { const key = createTransactionKeyFromRecord(tx); seenTransactions.add(key); - console.log('Added existing transaction key to Set:', key); }); }); - console.log('Finished adding existing transactions. Set size:', seenTransactions.size); - console.log('Current Set contents:', Array.from(seenTransactions)); let duplicates = 0; lines.forEach((line, index) => { - console.log(`\nProcessing line ${index + 1}:`, line); const parsed = parseTransactionLine(line); if (parsed) { const transactionKey = createTransactionKey(parsed); const isDuplicate = seenTransactions.has(transactionKey); - console.log('Transaction check:', { - key: transactionKey, - isDuplicate, - setSize: seenTransactions.size, - setContains: Array.from(seenTransactions).includes(transactionKey) - }); if (isDuplicate) { - console.log('DUPLICATE FOUND:', transactionKey); duplicates++; } - if (!isDuplicate) { - console.log('New transaction - Adding to Set:', transactionKey); - seenTransactions.add(transactionKey); + // Check if this exact transaction already exists in our paste data + if (pasteTransactionMap.has(transactionKey)) { + // Merge with existing transaction in paste + const existing = pasteTransactionMap.get(transactionKey)!; + existing.quantity += parsed.quantity; + existing.totalPrice += Math.abs(parsed.totalAmount); + } else { + // Add new transaction + const newTransaction: ParsedTransaction = { + date: parsed.date.toISOString(), + quantity: parsed.quantity, + itemName: parsed.itemName, + unitPrice: parsed.unitPrice, + totalPrice: Math.abs(parsed.totalAmount), + buyer: parsed.buyer, + location: parsed.location, + corporation: parsed.corporation, + wallet: parsed.wallet, + assignedJobId: !isDuplicate ? findMatchingJob(parsed.itemName) : undefined, + isDuplicate + }; + pasteTransactionMap.set(transactionKey, newTransaction); + + if (!isDuplicate) { + seenTransactions.add(transactionKey); + } } - - const matchingJobId = !isDuplicate ? findMatchingJob(parsed.itemName) : undefined; - - transactions.push({ - date: parsed.date.toISOString(), - quantity: parsed.quantity, - itemName: parsed.itemName, - unitPrice: parsed.unitPrice, - totalPrice: Math.abs(parsed.totalAmount), - buyer: parsed.buyer, - location: parsed.location, - corporation: parsed.corporation, - wallet: parsed.wallet, - assignedJobId: matchingJobId, - isDuplicate - }); - } else { - console.log('Failed to parse line:', line); } }); - console.log('Final results:', { - processedLines: lines.length, - validTransactions: transactions.length, - duplicatesFound: duplicates, - finalSetSize: seenTransactions.size - }); - + // Convert map to array for display - each transaction is individual + const transactionList = Array.from(pasteTransactionMap.values()); setDuplicatesFound(duplicates); - // Group transactions by item name - const groups = transactions.reduce((acc, tx) => { - const existing = acc.find(g => g.itemName === tx.itemName); - if (existing) { - existing.transactions.push(tx); - existing.totalQuantity += tx.quantity; - existing.totalValue += tx.totalPrice; - } else { - acc.push({ - itemName: tx.itemName, - transactions: [tx], - totalQuantity: tx.quantity, - totalValue: tx.totalPrice - }); - } - return acc; - }, [] as TransactionGroup[]); + // Create individual transaction groups (no grouping by item name) + const groups = transactionList.map(tx => ({ + itemName: tx.itemName, + transactions: [tx], + totalQuantity: tx.quantity, + totalValue: tx.totalPrice + })); setTransactionGroups(groups); }; @@ -261,7 +239,7 @@ const BatchTransactionForm: React.FC = ({ onClose, on
- {transactionGroups.length} item types found + {transactionGroups.length} transactions found {duplicatesFound > 0 && ( diff --git a/src/components/JobCardHeader.tsx b/src/components/JobCardHeader.tsx index d96a31e..26766f0 100644 --- a/src/components/JobCardHeader.tsx +++ b/src/components/JobCardHeader.tsx @@ -256,7 +256,7 @@ const JobCardHeader: React.FC = ({ ) : ( diff --git a/src/components/JobCategory.tsx b/src/components/JobCategory.tsx new file mode 100644 index 0000000..2646a1d --- /dev/null +++ b/src/components/JobCategory.tsx @@ -0,0 +1,107 @@ +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { IndJob, IndJobStatusOptions } from '@/types/industry'; +import JobCard from './JobCard'; +import { formatISK } from '@/utils/currency'; +import { ChevronDown, ChevronRight } from 'lucide-react'; + +interface JobCategoryProps { + status: IndJobStatusOptions; + jobs: IndJob[]; + onEdit: (job: any) => void; + onDelete: (jobId: string) => void; + onUpdateProduced?: (jobId: string, produced: number) => void; + onImportBOM?: (jobId: string, items: { name: string; quantity: number }[]) => void; +} + +export function JobCategory({ status, jobs, onEdit, onDelete, onUpdateProduced, onImportBOM }: JobCategoryProps) { + const [isCollapsed, setIsCollapsed] = useState(false); + + // Load collapsed state from localStorage + useEffect(() => { + const stored = localStorage.getItem(`job-category-collapsed-${status}`); + if (stored) { + setIsCollapsed(JSON.parse(stored)); + } + }, [status]); + + // Save collapsed state to localStorage + const toggleCollapsed = () => { + const newCollapsed = !isCollapsed; + setIsCollapsed(newCollapsed); + localStorage.setItem(`job-category-collapsed-${status}`, JSON.stringify(newCollapsed)); + }; + + // Calculate totals (excluding Tracked status) + const shouldIncludeInTotals = status !== IndJobStatusOptions.Tracked; + const totalExpenditure = shouldIncludeInTotals + ? jobs.reduce((sum, job) => sum + (job.expenditures?.reduce((s, t) => s + t.totalPrice, 0) || 0), 0) + : 0; + const totalIncome = shouldIncludeInTotals + ? jobs.reduce((sum, job) => sum + (job.income?.reduce((s, t) => s + t.totalPrice, 0) || 0), 0) + : 0; + const totalProfit = totalIncome - totalExpenditure; + + const getCategoryColor = (status: IndJobStatusOptions) => { + switch (status) { + case IndJobStatusOptions.Planned: return 'border-l-muted'; + case IndJobStatusOptions.Acquisition: return 'border-l-warning'; + case IndJobStatusOptions.Running: return 'border-l-primary'; + case IndJobStatusOptions.Done: return 'border-l-success'; + case IndJobStatusOptions.Selling: return 'border-l-accent'; + case IndJobStatusOptions.Closed: return 'border-l-muted'; + case IndJobStatusOptions.Tracked: return 'border-l-secondary'; + default: return 'border-l-muted'; + } + }; + + if (jobs.length === 0) return null; + + return ( + + + +
+ {isCollapsed ? : } + {status} ({jobs.length}) +
+ + {shouldIncludeInTotals && ( +
+ Cost: {formatISK(totalExpenditure)} + Income: {formatISK(totalIncome)} + = 0 ? 'text-success' : 'text-destructive'}> + Profit: {formatISK(totalProfit)} + +
+ )} + + {status === IndJobStatusOptions.Tracked && ( + + (Not included in totals) + + )} +
+
+ + {!isCollapsed && ( + + {jobs.map(job => ( + + ))} + + )} +
+ ); +} \ No newline at end of file