diff --git a/src/components/BatchTransactionForm.tsx b/src/components/BatchTransactionForm.tsx index 0708366..af8369e 100644 --- a/src/components/BatchTransactionForm.tsx +++ b/src/components/BatchTransactionForm.tsx @@ -1,14 +1,13 @@ -import { useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { Badge } from '@/components/ui/badge'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { parseTransactionLine, formatISK, PastedTransaction } from '@/utils/priceUtils'; -import { IndTransactionRecordNoId, IndJobStatusOptions } from '@/lib/pbtypes'; + +import { Card, CardContent } from '@/components/ui/card'; +import { IndTransactionRecordNoId } from '@/lib/pbtypes'; import { IndJob } from '@/lib/types'; -import { X } from 'lucide-react'; +import { useBatchTransactionLogic } from '@/hooks/useBatchTransactionLogic'; +import BatchTransactionHeader from '@/components/batch-transaction/BatchTransactionHeader'; +import PasteTransactionInput from '@/components/batch-transaction/PasteTransactionInput'; +import TransactionStats from '@/components/batch-transaction/TransactionStats'; +import TransactionTable from '@/components/batch-transaction/TransactionTable'; +import TransactionActions from '@/components/batch-transaction/TransactionActions'; interface BatchTransactionFormProps { onClose: () => void; @@ -16,179 +15,24 @@ interface BatchTransactionFormProps { jobs: IndJob[]; } -interface TransactionGroup { - itemName: string; - transactions: PastedTransaction[]; - totalQuantity: number; - totalValue: number; -} - const BatchTransactionForm: React.FC = ({ onClose, onTransactionsAssigned, jobs }) => { - const [pastedData, setPastedData] = useState(''); - const [transactionGroups, setTransactionGroups] = useState([]); - const [duplicatesFound, setDuplicatesFound] = useState(0); - - // Filter jobs that are either running, selling, or tracked - const eligibleJobs = jobs.filter(job => - job.status === IndJobStatusOptions.Running || - job.status === IndJobStatusOptions.Selling || - job.status === IndJobStatusOptions.Tracked - ); - - const findMatchingJob = (itemName: string): string | undefined => { - // First try exact match - const exactMatch = eligibleJobs.find(job => job.outputItem === itemName); - if (exactMatch) return exactMatch.id; - - // Then try case-insensitive match - const caseInsensitiveMatch = eligibleJobs.find(job => - job.outputItem.toLowerCase() === itemName.toLowerCase() - ); - if (caseInsensitiveMatch) return caseInsensitiveMatch.id; - - return undefined; - }; - - const normalizeDate = (dateStr: string): string => { - // Convert any ISO date string to consistent format with space - return dateStr.replace('T', ' '); - }; - - const createTransactionKey = (parsed: PastedTransaction): string => { - if (!parsed) return ''; - const key = [ - normalizeDate(parsed.date.toString()), - parsed.itemName, - parsed.quantity.toString(), - parsed.totalPrice.toString(), - parsed.buyer, - parsed.location - ].join('|'); - return key; - }; - - const createTransactionKeyFromRecord = (tx: IndTransactionRecordNoId): string => { - const key = [ - normalizeDate(tx.date), - tx.itemName, - tx.quantity.toString(), - tx.totalPrice.toString(), - tx.buyer, - tx.location - ].join('|'); - return key; - }; - - const handlePaste = (value: string) => { - setPastedData(value); - const lines = value.trim().split('\n'); - const pasteTransactionMap = new Map(); - - // STEP 1: First combine all identical transactions within the pasted data - lines.forEach((line) => { - const parsed: PastedTransaction | null = parseTransactionLine(line); - if (parsed) { - const transactionKey: string = createTransactionKey(parsed); - - if (pasteTransactionMap.has(transactionKey)) { - // Merge with existing transaction in paste - const existing = pasteTransactionMap.get(transactionKey)!; - existing.quantity += parsed.quantity; - existing.totalPrice += Math.abs(parsed.totalPrice); - const newKey = createTransactionKey(existing); - pasteTransactionMap.set(newKey, existing); - pasteTransactionMap.delete(transactionKey); // Remove old key - } else { - // Add new transaction - pasteTransactionMap.set(transactionKey, parsed); - } - } - }); - - // STEP 2: Identify which jobs these transactions belong to - const relevantJobIds = new Set(); - pasteTransactionMap.forEach((transaction) => { - const matchingJobId = findMatchingJob(transaction.itemName); - if (matchingJobId) { - relevantJobIds.add(matchingJobId); - transaction.assignedJobId = matchingJobId; - } - }); - - // STEP 3: Only check against transactions from relevant jobs - const existingTransactionKeys = new Set(); - eligibleJobs.forEach(job => { - if (relevantJobIds.has(job.id)) { - job.income.forEach(tx => { - const key = createTransactionKeyFromRecord(tx); - existingTransactionKeys.add(key); - }); - } - }); - - // STEP 4: Mark duplicates and assign jobs - let duplicates = 0; - pasteTransactionMap.forEach((transaction, key) => { - const isDuplicate = existingTransactionKeys.has(key); - transaction.isDuplicate = isDuplicate; - - if (isDuplicate) { - duplicates++; - transaction.assignedJobId = undefined; - } else if (!!transaction.assignedJobId) { - transaction.assignedJobId = findMatchingJob(transaction.itemName); - } - }); - - // Convert map to array for display - const transactionList = Array.from(pasteTransactionMap.values()); - setDuplicatesFound(duplicates); - - // 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); - }; - - const handleAssignJob = (groupIndex: number, jobId: string) => { - setTransactionGroups(prev => { - const newGroups = [...prev]; - newGroups[groupIndex].transactions.forEach(tx => { - tx.assignedJobId = jobId; - }); - return newGroups; - }); - }; + const { + pastedData, + transactionGroups, + duplicatesFound, + eligibleJobs, + handlePaste, + handleAssignJob, + getAssignments, + canSubmit + } = useBatchTransactionLogic(jobs); const handleSubmit = () => { - // Group transactions by assigned job - const assignments = transactionGroups - .flatMap(group => group.transactions) - .filter(tx => tx.assignedJobId) - .reduce((acc, tx) => { - const jobId = tx.assignedJobId!; - const existing = acc.find(a => a.jobId === jobId); - if (existing) { - existing.transactions.push(tx); - } else { - acc.push({ jobId, transactions: [tx] }); - } - return acc; - }, [] as { jobId: string, transactions: IndTransactionRecordNoId[] }[]); - + const assignments = getAssignments(); onTransactionsAssigned(assignments); onClose(); }; - const allAssigned = transactionGroups.every(group => - group.transactions.every(tx => tx.assignedJobId) - ); - return (
= ({ onClose, on className="bg-gray-900 border-gray-700 text-white w-full max-w-4xl max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()} > - - Batch Transaction Assignment - - + + -
- -