feat: Implement combined quote PDF with AGBs and recurring pricing, utilizing shared PDF UI components.

This commit is contained in:
2026-02-03 00:16:24 +01:00
parent 083be92c5b
commit 9751d2f61f
10 changed files with 511 additions and 416 deletions

View File

@@ -0,0 +1,231 @@
'use client';
import * as React from 'react';
import {
Text as PDFText,
View as PDFView,
StyleSheet as PDFStyleSheet,
Image as PDFImage
} from '@react-pdf/renderer';
export const pdfStyles = PDFStyleSheet.create({
page: {
paddingTop: 45, // DIN 5008
paddingLeft: 70, // ~25mm
paddingRight: 57, // ~20mm
paddingBottom: 48,
backgroundColor: '#ffffff',
fontFamily: 'Helvetica',
fontSize: 10,
color: '#000000',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 20,
minHeight: 120,
},
addressBlock: {
width: '55%',
marginTop: 45, // DIN 5008 positioning for window
},
senderLine: {
fontSize: 7,
textDecoration: 'underline',
color: '#666666',
marginBottom: 8,
},
recipientAddress: {
fontSize: 10,
lineHeight: 1.4,
},
brandLogoContainer: {
width: '40%',
alignItems: 'flex-end',
},
brandIconContainer: {
width: 40,
height: 40,
backgroundColor: '#000000',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
},
brandIconText: {
color: '#ffffff',
fontSize: 20,
fontWeight: 'bold',
},
titleInfo: {
marginBottom: 24,
},
mainTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 4,
color: '#000000',
textTransform: 'uppercase',
letterSpacing: 1,
},
subTitle: {
fontSize: 9,
color: '#666666',
marginTop: 2,
},
section: {
marginBottom: 32,
},
sectionTitle: {
fontSize: 8,
fontWeight: 'bold',
textTransform: 'uppercase',
letterSpacing: 1,
color: '#999999',
marginBottom: 8,
},
footer: {
position: 'absolute',
bottom: 32,
left: 70,
right: 57,
borderTopWidth: 1,
borderTopColor: '#f1f5f9',
paddingTop: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
footerColumn: {
flex: 1,
alignItems: 'flex-start',
},
footerLogo: {
height: 20,
width: 'auto',
objectFit: 'contain',
marginBottom: 8,
},
footerText: {
fontSize: 7,
color: '#94a3b8',
lineHeight: 1.5,
},
footerLabel: {
fontWeight: 'bold',
color: '#64748b',
},
pageNumber: {
fontSize: 7,
color: '#cbd5e1',
fontWeight: 'bold',
marginTop: 8,
textAlign: 'right',
},
foldingMark: {
position: 'absolute',
left: 20,
width: 10,
borderTopWidth: 0.5,
borderTopColor: '#cbd5e1',
}
});
export const FoldingMarks = () => (
<>
<PDFView style={[pdfStyles.foldingMark, { top: 297.6 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 420.9, width: 15 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 595.3 }]} fixed />
</>
);
export const Footer = ({ logo, companyData, bankData, showDetails = true }: { logo?: string; companyData: any; bankData: any; showDetails?: boolean }) => (
<PDFView style={pdfStyles.footer} fixed>
<PDFView style={pdfStyles.footerColumn}>
{logo ? (
<PDFImage src={logo} style={pdfStyles.footerLogo} />
) : (
<PDFText style={{ fontSize: 12, fontWeight: 'bold', marginBottom: 8 }}>marc mintel</PDFText>
)}
</PDFView>
{showDetails && (
<>
<PDFView style={pdfStyles.footerColumn}>
<PDFText style={pdfStyles.footerText}>
<PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>{"\n"}
{companyData.address1}{"\n"}
{companyData.address2}{"\n"}
UST: {companyData.ustId}
</PDFText>
</PDFView>
<PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}>
<PDFText style={[pdfStyles.footerText, { textAlign: 'right' }]}>
<PDFText style={pdfStyles.footerLabel}>{bankData.name}</PDFText>{"\n"}
{bankData.bic}{"\n"}
{bankData.iban}
</PDFText>
<PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />
</PDFView>
</>
)}
{!showDetails && (
<PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}>
<PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />
</PDFView>
)}
</PDFView>
);
export const Header = ({
sender,
recipient,
icon,
showAddress = true
}: {
sender?: string;
recipient?: { title: string; subtitle?: string; email?: string };
icon?: string;
showAddress?: boolean;
}) => (
<PDFView style={pdfStyles.header}>
<PDFView style={pdfStyles.addressBlock}>
{showAddress && sender && (
<>
<PDFText style={pdfStyles.senderLine}>{sender}</PDFText>
{recipient && (
<PDFView style={pdfStyles.recipientAddress}>
<PDFText style={{ fontWeight: 'bold' }}>{recipient.title}</PDFText>
{recipient.subtitle && <PDFText>{recipient.subtitle}</PDFText>}
{recipient.email && <PDFText>{recipient.email}</PDFText>}
</PDFView>
)}
</>
)}
</PDFView>
<PDFView style={pdfStyles.brandLogoContainer}>
<PDFView style={pdfStyles.brandIconContainer}>
{icon ? (
<PDFImage src={icon} style={{ width: 24, height: 24 }} />
) : (
<PDFText style={pdfStyles.brandIconText}>M</PDFText>
)}
</PDFView>
</PDFView>
</PDFView>
);
export const DocumentTitle = ({ title, subLines }: { title: string; subLines?: string[] }) => (
<PDFView style={pdfStyles.titleInfo}>
<PDFText style={pdfStyles.mainTitle}>{title}</PDFText>
{subLines?.map((line, i) => (
<PDFText key={i} style={[pdfStyles.subTitle, i === 1 ? { fontWeight: 'bold', color: '#000000' } : {}]}>
{line}
</PDFText>
))}
</PDFView>
);