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

154
src/components/AgbsPDF.tsx Normal file
View File

@@ -0,0 +1,154 @@
'use client';
import * as React from 'react';
import {
Page as PDFPage,
Text as PDFText,
View as PDFView,
StyleSheet as PDFStyleSheet,
} from '@react-pdf/renderer';
import { pdfStyles, Header, Footer, FoldingMarks, DocumentTitle } from './pdf/SharedUI';
const localStyles = PDFStyleSheet.create({
sectionContainer: {
marginTop: 0,
},
agbSection: {
marginBottom: 20,
},
labelRow: {
flexDirection: 'row',
alignItems: 'baseline',
marginBottom: 6,
},
monoNumber: {
fontSize: 7,
fontWeight: 'bold',
color: '#94a3b8',
letterSpacing: 2,
width: 25,
},
sectionTitle: {
fontSize: 9,
fontWeight: 'bold',
color: '#000000',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
officialText: {
fontSize: 8,
lineHeight: 1.5,
color: '#334155',
textAlign: 'justify',
paddingLeft: 25,
}
});
const AGBSection = ({ index, title, children }: { index: string; title: string; children: React.ReactNode }) => (
<PDFView style={localStyles.agbSection} wrap={false}>
<PDFView style={localStyles.labelRow}>
<PDFText style={localStyles.monoNumber}>{index}</PDFText>
<PDFText style={localStyles.sectionTitle}>{title}</PDFText>
</PDFView>
<PDFText style={localStyles.officialText}>{children}</PDFText>
</PDFView>
);
interface AgbsPDFProps {
state: any;
headerIcon?: string;
footerLogo?: string;
}
export const AgbsPDF = ({ state, headerIcon, footerLogo }: AgbsPDFProps) => {
const date = new Date().toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065"
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65"
};
return (
<PDFPage size="A4" style={pdfStyles.page}>
<FoldingMarks />
<Header icon={headerIcon} showAddress={false} />
<DocumentTitle
title="Allgemeine Geschäftsbedingungen"
subLines={[
`Stand: ${date}`
]}
/>
<PDFView style={localStyles.sectionContainer}>
<AGBSection
index="01"
title="Geltungsbereich"
>
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend Auftragnehmer) und dem Auftraggeber. Abweichende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil.
</AGBSection>
<AGBSection
index="02"
title="Vertragsgegenstand"
>
Dienstleistungen in Webentwicklung, technischer Umsetzung und Hosting. Der Auftragnehmer schuldet eine fachgerechte technische Ausführung, jedoch keinen wirtschaftlichen Erfolg.
</AGBSection>
<AGBSection
index="03"
title="Mitwirkungspflichten"
>
Der Auftraggeber stellt alle erforderlichen Inhalte und Zugänge rechtzeitig bereit. Verzögerungen durch fehlende Mitwirkung gehen zu Lasten der Projektlaufzeit.
</AGBSection>
<AGBSection
index="04"
title="Abnahme"
>
Die Abnahme erfolgt durch produktive Nutzung oder Ablauf von 7 Tagen nach Projektabschluss. Subjektives Nichtgefallen stellt keinen technischen Mangel dar.
</AGBSection>
<AGBSection
index="05"
title="Haftung"
>
Haftung besteht nur bei Vorsatz oder grober Fahrlässigkeit. Die Haftung für indirekte Schäden oder entgangenen Gewinn wird ausgeschlossen.
</AGBSection>
<AGBSection
index="06"
title="Hosting & Wartung"
>
Wartung sichert den Betrieb des Ist-Zustands. Erweiterungen oder Funktionsänderungen sind separat zu beauftragen.
</AGBSection>
<AGBSection
index="07"
title="Zahlung & Verzug"
>
Alle Preise netto. Fälligkeit innerhalb von 7 Tagen. Bei erheblichem Verzug ist der Auftragnehmer berechtigt, die Leistung einzustellen.
</AGBSection>
</PDFView>
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
/>
</PDFPage>
);
};

View File

@@ -0,0 +1,29 @@
'use client';
import * as React from 'react';
import { Document as PDFDocument } from '@react-pdf/renderer';
import { EstimationPDF } from './EstimationPDF';
import { AgbsPDF } from './AgbsPDF';
interface CombinedProps {
estimationProps: any;
showAgbs?: boolean;
}
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true }: CombinedProps) => {
return (
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}>
{/* Estimation Sections */}
<EstimationPDF {...estimationProps} />
{/* AGB Section */}
{showAgbs && (
<AgbsPDF
state={estimationProps.state}
headerIcon={estimationProps.headerIcon}
footerLogo={estimationProps.footerLogo}
/>
)}
</PDFDocument>
);
};

View File

@@ -17,87 +17,10 @@ import {
calculatePositions
} from '../logic/pricing';
import { pdfStyles, Header, Footer, FoldingMarks, DocumentTitle } from './pdf/SharedUI';
const styles = 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: 40,
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',
},
quoteInfo: {
alignItems: 'flex-end',
},
quoteTitle: {
fontSize: 14, // Slightly smaller to prevent wrapping
fontWeight: 'bold',
marginBottom: 4,
color: '#000000',
textTransform: 'uppercase',
letterSpacing: 1,
textAlign: 'right',
},
quoteDate: {
fontSize: 9,
color: '#666666',
marginTop: 2,
textAlign: 'right',
},
section: {
marginBottom: 32,
},
sectionTitle: {
fontSize: 8,
fontWeight: 'bold',
textTransform: 'uppercase',
letterSpacing: 1,
color: '#999999',
marginBottom: 8,
},
...pdfStyles,
table: {
marginTop: 16,
minHeight: 300,
@@ -134,36 +57,56 @@ const styles = PDFStyleSheet.create({
priceText: { fontSize: 10, fontWeight: 'bold', color: '#000000' },
summaryContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 32,
},
summaryCard: {
width: '45%',
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
borderWidth: 1,
borderColor: '#000000',
marginTop: 24,
borderTopWidth: 1,
borderTopColor: '#000000',
paddingTop: 12,
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
justifyContent: 'flex-end',
paddingVertical: 4,
alignItems: 'baseline',
},
summaryLabel: {
fontSize: 7,
color: '#64748b',
textTransform: 'uppercase',
letterSpacing: 1,
fontWeight: 'bold',
marginRight: 12,
},
summaryValue: {
fontSize: 9,
fontWeight: 'bold',
color: '#0f172a',
textAlign: 'right',
width: 100,
},
summaryLabel: { fontSize: 8, color: '#666666' },
summaryValue: { fontSize: 9, fontWeight: 'bold', color: '#000000' },
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
justifyContent: 'flex-end',
paddingTop: 12,
marginTop: 8,
borderTopWidth: 1,
borderTopColor: '#eeeeee',
borderTopWidth: 2,
borderTopColor: '#000000',
alignItems: 'baseline',
},
totalLabel: {
fontSize: 8,
fontWeight: 'bold',
color: '#000000',
textTransform: 'uppercase',
letterSpacing: 1,
marginRight: 12,
},
totalValue: {
fontSize: 14,
fontWeight: 'bold',
color: '#000000',
textAlign: 'right',
width: 110,
},
totalLabel: { fontSize: 10, fontWeight: 'bold', color: '#000000' },
totalValue: { fontSize: 14, fontWeight: 'bold', color: '#000000' },
hostingBox: {
marginTop: 24,
@@ -206,52 +149,6 @@ const styles = PDFStyleSheet.create({
textTransform: 'uppercase',
textAlign: 'center',
},
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',
}
});
interface PDFProps {
@@ -274,75 +171,40 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
const positions = calculatePositions(state, pricing);
const FoldingMarks = () => (
<>
<PDFView style={[styles.foldingMark, { top: 297.6 }]} fixed />
<PDFView style={[styles.foldingMark, { top: 420.9, width: 15 }]} fixed />
<PDFView style={[styles.foldingMark, { top: 595.3 }]} fixed />
</>
);
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065"
};
const Footer = () => (
<PDFView style={styles.footer} fixed>
<PDFView style={styles.footerColumn}>
{footerLogo ? (
<PDFImage src={footerLogo} style={styles.footerLogo} />
) : (
<PDFText style={{ fontSize: 12, fontWeight: 'bold', marginBottom: 8 }}>marc mintel</PDFText>
)}
</PDFView>
<PDFView style={styles.footerColumn}>
<PDFText style={styles.footerText}>
<PDFText style={styles.footerLabel}>Marc Mintel</PDFText>{"\n"}
Georg-Meistermann-Straße 7{"\n"}
54586 Schüller{"\n"}
UST: DE367588065
</PDFText>
</PDFView>
<PDFView style={[styles.footerColumn, { alignItems: 'flex-end' }]}>
<PDFText style={[styles.footerText, { textAlign: 'right' }]}>
<PDFText style={styles.footerLabel}>N26</PDFText>{"\n"}
NTSBDEB1XXX{"\n"}
DE50100110012620432865
</PDFText>
<PDFText style={styles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />
</PDFView>
</PDFView>
);
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65"
};
return (
<PDFDocument title={`Kostenschätzung - ${state.companyName || state.name}`}>
<>
<PDFPage size="A4" style={styles.page}>
<FoldingMarks />
<PDFView style={styles.header}>
<PDFView style={styles.addressBlock}>
<PDFText style={styles.senderLine}>Marc Mintel | Georg-Meistermann-Straße 7 | 54586 Schüller</PDFText>
<PDFView style={styles.recipientAddress}>
<PDFText style={{ fontWeight: 'bold' }}>{state.companyName || state.name}</PDFText>
{state.companyName && <PDFText>{state.name}</PDFText>}
<PDFText>{state.email}</PDFText>
</PDFView>
</PDFView>
<Header
sender="Marc Mintel | Georg-Meistermann-Straße 7 | 54586 Schüller"
recipient={{
title: state.companyName || state.name,
subtitle: state.companyName ? state.name : undefined,
email: state.email
}}
icon={headerIcon}
/>
<PDFView style={styles.brandLogoContainer}>
<PDFView style={styles.brandIconContainer}>
{headerIcon ? (
<PDFImage src={headerIcon} style={{ width: 24, height: 24 }} />
) : (
<PDFText style={styles.brandIconText}>M</PDFText>
)}
</PDFView>
<PDFView style={styles.quoteInfo}>
<PDFText style={styles.quoteTitle}>Kostenschätzung</PDFText>
<PDFText style={styles.quoteDate}>Datum: {date}</PDFText>
<PDFText style={[styles.quoteDate, { fontWeight: 'bold', color: '#000000' }]}>
Projekt: {state.projectType === 'website' ? 'Website' : 'Web App'}
</PDFText>
</PDFView>
</PDFView>
</PDFView>
<DocumentTitle
title="Kostenschätzung"
subLines={[
`Datum: ${date}`,
`Projekt: ${state.projectType === 'website' ? 'Website' : 'Web App'}`
]}
/>
<PDFView style={styles.table}>
<PDFView style={styles.tableHeader}>
@@ -368,51 +230,27 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
</PDFView>
<PDFView style={styles.summaryContainer}>
<PDFView style={styles.summaryCard}>
<PDFView style={styles.summaryRow}>
<PDFText style={styles.summaryLabel}>Zwischensumme (Netto)</PDFText>
<PDFText style={styles.summaryValue}>{totalPrice.toLocaleString('de-DE')} </PDFText>
</PDFView>
<PDFView style={styles.totalRow}>
<PDFText style={styles.totalLabel}>Gesamtsumme</PDFText>
<PDFText style={styles.totalValue}>{totalPrice.toLocaleString('de-DE')} </PDFText>
</PDFView>
<PDFView style={styles.summaryRow}>
<PDFText style={styles.summaryLabel}>Zwischensumme (Netto)</PDFText>
<PDFText style={styles.summaryValue}>{totalPrice.toLocaleString('de-DE')} </PDFText>
</PDFView>
<PDFView style={styles.summaryRow}>
<PDFText style={styles.summaryLabel}>zzgl. 19% MwSt.</PDFText>
<PDFText style={styles.summaryValue}>{(totalPrice * 0.19).toLocaleString('de-DE')} </PDFText>
</PDFView>
<PDFView style={styles.totalRow}>
<PDFText style={styles.totalLabel}>Gesamtsumme (Brutto)</PDFText>
<PDFText style={styles.totalValue}>{(totalPrice * 1.19).toLocaleString('de-DE')} </PDFText>
</PDFView>
</PDFView>
{state.projectType === 'website' && (
<PDFView style={styles.hostingBox}>
<PDFText style={{ color: '#666666', fontSize: 8, fontWeight: 'bold', textTransform: 'uppercase' }}>Betrieb & Hosting</PDFText>
<PDFText style={{ fontSize: 10, fontWeight: 'bold', color: '#000000' }}>{monthlyPrice.toLocaleString('de-DE')} / Monat</PDFText>
</PDFView>
)}
<Footer />
<Footer logo={footerLogo} companyData={companyData} bankData={bankData} />
</PDFPage>
<PDFPage size="A4" style={styles.page}>
<FoldingMarks />
<PDFView style={styles.header}>
<PDFView style={styles.addressBlock}>
<PDFText style={styles.senderLine}>Marc Mintel | Georg-Meistermann-Straße 7 | 54586 Schüller</PDFText>
<PDFView style={styles.recipientAddress}>
<PDFText style={{ fontWeight: 'bold' }}>{state.companyName || state.name}</PDFText>
</PDFView>
</PDFView>
<PDFView style={styles.brandLogoContainer}>
<PDFView style={styles.brandIconContainer}>
{headerIcon ? (
<PDFImage src={headerIcon} style={{ width: 24, height: 24 }} />
) : (
<PDFText style={styles.brandIconText}>M</PDFText>
)}
</PDFView>
<PDFView style={styles.quoteInfo}>
<PDFText style={styles.quoteTitle}>Projektdetails</PDFText>
</PDFView>
</PDFView>
</PDFView>
<Header icon={headerIcon} showAddress={false} />
<DocumentTitle title="Projektdetails" />
<PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Konfiguration & Wünsche</PDFText>
@@ -484,8 +322,8 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
</PDFView>
)}
<Footer />
<Footer logo={footerLogo} companyData={companyData} bankData={bankData} />
</PDFPage>
</PDFDocument>
</>
);
};

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>
);

View File

@@ -114,6 +114,15 @@ export function calculatePositions(state: FormState, pricing: any): Position[] {
price: Math.round(factorPrice)
});
}
const monthlyRate = pricing.HOSTING_MONTHLY + (state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY);
positions.push({
pos: pos++,
title: 'Betriebs- und Pflegeleistung (12 Monate)',
desc: `Bereitstellung der Infrastruktur, technische Instandhaltung, Sicherheits-Updates und Backup-Management gemäß AGB Punkt 7a. Inklusive ${state.storageExpansion > 0 ? state.storageExpansion + ' GB Speicher-Erweiterung' : 'Basis-Infrastruktur'}.`,
qty: 1,
price: monthlyRate * 12
});
} else {
positions.push({
pos: pos++,

View File

@@ -64,4 +64,5 @@ export interface Position {
desc: string;
qty: number;
price: number;
isRecurring?: boolean;
}