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:
@@ -107,90 +107,68 @@ 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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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:', {
|
// 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);
|
itemName: tx.itemName,
|
||||||
if (existing) {
|
transactions: [tx],
|
||||||
existing.transactions.push(tx);
|
totalQuantity: tx.quantity,
|
||||||
existing.totalQuantity += tx.quantity;
|
totalValue: tx.totalPrice
|
||||||
existing.totalValue += tx.totalPrice;
|
}));
|
||||||
} else {
|
|
||||||
acc.push({
|
|
||||||
itemName: tx.itemName,
|
|
||||||
transactions: [tx],
|
|
||||||
totalQuantity: tx.quantity,
|
|
||||||
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">
|
||||||
|
@@ -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
|
||||||
>
|
>
|
||||||
|
107
src/components/JobCategory.tsx
Normal file
107
src/components/JobCategory.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user