Fix display and batch import issues

Fixes "Sold" count display and improves batch transaction import handling, including duplicate transaction grouping.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-07 17:19:00 +00:00
committed by PhatPhuckDave
parent 897d15ce4d
commit a7f9bad5ea
3 changed files with 145 additions and 60 deletions

View File

@@ -107,47 +107,36 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
const lines = value.trim().split('\n'); const lines = value.trim().split('\n');
const transactions: ParsedTransaction[] = []; const transactions: ParsedTransaction[] = [];
const seenTransactions = new Set<string>(); const seenTransactions = new Set<string>();
const pasteTransactionMap = new Map<string, ParsedTransaction>();
// Pre-populate seenTransactions with existing transactions from jobs // Pre-populate seenTransactions with existing transactions from jobs
console.log('Starting to check existing transactions from jobs...');
eligibleJobs.forEach(job => { eligibleJobs.forEach(job => {
console.log(`Checking job ${job.id} (${job.outputItem}) with ${job.income.length} income transactions`);
job.income.forEach(tx => { job.income.forEach(tx => {
const key = createTransactionKeyFromRecord(tx); const key = createTransactionKeyFromRecord(tx);
seenTransactions.add(key); 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; let duplicates = 0;
lines.forEach((line, index) => { lines.forEach((line, index) => {
console.log(`\nProcessing line ${index + 1}:`, line);
const parsed = parseTransactionLine(line); const parsed = parseTransactionLine(line);
if (parsed) { if (parsed) {
const transactionKey = createTransactionKey(parsed); const transactionKey = createTransactionKey(parsed);
const isDuplicate = seenTransactions.has(transactionKey); const isDuplicate = seenTransactions.has(transactionKey);
console.log('Transaction check:', {
key: transactionKey,
isDuplicate,
setSize: seenTransactions.size,
setContains: Array.from(seenTransactions).includes(transactionKey)
});
if (isDuplicate) { if (isDuplicate) {
console.log('DUPLICATE FOUND:', transactionKey);
duplicates++; duplicates++;
} }
if (!isDuplicate) { // Check if this exact transaction already exists in our paste data
console.log('New transaction - Adding to Set:', transactionKey); if (pasteTransactionMap.has(transactionKey)) {
seenTransactions.add(transactionKey); // Merge with existing transaction in paste
} const existing = pasteTransactionMap.get(transactionKey)!;
existing.quantity += parsed.quantity;
const matchingJobId = !isDuplicate ? findMatchingJob(parsed.itemName) : undefined; existing.totalPrice += Math.abs(parsed.totalAmount);
} else {
transactions.push({ // Add new transaction
const newTransaction: ParsedTransaction = {
date: parsed.date.toISOString(), date: parsed.date.toISOString(),
quantity: parsed.quantity, quantity: parsed.quantity,
itemName: parsed.itemName, itemName: parsed.itemName,
@@ -157,40 +146,29 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
location: parsed.location, location: parsed.location,
corporation: parsed.corporation, corporation: parsed.corporation,
wallet: parsed.wallet, wallet: parsed.wallet,
assignedJobId: matchingJobId, assignedJobId: !isDuplicate ? findMatchingJob(parsed.itemName) : undefined,
isDuplicate isDuplicate
}); };
} else { pasteTransactionMap.set(transactionKey, newTransaction);
console.log('Failed to parse line:', line);
if (!isDuplicate) {
seenTransactions.add(transactionKey);
}
}
} }
}); });
console.log('Final results:', { // Convert map to array for display - each transaction is individual
processedLines: lines.length, const transactionList = Array.from(pasteTransactionMap.values());
validTransactions: transactions.length,
duplicatesFound: duplicates,
finalSetSize: seenTransactions.size
});
setDuplicatesFound(duplicates); setDuplicatesFound(duplicates);
// Group transactions by item name // Create individual transaction groups (no grouping by item name)
const groups = transactions.reduce((acc, tx) => { const groups = transactionList.map(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, itemName: tx.itemName,
transactions: [tx], transactions: [tx],
totalQuantity: tx.quantity, totalQuantity: tx.quantity,
totalValue: tx.totalPrice totalValue: tx.totalPrice
}); }));
}
return acc;
}, [] as TransactionGroup[]);
setTransactionGroups(groups); setTransactionGroups(groups);
}; };
@@ -261,7 +239,7 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Badge variant="outline" className="text-blue-400 border-blue-400"> <Badge variant="outline" className="text-blue-400 border-blue-400">
{transactionGroups.length} item types found {transactionGroups.length} transactions found
</Badge> </Badge>
{duplicatesFound > 0 && ( {duplicatesFound > 0 && (
<Badge variant="outline" className="text-yellow-400 border-yellow-400"> <Badge variant="outline" className="text-yellow-400 border-yellow-400">

View File

@@ -256,7 +256,7 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
) : ( ) : (
<span <span
onClick={handleProducedClick} onClick={handleProducedClick}
className={`inline-block min-w-[96px] h-5 leading-5 ${job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : ""}`} className={`inline-block w-20 h-5 leading-5 text-right ${job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : ""}`}
title={job.status !== 'Closed' ? "Click to edit" : undefined} title={job.status !== 'Closed' ? "Click to edit" : undefined}
data-no-navigate data-no-navigate
> >

View File

@@ -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 (
<Card className={`mb-6 border-l-4 ${getCategoryColor(status)}`}>
<CardHeader>
<CardTitle
className="flex items-center justify-between cursor-pointer"
onClick={toggleCollapsed}
>
<div className="flex items-center gap-2">
{isCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
<span>{status} ({jobs.length})</span>
</div>
{shouldIncludeInTotals && (
<div className="flex items-center gap-4 text-sm font-normal">
<span className="text-destructive">Cost: {formatISK(totalExpenditure)}</span>
<span className="text-success">Income: {formatISK(totalIncome)}</span>
<span className={totalProfit >= 0 ? 'text-success' : 'text-destructive'}>
Profit: {formatISK(totalProfit)}
</span>
</div>
)}
{status === IndJobStatusOptions.Tracked && (
<span className="text-sm font-normal text-muted-foreground">
(Not included in totals)
</span>
)}
</CardTitle>
</CardHeader>
{!isCollapsed && (
<CardContent>
{jobs.map(job => (
<JobCard
key={job.id}
job={job}
onEdit={onEdit}
onDelete={onDelete}
onUpdateProduced={onUpdateProduced}
onImportBOM={onImportBOM}
/>
))}
</CardContent>
)}
</Card>
);
}