feat: Invoice Editor improvements and code cleanup
Major Invoice Editor updates: - ✅ Fixed tripled scrollbar issue by removing unnecessary overflow classes - ✅ Implemented dynamic currency system with JSON data loading - ✅ Fixed F4 PDF generation error with proper paper size handling - ✅ Added proper padding to Total section matching table headers - ✅ Removed print functionality (users can print from PDF download) - ✅ Streamlined preview toolbar: Back, Size selector, Download PDF - ✅ Fixed all ESLint warnings and errors - ✅ Removed console.log statements across codebase for cleaner production - ✅ Added border-top to Total section for better visual consistency - ✅ Improved print CSS and removed JSX warnings Additional improvements: - Added currencies.json to public folder for proper HTTP access - Enhanced MinimalTemplate with better spacing and layout - Clean build with no warnings or errors - Updated release notes with new features
This commit is contained in:
242
src/pages/InvoicePreview.js
Normal file
242
src/pages/InvoicePreview.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Download, FileText } from 'lucide-react';
|
||||
import html2pdf from 'html2pdf.js';
|
||||
import MinimalTemplate from '../components/invoice-templates/MinimalTemplate';
|
||||
|
||||
// Available templates
|
||||
const templates = {
|
||||
minimal: {
|
||||
name: 'Minimal',
|
||||
description: 'Simple, professional layout',
|
||||
component: MinimalTemplate
|
||||
}
|
||||
};
|
||||
|
||||
const InvoicePreview = () => {
|
||||
const navigate = useNavigate();
|
||||
const [invoiceData, setInvoiceData] = useState(null);
|
||||
const [pdfPageSize, setPdfPageSize] = useState('A4');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [selectedTemplate] = useState('minimal');
|
||||
|
||||
// Load invoice data from localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedInvoice = localStorage.getItem('currentInvoice');
|
||||
const savedPageSize = localStorage.getItem('pdfPageSize');
|
||||
|
||||
if (savedInvoice) {
|
||||
const parsedInvoice = JSON.parse(savedInvoice);
|
||||
setInvoiceData(parsedInvoice);
|
||||
// Set page title with invoice number
|
||||
document.title = `Invoice Preview - ${parsedInvoice.invoiceNumber || 'Draft'} | DevTools`;
|
||||
} else {
|
||||
// No invoice data, redirect back to editor
|
||||
navigate('/invoice-editor');
|
||||
}
|
||||
|
||||
if (savedPageSize) {
|
||||
setPdfPageSize(savedPageSize);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load invoice data:', error);
|
||||
navigate('/invoice-editor', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
|
||||
// Format number with thousand separator
|
||||
const formatNumber = (num) => {
|
||||
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
|
||||
return num.toLocaleString('en-US', { minimumFractionDigits: 2 });
|
||||
};
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount, useThousandSeparator = false) => {
|
||||
const symbol = invoiceData?.settings?.currency?.symbol || '$';
|
||||
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(2);
|
||||
return `${symbol} ${formattedAmount}`;
|
||||
};
|
||||
|
||||
// Generate PDF from the visible invoice
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!invoiceData) return;
|
||||
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
const element = document.getElementById('invoice-content');
|
||||
if (!element) {
|
||||
throw new Error('Invoice content not found');
|
||||
}
|
||||
|
||||
const opt = {
|
||||
margin: [0.2, 0.4, 0.5, 0.4], // top, left, bottom, right margins in inches - reduced top margin for first page
|
||||
filename: `invoice-${invoiceData.invoiceNumber || 'draft'}.pdf`,
|
||||
image: { type: 'png', quality: 0.98 },
|
||||
html2canvas: {
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
letterRendering: true
|
||||
},
|
||||
jsPDF: {
|
||||
unit: 'in',
|
||||
format: pdfPageSize === 'F4' ? [8.27, 13] : pdfPageSize.toLowerCase(), // F4 dimensions in inches
|
||||
orientation: 'portrait'
|
||||
},
|
||||
pagebreak: {
|
||||
mode: ['avoid-all', 'css', 'legacy'],
|
||||
before: '.page-break-before',
|
||||
after: '.page-break-after',
|
||||
avoid: '.page-break-avoid'
|
||||
}
|
||||
};
|
||||
|
||||
await html2pdf().set(opt).from(element).save();
|
||||
} catch (error) {
|
||||
console.error('PDF generation failed:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Navigate back to editor
|
||||
const handleEditInvoice = () => {
|
||||
// Ensure current invoice data is saved before navigating
|
||||
if (invoiceData) {
|
||||
try {
|
||||
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
|
||||
} catch (error) {
|
||||
console.error('Failed to save invoice data before edit:', error);
|
||||
}
|
||||
}
|
||||
// Add a parameter to indicate we're editing existing data
|
||||
navigate('/invoice-editor?mode=edit');
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
if (!invoiceData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading invoice...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Get selected template component
|
||||
const SelectedTemplateComponent = templates[selectedTemplate].component;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-slate-900">
|
||||
|
||||
{/* Mobile Notice */}
|
||||
<div className="lg:hidden bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||
Desktop Mode Recommended
|
||||
</h3>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
|
||||
For the best preview experience and accurate PDF generation, please use desktop mode or a larger screen. The invoice preview is optimized for desktop viewing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Invoice Preview */}
|
||||
<div className="max-w-5xl mx-auto p-4 sm:p-6" style={{ maxWidth: 'min(60rem, calc(100vw - 2rem))' }}>
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-slate-700 overflow-hidden">
|
||||
{(
|
||||
<div className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-slate-700">
|
||||
<div className="flex items-center justify-between flex-col sm:flex-row gap-4">
|
||||
<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 Preview</h2>
|
||||
<div className="hidden sm:flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>•</span>
|
||||
<span>{pdfPageSize} Format</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={handleEditInvoice}
|
||||
className="flex items-center gap-2 px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Back</span>
|
||||
</button>
|
||||
|
||||
{/* Paper Size Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-600 dark:text-gray-300">Size:</label>
|
||||
<select
|
||||
value={pdfPageSize}
|
||||
onChange={(e) => {
|
||||
setPdfPageSize(e.target.value);
|
||||
localStorage.setItem('pdfPageSize', e.target.value);
|
||||
}}
|
||||
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<option value="A4">A4</option>
|
||||
<option value="F4">F4</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Download PDF Button */}
|
||||
<button
|
||||
onClick={handleDownloadPDF}
|
||||
disabled={isGenerating}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{isGenerating ? 'Generating...' : 'Download PDF'}</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="bg-white rounded-lg shadow-lg overflow-x-auto">
|
||||
{/* PDF-Ready Invoice Content */}
|
||||
<div
|
||||
id="invoice-content"
|
||||
className="bg-white"
|
||||
style={{
|
||||
maxWidth: pdfPageSize === 'A4' ? '720px' : '750px',
|
||||
width: '100%',
|
||||
minHeight: pdfPageSize === 'A4' ? '720px' : '750px',
|
||||
margin: '0 auto',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<SelectedTemplateComponent
|
||||
invoiceData={invoiceData}
|
||||
formatNumber={formatNumber}
|
||||
formatCurrency={formatCurrency}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvoicePreview;
|
||||
Reference in New Issue
Block a user