From d645dbde2f36895e44d45dacbcec03f49f67b447 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:29:39 +0000 Subject: [PATCH] Refactor: Split BatchExpenditureForm into smaller components Refactors the BatchExpenditureForm component into smaller, more manageable files to improve code organization and readability. --- src/components/BatchExpenditureForm.tsx | 351 ++---------------- .../BatchExpenditureHeader.tsx | 26 ++ .../batch-expenditure/ExpenditureActions.tsx | 31 ++ .../batch-expenditure/ExpenditureStats.tsx | 26 ++ .../batch-expenditure/ExpenditureTable.tsx | 106 ++++++ .../PasteExpenditureInput.tsx | 36 ++ src/hooks/useBatchExpenditureLogic.ts | 192 ++++++++++ 7 files changed, 452 insertions(+), 316 deletions(-) create mode 100644 src/components/batch-expenditure/BatchExpenditureHeader.tsx create mode 100644 src/components/batch-expenditure/ExpenditureActions.tsx create mode 100644 src/components/batch-expenditure/ExpenditureStats.tsx create mode 100644 src/components/batch-expenditure/ExpenditureTable.tsx create mode 100644 src/components/batch-expenditure/PasteExpenditureInput.tsx create mode 100644 src/hooks/useBatchExpenditureLogic.ts diff --git a/src/components/BatchExpenditureForm.tsx b/src/components/BatchExpenditureForm.tsx index 9577e8e..d575d52 100644 --- a/src/components/BatchExpenditureForm.tsx +++ b/src/components/BatchExpenditureForm.tsx @@ -1,14 +1,13 @@ -import { useState, useRef, 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, 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 BatchExpenditureHeader from './batch-expenditure/BatchExpenditureHeader'; +import PasteExpenditureInput from './batch-expenditure/PasteExpenditureInput'; +import ExpenditureStats from './batch-expenditure/ExpenditureStats'; +import ExpenditureTable from './batch-expenditure/ExpenditureTable'; +import ExpenditureActions from './batch-expenditure/ExpenditureActions'; +import { useBatchExpenditureLogic } from '@/hooks/useBatchExpenditureLogic'; interface BatchExpenditureFormProps { onClose: () => void; @@ -16,188 +15,16 @@ interface BatchExpenditureFormProps { jobs: IndJob[]; } -interface TransactionGroup { - itemName: string; - transactions: PastedTransaction[]; - totalQuantity: number; - totalValue: number; -} - const BatchExpenditureForm: React.FC = ({ onClose, onTransactionsAssigned, jobs }) => { - const [pastedData, setPastedData] = useState(''); - const [transactionGroups, setTransactionGroups] = useState([]); - const [duplicatesFound, setDuplicatesFound] = useState(0); - const textareaRef = useRef(null); - - // Auto focus the textarea when component mounts - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.focus(); - } - }, []); - - // Filter jobs that are in acquisition status - const eligibleJobs = jobs.filter(job => job.status === IndJobStatusOptions.Acquisition); - - const findMatchingJob = (itemName: string): string | undefined => { - // Find jobs where the item is in the bill of materials and not satisfied - for (const job of eligibleJobs) { - const billItem = job.billOfMaterials?.find(item => - item.name.toLowerCase() === itemName.toLowerCase() - ); - - if (billItem) { - // Check if this material is already satisfied - const ownedQuantity = job.expenditures?.reduce((total, exp) => - exp.itemName.toLowerCase() === itemName.toLowerCase() ? total + exp.quantity : total, 0 - ) || 0; - - // Only return this job if we still need more of this material - if (ownedQuantity < billItem.quantity) { - return job.id; - } - } - } - return undefined; - }; - - const normalizeDate = (dateStr: string): string => { - return dateStr.replace('T', ' '); - }; - - const createTransactionKey = (parsed: PastedTransaction): string => { - if (!parsed) return ''; - const key = [ - normalizeDate(parsed.date.toString()), - parsed.itemName, - parsed.quantity.toString(), - Math.abs(parsed.totalPrice).toString(), // Use absolute value for expenditures - parsed.buyer, - parsed.location - ].join('|'); - return key; - }; - - const createTransactionKeyFromRecord = (tx: IndTransactionRecordNoId): string => { - const key = [ - normalizeDate(tx.date), - tx.itemName, - tx.quantity.toString(), - Math.abs(tx.totalPrice).toString(), // Use absolute value for expenditures - tx.buyer, - tx.location - ].join('|'); - return key; - }; - - const handlePaste = (value: string) => { - console.log('Handling paste with value:', value); - setPastedData(value); - const lines = value.trim().split('\n').filter(line => line.trim().length > 0); - console.log('Processing lines:', lines); - - const pasteTransactionMap = new Map(); - - // STEP 1: Combine identical transactions within the pasted data - lines.forEach((line, index) => { - console.log(`Processing line ${index}:`, line); - const parsed: PastedTransaction | null = parseTransactionLine(line); - - if (parsed) { - console.log('Parsed transaction:', parsed); - - // For expenditures, we expect negative amounts, but handle both cases - const isExpenditure = parsed.totalPrice < 0; - if (isExpenditure) { - // Convert to positive values for expenditures - parsed.totalPrice = Math.abs(parsed.totalPrice); - parsed.unitPrice = Math.abs(parsed.unitPrice); - } else { - // If it's positive, we might still want to treat it as expenditure - // based on context, but let's keep it as is for now - console.log('Transaction has positive amount, treating as expenditure anyway'); - } - - const transactionKey: string = createTransactionKey(parsed); - console.log('Transaction key:', transactionKey); - - if (pasteTransactionMap.has(transactionKey)) { - const existing = pasteTransactionMap.get(transactionKey)!; - existing.quantity += parsed.quantity; - existing.totalPrice += parsed.totalPrice; - const newKey = createTransactionKey(existing); - pasteTransactionMap.set(newKey, existing); - pasteTransactionMap.delete(transactionKey); - } else { - pasteTransactionMap.set(transactionKey, parsed); - } - } else { - console.log('Failed to parse line:', line); - } - }); - - console.log('Parsed transactions map:', pasteTransactionMap); - - // 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: Check against existing expenditures from relevant jobs - const existingTransactionKeys = new Set(); - eligibleJobs.forEach(job => { - if (relevantJobIds.has(job.id)) { - job.expenditures?.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); - } - }); - - const transactionList = Array.from(pasteTransactionMap.values()); - console.log('Final transaction list:', transactionList); - setDuplicatesFound(duplicates); - - // Create individual transaction groups - const groups = transactionList.map(tx => ({ - itemName: tx.itemName, - transactions: [tx], - totalQuantity: tx.quantity, - totalValue: tx.totalPrice - })); - - console.log('Transaction groups:', groups); - 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, + canSubmit + } = useBatchExpenditureLogic(jobs); const handleSubmit = () => { // Group transactions by assigned job @@ -228,137 +55,29 @@ const BatchExpenditureForm: React.FC = ({ 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 Expenditure Assignment - - + -
- -