From 8c4ab2ba29338df3ed824ea779ab24aa695d9e4a Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Sun, 6 Jul 2025 03:25:10 +0200 Subject: [PATCH] Implement batch transactionator form --- .../src/components/BatchTransactionForm.tsx | 369 ++++++++++++++++++ frontend/src/pages/Index.tsx | 61 ++- 2 files changed, 419 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/BatchTransactionForm.tsx diff --git a/frontend/src/components/BatchTransactionForm.tsx b/frontend/src/components/BatchTransactionForm.tsx new file mode 100644 index 0000000..70cef6a --- /dev/null +++ b/frontend/src/components/BatchTransactionForm.tsx @@ -0,0 +1,369 @@ +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 } 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(); + + // 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); + } + + 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 + }); + + 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[]); + + 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 + + + +
+ +