335 lines
9.5 KiB
React
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;
|