Files
dewedev/src/pages/InvoicePreview.js

413 lines
15 KiB
JavaScript
Executable File

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');
// Calculate totals (same logic as InvoiceEditor)
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 };
};
// Load invoice data from localStorage
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
// Recalculate totals and installments to ensure accuracy
const { subtotal, total } = calculateTotals(
parsedInvoice.items || [],
parsedInvoice.discount || 0,
parsedInvoice.fees || [],
parsedInvoice.discounts || []
);
// Update totals
parsedInvoice.subtotal = subtotal;
parsedInvoice.total = total;
// Recalculate installments if they exist
if (parsedInvoice.paymentTerms?.installments?.length > 0) {
const updatedInstallments = parsedInvoice.paymentTerms.installments.map(installment => {
if (installment.type === 'percentage') {
const baseAmount = parsedInvoice.paymentTerms?.type === 'downpayment'
? total - (parsedInvoice.paymentTerms?.downPayment?.amount || 0)
: total;
const amount = (baseAmount * (installment.percentage || 0)) / 100;
return { ...installment, amount };
}
return installment;
});
parsedInvoice.paymentTerms = {
...parsedInvoice.paymentTerms,
installments: updatedInstallments
};
}
setInvoiceData(parsedInvoice);
} else {
// No invoice data found, redirect to editor
navigate('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
navigate('/invoice-editor');
}
}, [navigate]);
// Format number with thousand separator
const formatNumber = (num) => {
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
if (!invoiceData?.settings?.thousandSeparator) return num.toFixed(decimalDigits);
return num.toLocaleString('en-US', { minimumFractionDigits: decimalDigits, maximumFractionDigits: decimalDigits });
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(decimalDigits);
return `${symbol} ${formattedAmount}`;
};
// Utility function to temporarily apply print-optimized styles
const applyPrintStyles = () => {
const element = document.getElementById('invoice-content');
if (!element) return null;
// Store original styles
const originalStyles = new Map();
// Find all table elements and store their original styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
originalStyles.set(`table-${index}`, table.style.cssText);
table.style.borderCollapse = 'collapse';
});
// Find all th and td elements and apply print styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
originalStyles.set(`cell-${index}`, cell.style.cssText);
cell.style.verticalAlign = 'middle';
cell.style.padding = '4px 12px 20px';
cell.style.lineHeight = '1.4';
});
// Find all totals rows and apply print styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
originalStyles.set(`totals-${index}`, row.style.cssText);
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '4px 12px 20px';
row.style.minHeight = '44px';
});
// Find all stamp cards and apply print styles
const stampCards = element.querySelectorAll('.invoice-payment-status-stamp');
stampCards.forEach((stampCard, index) => {
originalStyles.set(`stamp-${index}`, stampCard.style.cssText);
stampCard.style.padding = '4px 12px 20px';
});
// Find all cards and apply print styles
const fromToCards = element.querySelectorAll('.invoice-from-to-card');
fromToCards.forEach((fromToCard, index) => {
originalStyles.set(`fromTo-${index}`, fromToCard.style.cssText);
fromToCard.style.padding = '4px 12px 20px';
});
return originalStyles;
};
// Utility function to restore original styles
const restoreOriginalStyles = (originalStyles) => {
if (!originalStyles) return;
const element = document.getElementById('invoice-content');
if (!element) return;
// Restore table styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
const originalStyle = originalStyles.get(`table-${index}`);
if (originalStyle !== undefined) {
table.style.cssText = originalStyle;
}
});
// Restore cell styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
const originalStyle = originalStyles.get(`cell-${index}`);
if (originalStyle !== undefined) {
cell.style.cssText = originalStyle;
}
});
// Restore totals row styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
const originalStyle = originalStyles.get(`totals-${index}`);
if (originalStyle !== undefined) {
row.style.cssText = originalStyle;
}
});
// Restore stamp card styles
const stampCards = element.querySelectorAll('.invoice-payment-status-stamp');
stampCards.forEach((stampCard, index) => {
const originalStyle = originalStyles.get(`stamp-${index}`);
if (originalStyle !== undefined) {
stampCard.style.cssText = originalStyle;
}
});
// Restore fromTo card styles
const fromToCards = element.querySelectorAll('.invoice-from-to-card');
fromToCards.forEach((fromToCard, index) => {
const originalStyle = originalStyles.get(`fromTo-${index}`);
if (originalStyle !== undefined) {
fromToCard.style.cssText = originalStyle;
}
});
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
let originalStyles = null;
try {
const element = document.getElementById('invoice-content');
if (!element) {
throw new Error('Invoice content not found');
}
// Apply print-optimized styles temporarily
originalStyles = applyPrintStyles();
// Small delay to ensure styles are applied
await new Promise(resolve => setTimeout(resolve, 100));
const opt = {
margin: [0.2, 0.4, 0.8, 0.4], // top, left, bottom, right margins in inches - increased bottom margin to prevent elements dropping
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) {
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay
setTimeout(() => {
restoreOriginalStyles(originalStyles);
}, 500);
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) {
// Failed to save invoice data before edit
}
}
// 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-600 dark:text-gray-600">
<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;