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'; 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 (
{/* Input Section */}
{/* Tab Content */} {(activeTab !== 'create' || !createNewCompleted) && (
{activeTab === 'create' && (

Start Building Your Invoice

Choose how you'd like to begin creating your professional invoice

)} {activeTab === 'url' && (
setUrl(e.target.value)} placeholder="https://api.telegram.org/bot/getMe" className="tool-input w-full" onKeyPress={(e) => e.key === 'Enter' && handleUrlFetch()} />

Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.

)} {activeTab === 'paste' && ( pasteCollapsed ? (
✓ Invoice loaded: {pasteDataSummary.invoiceNumber || 'New Invoice'}
) : (
{error && (

Invalid Data: {error}

)}
Supports JSON invoice templates
) )} {activeTab === 'open' && (

🔒 Privacy: Your data stays in your browser. We don't store or upload anything - just help you open, edit, and export your files locally.

)}
)}
{/* Main Editor Section */} { (

Invoice Editor

{( )}
{!createNewCompleted ? (

No Invoice Data Loaded

Use the input section above to create a new invoice or load existing data.

) : (
{/* Invoice Header - Like Final Invoice */}
{/* Logo and Company Name */}
{invoiceData.company.logo ? ( Company Logo ) : (
)}

INVOICE

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" />
{/* Invoice Details */}
# 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" />
Date: 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" />
Due: 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" />
{/* From and To Section - Invoice Layout Style */}
{/* From Section */}

FROM

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" /> 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" /> 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" />
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" /> | 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" />
{/* To Section */}

TO

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" /> 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" /> 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" />
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" /> | 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" />
{/* Items */}
{invoiceData.items.map((item, index) => ( ))} {/* Add Item Row */}
Items Qty Rate Amount
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..." /> { 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); }} />
{invoiceData.settings?.currency?.symbol || '$'} { 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); }} />
{formatCurrency(item.amount, true)}
{/* Totals and Tax */}

Fees & Discounts

{/* Fees Section */}
{(invoiceData.fees || []).map((fee, index) => (
{/* Main Content Area */}
{/* Fee Name */} 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 */}
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" />
{/* Action Buttons - Always on Right */}
))}
{/* Discounts Section */}
{(invoiceData.discounts || []).map((discount, index) => (
{/* Main Content Area */}
{/* Discount Name */} 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 */}
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" />
{/* Action Buttons - Always on Right */}
))}

Invoice Total

Subtotal: {formatCurrency(invoiceData.subtotal, true)}
{/* Dynamic Fees */} {(invoiceData.fees || []).map((fee) => (
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}: +{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
))} {/* Dynamic Discounts */} {(invoiceData.discounts || []).map((discount) => (
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}: -{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
))} {/* Legacy Discount */} {invoiceData.discount > 0 && (
Discount: -{formatCurrency(invoiceData.discount, true)}
)}
Total: {formatCurrency(invoiceData.total, true)}
{/* Payment Terms - Optional Section */}

Payment Terms (Optional)

{/* Payment Type Selection */}
{/* Down Payment Section */} {(invoiceData.paymentTerms?.type === 'downpayment') && (

Down Payment

{/* Main Content Area */}
{/* Down Payment Label */} {/* Controls Row */}
{ 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" /> 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" />
{invoiceData.paymentTerms?.downPayment?.amount > 0 && (
Remaining Balance: {formatCurrency(invoiceData.total - (invoiceData.paymentTerms?.downPayment?.amount || 0), true)}
)}
)} {/* Installments Section */} {(invoiceData.paymentTerms?.type === 'installment' || invoiceData.paymentTerms?.type === 'downpayment') && (

{invoiceData.paymentTerms?.type === 'downpayment' ? 'Remaining Balance Installments' : 'Payment Installments'}

{(invoiceData.paymentTerms?.installments || []).map((installment, index) => (
{/* Main Content Area */}
{/* Installment Name - Full Width */} { 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 */}
{ 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" /> { 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" />
{/* Action Buttons - Always on Right */}
))}
)}
{/* Additional Notes & Signature - Independent Card */}

Additional Notes & Signature

{/* Additional Notes */}