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'; 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); // 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').format(num); }; const formatCurrency = (amount, showSymbol = true) => { const currency = invoiceData.settings?.currency || { code: 'USD', symbol: '$' }; const formattedAmount = formatNumber(amount.toFixed(2)); 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); }; const handleFileImport = (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); setActiveTab('create'); 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://drive.google.com/file/d/... or any JSON URL" 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" />

Google Drive: Use share links like "drive.google.com/file/d/..." - we'll convert them automatically.

)} {activeTab === 'paste' && (