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

393 lines
11 KiB
React

/**
* Receipt PDF Template
* React-PDF template for generating receipt 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
receiptTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
paidBadge: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 10,
color: '#059669', // Green color for PAID
},
datesSection: {
marginBottom: 25,
fontSize: 9,
},
dateItem: {
marginBottom: 3,
},
dateLabel: {
fontWeight: 'normal',
},
// Transaction section
transactionSection: {
marginBottom: 25,
padding: 10,
backgroundColor: '#f3f4f6',
borderRadius: 4,
},
transactionTitle: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
},
transactionItem: {
fontSize: 9,
marginBottom: 3,
},
// 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',
color: '#059669', // Green for paid amount
},
// 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',
});
};
const PAYMENT_METHOD_LABELS = {
stripe: 'Carte de crédit (Stripe)',
cash: 'Comptant',
check: 'Chèque',
wire: 'Virement bancaire',
interac: 'Virement Interac',
other: 'Autre',
};
/**
* Format payment method for display
*/
const formatPaymentMethod = (method) => {
return PAYMENT_METHOD_LABELS[method] || method;
};
/**
* Receipt PDF Document Component
*/
const ReceiptPDFTemplate = ({ invoice, transaction, companyInfo = {} }) => {
if (!invoice) {
return null;
}
const clientName = invoice.company_name || `${invoice.first_name} ${invoice.last_name}`;
const interestAmount = parseFloat(invoice.interest_amount || 0);
const hasInterest = interestAmount > 0;
const totalWithInterest = parseFloat(invoice.total_amount) + interestAmount;
// 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: Receipt Number and Status */}
<View>
<Text style={styles.receiptTitle}>REÇU #{invoice.invoice_number}</Text>
<Text style={styles.paidBadge}>PAYÉ</Text>
<View style={styles.datesSection}>
<Text style={styles.dateItem}>
<Text style={styles.dateLabel}>Date de facture : </Text>
<Text style={{ fontWeight: 'bold' }}>{formatDate(invoice.issue_date)}</Text>
</Text>
{transaction && (
<Text style={styles.dateItem}>
<Text style={styles.dateLabel}>Date de paiement : </Text>
<Text style={{ fontWeight: 'bold' }}>{formatDate(transaction.transaction_date)}</Text>
</Text>
)}
</View>
</View>
{/* Transaction Information */}
{transaction && (
<View style={styles.transactionSection}>
<Text style={styles.transactionTitle}>Informations de paiement</Text>
<Text style={styles.transactionItem}>
<Text style={styles.dateLabel}>Numéro de transaction : </Text>
<Text style={{ fontWeight: 'bold' }}>{transaction.transaction_number}</Text>
</Text>
<Text style={styles.transactionItem}>
<Text style={styles.dateLabel}>Mode de paiement : </Text>
<Text style={{ fontWeight: 'bold' }}>{formatPaymentMethod(transaction.payment_method)}</Text>
</Text>
<Text style={styles.transactionItem}>
<Text style={styles.dateLabel}>Montant payé : </Text>
<Text style={{ fontWeight: 'bold' }}>{formatCurrency(transaction.amount)}</Text>
</Text>
</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}>Payé par :</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>
{/* Amount paid notice */}
<Text style={styles.priceNotice}>
{formatCurrency(transaction ? transaction.amount : invoice.total_amount)} payé en entier
</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 payé</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 ReceiptPDFTemplate;