import { useState, useEffect } 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 } from '@/utils/priceUtils'; import { IndTransactionRecordNoId, IndJobStatusOptions } from '@/lib/pbtypes'; import { IndJob } from '@/lib/types'; import { X } from 'lucide-react'; interface BatchTransactionFormProps { onClose: () => void; onTransactionsAssigned: (assignments: { jobId: string, transactions: IndTransactionRecordNoId[] }[]) => void; jobs: IndJob[]; } interface ParsedTransaction extends IndTransactionRecordNoId { assignedJobId?: string; isDuplicate?: boolean; } interface TransactionGroup { itemName: string; transactions: ParsedTransaction[]; 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: ReturnType): string => { if (!parsed) return ''; const key = [ normalizeDate(parsed.date.toISOString()), parsed.itemName, parsed.quantity.toString(), parsed.totalAmount.toString(), parsed.buyer, parsed.location ].join('|'); console.log('Created key from parsed transaction:', { key, date: normalizeDate(parsed.date.toISOString()), itemName: parsed.itemName, quantity: parsed.quantity, totalAmount: parsed.totalAmount, buyer: parsed.buyer, location: parsed.location }); 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('|'); console.log('Created key from existing transaction:', { key, date: normalizeDate(tx.date), itemName: tx.itemName, quantity: tx.quantity, totalPrice: tx.totalPrice, buyer: tx.buyer, location: tx.location }); return key; }; const handlePaste = (value: string) => { setPastedData(value); 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 eligibleJobs.forEach(job => { job.income.forEach(tx => { const key = createTransactionKeyFromRecord(tx); seenTransactions.add(key); }); }); let duplicates = 0; lines.forEach((line, index) => { const parsed = parseTransactionLine(line); if (parsed) { const transactionKey = createTransactionKey(parsed); const isDuplicate = seenTransactions.has(transactionKey); if (isDuplicate) { duplicates++; } // 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); } } } }); // Convert map to array for display - each transaction is individual 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 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[] }[]); onTransactionsAssigned(assignments); onClose(); }; const allAssigned = transactionGroups.every(group => group.transactions.every(tx => tx.assignedJobId) ); return (
Batch Transaction Assignment