etsy-finance-tracker/client/src/pages/DataImport.tsx
dlawler489 9e1a098a70 Initial commit: Complete Etsy Business Tracker with Profit Analysis Dashboard
Features:
- React + TypeScript frontend with Tailwind CSS
- Node.js + Express backend with TypeScript
- Comprehensive order tracking and management
- Product catalog with inventory tracking
- Customer data management
- Expense tracking and categorization
- Advanced Profit Analysis Dashboard with:
  - Real-time profit metrics and KPI visualization
  - Detailed order-level profit breakdown
  - Product performance analysis
  - Enhanced time range filtering (monthly, quarterly, yearly)
  - Interactive expandable order analysis
  - Performance categorization and color coding
- CSV import functionality for Etsy statements
- PDF parsing capabilities
- Redux state management with persistence
- Responsive design with mobile support
- Australian date formatting and currency display
2026-04-20 09:44:54 +10:00

838 lines
No EOL
33 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { csvImportService, ParsedEtsyOrder, ParsedShippingRecord } from '../utils/csvImportService';
import { pdfParser, ParsedPackingSlip } from '../utils/pdfParser';
import { matchOrderItemsToProducts } from '../utils/productMatcher';
import { MissingProductsModal, MissingProduct } from '../components/MissingProductsModal';
import { DataManager } from '../utils/dataManager';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../store';
import { addOrder, updateOrder, setOrders } from '../store/slices/orderSlice';
import { Order } from '../store/slices/orderSlice';
import { Upload, FileText, Package, Truck, Trash2 } from 'lucide-react';
import toast from 'react-hot-toast';
import { dateTestResults } from '../utils/testDateParsing';
interface ImportResults {
etsyOrders: ParsedEtsyOrder[];
shippingRecords: ParsedShippingRecord[];
matchedData: Array<{
order: ParsedEtsyOrder;
shipping?: ParsedShippingRecord;
confidence: number;
}>;
orderCosts: Array<{
orderNumber: string;
date: string;
productName?: string;
grossRevenue: number;
etsyFees: number;
netRevenue: number;
shippingCost: number;
printingCost: number;
totalCosts: number;
grossProfit: number;
grossMargin: number;
netMargin: number;
shippingConfidence: number;
hasShippingData: boolean;
hasPrintingData: boolean;
}>;
summary: ReturnType<typeof csvImportService.generateSummary>;
}
export default function DataImport() {
// Test date parsing immediately
console.log('=== DATE PARSING TEST RESULTS ===');
console.log('Date test results:', dateTestResults);
const dispatch = useDispatch();
const orders = useSelector((state: RootState) => state.orders.orders);
const products = useSelector((state: RootState) => state.products.products);
// CSV Import State
const [etsyFile, setEtsyFile] = useState<File | null>(null);
const [shippingFile, setShippingFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [results, setResults] = useState<ImportResults | null>(null);
// PDF Import State
const [pdfFiles, setPdfFiles] = useState<File[]>([]);
const [isPdfProcessing, setIsPdfProcessing] = useState(false);
const [pdfResults, setPdfResults] = useState<ParsedPackingSlip[]>([]);
const [showMissingProductsModal, setShowMissingProductsModal] = useState(false);
const [missingProducts, setMissingProducts] = useState<MissingProduct[]>([]);
const [currentOrderForMissing, setCurrentOrderForMissing] = useState<string>('');
// UI State
const [activeTab, setActiveTab] = useState<'csv' | 'pdf'>('csv');
const [error, setError] = useState<string>('');
const etsyFileRef = useRef<HTMLInputElement>(null);
const shippingFileRef = useRef<HTMLInputElement>(null);
const pdfFileRef = useRef<HTMLInputElement>(null);
const handleFileChange = (
event: React.ChangeEvent<HTMLInputElement>,
type: 'etsy' | 'shipping' | 'pdf'
) => {
const files = event.target.files;
if (!files) return;
if (type === 'pdf') {
const pdfFiles = Array.from(files).filter(file => file.type === 'application/pdf');
if (pdfFiles.length > 0) {
setPdfFiles(pdfFiles);
setError('');
} else {
setError('Please select valid PDF files');
}
} else {
const file = files[0];
if (file && file.type === 'text/csv') {
if (type === 'etsy') {
setEtsyFile(file);
} else {
setShippingFile(file);
}
setError('');
} else {
setError('Please select a valid CSV file');
}
}
};
const readFileAsText = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as string);
reader.onerror = reject;
reader.readAsText(file);
});
};
const processCsvFiles = async () => {
if (!etsyFile) {
setError('Please select an Etsy statement CSV file');
return;
}
setIsLoading(true);
setError('');
try {
const etsyContent = await readFileAsText(etsyFile);
const etsyOrders = csvImportService.parseEtsyStatement(etsyContent);
let shippingRecords: ParsedShippingRecord[] = [];
if (shippingFile) {
const shippingContent = await readFileAsText(shippingFile);
shippingRecords = csvImportService.parseAustraliaPostShipping(shippingContent);
}
const matchedData = csvImportService.matchOrdersWithShipping(etsyOrders, shippingRecords);
const printingCosts = new Map<string, number>();
orders.forEach(order => {
if (order.items) {
const totalPrinting = order.items.reduce((sum, item) =>
sum + (item.printingCost || 0), 0);
if (totalPrinting > 0) {
printingCosts.set(order.orderNumber, totalPrinting);
}
}
});
const orderCosts = csvImportService.calculateOrderCosts(matchedData, printingCosts);
const summary = csvImportService.generateSummary(orderCosts);
setResults({
etsyOrders,
shippingRecords,
matchedData,
orderCosts,
summary
});
// Automatically create orders from CSV data
const csvOrders = etsyOrders.map(csvOrder => {
// Check if order already exists
const existingOrder = orders.find(order => order.orderNumber === csvOrder.orderNumber);
if (existingOrder) {
// Update existing order with CSV revenue data
return {
...existingOrder,
total: csvOrder.saleAmount,
fees: {
etsy: csvOrder.totalFees || 0,
processing: 0,
shipping: 0
}
};
} else {
// Create new order from CSV data
return {
_id: `csv-${csvOrder.orderNumber}`,
orderNumber: csvOrder.orderNumber,
total: csvOrder.saleAmount,
status: 'delivered' as const,
dateOrdered: csvOrder.date,
customer: {
name: 'Etsy Customer',
email: ''
},
items: [{
title: csvOrder.productName || 'Product from Etsy',
quantity: 1,
price: csvOrder.saleAmount,
printingCost: 0
}],
fees: {
etsy: csvOrder.totalFees || 0,
processing: 0,
shipping: 0
}
};
}
});
// Update existing orders and add new ones
const existingOrderNumbers = new Set(orders.map(o => o.orderNumber));
const ordersToUpdate = csvOrders.filter(o => existingOrderNumbers.has(o.orderNumber));
const ordersToAdd = csvOrders.filter(o => !existingOrderNumbers.has(o.orderNumber));
ordersToUpdate.forEach(order => dispatch(updateOrder(order)));
ordersToAdd.forEach(order => dispatch(addOrder(order)));
toast.success(`CSV imported! Created ${ordersToAdd.length} new orders and updated ${ordersToUpdate.length} existing orders.`);
} catch (err) {
console.error('Error processing CSV files:', err);
setError('Error processing CSV files. Please check the file format.');
} finally {
setIsLoading(false);
}
};
const processPdfFiles = async () => {
if (pdfFiles.length === 0) {
setError('Please select PDF packing slip files');
return;
}
setIsPdfProcessing(true);
setError('');
try {
const parsedSlips: ParsedPackingSlip[] = [];
for (const file of pdfFiles) {
console.log(`Processing PDF: ${file.name}`);
const slip = await pdfParser.parsePackingSlip(file);
if (slip) {
parsedSlips.push(slip);
}
}
setPdfResults(parsedSlips);
for (const slip of parsedSlips) {
await createOrUpdateOrderFromSlip(slip);
}
} catch (err) {
console.error('Error processing PDF files:', err);
setError('Error processing PDF files. Please check the file format.');
} finally {
setIsPdfProcessing(false);
}
};
const createOrUpdateOrderFromSlip = async (slip: ParsedPackingSlip, customProducts?: any[]) => {
console.log('Creating/updating order for slip:', slip);
const existingOrder = orders.find(order => order.orderNumber === slip.orderNumber);
// Check if we have CSV results for this order number to get revenue data
let csvOrderData = null;
if (results && results.etsyOrders) {
csvOrderData = results.etsyOrders.find(csvOrder => csvOrder.orderNumber === slip.orderNumber);
console.log('Found matching CSV data for order:', csvOrderData);
}
const productsToUse = customProducts || products;
const { matches, missingProducts: missing } = matchOrderItemsToProducts(slip.items, productsToUse);
if (missing.length > 0) {
setMissingProducts(missing);
setCurrentOrderForMissing(slip.orderNumber);
setShowMissingProductsModal(true);
return;
}
const orderItems = matches.map((match: any) => ({
title: match.orderItem.title,
quantity: match.orderItem.quantity,
price: match.orderItem.price || 0,
printingCost: match.matchedProduct?.printingCost || 0
}));
// Parse and format the order date
let formattedOrderDate = new Date().toISOString();
if (slip.orderDate) {
try {
// Convert "21 Jul, 2025" format to ISO date
const parsedDate = new Date(slip.orderDate);
if (!isNaN(parsedDate.getTime())) {
formattedOrderDate = parsedDate.toISOString();
}
} catch (error) {
console.warn('Failed to parse order date:', slip.orderDate, error);
}
}
const orderData: Order = {
_id: existingOrder?._id || Date.now().toString(),
orderNumber: slip.orderNumber,
total: csvOrderData?.saleAmount || existingOrder?.total || 0, // Use CSV revenue data if available
status: existingOrder?.status || 'processing',
dateOrdered: formattedOrderDate,
customer: existingOrder?.customer || {
name: slip.customerName,
email: slip.customerEmail || '',
},
items: orderItems,
fees: csvOrderData ? {
etsy: csvOrderData.totalFees || 0,
processing: 0,
shipping: 0
} : existingOrder?.fees
};
if (existingOrder) {
dispatch(updateOrder(orderData));
} else {
dispatch(addOrder(orderData));
}
};
const handleMissingProductsSubmit = (newProducts: any[]) => {
// Products are already created by the MissingProductsModal
// We need to use the updated product list for matching
setShowMissingProductsModal(false);
const slip = pdfResults.find(slip => slip.orderNumber === currentOrderForMissing);
if (slip) {
// Use the combined product list (existing + new) for matching
const updatedProducts = [...products, ...newProducts];
createOrUpdateOrderFromSlip(slip, updatedProducts);
}
};
const handleClearTestData = () => {
if (window.confirm('Clear all existing data for testing? This will download a backup first.')) {
DataManager.clearWithBackup();
toast.success('Data cleared and backup downloaded!');
setTimeout(() => window.location.reload(), 1000);
}
};
const handleClearAllOrders = () => {
const orderCount = orders.length;
if (window.confirm(`This will delete all ${orderCount} existing orders. You'll need to re-upload your packing slips to get the correct dates. Are you sure?`)) {
dispatch(setOrders([]));
toast.success(`All ${orderCount} orders cleared! Now re-upload your packing slips with fixed date parsing.`);
}
};
const createOrdersFromCSV = () => {
if (!results || !results.etsyOrders || results.etsyOrders.length === 0) {
toast.error('No CSV data available. Please import Etsy CSV first.');
return;
}
const csvOrders = results.etsyOrders.map(csvOrder => {
// Check if order already exists (from packing slip import)
const existingOrder = orders.find(order => order.orderNumber === csvOrder.orderNumber);
if (existingOrder) {
// Update existing order with CSV revenue data
return {
...existingOrder,
total: csvOrder.saleAmount,
fees: {
etsy: csvOrder.totalFees || 0,
processing: 0,
shipping: 0
}
};
} else {
// Create new order from CSV data (no packing slip available)
return {
_id: `csv-${csvOrder.orderNumber}`,
orderNumber: csvOrder.orderNumber,
total: csvOrder.saleAmount,
status: 'delivered' as const,
dateOrdered: csvOrder.date,
customer: {
name: 'Etsy Customer',
email: ''
},
items: [{
title: csvOrder.productName || 'Product from Etsy',
quantity: 1,
price: csvOrder.saleAmount,
printingCost: 0
}],
fees: {
etsy: csvOrder.totalFees || 0,
processing: 0,
shipping: 0
}
};
}
});
// Update existing orders and add new ones
const existingOrderNumbers = new Set(orders.map(o => o.orderNumber));
const ordersToUpdate = csvOrders.filter(o => existingOrderNumbers.has(o.orderNumber));
const ordersToAdd = csvOrders.filter(o => !existingOrderNumbers.has(o.orderNumber));
ordersToUpdate.forEach(order => dispatch(updateOrder(order)));
ordersToAdd.forEach(order => dispatch(addOrder(order)));
toast.success(`Updated ${ordersToUpdate.length} existing orders and created ${ordersToAdd.length} new orders from CSV data.`);
};
const debugDataState = () => {
console.log('=== DEBUGGING DATA STATE ===');
console.log('Current orders in Redux:', orders.length);
orders.forEach((order, i) => {
console.log(`Order ${i + 1}:`, {
orderNumber: order.orderNumber,
total: order.total,
items: order.items?.length || 0,
fees: order.fees,
customer: order.customer?.name
});
});
console.log('CSV Results:', results ? {
etsyOrders: results.etsyOrders?.length || 0,
sampleOrder: results.etsyOrders?.[0]
} : 'No CSV results');
console.log('Products:', products.length);
toast.success(`Debug info logged to console. Orders: ${orders.length}, CSV: ${results?.etsyOrders?.length || 0}, Products: ${products.length}`);
};
const testActualPDF = async () => {
try {
console.log('=== TESTING PDF PARSER ===');
// First, let's check what products we have in the database
console.log('Current products in database:', products.map(p => ({ id: p._id, title: p.title })));
console.log('Testing actual packing slip PDF...');
const response = await fetch('/3748364725.pdf');
const arrayBuffer = await response.arrayBuffer();
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
const file = new File([blob], '3748364725.pdf', { type: 'application/pdf' });
console.log('File size:', file.size, 'bytes');
// Let's also extract the raw text to see what we're working with
const { getDocument } = await import('pdfjs-dist');
const pdf = await getDocument(arrayBuffer).promise;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items.map((item: any) => item.str).join(' ');
fullText += pageText + '\n';
console.log(`Page ${i} text:`, pageText);
}
console.log('\n=== FULL EXTRACTED TEXT ===');
console.log(fullText);
// Now try our parser
const result = await pdfParser.parsePackingSlip(file);
console.log('\n=== PARSER RESULT ===');
console.log('Parse Result:', JSON.stringify(result, null, 2));
if (result.items && result.items.length > 0) {
console.log('\n=== PARSED ITEMS ===');
console.log('Order Number:', result.orderNumber);
result.items.forEach((item, index) => {
console.log(`Item ${index + 1}: ${item.title} (Qty: ${item.quantity})`);
});
} else {
console.log('❌ No items found - we need to update the parser patterns based on the extracted text above');
}
} catch (error) {
console.error('Error testing PDF:', error);
}
};
return (
<div className="p-6">
<div className="mb-6">
<div className="flex justify-between items-start">
<div>
<h1 className="text-2xl font-bold text-gray-800 mb-2">Data Import & Analysis</h1>
<p className="text-gray-600">
Import Etsy statements, Australia Post shipping data, and PDF packing slips for complete business analysis.
</p>
</div>
{/* Testing Helper */}
<div className="flex flex-col gap-2">
<button
onClick={handleClearAllOrders}
className="flex items-center gap-2 px-3 py-2 bg-orange-100 text-orange-700 rounded-lg hover:bg-orange-200 text-sm"
>
<Trash2 className="w-4 h-4" />
Clear All Orders
</button>
<button
onClick={handleClearTestData}
className="flex items-center gap-2 px-3 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 text-sm"
>
<Trash2 className="w-4 h-4" />
Clear for Testing
</button>
<button
onClick={testActualPDF}
className="flex items-center gap-2 px-3 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 text-sm"
>
<FileText className="w-4 h-4" />
Test PDF Parser
</button>
<button
onClick={debugDataState}
className="flex items-center gap-2 px-3 py-2 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 text-sm"
>
<Package className="w-4 h-4" />
Debug Data
</button>
<p className="text-xs text-gray-500 max-w-32 text-center">
Fix dates | Clear all | Test parsing | Debug
</p>
</div>
</div>
</div>
{/* Date Fix Notice */}
{orders.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
Date Fix Required for Existing Orders
</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
You have {orders.length} existing orders that may have incorrect dates (showing today's date instead of actual order dates).
To fix this: <strong>Click "Clear All Orders"</strong> above, then re-upload your packing slip PDFs.
The updated date parsing will now extract the correct order dates.
</p>
</div>
</div>
</div>
</div>
)}
{/* Import Options Tabs */}
<div className="bg-white rounded-lg shadow-md mb-6">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8 px-6" aria-label="Tabs">
<button
className={`${activeTab === 'csv' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm`}
onClick={() => setActiveTab('csv')}
>
<FileText className="w-5 h-5 inline mr-2" />
CSV Import (Financial Data)
</button>
<button
className={`${activeTab === 'pdf' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm`}
onClick={() => setActiveTab('pdf')}
>
<Package className="w-5 h-5 inline mr-2" />
PDF Import (Packing Slips)
</button>
</nav>
</div>
{/* CSV Import Section */}
{activeTab === 'csv' && (
<div className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center">
<Truck className="w-6 h-6 mr-2 text-blue-600" />
Upload CSV Files for Financial Analysis
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Etsy CSV Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Etsy Statement CSV (Required)
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
<input
type="file"
ref={etsyFileRef}
accept=".csv"
onChange={(e) => handleFileChange(e, 'etsy')}
className="hidden"
/>
<div className="text-center">
{etsyFile ? (
<div className="text-green-600">
<svg className="mx-auto h-12 w-12" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<p className="mt-2 text-sm">{etsyFile.name}</p>
</div>
) : (
<div>
<FileText className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-600">Click to upload Etsy statement</p>
<p className="text-xs text-gray-500 mt-1">Contains sales, fees, and order data</p>
</div>
)}
<button
onClick={() => etsyFileRef.current?.click()}
className="mt-2 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Select File
</button>
</div>
</div>
</div>
{/* Australia Post CSV Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Australia Post Shipping CSV (Optional)
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
<input
type="file"
ref={shippingFileRef}
accept=".csv"
onChange={(e) => handleFileChange(e, 'shipping')}
className="hidden"
/>
<div className="text-center">
{shippingFile ? (
<div className="text-green-600">
<svg className="mx-auto h-12 w-12" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<p className="mt-2 text-sm">{shippingFile.name}</p>
</div>
) : (
<div>
<Truck className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-600">Click to upload shipping data</p>
<p className="text-xs text-gray-500 mt-1">Contains actual shipping costs and tracking</p>
</div>
)}
<button
onClick={() => shippingFileRef.current?.click()}
className="mt-2 bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
Select File
</button>
</div>
</div>
</div>
</div>
<div className="mt-6 flex gap-4">
<button
onClick={processCsvFiles}
disabled={!etsyFile || isLoading}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{isLoading ? 'Processing...' : 'Analyze Financial Data'}
</button>
</div>
</div>
)}
{/* PDF Import Section */}
{activeTab === 'pdf' && (
<div className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center">
<Package className="w-6 h-6 mr-2 text-purple-600" />
Upload PDF Packing Slips for Item Details
</h2>
<div className="grid grid-cols-1 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
PDF Packing Slips (Multiple files supported)
</label>
<div className="border-2 border-dashed border-purple-300 rounded-lg p-6">
<input
type="file"
ref={pdfFileRef}
accept=".pdf"
multiple
onChange={(e) => handleFileChange(e, 'pdf')}
className="hidden"
/>
<div className="text-center">
{pdfFiles.length > 0 ? (
<div className="text-green-600">
<svg className="mx-auto h-12 w-12" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<p className="mt-2 text-sm">{pdfFiles.length} PDF file(s) selected</p>
<div className="mt-2 max-h-20 overflow-y-auto">
{pdfFiles.map((file, index) => (
<p key={index} className="text-xs text-gray-600">{file.name}</p>
))}
</div>
</div>
) : (
<div>
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-lg text-gray-600">Drag & drop PDF packing slips here</p>
<p className="text-sm text-gray-500 mt-1">Or click to browse and select multiple files</p>
<p className="text-xs text-gray-400 mt-2">Extracts item details, quantities, and customer info</p>
</div>
)}
<button
onClick={() => pdfFileRef.current?.click()}
className="mt-4 bg-purple-500 text-white px-6 py-2 rounded-lg hover:bg-purple-600"
>
Select PDF Files
</button>
</div>
</div>
</div>
</div>
<div className="mt-6 flex gap-4">
<button
onClick={processPdfFiles}
disabled={pdfFiles.length === 0 || isPdfProcessing}
className="bg-purple-600 text-white px-6 py-2 rounded-lg hover:bg-purple-700 disabled:bg-gray-400"
>
{isPdfProcessing ? 'Processing PDFs...' : 'Extract Order Details'}
</button>
</div>
{/* PDF Results Display */}
{pdfResults.length > 0 && (
<div className="mt-6 bg-gray-50 rounded-lg p-4">
<h3 className="text-md font-semibold mb-3">PDF Processing Results</h3>
<div className="space-y-2">
{pdfResults.map((slip, index) => (
<div key={index} className="bg-white rounded p-3 shadow-sm">
<p className="font-medium text-gray-900">Order #{slip.orderNumber}</p>
<p className="text-sm text-gray-600">Customer: {slip.customerName}</p>
<p className="text-sm text-gray-600">Items: {slip.items.length}</p>
</div>
))}
</div>
</div>
)}
</div>
)}
{error && (
<div className="mx-6 mb-4 bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
</div>
{/* Results Section - Show for CSV analysis only */}
{results && activeTab === 'csv' && (
<>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-sm font-medium text-gray-500">Total Orders</h3>
<p className="text-2xl font-bold text-gray-900">{results.summary.ordersProcessed}</p>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-sm font-medium text-gray-500">Total Revenue</h3>
<p className="text-2xl font-bold text-green-600">${results.summary.totalRevenue.toFixed(2)}</p>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-sm font-medium text-gray-500">Total Profit</h3>
<p className="text-2xl font-bold text-blue-600">${results.summary.totalProfit.toFixed(2)}</p>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-sm font-medium text-gray-500">Avg Profit Margin</h3>
<p className="text-2xl font-bold text-purple-600">{results.summary.averageGrossMargin.toFixed(1)}%</p>
</div>
</div>
{/* Action Button to Create Orders */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-blue-800">Re-sync Orders from CSV Data</h3>
<p className="text-sm text-blue-600 mt-1">
Orders were automatically created during CSV import. Use this button to re-sync if you need to update the data.
</p>
</div>
<button
onClick={createOrdersFromCSV}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 ml-4"
>
<Package className="w-4 h-4" />
Re-sync Orders
</button>
</div>
</div>
{/* Cost Breakdown */}
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h3 className="text-lg font-semibold mb-4">Cost Breakdown</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<p className="text-sm text-gray-500">Etsy Fees</p>
<p className="text-xl font-bold text-red-600">${results.summary.totalEtsyFees.toFixed(2)}</p>
</div>
<div className="text-center">
<p className="text-sm text-gray-500">Shipping Costs</p>
<p className="text-xl font-bold text-orange-600">${results.summary.totalShippingCosts.toFixed(2)}</p>
</div>
<div className="text-center">
<p className="text-sm text-gray-500">Printing Costs</p>
<p className="text-xl font-bold text-yellow-600">${results.summary.totalPrintingCosts.toFixed(2)}</p>
</div>
</div>
</div>
</>
)}
{/* Missing Products Modal */}
{showMissingProductsModal && (
<MissingProductsModal
missingProducts={missingProducts}
onClose={() => setShowMissingProductsModal(false)}
onComplete={handleMissingProductsSubmit}
/>
)}
</div>
);
}