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 /> + + + + ); };