Integrate Data Import with Expenses tab
✅ FIXES MAJOR WORKFLOW ISSUE: - Data Import CSV processing now automatically creates expenses - Etsy fees → 'Transaction Fees' category expenses - Australia Post shipping → 'Shipping & Postage' expenses - Prevents duplicate expense creation with reference checking - Added user notices in both Data Import and Expenses tabs 🔄 INTEGRATION FEATURES: - Automatic expense creation from CSV order costs - Duplicate prevention by order number/tracking number - Proper categorization and tax-deductible flagging - Clear user notifications and workflow guidance 📊 USER EXPERIENCE: - Expenses tab now shows integration notice - Data Import shows automatic expense creation info - Seamless workflow between sales data and expense tracking
This commit is contained in:
parent
f39d4ca266
commit
22799cb732
3 changed files with 169 additions and 1 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
**Repository**: https://github.com/dlawler489/etsy-finance-tracker
|
**Repository**: https://github.com/dlawler489/etsy-finance-tracker
|
||||||
**Status**: ✅ **LIVE IN PRODUCTION ON MAC MINI**
|
**Status**: ✅ **LIVE IN PRODUCTION ON MAC MINI**
|
||||||
**Deployment Method**: `docker-compose.deploy.yml` (GitHub Container Registry)
|
**Deployment Method**: `docker-compose.deploy.yml` (GitHub Container Registry)
|
||||||
**Last Updated**: May 1, 2026
|
**Last Updated**: May 1, 2026 - **Modal Bug Fix Deployed**
|
||||||
**Confirmed Working**: ✅ Successfully deployed and operational
|
**Confirmed Working**: ✅ Successfully deployed and operational
|
||||||
|
|
||||||
### 🏭 **Production Environment Details**
|
### 🏭 **Production Environment Details**
|
||||||
|
|
@ -97,6 +97,29 @@ Your Etsy Finance Tracker is now **live and running in production** with all cor
|
||||||
|
|
||||||
### For Command Line Users
|
### For Command Line Users
|
||||||
|
|
||||||
|
## 🔄 **Latest Updates - Modal Bug Fix Deployment**
|
||||||
|
|
||||||
|
### **May 1, 2026 - Critical Modal Fix** ✅ **COMMITTED & BUILDING**
|
||||||
|
- 🐛 **Issue Fixed**: Modal persistence preventing proper closure after product creation
|
||||||
|
- ⚡ **Enhancement**: Added duplicate prevention with `isProcessing` state
|
||||||
|
- 🎛️ **Improvement**: Loading spinner and disabled states during processing
|
||||||
|
- 🔄 **Workflow**: Enhanced modal closure logic in `DataImport.tsx`
|
||||||
|
- 🛡️ **Prevention**: Proper state reset on modal close to prevent duplicates
|
||||||
|
|
||||||
|
**Deployment Status**:
|
||||||
|
- ✅ **Code Committed**: Pushed to main branch with commit `f39d4ca`
|
||||||
|
- 🏗️ **GitHub Action**: Automatically building Docker images in CI/CD pipeline
|
||||||
|
- 📦 **Container Registry**: New images will be available at `ghcr.io/dlawler489/etsy-finance-tracker:latest`
|
||||||
|
|
||||||
|
**To Deploy Latest Fix**:
|
||||||
|
```bash
|
||||||
|
# Pull latest code and redeploy
|
||||||
|
git pull origin main
|
||||||
|
docker compose -f docker-compose.deploy.yml pull
|
||||||
|
docker compose -f docker-compose.deploy.yml down
|
||||||
|
docker compose -f docker-compose.deploy.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
1. **If Docker is available**:
|
1. **If Docker is available**:
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.simple.yml up --build
|
docker compose -f docker-compose.simple.yml up --build
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { DataManager } from '../utils/dataManager';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { RootState } from '../store';
|
import { RootState } from '../store';
|
||||||
import { addOrder, updateOrder, setOrders } from '../store/slices/orderSlice';
|
import { addOrder, updateOrder, setOrders } from '../store/slices/orderSlice';
|
||||||
|
import { addExpenses } from '../store/slices/expenseSlice';
|
||||||
import { Upload, FileText, Package, Truck, Trash2 } from 'lucide-react';
|
import { Upload, FileText, Package, Truck, Trash2 } from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { dateTestResults } from '../utils/testDateParsing';
|
import { dateTestResults } from '../utils/testDateParsing';
|
||||||
|
|
@ -110,6 +111,93 @@ export default function DataImport() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create expenses from CSV import data
|
||||||
|
const createExpensesFromCsvData = async (
|
||||||
|
orderCosts: ImportResults['orderCosts'],
|
||||||
|
shippingRecords: ParsedShippingRecord[]
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// Get existing expenses to avoid duplicates
|
||||||
|
const existingExpensesRes = await api.get('/expenses?limit=1000');
|
||||||
|
const existingExpenses = existingExpensesRes.data.expenses || [];
|
||||||
|
|
||||||
|
const expensesToCreate: any[] = [];
|
||||||
|
|
||||||
|
// Create Etsy fee expenses (check for duplicates by order number)
|
||||||
|
orderCosts.forEach(order => {
|
||||||
|
if (order.etsyFees > 0) {
|
||||||
|
const isDuplicate = existingExpenses.some((expense: any) =>
|
||||||
|
expense.reference === order.orderNumber &&
|
||||||
|
expense.vendor === 'Etsy' &&
|
||||||
|
expense.category === 'Transaction Fees'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
expensesToCreate.push({
|
||||||
|
description: `Etsy Fees - Order #${order.orderNumber}`,
|
||||||
|
amount: order.etsyFees,
|
||||||
|
category: 'Transaction Fees',
|
||||||
|
date: order.date,
|
||||||
|
taxDeductible: true,
|
||||||
|
vendor: 'Etsy',
|
||||||
|
reference: order.orderNumber
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create shipping expenses (check for duplicates by tracking number)
|
||||||
|
shippingRecords.forEach(shipping => {
|
||||||
|
if (shipping.totalCost > 0) {
|
||||||
|
const isDuplicate = existingExpenses.some((expense: any) =>
|
||||||
|
expense.reference === shipping.trackingNumber &&
|
||||||
|
expense.vendor === 'Australia Post'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
expensesToCreate.push({
|
||||||
|
description: `Australia Post - ${shipping.trackingNumber}`,
|
||||||
|
amount: shipping.totalCost,
|
||||||
|
category: 'Shipping & Postage',
|
||||||
|
date: shipping.date,
|
||||||
|
taxDeductible: true,
|
||||||
|
vendor: 'Australia Post',
|
||||||
|
reference: shipping.trackingNumber
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save expenses to database
|
||||||
|
if (expensesToCreate.length > 0) {
|
||||||
|
const savedExpenses = [];
|
||||||
|
let created = 0;
|
||||||
|
|
||||||
|
for (const expense of expensesToCreate) {
|
||||||
|
try {
|
||||||
|
const res = await api.post('/expenses', expense);
|
||||||
|
savedExpenses.push(res.data);
|
||||||
|
created++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create expense:', expense, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedExpenses.length > 0) {
|
||||||
|
dispatch(addExpenses(savedExpenses));
|
||||||
|
toast.success(`Created ${created} expenses from CSV data (Etsy fees + shipping costs)`);
|
||||||
|
} else if (expensesToCreate.length === 0) {
|
||||||
|
toast.success('All expenses from CSV data already exist - no duplicates created');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.success('No new expenses to create from CSV data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating expenses:', error);
|
||||||
|
toast.error('Failed to create expenses from CSV data');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const processCsvFiles = async () => {
|
const processCsvFiles = async () => {
|
||||||
if (!etsyFile) {
|
if (!etsyFile) {
|
||||||
setError('Please select an Etsy statement CSV file');
|
setError('Please select an Etsy statement CSV file');
|
||||||
|
|
@ -179,6 +267,10 @@ export default function DataImport() {
|
||||||
// Reload orders from API after bulk upsert
|
// Reload orders from API after bulk upsert
|
||||||
const ordersRes = await api.get('/orders?limit=1000');
|
const ordersRes = await api.get('/orders?limit=1000');
|
||||||
dispatch(setOrders(ordersRes.data.orders));
|
dispatch(setOrders(ordersRes.data.orders));
|
||||||
|
|
||||||
|
// Create expenses from CSV data
|
||||||
|
await createExpensesFromCsvData(orderCosts, shippingRecords);
|
||||||
|
|
||||||
toast.success(`CSV imported! Created ${res.data.created} new orders and updated ${res.data.updated} existing orders.`);
|
toast.success(`CSV imported! Created ${res.data.created} new orders and updated ${res.data.updated} existing orders.`);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to save orders to database');
|
toast.error('Failed to save orders to database');
|
||||||
|
|
@ -658,6 +750,34 @@ export default function DataImport() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Automatic Expense Creation Notice */}
|
||||||
|
<div className="mt-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-green-800">
|
||||||
|
Automatic Expense Creation
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2 text-sm text-green-700">
|
||||||
|
<p>
|
||||||
|
When you process CSV files, expenses will be automatically created in the <strong>Expenses tab</strong>:
|
||||||
|
</p>
|
||||||
|
<ul className="mt-1 list-disc list-inside">
|
||||||
|
<li><strong>Etsy fees</strong> from each order (Transaction Fees category)</li>
|
||||||
|
<li><strong>Australia Post shipping costs</strong> (Shipping & Postage category)</li>
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2 text-xs">
|
||||||
|
This ensures your profit analysis matches your expense tracking automatically!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex gap-4">
|
<div className="mt-6 flex gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={processCsvFiles}
|
onClick={processCsvFiles}
|
||||||
|
|
|
||||||
|
|
@ -373,6 +373,31 @@ const Expenses = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Data Import Integration Notice */}
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">
|
||||||
|
💡 Automatic Expense Creation
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2 text-sm text-blue-700">
|
||||||
|
<p>
|
||||||
|
<strong>Etsy fees</strong> and <strong>Australia Post shipping costs</strong> are automatically created as expenses when you process CSV files in the{' '}
|
||||||
|
<a href="/data-import" className="font-semibold underline hover:text-blue-900">Data Import</a> section.
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
|
Manual CSV imports here are for additional expenses not captured in your sales data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
<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 p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue