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:
dwindown
2025-09-28 00:09:06 +07:00
parent b2850ea145
commit 04db088ff9
29 changed files with 5471 additions and 482 deletions

242
src/pages/InvoicePreview.js Normal file
View 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;