diff --git a/src/components/ContactForm/components/PriceCalculation.tsx b/src/components/ContactForm/components/PriceCalculation.tsx
index 0effd08..e3fcf8d 100644
--- a/src/components/ContactForm/components/PriceCalculation.tsx
+++ b/src/components/ContactForm/components/PriceCalculation.tsx
@@ -5,7 +5,8 @@ import { FormState } from '../types';
import { PRICING } from '../constants';
import { AnimatedNumber } from './AnimatedNumber';
import { ConceptPrice, ConceptAutomation } from '../../Landing/ConceptIllustrations';
-import { Info, Download, Share2 } from 'lucide-react';
+import { Info, Download, Share2, RefreshCw } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
import dynamic from 'next/dynamic';
import { EstimationPDF } from '../../EstimationPDF';
@@ -42,6 +43,43 @@ export function PriceCalculation({
const [pdfLoading, setPdfLoading] = React.useState(false);
const languagesCount = state.languagesList.length || 1;
+ const handleDownload = async () => {
+ if (pdfLoading) return;
+
+ setPdfLoading(true);
+
+ try {
+ const { pdf } = await import('@react-pdf/renderer');
+ const doc = ;
+
+ // Minimum loading time of 2 seconds for better UX
+ const [blob] = await Promise.all([
+ pdf(doc).toBlob(),
+ new Promise(resolve => setTimeout(resolve, 2000))
+ ]);
+
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `kalkulation-${state.name.toLowerCase().replace(/\s+/g, '-') || 'projekt'}.pdf`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error('PDF generation failed:', error);
+ } finally {
+ setPdfLoading(false);
+ }
+ };
+
return (
@@ -76,26 +114,46 @@ export function PriceCalculation({
{isClient && (
-
}
- fileName={`kalkulation-${state.name.toLowerCase().replace(/\s+/g, '-') || 'projekt'}.pdf`}
- className="w-full flex items-center justify-center gap-3 px-8 py-4 rounded-full border border-slate-200 text-slate-900 font-bold text-sm uppercase tracking-widest hover:bg-white hover:border-slate-900 transition-all focus:outline-none overflow-hidden relative"
- onClick={(e) => {
- if (pdfLoading) {
- e.preventDefault();
- return;
- }
- setPdfLoading(true);
- setTimeout(() => setPdfLoading(false), 2000);
- }}
+
)}
{onShare && (
diff --git a/src/components/ContactForm/constants.tsx b/src/components/ContactForm/constants.tsx
index 1cd78c0..f6a3491 100644
--- a/src/components/ContactForm/constants.tsx
+++ b/src/components/ContactForm/constants.tsx
@@ -186,3 +186,65 @@ export const SOCIAL_MEDIA_OPTIONS = [
{ id: 'tiktok', label: 'TikTok' },
{ id: 'youtube', label: 'YouTube' },
];
+
+export const VIBE_LABELS: Record
= {
+ minimal: 'Minimalistisch',
+ bold: 'Mutig & Laut',
+ nature: 'Natürlich',
+ tech: 'Technisch'
+};
+
+export const DEADLINE_LABELS: Record = {
+ asap: 'So schnell wie möglich',
+ '2-3-months': 'In 2-3 Monaten',
+ '3-6-months': 'In 3-6 Monaten',
+ flexible: 'Flexibel'
+};
+
+export const ASSET_LABELS: Record = {
+ logo: 'Logo',
+ styleguide: 'Styleguide',
+ content_concept: 'Inhalts-Konzept',
+ media: 'Bild/Video-Material',
+ icons: 'Icons',
+ illustrations: 'Illustrationen',
+ fonts: 'Fonts'
+};
+
+export const FEATURE_LABELS: Record = {
+ blog_news: 'Blog / News',
+ products: 'Produktbereich',
+ jobs: 'Karriere / Jobs',
+ refs: 'Referenzen / Cases',
+ events: 'Events / Termine'
+};
+
+export const FUNCTION_LABELS: Record = {
+ search: 'Suche',
+ filter: 'Filter-Systeme',
+ pdf: 'PDF-Export',
+ forms: 'Erweiterte Formulare',
+ members: 'Mitgliederbereich',
+ calendar: 'Event-Kalender',
+ multilang: 'Mehrsprachigkeit',
+ chat: 'Echtzeit-Chat'
+};
+
+export const API_LABELS: Record = {
+ crm_erp: 'CRM / ERP',
+ payment: 'Payment',
+ marketing: 'Marketing',
+ ecommerce: 'E-Commerce',
+ maps: 'Google Maps / Places',
+ social: 'Social Media Sync',
+ analytics: 'Custom Analytics'
+};
+
+export const SOCIAL_LABELS: Record = {
+ instagram: 'Instagram',
+ linkedin: 'LinkedIn',
+ facebook: 'Facebook',
+ twitter: 'Twitter / X',
+ tiktok: 'TikTok',
+ youtube: 'YouTube'
+};
diff --git a/src/components/ContactForm/utils.ts b/src/components/ContactForm/utils.ts
new file mode 100644
index 0000000..fcf2a02
--- /dev/null
+++ b/src/components/ContactForm/utils.ts
@@ -0,0 +1,139 @@
+import { FormState } from './types';
+import { FEATURE_LABELS, FUNCTION_LABELS, API_LABELS } from './constants';
+
+export interface Position {
+ pos: number;
+ title: string;
+ desc: string;
+ qty: number;
+ price: number;
+}
+
+export function calculatePositions(state: FormState, pricing: any): Position[] {
+ const positions: Position[] = [];
+ let pos = 1;
+
+ if (state.projectType === 'website') {
+ positions.push({
+ pos: pos++,
+ title: 'Basis Website Setup',
+ desc: 'Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.',
+ qty: 1,
+ price: pricing.BASE_WEBSITE
+ });
+
+ const totalPagesCount = state.selectedPages.length + state.otherPages.length + (state.otherPagesCount || 0);
+ const allPages = [...state.selectedPages.map((p: string) => p === 'Home' ? 'Startseite' : p), ...state.otherPages];
+
+ positions.push({
+ pos: pos++,
+ title: 'Individuelle Seiten',
+ desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${allPages.join(', ')}).`,
+ qty: totalPagesCount,
+ price: totalPagesCount * pricing.PAGE
+ });
+
+ if (state.features.length > 0 || state.otherFeatures.length > 0) {
+ const allFeatures = [...state.features.map((f: string) => FEATURE_LABELS[f] || f), ...state.otherFeatures];
+ positions.push({
+ pos: pos++,
+ title: 'System-Module',
+ desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(', ')}. Inklusive Datenstruktur und Darstellung.`,
+ qty: allFeatures.length,
+ price: allFeatures.length * pricing.FEATURE
+ });
+ }
+
+ if (state.functions.length > 0 || state.otherFunctions.length > 0) {
+ const allFunctions = [...state.functions.map((f: string) => FUNCTION_LABELS[f] || f), ...state.otherFunctions];
+ positions.push({
+ pos: pos++,
+ title: 'Logik-Funktionen',
+ desc: `Erweiterte Funktionen: ${allFunctions.join(', ')}.`,
+ qty: allFunctions.length,
+ price: allFunctions.length * pricing.FUNCTION
+ });
+ }
+
+ if (state.apiSystems.length > 0 || state.otherTech.length > 0) {
+ const allApis = [...state.apiSystems.map((a: string) => API_LABELS[a] || a), ...state.otherTech];
+ positions.push({
+ pos: pos++,
+ title: 'Schnittstellen (API)',
+ desc: `Anbindung externer Systeme zur Datensynchronisation: ${allApis.join(', ')}.`,
+ qty: allApis.length,
+ price: allApis.length * pricing.API_INTEGRATION
+ });
+ }
+
+ if (state.cmsSetup) {
+ const totalFeatures = state.features.length + state.otherFeatures.length + (state.otherFeaturesCount || 0);
+ positions.push({
+ pos: pos++,
+ title: 'Inhaltsverwaltung (CMS)',
+ desc: 'Einrichtung eines Systems zur eigenständigen Pflege von Inhalten und Datensätzen.',
+ qty: 1,
+ price: pricing.CMS_SETUP + totalFeatures * pricing.CMS_CONNECTION_PER_FEATURE
+ });
+ }
+
+ if (state.newDatasets > 0) {
+ positions.push({
+ pos: pos++,
+ title: 'Inhaltspflege (Initial)',
+ desc: `Manuelle Einpflege von ${state.newDatasets} Datensätzen (z.B. Produkte, Blogartikel).`,
+ qty: state.newDatasets,
+ price: state.newDatasets * pricing.NEW_DATASET
+ });
+ }
+
+ if (state.visualStaging && Number(state.visualStaging) > 0) {
+ const count = Number(state.visualStaging);
+ positions.push({
+ pos: pos++,
+ title: 'Visuelle Inszenierung',
+ desc: `Umsetzung von ${count} Hero-Stories, Scroll-Effekten oder speziell inszenierten Sektionen.`,
+ qty: count,
+ price: count * (pricing.VISUAL_STAGING || 1500)
+ });
+ }
+
+ if (state.complexInteractions && Number(state.complexInteractions) > 0) {
+ const count = Number(state.complexInteractions);
+ positions.push({
+ pos: pos++,
+ title: 'Komplexe Interaktion',
+ desc: `Umsetzung von ${count} Konfiguratoren, Live-Previews oder mehrstufigen Auswahlprozessen.`,
+ qty: count,
+ price: count * (pricing.COMPLEX_INTERACTION || 2500)
+ });
+ }
+
+ const languagesCount = state.languagesList.length || 1;
+ if (languagesCount > 1) {
+ // This is a bit tricky because the factor applies to the total.
+ // For the PDF we show it as a separate position.
+ // We calculate the subtotal first.
+ const subtotal = positions.reduce((sum, p) => sum + p.price, 0);
+ const factorPrice = subtotal * ((languagesCount - 1) * 0.2);
+
+ positions.push({
+ pos: pos++,
+ title: 'Mehrsprachigkeit',
+ desc: `Erweiterung des Systems auf ${languagesCount} Sprachen (Struktur & Logik).`,
+ qty: languagesCount,
+ price: Math.round(factorPrice)
+ });
+ }
+ } else {
+ positions.push({
+ pos: pos++,
+ title: 'Web App / Software Entwicklung',
+ desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.',
+ qty: 1,
+ price: 0
+ });
+ }
+
+ return positions;
+}
diff --git a/src/components/EstimationPDF.tsx b/src/components/EstimationPDF.tsx
index 576f1f1..40f2942 100644
--- a/src/components/EstimationPDF.tsx
+++ b/src/components/EstimationPDF.tsx
@@ -1,178 +1,280 @@
'use client';
import * as React from 'react';
-import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer';
+import {
+ Document as PDFDocument,
+ Page as PDFPage,
+ Text as PDFText,
+ View as PDFView,
+ StyleSheet as PDFStyleSheet,
+ Image as PDFImage
+} from '@react-pdf/renderer';
+import {
+ VIBE_LABELS,
+ DEADLINE_LABELS,
+ ASSET_LABELS,
+ SOCIAL_LABELS
+} from './ContactForm/constants';
+import { calculatePositions } from './ContactForm/utils';
-const styles = StyleSheet.create({
+const styles = PDFStyleSheet.create({
page: {
- padding: 40,
+ padding: 48,
backgroundColor: '#ffffff',
fontFamily: 'Helvetica',
fontSize: 10,
- color: '#1a1a1a',
+ color: '#000000',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
- marginBottom: 40,
- borderBottom: 2,
+ alignItems: 'flex-start',
+ marginBottom: 64,
+ borderBottomWidth: 1,
borderBottomColor: '#000000',
- paddingBottom: 20,
+ paddingBottom: 24,
},
- brand: {
- fontSize: 18,
+ brandIconContainer: {
+ width: 40,
+ height: 40,
+ backgroundColor: '#000000',
+ borderRadius: 8,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ brandIconText: {
+ color: '#ffffff',
+ fontSize: 20,
fontWeight: 'bold',
- letterSpacing: -0.5,
},
quoteInfo: {
textAlign: 'right',
},
quoteTitle: {
- fontSize: 14,
+ fontSize: 10,
fontWeight: 'bold',
marginBottom: 4,
+ color: '#000000',
+ textTransform: 'uppercase',
+ letterSpacing: 1,
},
quoteDate: {
fontSize: 9,
color: '#666666',
},
- recipientSection: {
- marginBottom: 30,
+
+ section: {
+ marginBottom: 32,
+ },
+ sectionTitle: {
+ fontSize: 8,
+ fontWeight: 'bold',
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ color: '#999999',
+ marginBottom: 12,
+ },
+
+ card: {
+ backgroundColor: '#ffffff',
+ borderRadius: 12,
+ padding: 24,
+ marginBottom: 24,
+ borderWidth: 1,
+ borderColor: '#eeeeee',
+ },
+ cardTitle: {
+ fontSize: 10,
+ fontWeight: 'bold',
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ color: '#64748b',
+ marginBottom: 16,
+ },
+
+ recipientGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 32,
+ },
+ recipientItem: {
+ flexDirection: 'column',
},
recipientLabel: {
- fontSize: 8,
+ fontSize: 7,
color: '#999999',
textTransform: 'uppercase',
marginBottom: 4,
- },
- recipientName: {
- fontSize: 12,
fontWeight: 'bold',
},
- recipientRole: {
+ recipientValue: {
fontSize: 10,
- color: '#666666',
+ fontWeight: 'bold',
+ color: '#000000',
},
+
table: {
- display: 'flex',
- width: 'auto',
- marginBottom: 30,
+ marginTop: 16,
},
tableHeader: {
flexDirection: 'row',
- backgroundColor: '#f8fafc',
- borderBottom: 1,
- borderBottomColor: '#e2e8f0',
- paddingVertical: 8,
- paddingHorizontal: 12,
+ paddingBottom: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: '#000000',
+ marginBottom: 12,
},
tableRow: {
flexDirection: 'row',
- borderBottom: 1,
- borderBottomColor: '#f1f5f9',
- paddingVertical: 10,
- paddingHorizontal: 12,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: '#eeeeee',
alignItems: 'flex-start',
},
- colPos: { width: '8%' },
- colDesc: { width: '62%' },
+ colPos: { width: '6%' },
+ colDesc: { width: '64%' },
colQty: { width: '10%', textAlign: 'center' },
colPrice: { width: '20%', textAlign: 'right' },
headerText: {
- fontSize: 8,
+ fontSize: 7,
fontWeight: 'bold',
- color: '#64748b',
+ color: '#000000',
textTransform: 'uppercase',
+ letterSpacing: 1,
},
- posText: { fontSize: 9, color: '#94a3b8' },
- itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 2 },
- itemDesc: { fontSize: 8, color: '#64748b', lineHeight: 1.4 },
- priceText: { fontSize: 10, fontWeight: 'bold' },
+ posText: { fontSize: 8, color: '#999999' },
+ itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4, color: '#000000' },
+ itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 },
+ priceText: { fontSize: 10, fontWeight: 'bold', color: '#000000' },
- summarySection: {
+ summaryContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
- marginTop: 10,
+ marginTop: 32,
},
- summaryTable: {
+ summaryCard: {
width: '40%',
+ backgroundColor: '#ffffff',
+ borderRadius: 12,
+ padding: 20,
+ borderWidth: 1,
+ borderColor: '#000000',
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 4,
},
+ summaryLabel: { fontSize: 8, color: '#666666' },
+ summaryValue: { fontSize: 9, fontWeight: 'bold', color: '#000000' },
+
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
- paddingVertical: 12,
- borderTop: 1,
- borderTopColor: '#000000',
+ paddingTop: 12,
marginTop: 8,
+ borderTopWidth: 1,
+ borderTopColor: '#eeeeee',
+ },
+ totalLabel: { fontSize: 10, fontWeight: 'bold', color: '#000000' },
+ totalValue: { fontSize: 14, fontWeight: 'bold', color: '#000000' },
+
+ hostingBox: {
+ marginTop: 24,
+ padding: 16,
+ borderWidth: 1,
+ borderColor: '#eeeeee',
+ borderRadius: 12,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
},
- totalLabel: { fontSize: 12, fontWeight: 'bold' },
- totalValue: { fontSize: 16, fontWeight: 'bold' },
- configSection: {
- marginTop: 20,
- padding: 20,
- backgroundColor: '#f8fafc',
- borderRadius: 8,
- },
- configTitle: {
- fontSize: 10,
- fontWeight: 'bold',
- marginBottom: 10,
- textTransform: 'uppercase',
- color: '#475569',
- },
configGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
- gap: 20,
+ gap: 24,
},
configItem: {
- width: '45%',
- marginBottom: 10,
+ width: '30%',
+ marginBottom: 16,
},
- configLabel: { fontSize: 8, color: '#94a3b8', marginBottom: 2 },
- configValue: { fontSize: 9, color: '#1e293b' },
+ configLabel: { fontSize: 7, color: '#999999', marginBottom: 4, textTransform: 'uppercase', fontWeight: 'bold' },
+ configValue: { fontSize: 8, color: '#000000', fontWeight: 'bold' },
- qrSection: {
- marginTop: 30,
+ colorGrid: {
+ flexDirection: 'row',
+ gap: 12,
+ marginTop: 8,
+ },
+ colorSwatch: {
+ width: 24,
+ height: 24,
+ borderRadius: 4,
+ borderWidth: 1,
+ borderColor: '#eeeeee',
+ },
+ colorHex: {
+ fontSize: 6,
+ color: '#999999',
+ marginTop: 4,
+ textAlign: 'center',
+ fontWeight: 'bold',
+ },
+
+ qrContainer: {
+ position: 'absolute',
+ bottom: 120,
+ right: 48,
alignItems: 'center',
- justifyContent: 'center',
},
qrImage: {
- width: 80,
- height: 80,
+ width: 60,
+ height: 60,
},
qrText: {
- fontSize: 7,
- color: '#94a3b8',
- marginTop: 5,
+ fontSize: 6,
+ color: '#999999',
+ marginTop: 8,
+ fontWeight: 'bold',
+ textTransform: 'uppercase',
+ textAlign: 'center',
},
footer: {
position: 'absolute',
- bottom: 30,
- left: 40,
- right: 40,
- borderTop: 1,
+ bottom: 48,
+ left: 48,
+ right: 48,
+ borderTopWidth: 1,
borderTopColor: '#f1f5f9',
- paddingTop: 20,
+ paddingTop: 24,
flexDirection: 'row',
justifyContent: 'space-between',
+ alignItems: 'flex-end',
+ },
+ footerBrand: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ letterSpacing: -1,
+ color: '#000000',
+ textTransform: 'lowercase',
+ },
+ footerRight: {
+ alignItems: 'flex-end',
+ },
+ footerContact: {
fontSize: 8,
color: '#94a3b8',
+ fontWeight: 'bold',
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ marginBottom: 4,
},
pageNumber: {
- position: 'absolute',
- bottom: 30,
- right: 40,
- fontSize: 8,
- color: '#94a3b8',
+ fontSize: 7,
+ color: '#cbd5e1',
+ fontWeight: 'bold',
}
});
@@ -192,408 +294,252 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
day: 'numeric',
});
- const vibeLabels: Record = {
- minimal: 'Minimalistisch',
- bold: 'Mutig & Laut',
- nature: 'Natürlich',
- tech: 'Technisch'
- };
-
- const deadlineLabels: Record = {
- asap: 'So schnell wie möglich',
- '2-3-months': 'In 2-3 Monaten',
- '3-6-months': 'In 3-6 Monaten',
- flexible: 'Flexibel'
- };
-
- const assetLabels: Record = {
- logo: 'Logo',
- styleguide: 'Styleguide',
- content_concept: 'Inhalts-Konzept',
- media: 'Bild/Video-Material',
- icons: 'Icons',
- illustrations: 'Illustrationen',
- fonts: 'Fonts'
- };
-
- const featureLabels: Record = {
- blog_news: 'Blog / News',
- products: 'Produktbereich',
- jobs: 'Karriere / Jobs',
- refs: 'Referenzen / Cases',
- events: 'Events / Termine'
- };
-
- const functionLabels: Record = {
- search: 'Suche',
- filter: 'Filter-Systeme',
- pdf: 'PDF-Export',
- forms: 'Erweiterte Formulare'
- };
-
- const apiLabels: Record = {
- crm: 'CRM System',
- erp: 'ERP / Warenwirtschaft',
- stripe: 'Stripe / Payment',
- newsletter: 'Newsletter / Marketing',
- ecommerce: 'E-Commerce / Shop',
- hr: 'HR / Recruiting',
- realestate: 'Immobilien',
- calendar: 'Termine / Booking',
- social: 'Social Media Sync',
- maps: 'Google Maps / Places',
- auth: 'Auth-Provider'
- };
-
- const socialLabels: Record = {
- instagram: 'Instagram',
- linkedin: 'LinkedIn',
- facebook: 'Facebook',
- twitter: 'Twitter / X',
- tiktok: 'TikTok',
- youtube: 'YouTube'
- };
-
- const positions = [];
- let pos = 1;
-
- if (state.projectType === 'website') {
- positions.push({
- pos: pos++,
- title: 'Basis Website Setup',
- desc: 'Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.',
- qty: 1,
- price: pricing.BASE_WEBSITE
- });
-
- const allPages = [...state.selectedPages.map((p: string) => p === 'Home' ? 'Startseite' : p), ...state.otherPages];
- positions.push({
- pos: pos++,
- title: 'Individuelle Seiten',
- desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${allPages.join(', ')}).`,
- qty: totalPagesCount,
- price: totalPagesCount * pricing.PAGE
- });
-
- if (state.features.length > 0 || state.otherFeatures.length > 0) {
- const allFeatures = [...state.features.map((f: string) => featureLabels[f] || f), ...state.otherFeatures];
- positions.push({
- pos: pos++,
- title: 'System-Module',
- desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(', ')}. Inklusive Datenstruktur und Darstellung.`,
- qty: allFeatures.length,
- price: allFeatures.length * pricing.FEATURE
- });
- }
-
- if (state.functions.length > 0 || state.otherFunctions.length > 0) {
- const allFunctions = [...state.functions.map((f: string) => functionLabels[f] || f), ...state.otherFunctions];
- positions.push({
- pos: pos++,
- title: 'Logik-Funktionen',
- desc: `Erweiterte Funktionen: ${allFunctions.join(', ')}.`,
- qty: allFunctions.length,
- price: allFunctions.length * pricing.FUNCTION
- });
- }
-
- if (state.apiSystems.length > 0 || state.otherTech.length > 0) {
- const allApis = [...state.apiSystems.map((a: string) => apiLabels[a] || a), ...state.otherTech];
- positions.push({
- pos: pos++,
- title: 'Schnittstellen (API)',
- desc: `Anbindung externer Systeme zur Datensynchronisation: ${allApis.join(', ')}.`,
- qty: allApis.length,
- price: allApis.length * pricing.API_INTEGRATION
- });
- }
-
- if (state.cmsSetup) {
- positions.push({
- pos: pos++,
- title: 'Inhaltsverwaltung (CMS)',
- desc: 'Einrichtung eines Systems zur eigenständigen Pflege von Inhalten und Datensätzen.',
- qty: 1,
- price: pricing.CMS_SETUP + (state.features.length + state.otherFeatures.length) * pricing.CMS_CONNECTION_PER_FEATURE
- });
- }
-
- if (state.newDatasets > 0) {
- positions.push({
- pos: pos++,
- title: 'Inhaltspflege (Initial)',
- desc: `Manuelle Einpflege von ${state.newDatasets} Datensätzen (z.B. Produkte, Blogartikel).`,
- qty: state.newDatasets,
- price: state.newDatasets * pricing.NEW_DATASET
- });
- }
-
- if (state.visualStaging > 0) {
- positions.push({
- pos: pos++,
- title: 'Visuelle Inszenierung',
- desc: `Umsetzung von ${state.visualStaging} Hero-Stories, Scroll-Effekten oder speziell inszenierten Sektionen.`,
- qty: state.visualStaging,
- price: state.visualStaging * pricing.VISUAL_STAGING
- });
- }
-
- if (state.complexInteractions > 0) {
- positions.push({
- pos: pos++,
- title: 'Komplexe Interaktion',
- desc: `Umsetzung von ${state.complexInteractions} Konfiguratoren, Live-Previews oder mehrstufigen Auswahlprozessen.`,
- qty: state.complexInteractions,
- price: state.complexInteractions * pricing.COMPLEX_INTERACTION
- });
- }
-
- if (state.languagesCount > 1) {
- const factorPrice = totalPrice - (totalPrice / (1 + (state.languagesCount - 1) * 0.2));
- positions.push({
- pos: pos++,
- title: 'Mehrsprachigkeit',
- desc: `Erweiterung des Systems auf ${state.languagesCount} Sprachen (Struktur & Logik).`,
- qty: state.languagesCount,
- price: Math.round(factorPrice)
- });
- }
- } else {
- positions.push({
- pos: pos++,
- title: 'Web App / Software Entwicklung',
- desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.',
- qty: 1,
- price: 0
- });
- }
+ const positions = calculatePositions(state, pricing);
return (
-
-
-
-
- marc mintel
- Digital Systems & Design
-
-
- Kostenschätzung
- {date}
- Projekt: {state.projectType === 'website' ? 'Website' : 'Web App'}
-
-
+
+
+
+
+ M
+
+
+ Kostenschätzung
+ {date}
+
+ Projekt: {state.projectType === 'website' ? 'Website' : 'Web App'}
+
+
+
-
- Ansprechpartner
- {state.name || 'Interessent'}
- {state.companyName && {state.companyName}}
- {state.role && {state.role}}
- {state.email}
-
+
+ Ansprechpartner
+
+
+ Name
+ {state.name || 'Interessent'}
+
+ {state.companyName && (
+
+ Unternehmen
+ {state.companyName}
+
+ )}
+ {state.email && (
+
+ E-Mail
+ {state.email}
+
+ )}
+
+
-
-
- Pos
- Beschreibung
- Menge
- Betrag
-
+
+
+ Pos
+ Beschreibung
+ Menge
+ Betrag
+
{positions.map((item, i) => (
-
- {item.pos}
-
- {item.title}
- {item.desc}
-
- {item.qty}
-
+
+ {item.pos.toString().padStart(2, '0')}
+
+ {item.title}
+ {item.desc}
+
+ {item.qty}
+
{item.price > 0 ? `${item.price.toLocaleString()} €` : 'n. A.'}
-
-
+
+
))}
-
+
-
-
-
- Zwischensumme (Netto)
- {totalPrice.toLocaleString()} €
-
-
- Umsatzsteuer (0%)*
- 0,00 €
-
-
- Gesamtsumme
- {totalPrice.toLocaleString()} €
-
-
- *Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.
-
- {state.projectType === 'website' && (
-
- Betrieb & Hosting
- {monthlyPrice.toLocaleString()} € / Monat
-
- )}
-
-
+
+
+
+ Zwischensumme (Netto)
+ {totalPrice.toLocaleString()} €
+
+
+ Gesamtsumme
+ {totalPrice.toLocaleString()} €
+
+
+
-
- marc@mintel.me
- mintel.me
- Digital Systems & Design
-
- `Seite ${pageNumber} von ${totalPages}`} fixed />
-
+ {state.projectType === 'website' && (
+
+ Betrieb & Hosting
+ {monthlyPrice.toLocaleString()} € / Monat
+
+ )}
-
-
-
- marc mintel
-
-
- Projektdetails
-
-
+
+ marc mintel
+
+ marc@mintel.me
+ `${pageNumber} / ${totalPages}`} fixed />
+
+
+
-
- Konfiguration & Wünsche
-
+
+
+
+ M
+
+
+ Projektdetails
+
+
+
+
+ Konfiguration & Wünsche
+
{state.projectType === 'website' ? (
<>
-
- Thema
- {state.websiteTopic || 'Nicht angegeben'}
-
-
- Design-Vibe
- {vibeLabels[state.designVibe] || state.designVibe}
-
-
- Farbschema
- {state.colorScheme.join(', ')}
-
+
+ Thema
+ {state.websiteTopic || 'Nicht angegeben'}
+
+
+ Design-Vibe
+ {VIBE_LABELS[state.designVibe] || state.designVibe}
+
+
+ Farbschema
+
+ {state.colorScheme.map((color: string, i: number) => (
+
+
+ {color.toUpperCase()}
+
+ ))}
+
+
>
) : (
<>
-
- Zielgruppe
- {state.targetAudience === 'internal' ? 'Internes Tool' : 'Kunden-Portal'}
-
-
- Plattform
- {state.platformType.toUpperCase()}
-
-
- Sicherheit
- {state.dataSensitivity === 'high' ? 'Sensibel' : 'Standard'}
-
-
- Rollen
- {state.userRoles.join(', ') || 'Keine'}
-
+
+ Zielgruppe
+ {state.targetAudience === 'internal' ? 'Internes Tool' : 'Kunden-Portal'}
+
+
+ Plattform
+ {state.platformType.toUpperCase()}
+
+
+ Sicherheit
+ {state.dataSensitivity === 'high' ? 'Sensibel' : 'Standard'}
+
+
+ Rollen
+ {state.userRoles.join(', ') || 'Keine'}
+
>
)}
-
- Mitarbeiter
- {state.employeeCount || 'Nicht angegeben'}
-
-
- Bestehende Website
- {state.existingWebsite || 'Keine'}
-
-
- Bestehende Domain
- {state.existingDomain || 'Keine'}
-
-
- Wunsch-Domain
- {state.wishedDomain || 'Keine'}
-
-
- Zeitplan
- {deadlineLabels[state.deadline] || state.deadline}
-
-
- Assets vorhanden
- {state.assets.map((a: string) => assetLabels[a] || a).join(', ') || 'Keine angegeben'}
-
+
+ Mitarbeiter
+ {state.employeeCount || 'Nicht angegeben'}
+
+
+ Bestehende Website
+ {state.existingWebsite || 'Keine'}
+
+
+ Bestehende Domain
+ {state.existingDomain || 'Keine'}
+
+
+ Wunsch-Domain
+ {state.wishedDomain || 'Keine'}
+
+
+ Zeitplan
+ {DEADLINE_LABELS[state.deadline] || state.deadline}
+
+
+ Assets vorhanden
+ {state.assets.map((a: string) => ASSET_LABELS[a] || a).join(', ') || 'Keine angegeben'}
+
{state.otherAssets.length > 0 && (
-
- Weitere Assets
- {state.otherAssets.join(', ')}
-
+
+ Weitere Assets
+ {state.otherAssets.join(', ')}
+
)}
-
- Sprachen
- {state.languagesCount} ({state.languagesList.join(', ')})
-
+
+ Sprachen
+ {state.languagesList.length} ({state.languagesList.join(', ')})
+
{state.projectType === 'website' && (
<>
-
- CMS (Inhaltsverwaltung)
- {state.cmsSetup ? 'Ja' : 'Nein'}
-
-
- Änderungsfrequenz
-
+
+ CMS (Inhaltsverwaltung)
+ {state.cmsSetup ? 'Ja' : 'Nein'}
+
+
+ Änderungsfrequenz
+
{state.expectedAdjustments === 'low' ? 'Selten' :
state.expectedAdjustments === 'medium' ? 'Regelmäßig' : 'Häufig'}
-
-
+
+
>
)}
-
+
{state.socialMedia.length > 0 && (
-
- Social Media Accounts
+
+ Social Media Accounts
{state.socialMedia.map((id: string) => (
-
- {socialLabels[id] || id}: {state.socialMediaUrls[id] || 'Keine URL angegeben'}
-
+
+ {SOCIAL_LABELS[id] || id}: {state.socialMediaUrls[id] || 'Keine URL angegeben'}
+
))}
-
+
)}
{state.designWishes && (
-
- Design-Vorstellungen
- {state.designWishes}
-
+
+ Design-Vorstellungen
+ {state.designWishes}
+
)}
{state.references.length > 0 && (
-
- Referenzen
- {state.references.join('\n')}
-
+
+ Referenzen
+ {state.references.join('\n')}
+
)}
{state.message && (
-
- Nachricht / Anmerkungen
- {state.message}
-
+
+ Nachricht / Anmerkungen
+ {state.message}
+
)}
-
+
{qrCodeData && (
-
-
- QR-Code scannen, um Konfiguration online zu öffnen
-
+
+
+ Online öffnen
+
)}
-
- marc@mintel.me
- mintel.me
- Digital Systems & Design
-
- `Seite ${pageNumber} von ${totalPages}`} fixed />
-
-
+
+ marc mintel
+
+ marc@mintel.me
+ `${pageNumber} / ${totalPages}`} fixed />
+
+
+
+
);
};