chore: import codes
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user