Files
core/src/modules/invoice/pdf/InvoicePDFTemplate.jsx
T
2026-04-12 12:50:14 -04:00

335 lines
9.5 KiB
React

/**
* Invoice PDF Template
* React-PDF template for generating invoice PDFs
*/
import React from 'react';
import {
Document,
Page,
Text,
View,
StyleSheet,
Image,
} from '@react-pdf/renderer';
import { formatDateForDisplay } from '../../../shared/lib/dates.js';
const LOCALE = 'fr-FR';
// Create styles - Simple black and white design
const styles = StyleSheet.create({
page: {
padding: 30,
fontSize: 10,
fontFamily: 'Helvetica',
color: '#000',
},
// Header section
invoiceTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
datesSection: {
marginBottom: 25,
fontSize: 9,
},
dateItem: {
marginBottom: 3,
},
dateLabel: {
fontWeight: 'normal',
},
// Company and Client Info
infoSection: {
flexDirection: 'row',
marginBottom: 25,
},
companySection: {
width: '50%',
paddingRight: 15,
},
clientSection: {
width: '50%',
paddingLeft: 15,
},
sectionTitle: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
},
infoText: {
fontSize: 9,
marginBottom: 2,
lineHeight: 1.3,
},
// Price notice
priceNotice: {
fontSize: 15,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'left',
},
// Table
table: {
marginTop: 10,
marginBottom: 20,
},
tableHeader: {
flexDirection: 'row',
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#E5E5E5',
fontSize: 9,
fontWeight: 'bold',
},
tableRow: {
flexDirection: 'row',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#E5E5E5',
fontSize: 9,
},
tableColDescription: {
width: '50%',
},
tableColQuantity: {
width: '15%',
textAlign: 'center',
},
tableColPrice: {
width: '17.5%',
textAlign: 'right',
},
tableColTotal: {
width: '17.5%',
textAlign: 'right',
},
itemDescription: {
fontSize: 8,
marginTop: 2,
lineHeight: 1.2,
},
// Totals
totalsSection: {
marginTop: 15,
marginBottom: 30,
alignItems: 'flex-end',
},
totalRow: {
flexDirection: 'row',
paddingVertical: 5,
width: '35%',
fontSize: 10,
},
totalLabel: {
width: '60%',
textAlign: 'right',
paddingRight: 15,
},
totalValue: {
width: '40%',
textAlign: 'right',
},
// Notes section
notesSection: {
marginTop: 20,
marginBottom: 20,
},
notesTitle: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
},
notesText: {
fontSize: 9,
lineHeight: 1.4,
},
logoSection: {
position: 'absolute',
top: 30,
right: 30,
alignItems: 'flex-end',
},
logoImage: {
maxWidth: 120,
maxHeight: 40,
objectFit: 'contain',
},
});
/**
* Format currency
*/
const formatCurrency = (amount) => {
const currency = process.env.ZEN_CURRENCY_SYMBOL || '$';
return `${currency}${parseFloat(amount).toFixed(2)}`;
};
/**
* Format date (using UTC date utilities)
*/
const formatDate = (dateString) => {
return formatDateForDisplay(dateString, LOCALE, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
/**
* Invoice PDF Document Component
*/
const InvoicePDFTemplate = ({ invoice, companyInfo = {} }) => {
if (!invoice) {
return null;
}
const clientName = invoice.company_name || `${invoice.first_name} ${invoice.last_name}`;
const principalAmount = parseFloat(invoice.total_amount);
const interestAmount = parseFloat(invoice.interest_amount || 0);
const totalWithInterest = principalAmount + interestAmount;
const paidAmount = parseFloat(invoice.paid_amount || 0);
const remainingAmount = totalWithInterest - paidAmount;
const hasInterest = interestAmount > 0;
// Default company info
const company = {
name: companyInfo.name || process.env.ZEN_MODULE_INVOICE_COMPANY_NAME || 'Your Company',
address: companyInfo.address || process.env.ZEN_MODULE_INVOICE_COMPANY_ADDRESS || '',
city: companyInfo.city || process.env.ZEN_MODULE_INVOICE_COMPANY_CITY || '',
province: companyInfo.province || process.env.ZEN_MODULE_INVOICE_COMPANY_PROVINCE || '',
postalCode: companyInfo.postalCode || process.env.ZEN_MODULE_INVOICE_COMPANY_POSTAL_CODE || '',
country: companyInfo.country || process.env.ZEN_MODULE_INVOICE_COMPANY_COUNTRY || '',
phone: companyInfo.phone || process.env.ZEN_MODULE_INVOICE_COMPANY_PHONE || '',
email: companyInfo.email || process.env.ZEN_MODULE_INVOICE_COMPANY_EMAIL || '',
};
const logoBlackUrl = companyInfo.publicLogoBlack || '';
return (
<Document>
<Page size="LETTER" style={styles.page}>
{/* Logo top right */}
{logoBlackUrl && (
<View style={styles.logoSection}>
<Image style={styles.logoImage} src={logoBlackUrl} />
</View>
)}
{/* Header: Invoice Number and Dates */}
<View>
<Text style={styles.invoiceTitle}>FACTURE #{invoice.invoice_number}</Text>
<View style={styles.datesSection}>
<Text style={styles.dateItem}>
<Text style={styles.dateLabel}>Date d'émission : </Text>
<Text style={{ fontWeight: 'bold' }}>{formatDate(invoice.issue_date)}</Text>
</Text>
<Text style={styles.dateItem}>
<Text style={styles.dateLabel}>Date d'échéance : </Text>
<Text style={{ fontWeight: 'bold' }}>{formatDate(invoice.due_date)}</Text>
</Text>
</View>
</View>
{/* Company and Client Info - Side by Side */}
<View style={styles.infoSection}>
{/* Company Info */}
<View style={styles.companySection}>
<Text style={styles.sectionTitle}>{company.name}</Text>
{company.address && <Text style={styles.infoText}>{company.address}</Text>}
{(company.city || company.province || company.postalCode) && (
<Text style={styles.infoText}>
{[company.city, company.province, company.postalCode]
.filter(Boolean)
.join(', ')}
</Text>
)}
{company.country && <Text style={styles.infoText}>{company.country}</Text>}
{company.phone && <Text style={styles.infoText}>{company.phone}</Text>}
{company.email && <Text style={styles.infoText}>{company.email}</Text>}
</View>
{/* Client Info */}
<View style={styles.clientSection}>
<Text style={styles.sectionTitle}>Facturé à :</Text>
<Text style={styles.infoText}>{clientName}</Text>
{invoice.address && <Text style={styles.infoText}>{invoice.address}</Text>}
{(invoice.city || invoice.province || invoice.postal_code) && (
<Text style={styles.infoText}>
{[invoice.city, invoice.province, invoice.postal_code]
.filter(Boolean)
.join(', ')}
</Text>
)}
{invoice.country && <Text style={styles.infoText}>{invoice.country}</Text>}
{invoice.client_email && <Text style={styles.infoText}>{invoice.client_email}</Text>}
</View>
</View>
{/* Price to be paid notice */}
<Text style={styles.priceNotice}>
{formatCurrency(remainingAmount > 0 ? remainingAmount : totalWithInterest)} à payer avant {formatDate(invoice.due_date)}
</Text>
{/* Items Table */}
<View style={styles.table}>
<View style={styles.tableHeader}>
<Text style={styles.tableColDescription}>Description</Text>
<Text style={styles.tableColQuantity}>Qté</Text>
<Text style={styles.tableColPrice}>Prix unitaire</Text>
<Text style={styles.tableColTotal}>Total</Text>
</View>
{invoice.items && invoice.items.map((item, index) => (
<View key={index} style={styles.tableRow}>
<View style={styles.tableColDescription}>
<Text style={{ fontWeight: 'bold' }}>{item.name}</Text>
{item.description && (
<Text style={styles.itemDescription}>
{item.description}
</Text>
)}
</View>
<Text style={styles.tableColQuantity}>{item.quantity}</Text>
<Text style={styles.tableColPrice}>{formatCurrency(item.unit_price)}</Text>
<Text style={styles.tableColTotal}>{formatCurrency(item.total)}</Text>
</View>
))}
</View>
{/* Totals - Simple Subtotal and Total */}
<View style={styles.totalsSection}>
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Sous-total</Text>
<Text style={styles.totalValue}>{formatCurrency(invoice.subtotal)}</Text>
</View>
{hasInterest && (
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Intérêts</Text>
<Text style={styles.totalValue}>{formatCurrency(interestAmount)}</Text>
</View>
)}
<View style={[styles.totalRow, { fontWeight: 'bold', fontSize: 12 }]}>
<Text style={styles.totalLabel}>Total</Text>
<Text style={styles.totalValue}>{formatCurrency(totalWithInterest)}</Text>
</View>
</View>
{/* Notes Section */}
{invoice.notes && (
<View style={styles.notesSection}>
<Text style={styles.notesTitle}>Notes</Text>
<Text style={styles.notesText}>{invoice.notes}</Text>
</View>
)}
</Page>
</Document>
);
};
export default InvoicePDFTemplate;