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
838 lines
No EOL
33 KiB
TypeScript
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>
|
|
);
|
|
} |