Files
dewedev/src/pages/InvoiceEditor.js
dwindown fb9c944366 feat: major update - Markdown Editor, CodeMirror upgrades, SEO improvements, tool cleanup
- Added new Markdown Editor with live preview, GFM support, PDF/HTML/DOCX export
- Upgraded all paste fields to CodeMirror with syntax highlighting and expand/collapse
- Enhanced Object Editor with advanced URL fetching and preview mode
- Improved export views with syntax highlighting in Table/Object editors
- Implemented SEO improvements (FAQ schema, breadcrumbs, internal linking)
- Added Related Tools recommendations component
- Created custom 404 page with tool suggestions
- Consolidated tools: removed JSON, Serialize, CSV-JSON (merged into main editors)
- Updated documentation and cleaned up redundant files
- Updated release notes with user-centric improvements
2025-10-22 15:20:22 +07:00

2994 lines
151 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
FileText, Building2, User, Plus, Trash2, Upload, Download,
Globe, AlertTriangle, ChevronUp, ChevronDown
} from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import SEO from '../components/SEO';
import RelatedTools from '../components/RelatedTools';
const InvoiceEditor = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// State management following TableEditor pattern
const [activeTab, setActiveTab] = useState('create');
const [createNewCompleted, setCreateNewCompleted] = useState(false);
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null);
const [inputText, setInputText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const fileInputRef = useRef(null);
const logoInputRef = useRef(null);
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [exportExpanded, setExportExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
// Invoice data structure
const [invoiceData, setInvoiceData] = useState({
invoiceNumber: '',
date: new Date().toISOString().split('T')[0],
dueDate: '',
company: {
name: '',
address: '',
city: '',
phone: '',
email: '',
logo: null
},
client: {
name: '',
address: '',
city: '',
phone: '',
email: ''
},
items: [],
fees: [], // Array of {id, label, type: 'fixed'|'percentage', amount, value}
discounts: [], // Array of {id, label, type: 'fixed'|'percentage', amount, value}
subtotal: 0,
discount: 0,
total: 0,
notes: '',
thankYouMessage: '',
authorizedSignedText: '',
digitalSignature: null,
paymentTerms: {
type: 'full', // 'full', 'installment', 'downpayment'
downPayment: {
amount: 0,
percentage: 0,
dueDate: ''
},
installments: []
},
settings: {
colorScheme: '#3B82F6',
currency: { code: 'USD', symbol: '$' },
thousandSeparator: true,
showFromSection: true
},
paymentMethod: {
type: 'none', // 'none', 'bank', 'link', 'qr'
bankDetails: {
bankName: '',
accountName: '',
accountNumber: '',
routingNumber: '',
swiftCode: '',
iban: ''
},
paymentLink: {
url: '',
label: 'Pay Online'
},
qrCode: {
enabled: false,
customImage: null
}
}
});
// Additional state variables
const [showSettingsModal, setShowSettingsModal] = useState(false);
const [showSignaturePad, setShowSignaturePad] = useState(false);
const [pdfPageSize, setPdfPageSize] = useState('A4'); // A4 or F4
const [currencies, setCurrencies] = useState([]);
// Load currencies from JSON file
useEffect(() => {
const loadCurrencies = async () => {
try {
const response = await fetch('/data/currencies.json');
const currencyData = await response.json();
setCurrencies(currencyData);
} catch (error) {
console.error('Failed to load currencies:', error);
// Fallback to basic currencies
setCurrencies([
{ code: 'IDR', name: 'Indonesian Rupiah', symbol: 'Rp' },
{ code: 'USD', name: 'US Dollar', symbol: '$' },
{ code: 'EUR', name: 'Euro', symbol: '€' },
{ code: 'GBP', name: 'British Pound', symbol: '£' },
{ code: 'JPY', name: 'Japanese Yen', symbol: '¥' }
]);
}
};
loadCurrencies();
}, []);
// Load saved data on component mount
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
const isEditMode = searchParams.get('mode') === 'edit';
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
// Ensure backward compatibility with new fields
const updatedInvoice = {
...parsedInvoice,
fees: parsedInvoice.fees || [],
discounts: parsedInvoice.discounts || [],
company: {
...parsedInvoice.company,
bankName: parsedInvoice.company?.bankName || '',
accountName: parsedInvoice.company?.accountName || '',
accountNumber: parsedInvoice.company?.accountNumber || ''
}
};
setInvoiceData(updatedInvoice);
// If we're in edit mode or have saved data, show the editor section
const hasData = updatedInvoice.invoiceNumber || updatedInvoice.company.name || updatedInvoice.items.length > 0;
if (isEditMode || hasData) {
setCreateNewCompleted(true);
setActiveTab('create');
}
} else if (isEditMode) {
// Edit mode but no data - redirect back to editor without mode
window.location.replace('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load saved invoice:', error);
}
}, [searchParams]);
// Auto-save invoice data to localStorage whenever it changes
useEffect(() => {
// Only auto-save if we have meaningful data or if createNewCompleted is true
if (createNewCompleted || invoiceData.invoiceNumber || invoiceData.company.name || invoiceData.items.length > 0) {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice:', error);
}
}
}, [invoiceData, createNewCompleted]);
// Safety recalculation - ensure totals are always correct
useEffect(() => {
const { subtotal, total } = calculateTotals(
invoiceData.items,
invoiceData.discount,
invoiceData.fees,
invoiceData.discounts
);
if (invoiceData.subtotal !== subtotal || invoiceData.total !== total) {
setInvoiceData(prev => {
const updated = {
...prev,
subtotal,
total
};
// Recalculate installments when total changes
if (prev.paymentTerms?.installments?.length > 0) {
const updatedInstallments = prev.paymentTerms.installments.map(installment => {
if (installment.type === 'percentage') {
const baseAmount = prev.paymentTerms?.type === 'downpayment'
? total - (prev.paymentTerms?.downPayment?.amount || 0)
: total;
const amount = (baseAmount * (installment.percentage || 0)) / 100;
return { ...installment, amount };
}
return installment;
});
updated.paymentTerms = {
...prev.paymentTerms,
installments: updatedInstallments
};
}
return updated;
});
}
}, [invoiceData.items, invoiceData.discount, invoiceData.fees, invoiceData.discounts, invoiceData.subtotal, invoiceData.total]);
// Save PDF page size to localStorage
useEffect(() => {
try {
localStorage.setItem('pdfPageSize', pdfPageSize);
} catch (error) {
console.error('Failed to save PDF page size:', error);
}
}, [pdfPageSize]);
// Sample invoice data
const sampleInvoiceData = {
invoiceNumber: 'INV-2024-001',
date: '2024-01-15',
dueDate: '2024-02-15',
company: {
name: 'DevTools Inc.',
address: '123 Tech Street',
city: 'San Francisco, CA 94105',
phone: '+1 (555) 123-4567',
email: 'billing@devtools.com',
logo: null,
bankName: 'Chase Bank',
accountName: 'DevTools Inc.',
accountNumber: '1234567890'
},
client: {
name: 'Acme Corporation',
address: '456 Business Ave',
city: 'New York, NY 10001',
phone: '+1 (555) 987-6543',
email: 'accounts@acme.com'
},
items: [
{ id: 1, description: 'Web Development Services', quantity: 40, rate: 125, amount: 5000 },
{ id: 2, description: 'UI/UX Design', quantity: 20, rate: 100, amount: 2000 },
{ id: 3, description: 'Project Management', quantity: 10, rate: 150, amount: 1500 }
],
fees: [
{ id: 1, label: 'Processing Fee', type: 'fixed', value: 50, amount: 50 }
],
discounts: [
{ id: 1, label: 'Early Payment Discount', type: 'percentage', value: 5, amount: 425 }
],
subtotal: 8500,
discount: 0,
total: 8125,
notes: 'Payment due within 30 days.',
thankYouMessage: 'Thank you for your business!',
authorizedSignedText: 'Authorized Signed',
digitalSignature: null,
settings: {
colorScheme: '#3B82F6',
currency: { code: 'USD', symbol: '$' },
thousandSeparator: true
}
};
// Utility functions for formatting
const formatNumber = (num, useThousandSeparator = invoiceData.settings?.thousandSeparator) => {
if (!useThousandSeparator) return num.toString();
return new Intl.NumberFormat('en-US', { minimumFractionDigits: invoiceData.settings?.decimalDigits ?? 2 }).format(num);
};
const formatCurrency = (amount, showSymbol = true) => {
const currency = invoiceData.settings?.currency || { code: 'USD', symbol: '$' };
const decimalDigits = invoiceData.settings?.decimalDigits ?? 2;
const formattedAmount = formatNumber(amount.toFixed(decimalDigits));
if (showSymbol && currency.symbol) {
return `${currency.symbol} ${formattedAmount}`;
} else {
return `${formattedAmount} ${currency.code}`;
}
};
// Helper functions following TableEditor pattern
const hasUserData = () => {
return invoiceData.invoiceNumber ||
invoiceData.company.name ||
invoiceData.client.name ||
invoiceData.items.length > 0;
};
const hasModifiedData = () => {
if (!hasUserData()) return false;
const isSampleData = JSON.stringify(invoiceData) === JSON.stringify(sampleInvoiceData);
return !isSampleData;
};
const clearAllData = () => {
setInvoiceData({
invoiceNumber: '',
date: new Date().toISOString().split('T')[0],
dueDate: '',
company: {
name: '',
address: '',
city: '',
phone: '',
email: '',
logo: null,
bankName: '',
accountName: '',
accountNumber: ''
},
client: {
name: '',
address: '',
city: '',
phone: '',
email: ''
},
items: [],
fees: [],
discounts: [],
subtotal: 0,
discount: 0,
total: 0,
notes: '',
thankYouMessage: '',
authorizedSignedText: '',
digitalSignature: null,
settings: {
colorScheme: '#3B82F6',
currency: { code: 'USD', symbol: '$' },
thousandSeparator: true
}
});
setCreateNewCompleted(false);
setInputText('');
setUrl('');
setError('');
};
// Tab change handling with confirmation
const handleTabChange = (newTab) => {
if (newTab === 'create' && activeTab !== 'create') {
if (hasModifiedData()) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
setCreateNewCompleted(false);
}
} else if (hasUserData() && activeTab !== newTab) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
if (newTab === 'create' && createNewCompleted) {
setCreateNewCompleted(false);
}
}
};
const confirmInputChange = () => {
if (pendingTabChange === 'create_empty') {
clearAllData();
setCreateNewCompleted(true);
} else if (pendingTabChange === 'create_sample') {
clearAllData();
setInvoiceData(sampleInvoiceData);
setCreateNewCompleted(true);
} else {
clearAllData();
setActiveTab(pendingTabChange);
if (pendingTabChange === 'create') {
setCreateNewCompleted(false);
}
}
setShowInputChangeModal(false);
setPendingTabChange(null);
};
const cancelInputChange = () => {
setShowInputChangeModal(false);
setPendingTabChange(null);
if (activeTab === 'create' && !createNewCompleted) {
setCreateNewCompleted(true);
}
};
// Create New button handlers
const handleStartEmpty = () => {
if (hasModifiedData()) {
setPendingTabChange('create_empty');
setShowInputChangeModal(true);
} else {
clearAllData();
setCreateNewCompleted(true);
}
};
const handleLoadSample = () => {
if (hasModifiedData()) {
setPendingTabChange('create_sample');
setShowInputChangeModal(true);
} else {
setInvoiceData(sampleInvoiceData);
setCreateNewCompleted(true);
}
};
// Calculate totals
const calculateTotals = (items, discount = 0, fees = [], discounts = []) => {
const subtotal = items.reduce((sum, item) => sum + (item.amount || 0), 0);
// Calculate total fees
const totalFees = fees.reduce((sum, fee) => {
const feeAmount = fee.type === 'percentage'
? (subtotal * fee.value) / 100
: fee.value;
return sum + feeAmount;
}, 0);
// Calculate total discounts
const totalDiscounts = discounts.reduce((sum, discountItem) => {
const discountAmount = discountItem.type === 'percentage'
? (subtotal * discountItem.value) / 100
: discountItem.value;
return sum + discountAmount;
}, 0);
const total = subtotal + totalFees - discount - totalDiscounts;
return { subtotal, total };
};
// Update invoice data
const updateInvoiceData = (field, value) => {
setInvoiceData(prev => {
const updated = { ...prev, [field]: value };
// Recalculate totals if items, discount, fees, or discounts changed
if (field === 'items' || field === 'discount' || field === 'fees' || field === 'discounts') {
const { subtotal, total } = calculateTotals(
field === 'items' ? value : updated.items,
field === 'discount' ? value : updated.discount,
field === 'fees' ? value : updated.fees,
field === 'discounts' ? value : updated.discounts
);
updated.subtotal = subtotal;
updated.total = total;
}
return updated;
});
};
// Line item management
const addLineItem = () => {
const newItem = {
id: Date.now(),
description: '',
quantity: 1,
rate: 0,
amount: 0
};
updateInvoiceData('items', [...invoiceData.items, newItem]);
};
const updateLineItem = (id, field, value) => {
const updatedItems = invoiceData.items.map(item => {
if (item.id === id) {
const updated = { ...item, [field]: value };
// Calculate amount when quantity or rate changes
if (field === 'quantity' || field === 'rate') {
updated.amount = updated.quantity * updated.rate;
}
return updated;
}
return item;
});
updateInvoiceData('items', updatedItems);
};
const removeLineItem = (id) => {
updateInvoiceData('items', invoiceData.items.filter(item => item.id !== id));
};
// Move up/down handlers
const moveItem = (arrayName, id, direction) => {
const array = invoiceData[arrayName];
const currentIndex = array.findIndex(item => item.id === id);
if (direction === 'up' && currentIndex > 0) {
const newArray = [...array];
[newArray[currentIndex], newArray[currentIndex - 1]] = [newArray[currentIndex - 1], newArray[currentIndex]];
updateInvoiceData(arrayName, newArray);
} else if (direction === 'down' && currentIndex < array.length - 1) {
const newArray = [...array];
[newArray[currentIndex], newArray[currentIndex + 1]] = [newArray[currentIndex + 1], newArray[currentIndex]];
updateInvoiceData(arrayName, newArray);
}
};
// Move installments (nested in paymentTerms)
const moveInstallment = (id, direction) => {
const installments = invoiceData.paymentTerms?.installments || [];
const currentIndex = installments.findIndex(item => item.id === id);
if (direction === 'up' && currentIndex > 0) {
const newArray = [...installments];
[newArray[currentIndex], newArray[currentIndex - 1]] = [newArray[currentIndex - 1], newArray[currentIndex]];
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: newArray
});
} else if (direction === 'down' && currentIndex < installments.length - 1) {
const newArray = [...installments];
[newArray[currentIndex], newArray[currentIndex + 1]] = [newArray[currentIndex + 1], newArray[currentIndex]];
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: newArray
});
}
};
// Fee management
const addFee = () => {
const newFee = {
id: Date.now(),
label: '',
type: 'fixed',
value: 0,
amount: 0
};
updateInvoiceData('fees', [...(invoiceData.fees || []), newFee]);
};
const updateFee = (id, field, value) => {
const updatedFees = (invoiceData.fees || []).map(fee => {
if (fee.id === id) {
const updatedFee = { ...fee, [field]: value };
// Calculate amount based on type
if (field === 'value' || field === 'type') {
updatedFee.amount = updatedFee.type === 'percentage'
? (invoiceData.subtotal * updatedFee.value) / 100
: updatedFee.value;
}
return updatedFee;
}
return fee;
});
updateInvoiceData('fees', updatedFees);
};
const removeFee = (id) => {
const updatedFees = (invoiceData.fees || []).filter(fee => fee.id !== id);
updateInvoiceData('fees', updatedFees);
};
// Discount management
const addDiscount = () => {
const newDiscount = {
id: Date.now(),
label: '',
type: 'fixed',
value: 0,
amount: 0
};
updateInvoiceData('discounts', [...(invoiceData.discounts || []), newDiscount]);
};
const updateDiscount = (id, field, value) => {
const updatedDiscounts = (invoiceData.discounts || []).map(discount => {
if (discount.id === id) {
const updatedDiscount = { ...discount, [field]: value };
// Calculate amount based on type
if (field === 'value' || field === 'type') {
updatedDiscount.amount = updatedDiscount.type === 'percentage'
? (invoiceData.subtotal * updatedDiscount.value) / 100
: updatedDiscount.value;
}
return updatedDiscount;
}
return discount;
});
updateInvoiceData('discounts', updatedDiscounts);
};
const removeDiscount = (id) => {
const updatedDiscounts = (invoiceData.discounts || []).filter(discount => discount.id !== id);
updateInvoiceData('discounts', updatedDiscounts);
};
// Navigate to Invoice Preview for PDF generation
const handleGeneratePreview = () => {
try {
// Save current invoice data to localStorage
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
localStorage.setItem('pdfPageSize', pdfPageSize);
// Navigate to preview page
navigate('/invoice-preview');
} catch (error) {
console.error('Failed to save invoice data:', error);
alert('Failed to save invoice data. Please try again.');
}
};
// JSON Export/Import
const exportJSON = () => {
const jsonString = JSON.stringify(invoiceData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `invoice-${invoiceData.invoiceNumber || 'template'}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Handle file import (same as Table Editor)
const handleFileSelect = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const importedData = JSON.parse(content);
setInvoiceData(importedData);
setCreateNewCompleted(true);
setError('');
} catch (err) {
setError('Invalid JSON file format');
}
};
reader.readAsText(file);
}
};
// URL fetching with Google Drive support
const handleUrlFetch = async () => {
if (!url.trim()) {
setError('Please enter a URL');
return;
}
setIsLoading(true);
setError('');
try {
let fetchUrl = url;
// Convert Google Drive share link to direct download link
if (url.includes('drive.google.com/file/d/')) {
const fileId = url.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1];
if (fileId) {
fetchUrl = `https://drive.google.com/uc?export=download&id=${fileId}`;
}
}
const response = await fetch(fetchUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
const importedData = JSON.parse(text);
setInvoiceData(importedData);
setCreateNewCompleted(true);
setActiveTab('create');
setError('');
} catch (err) {
setError(`Failed to fetch data: ${err.message}`);
} finally {
setIsLoading(false);
}
};
// Logo upload handling
const handleLogoUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
updateInvoiceData('company', {
...invoiceData.company,
logo: e.target.result
});
};
reader.readAsDataURL(file);
}
};
const removeLogo = () => {
updateInvoiceData('company', {
...invoiceData.company,
logo: null
});
};
// Handle signature upload
const handleSignatureUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
updateInvoiceData('digitalSignature', e.target.result);
};
reader.readAsDataURL(file);
}
};
// Settings update functions
const updateSettings = (key, value) => {
if (key === 'company') {
updateInvoiceData('company', value);
} else if (key === 'paymentMethod') {
updateInvoiceData('paymentMethod', value);
} else {
updateInvoiceData('settings', {
...invoiceData.settings,
[key]: value
});
}
};
return (
<>
<SEO
title="Free Invoice Generator - Professional Invoice Templates"
description="✓ Free invoice generator ✓ Professional templates ✓ PDF export ✓ Auto-calculate totals ✓ No signup. Create invoices now!"
keywords="invoice generator, invoice maker, invoice template, free invoice, invoice editor, pdf invoice, professional invoice, online invoice, invoice creator"
path="/invoice-editor"
toolId="invoice-editor"
/>
<ToolLayout
title="Invoice Editor"
description="Create, edit, and export professional invoices with PDF generation"
icon={FileText}
>
<div className="space-y-4 sm:space-y-6 w-full">
{/* Input Section */}
<div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="flex space-x-2 sm:space-x-8 px-4 sm:px-6 overflow-x-auto" aria-label="Tabs">
{[
{ id: 'create', name: 'Create New', icon: Plus },
{ id: 'url', name: 'URL', icon: Globe },
{ id: 'paste', name: 'Paste', icon: FileText },
{ id: 'open', name: 'Open', icon: Upload }
].map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={`${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
} whitespace-nowrap py-4 px-1 sm:px-2 border-b-2 font-medium text-sm flex items-center gap-1 sm:gap-2 transition-colors min-w-0 flex-shrink-0`}
>
<Icon className="h-4 w-4" />
{tab.name}
</button>
);
})}
</nav>
</div>
{/* Tab Content */}
{(activeTab !== 'create' || !createNewCompleted) && (
<div className="p-4">
{activeTab === 'create' && (
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Start Building Your Invoice
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-8 max-w-md mx-auto">
Choose how you'd like to begin creating your professional invoice
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button
onClick={() => {
if (hasModifiedData()) {
setPendingTabChange('create_empty');
setShowInputChangeModal(true);
} else {
handleStartEmpty();
}
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors group"
>
<Plus className="h-8 w-8 text-gray-400 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
Start Empty
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
Create a blank invoice
</span>
</button>
<button
onClick={() => {
if (hasModifiedData()) {
setPendingTabChange('create_sample');
setShowInputChangeModal(true);
} else {
handleLoadSample();
}
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors group"
>
<FileText className="h-8 w-8 text-gray-400 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
Load Sample
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
Start with example invoice
</span>
</button>
</div>
</div>
)}
{activeTab === 'url' && (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://your-url.com/invoice/your-invoice"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && handleUrlFetch()}
/>
</div>
<button
onClick={handleUrlFetch}
disabled={isLoading || !url.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{isLoading ? 'Fetching...' : 'Fetch Data'}
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Enter any URL that returns exported JSON data from your previous invoice work.
</p>
</div>
)}
{activeTab === 'paste' && (
pasteCollapsed ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm text-green-700 dark:text-green-300">
✓ Invoice loaded: {pasteDataSummary.invoiceNumber || 'New Invoice'}
</span>
<button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 hover:underline"
>
Edit Input ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div>
<CodeMirrorEditor
value={inputText}
onChange={setInputText}
placeholder="Paste your invoice JSON data here..."
language="json"
maxLines={12}
showToggle={true}
className="w-full"
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-600 dark:text-red-400">
<strong>Invalid Data:</strong> {error}
</p>
</div>
)}
<div className="flex items-center justify-between flex-shrink-0">
<div className="text-sm text-gray-600 dark:text-gray-400">
Supports JSON invoice templates
</div>
<button
onClick={() => {
try {
const parsed = JSON.parse(inputText);
setInvoiceData(parsed);
setCreateNewCompleted(true);
setPasteDataSummary({
invoiceNumber: parsed.invoiceNumber || 'New Invoice',
size: inputText.length
});
setPasteCollapsed(true);
setError('');
} catch (err) {
setError('Invalid JSON format: ' + err.message);
setPasteCollapsed(false);
}
}}
disabled={!inputText.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
>
Load Invoice
</button>
</div>
</div>
)
)}
{activeTab === 'open' && (
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
accept=".json"
onChange={handleFileSelect}
className="tool-input"
/>
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
<p className="text-xs text-green-700 dark:text-green-300">
🔒 <strong>Privacy:</strong> Your data stays in your browser. We don't store or upload anything - just help you open, edit, and export your files locally.
</p>
</div>
</div>
)}
</div>
)}
</div>
{/* Main Editor Section */}
{ (
<div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Invoice Editor</h2>
</div>
{(
<button
onClick={() => setShowSettingsModal(true)}
className="flex items-center gap-2 px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md transition-colors"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</button>
)}
</div>
</div>
<div className="p-4 sm:p-6 space-y-4 sm:space-y-6">
{!createNewCompleted ? (
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No Invoice Data Loaded</h3>
<p className="text-gray-500 dark:text-gray-400">
Use the input section above to create a new invoice or load existing data.
</p>
</div>
) : (
<div className="space-y-8">
{/* Invoice Header - Like Final Invoice */}
<div className="text-white rounded-xl p-6 border border-blue-200 dark:border-blue-800" style={{ background: `linear-gradient(to right, ${invoiceData.settings?.colorScheme || '#3B82F6'}, ${invoiceData.settings?.colorScheme || '#3B82F6'}dd)` }}>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4">
{/* Logo and Company Name */}
<div className="flex items-center gap-4">
{invoiceData.company.logo ? (
<img
src={invoiceData.company.logo}
alt="Company Logo"
className="h-12 w-12 object-contain bg-white rounded-lg p-1"
/>
) : (
<div className="h-12 w-12 bg-white/20 rounded-lg flex items-center justify-center">
<Building2 className="h-6 w-6 text-white" />
</div>
)}
<div>
<h1 className="text-3xl font-bold">INVOICE</h1>
<input
type="text"
value={invoiceData.company.name}
onChange={(e) => updateInvoiceData('company', { ...invoiceData.company, name: e.target.value })}
className="bg-transparent border-0 text-white placeholder-white/70 text-sm font-medium focus:outline-none focus:ring-0 p-0"
placeholder="Your Company Name"
/>
</div>
</div>
{/* Invoice Details */}
<div className="text-right space-y-1 flex flex-col items-end">
<div className="flex items-center gap-2 w-full">
<span className="text-sm font-medium">#</span>
<input
type="text"
value={invoiceData.invoiceNumber}
onChange={(e) => updateInvoiceData('invoiceNumber', e.target.value)}
className="bg-white/20 border border-white/30 text-white placeholder-white/70 text-sm font-medium rounded px-2 py-1 w-32 text-left sm:text-right focus:outline-none focus:ring-1 focus:ring-white/50 w-full"
placeholder="INV-001"
/>
</div>
<div className="flex items-center gap-2 text-sm w-full">
<span>Date:</span>
<input
type="date"
value={invoiceData.date}
onChange={(e) => updateInvoiceData('date', e.target.value)}
className="bg-white/20 border border-white/30 text-white text-xs rounded px-2 py-1 w-36 focus:outline-none focus:ring-1 focus:ring-white/50 w-full"
/>
</div>
<div className="flex items-center gap-2 text-sm w-full">
<span>Due:</span>
<input
type="date"
value={invoiceData.dueDate}
onChange={(e) => updateInvoiceData('dueDate', e.target.value)}
className="bg-white/20 border border-white/30 text-white text-xs rounded px-2 py-1 w-36 focus:outline-none focus:ring-1 focus:ring-white/50 w-full"
/>
</div>
</div>
</div>
</div>
{/* From and To Section - Invoice Layout Style */}
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-6">
{/* From Section */}
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-6 border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-lg flex items-center justify-center" style={{ backgroundColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}20`, color: invoiceData.settings?.colorScheme || '#3B82F6' }}>
<Building2 className="h-4 w-4" />
</div>
<h3 className="text-sm font-bold uppercase tracking-wide" style={{ color: invoiceData.settings?.colorScheme || '#3B82F6' }}>FROM</h3>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={invoiceData.settings?.showFromSection ?? true}
onChange={(e) => updateInvoiceData('settings', {
...invoiceData.settings,
showFromSection: e.target.checked
})}
className="w-4 h-4 rounded border-gray-300 focus:ring-2"
style={{
accentColor: invoiceData.settings?.colorScheme || '#3B82F6',
'--tw-ring-color': `${invoiceData.settings?.colorScheme || '#3B82F6'}40`
}}
/>
<span className="text-xs text-gray-500 dark:text-gray-400">Show in preview</span>
</label>
</div>
<div className={`space-y-3 ${!(invoiceData.settings?.showFromSection ?? true) ? 'opacity-50 pointer-events-none' : ''}`}>
<input
type="text"
value={invoiceData.company.name}
onChange={(e) => updateInvoiceData('company', { ...invoiceData.company, name: e.target.value })}
disabled={!(invoiceData.settings?.showFromSection ?? true)}
className="w-full text-lg font-semibold text-gray-900 dark:text-white bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-0 pb-1 disabled:cursor-not-allowed"
style={{ '--focus-color': invoiceData.colors?.from || '#3B82F6' }}
onFocus={(e) => e.target.style.borderBottomColor = invoiceData.colors?.from || '#3B82F6'}
onBlur={(e) => e.target.style.borderBottomColor = ''}
placeholder="Company Name"
/>
<input
type="text"
value={invoiceData.company.address}
onChange={(e) => updateInvoiceData('company', { ...invoiceData.company, address: e.target.value })}
disabled={!(invoiceData.settings?.showFromSection ?? true)}
className="w-full text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-blue-500 focus:outline-none focus:ring-0 pb-1 disabled:cursor-not-allowed"
placeholder="Street Address"
/>
<input
type="text"
value={invoiceData.company.city}
onChange={(e) => updateInvoiceData('company', { ...invoiceData.company, city: e.target.value })}
disabled={!(invoiceData.settings?.showFromSection ?? true)}
className="w-full text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-blue-500 focus:outline-none focus:ring-0 pb-1 disabled:cursor-not-allowed"
placeholder="City, State ZIP"
/>
<div className="flex gap-3 flex-col sm:flex-row">
<input
type="tel"
value={invoiceData.company.phone}
onChange={(e) => updateInvoiceData('company', { ...invoiceData.company, phone: e.target.value })}
disabled={!(invoiceData.settings?.showFromSection ?? true)}
className="flex-1 text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-blue-500 focus:outline-none focus:ring-0 pb-1 disabled:cursor-not-allowed"
placeholder="Phone"
/>
<span className="text-gray-400 text-sm self-end pb-1 hidden sm:block">|</span>
<input
type="email"
value={invoiceData.company.email}
onChange={(e) => updateInvoiceData('company', { ...invoiceData.company, email: e.target.value })}
disabled={!(invoiceData.settings?.showFromSection ?? true)}
className="flex-1 text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-blue-500 focus:outline-none focus:ring-0 pb-1 disabled:cursor-not-allowed"
placeholder="Email"
/>
</div>
</div>
</div>
{/* To Section */}
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-6 border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3 mb-4">
<div className="h-8 w-8 rounded-lg flex items-center justify-center" style={{ backgroundColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}20`, color: invoiceData.settings?.colorScheme || '#3B82F6' }}>
<User className="h-4 w-4" />
</div>
<h3 className="text-sm font-bold uppercase tracking-wide" style={{ color: invoiceData.settings?.colorScheme || '#3B82F6' }}>TO</h3>
</div>
<div className="space-y-3">
<input
type="text"
value={invoiceData.client.name}
onChange={(e) => updateInvoiceData('client', { ...invoiceData.client, name: e.target.value })}
className="w-full text-lg font-semibold text-gray-900 dark:text-white bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-green-500 focus:outline-none focus:ring-0 pb-1"
placeholder="Client Name"
/>
<input
type="text"
value={invoiceData.client.address}
onChange={(e) => updateInvoiceData('client', { ...invoiceData.client, address: e.target.value })}
className="w-full text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-green-500 focus:outline-none focus:ring-0 pb-1"
placeholder="Street Address"
/>
<input
type="text"
value={invoiceData.client.city}
onChange={(e) => updateInvoiceData('client', { ...invoiceData.client, city: e.target.value })}
className="w-full text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-green-500 focus:outline-none focus:ring-0 pb-1"
placeholder="City, State ZIP"
/>
<div className="flex gap-3 flex-col sm:flex-row">
<input
type="tel"
value={invoiceData.client.phone}
onChange={(e) => updateInvoiceData('client', { ...invoiceData.client, phone: e.target.value })}
className="flex-1 text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-green-500 focus:outline-none focus:ring-0 pb-1"
placeholder="Phone"
/>
<span className="text-gray-400 text-sm self-end pb-1 hidden sm:block">|</span>
<input
type="email"
value={invoiceData.client.email}
onChange={(e) => updateInvoiceData('client', { ...invoiceData.client, email: e.target.value })}
className="flex-1 text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-green-500 focus:outline-none focus:ring-0 pb-1"
placeholder="Email"
/>
</div>
</div>
</div>
</div>
{/* Items */}
<div className="rounded-xl border" style={{ backgroundColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}10`, borderColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}40` }}>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b" style={{ backgroundColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}20`, borderBottomColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}40` }}>
<th className="px-4 py-3 text-left text-sm font-semibold" style={{ color: invoiceData.settings?.colorScheme || '#3B82F6' }}>Items</th>
<th className="px-3 py-3 text-center text-sm font-semibold" style={{ color: invoiceData.settings?.colorScheme || '#3B82F6', width: 'auto' }}>Qty</th>
<th className="px-3 py-3 text-center text-sm font-semibold" style={{ color: invoiceData.settings?.colorScheme || '#3B82F6', width: 'auto' }}>Rate</th>
<th className="px-3 py-3 text-right text-sm font-semibold" style={{ color: invoiceData.settings?.colorScheme || '#3B82F6', width: 'auto' }}>Amount</th>
<th className="px-2 py-3 text-center w-12"></th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800">
{invoiceData.items.map((item, index) => (
<tr key={item.id} className="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td className="px-4 py-3 min-w-[225px]">
<input
type="text"
value={item.description}
onChange={(e) => updateLineItem(item.id, 'description', e.target.value)}
className="w-full px-2 py-1 border-0 bg-transparent focus:outline-none focus:ring-0 focus:bg-blue-50 dark:focus:bg-blue-900/20 rounded text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 transition-colors"
placeholder="Enter item description..."
/>
</td>
<td className="px-3 py-3 text-center" style={{ width: 'auto' }}>
<input
type="text"
value={formatNumber(item.quantity)}
onChange={(e) => {
const numValue = parseFloat(e.target.value.replace(/,/g, '')) || 0;
updateLineItem(item.id, 'quantity', numValue);
}}
className="w-full px-2 py-1 text-center border-0 bg-transparent focus:outline-none focus:ring-0 focus:bg-blue-50 dark:focus:bg-blue-900/20 rounded text-gray-900 dark:text-white transition-colors"
style={{ width: `${Math.max(formatNumber(item.quantity).length * 8 + 20, 60)}px` }}
onFocus={(e) => {
// Show raw number on focus
e.target.value = item.quantity.toString();
}}
onBlur={(e) => {
// Format with thousand separator on blur
e.target.value = formatNumber(parseFloat(e.target.value.replace(/,/g, '')) || 0);
}}
/>
</td>
<td className="px-3 py-3 text-center" style={{ width: 'auto' }}>
<div className="relative flex justify-end items-center">
<span className="text-gray-500 dark:text-gray-400 px-2 py-1 text-xs rounded-1 bg-gray-100 dark:bg-gray-900/20">{invoiceData.settings?.currency?.symbol || '$'}</span>
<input
type="text"
value={formatNumber(item.rate)}
onChange={(e) => {
const numValue = parseFloat(e.target.value.replace(/,/g, '')) || 0;
updateLineItem(item.id, 'rate', numValue);
}}
className="pl-2 py-1 text-center border-0 bg-transparent focus:outline-none focus:ring-0 focus:bg-blue-50 dark:focus:bg-blue-900/20 rounded text-gray-900 dark:text-white transition-colors"
style={{ width: `${Math.max(formatNumber(item.rate).length * 8 + 20, 40)}px` }}
onFocus={(e) => {
// Show raw number on focus
e.target.value = item.rate.toString();
}}
onBlur={(e) => {
// Format with thousand separator on blur
e.target.value = formatNumber(parseFloat(e.target.value.replace(/,/g, '')) || 0);
}}
/>
</div>
</td>
<td className="px-3 py-3 text-right font-semibold text-gray-900 dark:text-white" style={{ width: 'auto' }}>
<span style={{ minWidth: `${Math.max(formatCurrency(item.amount, true).length * 8 + 20, 100)}px`, display: 'inline-block' }}>
{formatCurrency(item.amount, true)}
</span>
</td>
<td className="px-2 py-3 text-center">
<div className="flex items-center gap-1">
<button
onClick={() => moveItem('items', item.id, 'up')}
disabled={index === 0}
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
</button>
<button
onClick={() => moveItem('items', item.id, 'down')}
disabled={index === invoiceData.items.length - 1}
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
</button>
<button
onClick={() => removeLineItem(item.id)}
className="p-1 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
title="Delete item"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
{/* Add Item Row */}
<tr className="border-b border-gray-100 dark:border-gray-700 transition-colors" style={{ backgroundColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}05` }}>
<td colSpan="5" className="px-4 py-3 relative">
<button
onClick={addLineItem}
className="text-left flex items-center gap-2 py-1 transition-colors sticky left-4"
style={{
color: invoiceData.settings?.colorScheme || '#3B82F6',
':hover': { color: `${invoiceData.settings?.colorScheme || '#3B82F6'}dd` }
}}
>
<Plus className="h-4 w-4" />
Add item
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Totals and Tax */}
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-6">
<div className="rounded-xl p-6 border" style={{ backgroundColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}10`, borderColor: `${invoiceData.settings?.colorScheme || '#3B82F6'}40` }}>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: invoiceData.settings?.colorScheme || '#3B82F6' }}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Fees & Discounts
</h3>
{/* Fees Section */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Additional Fees</label>
<button
onClick={addFee}
className="flex items-center gap-1 px-2 py-1 text-xs bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 rounded-md transition-colors"
>
<Plus className="h-3 w-3" />
Add Fee
</button>
</div>
{(invoiceData.fees || []).map((fee, index) => (
<div key={fee.id} className="flex gap-3 mb-2 p-3 bg-gray-50 dark:bg-gray-700 rounded-md">
{/* Main Content Area */}
<div className="flex-1 flex flex-col xl:flex-row gap-2">
{/* Fee Name */}
<input
type="text"
value={fee.label}
onChange={(e) => updateFee(fee.id, 'label', e.target.value)}
placeholder="Fee name"
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white font-medium"
/>
{/* Controls Row */}
<div className="flex flex-wrap gap-2 flex-col xl:flex-row">
<input
type="number"
value={fee.value}
onChange={(e) => updateFee(fee.id, 'value', parseFloat(e.target.value) || 0)}
placeholder="0"
className="flex-1 xl:flex-0 xl:w-20 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
min="0"
step="0.01"
/>
<select
value={fee.type}
onChange={(e) => updateFee(fee.id, 'type', e.target.value)}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
>
<option value="fixed">Fixed</option>
<option value="percentage">%</option>
</select>
</div>
</div>
{/* Action Buttons - Always on Right */}
<div className="flex flex-col xl:flex-row gap-1 items-center justify-center">
<button
onClick={() => moveItem('fees', fee.id, 'up')}
disabled={index === 0}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
</button>
<button
onClick={() => moveItem('fees', fee.id, 'down')}
disabled={index === (invoiceData.fees || []).length - 1}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
</button>
<button
onClick={() => removeFee(fee.id)}
className="p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
title="Delete fee"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
{/* Discounts Section */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Discounts</label>
<button
onClick={addDiscount}
className="flex items-center gap-1 px-2 py-1 text-xs bg-green-100 hover:bg-green-200 dark:bg-green-900 dark:hover:bg-green-800 text-green-700 dark:text-green-300 rounded-md transition-colors"
>
<Plus className="h-3 w-3" />
Add Discount
</button>
</div>
{(invoiceData.discounts || []).map((discount, index) => (
<div key={discount.id} className="flex gap-3 mb-2 p-3 bg-gray-50 dark:bg-gray-700 rounded-md">
{/* Main Content Area */}
<div className="flex-1 flex flex-col xl:flex-row gap-2">
{/* Discount Name */}
<input
type="text"
value={discount.label}
onChange={(e) => updateDiscount(discount.id, 'label', e.target.value)}
placeholder="Discount name"
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white font-medium"
/>
{/* Controls Row */}
<div className="flex flex-wrap gap-2 flex-col xl:flex-row">
<input
type="number"
value={discount.value}
onChange={(e) => updateDiscount(discount.id, 'value', parseFloat(e.target.value) || 0)}
placeholder="0"
className="flex-1 xl:flex-0 xl:w-20 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
min="0"
step="0.01"
/>
<select
value={discount.type}
onChange={(e) => updateDiscount(discount.id, 'type', e.target.value)}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
>
<option value="fixed">Fixed</option>
<option value="percentage">%</option>
</select>
</div>
</div>
{/* Action Buttons - Always on Right */}
<div className="flex flex-col xl:flex-row gap-1 items-center justify-center">
<button
onClick={() => moveItem('discounts', discount.id, 'up')}
disabled={index === 0}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
</button>
<button
onClick={() => moveItem('discounts', discount.id, 'down')}
disabled={index === (invoiceData.discounts || []).length - 1}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
</button>
<button
onClick={() => removeDiscount(discount.id)}
className="p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
title="Delete discount"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
</div>
<div className="bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20 rounded-xl p-6 border border-emerald-200 dark:border-emerald-800">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<svg className="h-5 w-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
Invoice Total
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">Subtotal:</span>
<span className="font-medium text-gray-900 dark:text-white flex-shrink-0">{formatCurrency(invoiceData.subtotal, true)}</span>
</div>
{/* Dynamic Fees */}
{(invoiceData.fees || []).map((fee) => (
<div key={fee.id} className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}:
</span>
<span className="font-medium text-blue-600 dark:text-blue-400 flex-shrink-0">
+{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
</span>
</div>
))}
{/* Dynamic Discounts */}
{(invoiceData.discounts || []).map((discount) => (
<div key={discount.id} className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}:
</span>
<span className="font-medium text-red-600 dark:text-red-400 flex-shrink-0">
-{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
</span>
</div>
))}
{/* Legacy Discount */}
{invoiceData.discount > 0 && (
<div className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">Discount:</span>
<span className="font-medium text-red-600 dark:text-red-400 flex-shrink-0">-{formatCurrency(invoiceData.discount, true)}</span>
</div>
)}
<div className="bg-emerald-100 dark:bg-emerald-900/30 rounded-lg p-4 mt-4">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-emerald-800 dark:text-emerald-200">Total:</span>
<span className="text-2xl font-bold text-emerald-800 dark:text-emerald-200 flex-shrink-0">{formatCurrency(invoiceData.total, true)}</span>
</div>
</div>
</div>
</div>
</div>
{/* Payment Terms - Optional Section */}
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl p-6 border border-blue-200 dark:border-blue-800">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<svg className="h-5 w-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
Payment Terms (Optional)
</h3>
<div className="space-y-4">
{/* Payment Type Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Payment Type
</label>
<select
value={invoiceData.paymentTerms?.type || 'full'}
onChange={(e) => updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
type: e.target.value
})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="full">Full Payment</option>
<option value="downpayment">Down Payment + Balance</option>
<option value="installment">Installments Only</option>
</select>
</div>
{/* Down Payment Section */}
{(invoiceData.paymentTerms?.type === 'downpayment') && (
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-600">
<h4 className="text-md font-medium text-gray-900 dark:text-white mb-3">Down Payment</h4>
<div className="space-y-2">
<div className="flex gap-3 mb-2 p-3 bg-gray-50 dark:bg-gray-700 rounded-md">
{/* Main Content Area */}
<div className="flex-1 flex flex-col lg:flex-row gap-2">
{/* Down Payment Label */}
<input
type="text"
value="Down Payment"
readOnly
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white bg-gray-100 dark:bg-gray-600 font-medium"
/>
{/* Controls Row */}
<div className="flex flex-wrap gap-2">
<input
type="number"
value={
invoiceData.paymentTerms?.downPayment?.type === 'fixed'
? (invoiceData.paymentTerms?.downPayment?.amount || 0)
: (invoiceData.paymentTerms?.downPayment?.percentage || 0)
}
onChange={(e) => {
const value = parseFloat(e.target.value) || 0;
const isFixed = invoiceData.paymentTerms?.downPayment?.type === 'fixed';
let amount, percentage;
if (isFixed) {
amount = value;
percentage = invoiceData.total > 0 ? (amount / invoiceData.total) * 100 : 0;
} else {
percentage = value;
amount = (invoiceData.total * percentage) / 100;
}
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
downPayment: {
...invoiceData.paymentTerms.downPayment,
percentage,
amount
}
});
}}
placeholder="0"
className="flex-1 lg:flex-0 w-20 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
min="0"
max="100"
step="0.01"
/>
<select
value={invoiceData.paymentTerms?.downPayment?.type || 'percentage'}
onChange={(e) => {
const type = e.target.value;
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
downPayment: {
...invoiceData.paymentTerms.downPayment,
type
}
});
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
>
<option value="percentage">%</option>
<option value="fixed">Fixed</option>
</select>
<input
type="date"
value={invoiceData.paymentTerms?.downPayment?.dueDate || ''}
onChange={(e) => updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
downPayment: {
...invoiceData.paymentTerms.downPayment,
dueDate: e.target.value
}
})}
className="flex-1 lg:flex-0 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
/>
<select
value={invoiceData.paymentTerms?.downPayment?.status || 'pending'}
onChange={(e) => updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
downPayment: {
...invoiceData.paymentTerms.downPayment,
status: e.target.value
}
})}
className={`flex-1 lg:flex-0 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white ${
invoiceData.paymentTerms?.downPayment?.status === 'paid' ? 'bg-green-50 text-green-800 dark:bg-green-900/20 dark:text-green-300' :
invoiceData.paymentTerms?.downPayment?.status === 'current' ? 'bg-yellow-50 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300' :
invoiceData.paymentTerms?.downPayment?.status === 'overdue' ? 'bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-300' :
'bg-gray-50 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
}`}
>
<option value="pending">Pending</option>
<option value="current">Current</option>
<option value="paid">Paid</option>
<option value="overdue">Overdue</option>
</select>
</div>
</div>
</div>
</div>
{invoiceData.paymentTerms?.downPayment?.amount > 0 && (
<div className="mt-3 p-2 bg-blue-50 dark:bg-blue-900/20 rounded text-sm text-blue-800 dark:text-blue-200">
Remaining Balance: {formatCurrency(invoiceData.total - (invoiceData.paymentTerms?.downPayment?.amount || 0), true)}
</div>
)}
</div>
)}
{/* Installments Section */}
{(invoiceData.paymentTerms?.type === 'installment' || invoiceData.paymentTerms?.type === 'downpayment') && (
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-600">
<div className="flex justify-between items-center mb-3">
<h4 className="text-md font-medium text-gray-900 dark:text-white">
{invoiceData.paymentTerms?.type === 'downpayment' ? 'Remaining Balance Installments' : 'Payment Installments'}
</h4>
<button
onClick={() => {
const newInstallment = {
id: Date.now(),
amount: 0,
percentage: 0,
dueDate: '',
description: `Installment ${(invoiceData.paymentTerms?.installments?.length || 0) + 1}`
};
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: [...(invoiceData.paymentTerms?.installments || []), newInstallment]
});
}}
className="flex items-center gap-1 px-2 py-1 text-xs bg-indigo-100 hover:bg-indigo-200 dark:bg-indigo-900 dark:hover:bg-indigo-800 text-indigo-700 dark:text-indigo-300 rounded-md transition-colors whitespace-nowrap"
>
<Plus className="h-3 w-3" />
Add Term
</button>
</div>
<div className="space-y-2">
{(invoiceData.paymentTerms?.installments || []).map((installment, index) => (
<div key={installment.id} className="flex gap-3 mb-2 p-3 bg-gray-50 dark:bg-gray-700 rounded-md">
{/* Main Content Area */}
<div className="flex-1 flex flex-col lg:flex-row gap-2">
{/* Installment Name - Full Width */}
<input
type="text"
value={installment.description}
onChange={(e) => {
const updatedInstallments = invoiceData.paymentTerms.installments.map(inst =>
inst.id === installment.id ? { ...inst, description: e.target.value } : inst
);
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: updatedInstallments
});
}}
placeholder="Installment name"
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white font-medium"
/>
{/* Controls Row */}
<div className="flex flex-wrap gap-2">
<input
type="number"
value={installment.type === 'percentage' ? (installment.percentage || 0) : (installment.amount || 0)}
onChange={(e) => {
const value = parseFloat(e.target.value) || 0;
const baseAmount = invoiceData.paymentTerms?.type === 'downpayment'
? invoiceData.total - (invoiceData.paymentTerms?.downPayment?.amount || 0)
: invoiceData.total;
let amount, percentage;
if (installment.type === 'percentage') {
percentage = value;
amount = (baseAmount * percentage) / 100;
} else {
amount = value;
percentage = baseAmount > 0 ? (amount / baseAmount) * 100 : 0;
}
const updatedInstallments = invoiceData.paymentTerms.installments.map(inst =>
inst.id === installment.id ? { ...inst, amount, percentage } : inst
);
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: updatedInstallments
});
}}
placeholder="0"
className="flex-1 lg:flex-0 w-20 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
min="0"
step="0.01"
/>
<select
value={installment.type || 'fixed'}
onChange={(e) => {
const updatedInstallments = invoiceData.paymentTerms.installments.map(inst =>
inst.id === installment.id ? { ...inst, type: e.target.value } : inst
);
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: updatedInstallments
});
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
>
<option value="fixed">Fixed</option>
<option value="percentage">%</option>
</select>
<input
type="date"
value={installment.dueDate}
onChange={(e) => {
const updatedInstallments = invoiceData.paymentTerms.installments.map(inst =>
inst.id === installment.id ? { ...inst, dueDate: e.target.value } : inst
);
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: updatedInstallments
});
}}
className="flex-1 lg:flex-0 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
/>
<select
value={installment.status || 'pending'}
onChange={(e) => {
const updatedInstallments = invoiceData.paymentTerms.installments.map(inst =>
inst.id === installment.id ? { ...inst, status: e.target.value } : inst
);
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: updatedInstallments
});
}}
className={`flex-1 lg:flex-0 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white ${
installment.status === 'paid' ? 'bg-green-50 text-green-800 dark:bg-green-900/20 dark:text-green-300' :
installment.status === 'current' ? 'bg-yellow-50 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300' :
installment.status === 'overdue' ? 'bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-300' :
'bg-gray-50 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
}`}
>
<option value="pending">Pending</option>
<option value="current">Current</option>
<option value="paid">Paid</option>
<option value="overdue">Overdue</option>
</select>
</div>
</div>
{/* Action Buttons - Always on Right */}
<div className="flex flex-col lg:flex-row gap-1 items-center justify-center">
<button
onClick={() => moveInstallment(installment.id, 'up')}
disabled={index === 0}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
</button>
<button
onClick={() => moveInstallment(installment.id, 'down')}
disabled={index === (invoiceData.paymentTerms?.installments || []).length - 1}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
</button>
<button
onClick={() => {
const updatedInstallments = invoiceData.paymentTerms.installments.filter(inst => inst.id !== installment.id);
updateInvoiceData('paymentTerms', {
...invoiceData.paymentTerms,
installments: updatedInstallments
});
}}
className="p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors mt-12 lg:mt-0"
title="Delete installment"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Additional Notes & Signature - Independent Card */}
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 rounded-xl p-6 border border-amber-200 dark:border-amber-800">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Additional Notes & Signature
</h3>
<div className="space-y-4">
{/* Additional Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Additional Notes
</label>
<textarea
value={invoiceData.notes}
onChange={(e) => updateInvoiceData('notes', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-amber-500 focus:border-amber-500 dark:bg-gray-700 dark:text-white resize-none"
rows="3"
placeholder="Payment terms, special instructions..."
/>
</div>
{/* Thank You Message */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Thank You Message
</label>
<input
type="text"
value={invoiceData.thankYouMessage}
onChange={(e) => updateInvoiceData('thankYouMessage', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-amber-500 focus:border-amber-500 dark:bg-gray-700 dark:text-white"
placeholder="Thank you for your business!"
/>
</div>
{/* Authorized Signature Text */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Signature Label
</label>
<input
type="text"
value={invoiceData.authorizedSignedText}
onChange={(e) => updateInvoiceData('authorizedSignedText', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-amber-500 focus:border-amber-500 dark:bg-gray-700 dark:text-white"
placeholder="Authorized Signed"
/>
</div>
{/* Digital Signature Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Digital Signature
</label>
<div className="flex items-center gap-3">
{invoiceData.digitalSignature ? (
<div className="flex items-center gap-3">
<img
src={invoiceData.digitalSignature}
alt="Digital Signature"
className="h-12 w-auto object-contain border border-gray-300 rounded-lg bg-white p-1"
/>
<button
onClick={() => updateInvoiceData('digitalSignature', null)}
className="px-3 py-2 text-sm text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 border border-red-300 hover:border-red-400 rounded-md transition-colors"
>
Remove
</button>
</div>
) : (
<div className="space-y-3">
<div className="flex gap-2">
<button
onClick={() => document.getElementById('signature-upload').click()}
className="flex items-center gap-2 px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors dark:text-white"
>
<Upload className="h-4 w-4" />
Upload Image
</button>
<button
onClick={() => setShowSignaturePad(true)}
className="flex items-center gap-2 px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors dark:text-white"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
Draw Signature
</button>
</div>
</div>
)}
</div>
<input
id="signature-upload"
type="file"
accept="image/*"
onChange={handleSignatureUpload}
className="hidden"
/>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Upload an image of your signature (PNG, JPG recommended)
</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
)}
{/* Export Section */}
{(activeTab !== 'create' || createNewCompleted) && createNewCompleted && (
<div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div
onClick={() => setExportExpanded(!exportExpanded)}
className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Export Invoice</h2>
{exportExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</div>
</div>
</div>
{exportExpanded && (
<div className="p-4 sm:p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
onClick={handleGeneratePreview}
className="flex items-center justify-center gap-3 px-6 py-4 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
<FileText className="h-5 w-5" />
<div className="text-left">
<div className="font-medium">Generate PDF</div>
<div className="text-sm opacity-90">Preview & download PDF</div>
</div>
</button>
<button
onClick={exportJSON}
className="flex items-center justify-center gap-3 px-6 py-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Download className="h-5 w-5" />
<div className="text-left">
<div className="font-medium">Download JSON</div>
<div className="text-sm opacity-90">Reusable invoice template</div>
</div>
</button>
</div>
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-300">
<strong>Tip:</strong> Save as JSON to create reusable invoice templates. Load them later using the "Open" tab above.
</p>
</div>
</div>
)}
</div>
)}
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden">
<div
onClick={() => setUsageTipsExpanded(!usageTipsExpanded)}
className="px-4 py-3 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors flex items-center justify-between"
>
<h4 className="text-blue-800 dark:text-blue-200 font-medium flex items-center gap-2">
💡 Usage Tips
</h4>
{usageTipsExpanded ? <ChevronUp className="h-4 w-4 text-blue-600 dark:text-blue-400" /> : <ChevronDown className="h-4 w-4 text-blue-600 dark:text-blue-400" />}
</div>
{usageTipsExpanded && (
<div className="px-4 pb-4 text-blue-700 dark:text-blue-300 text-sm space-y-3">
<div>
<p className="font-medium mb-1">📝 Input Methods:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Create New:</strong> Start empty or load sample invoice to explore features</li>
<li><strong>URL Import:</strong> Fetch invoice data directly from JSON endpoints</li>
<li><strong>Paste Data:</strong> Auto-detects JSON invoice templates</li>
<li><strong>Open Files:</strong> Import .json invoice files</li>
</ul>
</div>
<div>
<p className="font-medium mb-1"> Invoice Editing:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Company & Client:</strong> Fill in business details, addresses, and contact info</li>
<li><strong>Items:</strong> Add products/services with descriptions, quantities, and prices</li>
<li><strong>Fees & Discounts:</strong> Add additional fees or discounts (fixed or percentage)</li>
<li><strong>Payment Terms:</strong> Set full payment, installments, or down payment options</li>
<li><strong>Digital Signature:</strong> Draw or upload signature for professional invoices</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">🎨 Customization:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Settings:</strong> Change color scheme, currency, and display options</li>
<li><strong>Logo Upload:</strong> Add your company logo for branding</li>
<li><strong>Payment Methods:</strong> Add bank details, payment links, or QR codes</li>
<li><strong>Notes & Messages:</strong> Include payment terms, thank you messages, and authorized signatures</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">📤 Export Options:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>PDF:</strong> Generate professional PDF invoices for clients</li>
<li><strong>JSON:</strong> Save as reusable invoice templates</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">💾 Data Privacy:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Local Processing:</strong> All data stays in your browser</li>
<li><strong>No Upload:</strong> We don't store or transmit your invoice data</li>
<li><strong>Secure:</strong> Your business information remains private</li>
</ul>
</div>
</div>
)}
</div>
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
<p className="text-red-700 dark:text-red-300">{error}</p>
</div>
</div>
)}
{/* Settings Modal */}
{showSettingsModal && (
<InvoiceSettingsModal
invoiceData={invoiceData}
currencies={currencies}
onUpdateSettings={updateSettings}
onClose={() => setShowSettingsModal(false)}
logoInputRef={logoInputRef}
handleLogoUpload={handleLogoUpload}
removeLogo={removeLogo}
/>
)}
{/* Confirmation Modal */}
{showInputChangeModal && (
<InputChangeConfirmationModal
invoiceData={invoiceData}
currentMethod={activeTab}
newMethod={pendingTabChange}
onConfirm={confirmInputChange}
onCancel={cancelInputChange}
/>
)}
{/* Signature Drawing Modal */}
<SignaturePadModal
isOpen={showSignaturePad}
onClose={() => setShowSignaturePad(false)}
onSave={(dataURL) => {
updateInvoiceData('digitalSignature', dataURL);
setShowSignaturePad(false);
}}
/>
</div>
{/* Related Tools */}
<RelatedTools toolId="invoice-editor" />
</ToolLayout>
</>
);
};
// Searchable Currency Dropdown Component
const SearchableCurrencyDropdown = ({ currencies, selectedCurrency, onSelect }) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const filteredCurrencies = currencies.filter(currency =>
currency.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
currency.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
(currency.symbol && currency.symbol.includes(searchTerm))
);
return (
<div className="relative">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white text-left flex items-center justify-between"
>
<span>
{selectedCurrency?.symbol || selectedCurrency?.code || 'USD'} - {selectedCurrency?.name || 'US Dollar'} ({selectedCurrency?.code || 'USD'})
</span>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-60 overflow-y-auto">
<div className="p-2">
<input
type="text"
placeholder="Search currencies..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white text-sm"
/>
</div>
<div className="max-h-48 overflow-y-auto">
{filteredCurrencies.map(currency => (
<button
key={currency.code}
type="button"
onClick={() => {
onSelect(currency);
setIsOpen(false);
setSearchTerm('');
}}
className="w-full px-3 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 text-sm dark:text-white"
>
{currency.symbol || currency.code} - {currency.name} ({currency.code})
</button>
))}
</div>
</div>
)}
</div>
);
};
// Invoice Settings Modal Component
const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClose, logoInputRef, handleLogoUpload, removeLogo }) => {
const [activeTab, setActiveTab] = useState('general');
const updatePaymentMethod = (field, value) => {
onUpdateSettings('paymentMethod', {
...invoiceData.paymentMethod,
[field]: value
});
};
const updatePaymentMethodNested = (section, field, value) => {
onUpdateSettings('paymentMethod', {
...invoiceData.paymentMethod,
[section]: {
...invoiceData.paymentMethod[section],
[field]: value
}
});
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
<div className="px-6 py-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Invoice Settings</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-600">
<button
onClick={() => setActiveTab('general')}
className={`px-6 py-3 text-sm font-medium relative transition-all duration-200 ${
activeTab === 'general'
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
General
{activeTab === 'general' && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 dark:bg-blue-400"></div>
)}
</button>
<button
onClick={() => setActiveTab('layout')}
className={`px-6 py-3 text-sm font-medium relative transition-all duration-200 ${
activeTab === 'layout'
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
Layout
{activeTab === 'layout' && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 dark:bg-blue-400"></div>
)}
</button>
<button
onClick={() => setActiveTab('payment')}
className={`px-6 py-3 text-sm font-medium relative transition-all duration-200 ${
activeTab === 'payment'
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
Payment
{activeTab === 'payment' && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 dark:bg-blue-400"></div>
)}
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[60vh]">
{/* General Tab */}
{activeTab === 'general' && (
<div className="p-6 space-y-6">
{/* Logo Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Attach Your Logo
</label>
<div className="flex items-center gap-3">
{invoiceData.company.logo ? (
<div className="flex items-center gap-3">
<img
src={invoiceData.company.logo}
alt="Company Logo"
className="h-12 w-12 object-contain border border-gray-300 rounded-lg"
/>
<button
onClick={removeLogo}
className="px-3 py-2 text-sm text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 border border-red-300 hover:border-red-400 rounded-md transition-colors"
>
Remove Logo
</button>
</div>
) : (
<button
onClick={() => logoInputRef.current?.click()}
className="flex items-center gap-2 px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors dark:text-white"
>
<Upload className="h-4 w-4" />
Attach Logo
</button>
)}
</div>
<input
ref={logoInputRef}
type="file"
accept="image/*"
onChange={handleLogoUpload}
className="hidden"
/>
</div>
{/* Color Scheme */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Color Scheme
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={invoiceData.settings?.colorScheme || '#3B82F6'}
onChange={(e) => onUpdateSettings('colorScheme', e.target.value)}
className="w-12 h-12 rounded-lg border border-gray-300 cursor-pointer bg-transparent"
/>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">
This color will be used throughout the invoice and PDF
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Current: {invoiceData.settings?.colorScheme || '#3B82F6'}
</p>
</div>
</div>
</div>
{/* Currency */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Currency
</label>
<SearchableCurrencyDropdown
currencies={currencies}
selectedCurrency={invoiceData.settings?.currency}
onSelect={(currency) => onUpdateSettings('currency', currency)}
/>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Selected: {invoiceData.settings?.currency?.symbol || invoiceData.settings?.currency?.code || '$'} ({invoiceData.settings?.currency?.code || 'USD'})
</p>
</div>
{/* Thousand Separator */}
<div>
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={invoiceData.settings?.thousandSeparator !== false}
onChange={(e) => onUpdateSettings('thousandSeparator', e.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
<div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
Use Thousand Separator
</span>
<p className="text-xs text-gray-500 dark:text-gray-500">
Format numbers like 1,000.00 instead of 1000.00
</p>
</div>
</label>
</div>
{/* Decimal Digits */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Decimal Digits
</label>
<select
value={invoiceData.settings?.decimalDigits || 2}
onChange={(e) => onUpdateSettings('decimalDigits', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value={0}>0 (1000)</option>
<option value={1}>1 (1000.0)</option>
<option value={2}>2 (1000.00)</option>
<option value={3}>3 (1000.000)</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Number of decimal places to display
</p>
</div>
</div>
)}
{/* Layout Tab */}
{activeTab === 'layout' && (
<div className="p-6 space-y-6">
{/* Section Spacing */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Section Spacing
</label>
<select
value={invoiceData.settings?.sectionSpacing || 'normal'}
onChange={(e) => onUpdateSettings('sectionSpacing', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
>
<option value="compact">Compact (15px spacing)</option>
<option value="normal">Normal (25px spacing)</option>
<option value="spacious">Spacious (40px spacing)</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Controls the spacing between major sections for better multi-page layout
</p>
</div>
{/* Page Break Controls */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Page Break Controls
</label>
<div className="space-y-3">
<label className="flex items-center">
<input
type="checkbox"
checked={invoiceData.settings?.pageBreaks?.beforePaymentSchedule || false}
onChange={(e) => onUpdateSettings('pageBreaks', {
...invoiceData.settings?.pageBreaks,
beforePaymentSchedule: e.target.checked
})}
className="mr-3 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Force page break before Payment Schedule</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={invoiceData.settings?.pageBreaks?.beforeItemsTable || false}
onChange={(e) => onUpdateSettings('pageBreaks', {
...invoiceData.settings?.pageBreaks,
beforeItemsTable: e.target.checked
})}
className="mr-3 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Force page break before Items Table</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={invoiceData.settings?.pageBreaks?.beforePaymentMethod || false}
onChange={(e) => onUpdateSettings('pageBreaks', {
...invoiceData.settings?.pageBreaks,
beforePaymentMethod: e.target.checked
})}
className="mr-3 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Force page break before Payment Method</span>
</label>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Use page breaks to ensure important sections start on a new page in PDF output
</p>
</div>
</div>
)}
{/* Payment Methods Tab */}
{activeTab === 'payment' && (
<div className="p-6 space-y-6">
{/* Payment Method Type */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Payment Method Display
</label>
<select
value={invoiceData.paymentMethod?.type || 'none'}
onChange={(e) => updatePaymentMethod('type', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="none">No Payment Method</option>
<option value="bank">Bank Details</option>
<option value="link">Payment Link</option>
<option value="qr">QR Code</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Choose how payment information appears on your invoice
</p>
</div>
{/* Bank Details */}
{invoiceData.paymentMethod?.type === 'bank' && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">Bank Details</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Bank Name
</label>
<input
type="text"
value={invoiceData.paymentMethod?.bankDetails?.bankName || ''}
onChange={(e) => updatePaymentMethodNested('bankDetails', 'bankName', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="e.g., Chase Bank"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Account Name
</label>
<input
type="text"
value={invoiceData.paymentMethod?.bankDetails?.accountName || ''}
onChange={(e) => updatePaymentMethodNested('bankDetails', 'accountName', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="e.g., John Doe"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Account Number
</label>
<input
type="text"
value={invoiceData.paymentMethod?.bankDetails?.accountNumber || ''}
onChange={(e) => updatePaymentMethodNested('bankDetails', 'accountNumber', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="e.g., 1234567890"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Routing Number
</label>
<input
type="text"
value={invoiceData.paymentMethod?.bankDetails?.routingNumber || ''}
onChange={(e) => updatePaymentMethodNested('bankDetails', 'routingNumber', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="e.g., 021000021"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
SWIFT Code
</label>
<input
type="text"
value={invoiceData.paymentMethod?.bankDetails?.swiftCode || ''}
onChange={(e) => updatePaymentMethodNested('bankDetails', 'swiftCode', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="e.g., CHASUS33"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
IBAN
</label>
<input
type="text"
value={invoiceData.paymentMethod?.bankDetails?.iban || ''}
onChange={(e) => updatePaymentMethodNested('bankDetails', 'iban', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="e.g., GB29 NWBK 6016 1331 9268 19"
/>
</div>
</div>
</div>
)}
{/* Payment Link */}
{invoiceData.paymentMethod?.type === 'link' && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">Payment Link</h4>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Payment URL
</label>
<input
type="url"
value={invoiceData.paymentMethod?.paymentLink?.url || ''}
onChange={(e) => updatePaymentMethodNested('paymentLink', 'url', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="https://pay.stripe.com/..."
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Button Label
</label>
<input
type="text"
value={invoiceData.paymentMethod?.paymentLink?.label || 'Pay Online'}
onChange={(e) => updatePaymentMethodNested('paymentLink', 'label', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="Pay Online"
/>
</div>
</div>
</div>
)}
{/* QR Code */}
{invoiceData.paymentMethod?.type === 'qr' && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">QR Code Payment</h4>
{/* QR Code Type Selection */}
<div className="mb-3">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-2">
QR Code Type
</label>
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
name="qrType"
value="auto"
checked={invoiceData.paymentMethod?.qrCode?.customImage === undefined}
onChange={() => updatePaymentMethodNested('qrCode', 'customImage', undefined)}
className="mr-2"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Auto-generate from URL</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="qrType"
value="custom"
checked={invoiceData.paymentMethod?.qrCode?.customImage !== undefined}
onChange={() => updatePaymentMethodNested('qrCode', 'customImage', 'placeholder')}
className="mr-2"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Upload custom QR code</span>
</label>
</div>
</div>
{/* Auto-generate QR Code */}
{invoiceData.paymentMethod?.qrCode?.customImage === undefined && (
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Payment URL
</label>
<input
type="url"
value={invoiceData.paymentMethod?.qrCode?.url || ''}
onChange={(e) => updatePaymentMethodNested('qrCode', 'url', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="https://pay.stripe.com/..."
/>
</div>
<p className="text-xs text-gray-500 dark:text-gray-500">
QR code will be automatically generated from this URL
</p>
</div>
)}
{/* Custom QR Code Upload */}
{invoiceData.paymentMethod?.qrCode?.customImage !== undefined && (
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Upload QR Code Image
</label>
<div className="flex items-center gap-3">
{invoiceData.paymentMethod?.qrCode?.customImage && invoiceData.paymentMethod.qrCode.customImage !== 'placeholder' ? (
<div className="flex items-center gap-3">
<img
src={invoiceData.paymentMethod.qrCode.customImage}
alt="Custom QR Code"
className="h-16 w-16 object-contain border border-gray-300 rounded-lg"
/>
<button
onClick={() => updatePaymentMethodNested('qrCode', 'customImage', 'placeholder')}
className="px-3 py-2 text-sm text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 border border-red-300 hover:border-red-400 rounded-md transition-colors"
>
Remove QR Code
</button>
</div>
) : (
<button
onClick={() => document.getElementById('qr-upload').click()}
className="flex items-center gap-2 px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors dark:text-white"
>
<Upload className="h-4 w-4" />
Upload QR Code
</button>
)}
</div>
<input
id="qr-upload"
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
updatePaymentMethodNested('qrCode', 'customImage', e.target.result);
};
reader.readAsDataURL(file);
}
}}
className="hidden"
/>
</div>
</div>
)}
{/* Common QR Code Label */}
<div className="mt-3">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
QR Code Label
</label>
<input
type="text"
value={invoiceData.paymentMethod?.qrCode?.label || 'Scan to Pay'}
onChange={(e) => updatePaymentMethodNested('qrCode', 'label', e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:text-white"
placeholder="Scan to Pay"
/>
</div>
</div>
)}
{/* Payment Status */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Payment Status Stamp
</label>
<select
value={invoiceData.settings?.paymentStatus || ''}
onChange={(e) => onUpdateSettings('paymentStatus', e.target.value || null)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="">No Status Stamp</option>
<option value="PAID">PAID</option>
<option value="PARTIALLY PAID">PARTIALLY PAID</option>
<option value="UNPAID">UNPAID</option>
<option value="OVERDUE">OVERDUE</option>
<option value="PENDING">PENDING</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Add a status stamp to your invoice PDF
</p>
</div>
{/* Payment Date - only show when PAID is selected */}
{invoiceData.settings?.paymentStatus === 'PAID' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Payment Date
</label>
<input
type="date"
value={invoiceData.settings?.paymentDate || ''}
onChange={(e) => onUpdateSettings('paymentDate', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
/>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Date when payment was received
</p>
</div>
)}
</div>
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
>
Done
</button>
</div>
</div>
</div>
);
};
// Input Change Confirmation Modal Component
const InputChangeConfirmationModal = ({ invoiceData, currentMethod, newMethod, onConfirm, onCancel }) => {
const getMethodName = (method) => {
switch (method) {
case 'create': return 'Create New';
case 'create_empty': return 'Start Empty';
case 'create_sample': return 'Load Sample';
case 'url': return 'URL Import';
case 'paste': return 'Paste Data';
case 'open': return 'File Upload';
default: return method;
}
};
const hasItems = invoiceData.items && invoiceData.items.length > 0;
const hasCompanyInfo = invoiceData.company && invoiceData.company.name;
const hasClientInfo = invoiceData.client && invoiceData.client.name;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-amber-800 dark:text-amber-200">
Confirm Action
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300">
{newMethod === 'create_empty' || newMethod === 'create_sample'
? `Using ${getMethodName(newMethod)} will clear your current invoice data.`
: `Switching from ${getMethodName(currentMethod)} to ${getMethodName(newMethod)} will clear your current invoice data.`
}
</p>
</div>
</div>
</div>
<div className="px-6 py-4">
<div className="space-y-3">
<p className="text-sm text-gray-600 dark:text-gray-400">
You currently have:
</p>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-1 ml-4">
{invoiceData.invoiceNumber && (
<li> Invoice #{invoiceData.invoiceNumber}</li>
)}
{hasCompanyInfo && (
<li> Company information ({invoiceData.company.name})</li>
)}
{hasClientInfo && (
<li> Client information ({invoiceData.client.name})</li>
)}
{hasItems && (
<li> {invoiceData.items.length} line item{invoiceData.items.length !== 1 ? 's' : ''}</li>
)}
</ul>
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-xs text-blue-700 dark:text-blue-300">
<strong>Tip:</strong> Consider downloading your current invoice as JSON before proceeding to save your work.
</p>
</div>
</div>
</div>
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700/50 flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
>
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>
</div>
</div>
);
};
// Signature Drawing Modal Component
const SignaturePadModal = ({ isOpen, onClose, onSave }) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Draw Your Signature
</h3>
<div className="border-2 border-gray-300 dark:border-gray-600 rounded-lg mb-4">
<canvas
ref={(canvas) => {
if (canvas) {
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 150;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
let isDrawing = false;
let lastX = 0;
let lastY = 0;
const startDrawing = (e) => {
isDrawing = true;
const rect = canvas.getBoundingClientRect();
lastX = e.clientX - rect.left;
lastY = e.clientY - rect.top;
};
const draw = (e) => {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(currentX, currentY);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.stroke();
lastX = currentX;
lastY = currentY;
};
const stopDrawing = () => {
isDrawing = false;
};
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
}
}}
className="w-full h-32 cursor-crosshair"
style={{ touchAction: 'none' }}
/>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Draw your signature in the box above using your mouse or touch device.
</p>
<div className="flex gap-3">
<button
onClick={() => {
const canvas = document.querySelector('canvas');
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors dark:text-white"
>
Clear
</button>
<button
onClick={onClose}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors dark:text-white"
>
Cancel
</button>
<button
onClick={() => {
const canvas = document.querySelector('canvas');
if (canvas) {
const dataURL = canvas.toDataURL('image/png');
onSave(dataURL);
}
}}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Save Signature
</button>
</div>
</div>
</div>
);
};
export default InvoiceEditor;