Files
dewedev/src/pages/InvoiceEditor.js
dwindown 78570f04f0 feat: Enhanced release notes system, fixed invoice installments, and improved logo integration
- Updated release notes to use new JSON structure with individual commit timestamps
- Removed hash display from release notes for cleaner UI
- Fixed automatic recalculation of percentage-based installments in Invoice Editor and Preview
- Integrated custom logo.svg in header and footer with cleaner styling
- Moved all data files to /public/data/ for better organization
- Cleaned up unused release data files and improved file structure
2025-09-28 17:14:54 +07:00

2816 lines
142 KiB
JavaScript

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 (
<ToolLayout
title="Invoice Editor"
description="Create, edit, and export professional invoices with PDF generation"
>
<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="space-y-4">
<div className="text-center py-8">
<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">
Start Building Your Invoice
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-6">
Choose how you'd like to begin creating your professional invoice
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
<button
onClick={handleStartEmpty}
className="group relative overflow-hidden rounded-lg border-2 border-dashed border-blue-300 dark:border-blue-600 p-6 hover:border-blue-400 dark:hover:border-blue-500 transition-colors"
>
<div className="flex flex-col items-center">
<Plus className="h-8 w-8 text-blue-500 mb-3 group-hover:scale-110 transition-transform" />
<span className="text-sm font-medium text-blue-600 dark:text-blue-400">Start Empty</span>
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1">Create a blank invoice</span>
</div>
</button>
<button
onClick={handleLoadSample}
className="group relative overflow-hidden rounded-lg border-2 border-dashed border-green-300 dark:border-green-600 p-6 hover:border-green-400 dark:hover:border-green-500 transition-colors"
>
<div className="flex flex-col items-center">
<FileText className="h-8 w-8 text-green-500 mb-3 group-hover:scale-110 transition-transform" />
<span className="text-sm font-medium text-green-600 dark:text-green-400">Load Sample</span>
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1">Start with example invoice</span>
</div>
</button>
</div>
</div>
</div>
)}
{activeTab === 'url' && (
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Invoice JSON URL
</label>
<input
type="url"
value={url}
onChange={(e) => 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"
/>
</div>
<button
onClick={handleUrlFetch}
disabled={!url.trim() || isLoading}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{isLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Loading...
</>
) : (
<>
<Globe className="h-4 w-4" />
Fetch Invoice Data
</>
)}
</button>
<div className="flex items-center gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div className="flex-shrink-0">
<svg className="h-4 w-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
</div>
<p className="text-xs text-blue-700 dark:text-blue-300">
<strong>Google Drive:</strong> Use share links like "drive.google.com/file/d/..." - we'll convert them automatically.
</p>
</div>
</div>
)}
{activeTab === 'paste' && (
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Paste Invoice JSON Data
</label>
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Paste your invoice JSON data here..."
className="w-full h-32 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"
/>
</div>
<button
onClick={() => {
try {
const parsed = JSON.parse(inputText);
setInvoiceData(parsed);
setCreateNewCompleted(true);
setError('');
} catch (err) {
setError('Invalid JSON format');
}
}}
disabled={!inputText.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Load Invoice Data
</button>
</div>
)}
{activeTab === 'open' && (
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Choose File
</label>
<input
ref={fileInputRef}
type="file"
accept=".json"
onChange={handleFileImport}
className="block w-full text-sm text-gray-500 dark:text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 dark:file:bg-blue-900/20 dark:file:text-blue-300 dark:hover:file:bg-blue-900/30"
/>
</div>
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<div className="flex-shrink-0">
<svg className="h-4 w-4 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<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.
</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-center 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 pr-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 className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<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>
</div>
</div>
<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>
)}
{/* 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>
</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 Methods
{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>
</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>
)}
</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;