form
This commit is contained in:
@@ -5,7 +5,8 @@ import { FormState } from '../types';
|
|||||||
import { PRICING } from '../constants';
|
import { PRICING } from '../constants';
|
||||||
import { AnimatedNumber } from './AnimatedNumber';
|
import { AnimatedNumber } from './AnimatedNumber';
|
||||||
import { ConceptPrice, ConceptAutomation } from '../../Landing/ConceptIllustrations';
|
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 dynamic from 'next/dynamic';
|
||||||
import { EstimationPDF } from '../../EstimationPDF';
|
import { EstimationPDF } from '../../EstimationPDF';
|
||||||
|
|
||||||
@@ -42,6 +43,43 @@ export function PriceCalculation({
|
|||||||
const [pdfLoading, setPdfLoading] = React.useState(false);
|
const [pdfLoading, setPdfLoading] = React.useState(false);
|
||||||
const languagesCount = state.languagesList.length || 1;
|
const languagesCount = state.languagesList.length || 1;
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (pdfLoading) return;
|
||||||
|
|
||||||
|
setPdfLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { pdf } = await import('@react-pdf/renderer');
|
||||||
|
const doc = <EstimationPDF
|
||||||
|
state={state}
|
||||||
|
totalPrice={totalPrice}
|
||||||
|
monthlyPrice={monthlyPrice}
|
||||||
|
totalPagesCount={totalPagesCount}
|
||||||
|
pricing={PRICING}
|
||||||
|
qrCodeData={qrCodeData}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="lg:col-span-4 lg:sticky lg:top-32 self-start z-30">
|
<div className="lg:col-span-4 lg:sticky lg:top-32 self-start z-30">
|
||||||
<div className="bg-slate-50 border border-slate-100 rounded-[3rem] p-6 space-y-6">
|
<div className="bg-slate-50 border border-slate-100 rounded-[3rem] p-6 space-y-6">
|
||||||
@@ -76,26 +114,46 @@ export function PriceCalculation({
|
|||||||
|
|
||||||
<div className="pt-4 space-y-4">
|
<div className="pt-4 space-y-4">
|
||||||
{isClient && (
|
{isClient && (
|
||||||
<PDFDownloadLink
|
<button
|
||||||
document={<EstimationPDF state={state} totalPrice={totalPrice} monthlyPrice={monthlyPrice} totalPagesCount={totalPagesCount} pricing={PRICING} qrCodeData={qrCodeData} />}
|
type="button"
|
||||||
fileName={`kalkulation-${state.name.toLowerCase().replace(/\s+/g, '-') || 'projekt'}.pdf`}
|
disabled={pdfLoading}
|
||||||
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={handleDownload}
|
||||||
onClick={(e) => {
|
className="w-full h-14 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"
|
||||||
if (pdfLoading) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPdfLoading(true);
|
|
||||||
setTimeout(() => setPdfLoading(false), 2000);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{({ loading }) => (
|
<AnimatePresence mode="wait">
|
||||||
<div className="flex items-center gap-3">
|
{pdfLoading ? (
|
||||||
<Download size={18} />
|
<motion.div
|
||||||
<span>{loading || pdfLoading ? 'PDF wird erstellt...' : 'Als PDF speichern'}</span>
|
key="loading"
|
||||||
</div>
|
initial={{ opacity: 0 }}
|
||||||
)}
|
animate={{ opacity: 1 }}
|
||||||
</PDFDownloadLink>
|
exit={{ opacity: 0 }}
|
||||||
|
className="absolute inset-0 flex items-center justify-center bg-white"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-0 left-0 h-1 bg-slate-900"
|
||||||
|
initial={{ width: "0%" }}
|
||||||
|
animate={{ width: "100%" }}
|
||||||
|
transition={{ duration: 2, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<RefreshCw className="animate-spin" size={16} />
|
||||||
|
PDF wird erstellt...
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="idle"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="flex items-center justify-center gap-3"
|
||||||
|
>
|
||||||
|
<Download size={18} />
|
||||||
|
<span>Als PDF speichern</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{onShare && (
|
{onShare && (
|
||||||
|
|||||||
@@ -186,3 +186,65 @@ export const SOCIAL_MEDIA_OPTIONS = [
|
|||||||
{ id: 'tiktok', label: 'TikTok' },
|
{ id: 'tiktok', label: 'TikTok' },
|
||||||
{ id: 'youtube', label: 'YouTube' },
|
{ id: 'youtube', label: 'YouTube' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const VIBE_LABELS: Record<string, string> = {
|
||||||
|
minimal: 'Minimalistisch',
|
||||||
|
bold: 'Mutig & Laut',
|
||||||
|
nature: 'Natürlich',
|
||||||
|
tech: 'Technisch'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEADLINE_LABELS: Record<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
logo: 'Logo',
|
||||||
|
styleguide: 'Styleguide',
|
||||||
|
content_concept: 'Inhalts-Konzept',
|
||||||
|
media: 'Bild/Video-Material',
|
||||||
|
icons: 'Icons',
|
||||||
|
illustrations: 'Illustrationen',
|
||||||
|
fonts: 'Fonts'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FEATURE_LABELS: Record<string, string> = {
|
||||||
|
blog_news: 'Blog / News',
|
||||||
|
products: 'Produktbereich',
|
||||||
|
jobs: 'Karriere / Jobs',
|
||||||
|
refs: 'Referenzen / Cases',
|
||||||
|
events: 'Events / Termine'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FUNCTION_LABELS: Record<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
instagram: 'Instagram',
|
||||||
|
linkedin: 'LinkedIn',
|
||||||
|
facebook: 'Facebook',
|
||||||
|
twitter: 'Twitter / X',
|
||||||
|
tiktok: 'TikTok',
|
||||||
|
youtube: 'YouTube'
|
||||||
|
};
|
||||||
|
|||||||
139
src/components/ContactForm/utils.ts
Normal file
139
src/components/ContactForm/utils.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,178 +1,280 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
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: {
|
page: {
|
||||||
padding: 40,
|
padding: 48,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
fontFamily: 'Helvetica',
|
fontFamily: 'Helvetica',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: '#1a1a1a',
|
color: '#000000',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: 40,
|
alignItems: 'flex-start',
|
||||||
borderBottom: 2,
|
marginBottom: 64,
|
||||||
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#000000',
|
borderBottomColor: '#000000',
|
||||||
paddingBottom: 20,
|
paddingBottom: 24,
|
||||||
},
|
},
|
||||||
brand: {
|
brandIconContainer: {
|
||||||
fontSize: 18,
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
brandIconText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 20,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
letterSpacing: -0.5,
|
|
||||||
},
|
},
|
||||||
quoteInfo: {
|
quoteInfo: {
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
},
|
},
|
||||||
quoteTitle: {
|
quoteTitle: {
|
||||||
fontSize: 14,
|
fontSize: 10,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
|
color: '#000000',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 1,
|
||||||
},
|
},
|
||||||
quoteDate: {
|
quoteDate: {
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
color: '#666666',
|
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: {
|
recipientLabel: {
|
||||||
fontSize: 8,
|
fontSize: 7,
|
||||||
color: '#999999',
|
color: '#999999',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
|
||||||
recipientName: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
recipientRole: {
|
recipientValue: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: '#666666',
|
fontWeight: 'bold',
|
||||||
|
color: '#000000',
|
||||||
},
|
},
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
display: 'flex',
|
marginTop: 16,
|
||||||
width: 'auto',
|
|
||||||
marginBottom: 30,
|
|
||||||
},
|
},
|
||||||
tableHeader: {
|
tableHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
backgroundColor: '#f8fafc',
|
paddingBottom: 8,
|
||||||
borderBottom: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#e2e8f0',
|
borderBottomColor: '#000000',
|
||||||
paddingVertical: 8,
|
marginBottom: 12,
|
||||||
paddingHorizontal: 12,
|
|
||||||
},
|
},
|
||||||
tableRow: {
|
tableRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
borderBottom: 1,
|
paddingVertical: 12,
|
||||||
borderBottomColor: '#f1f5f9',
|
borderBottomWidth: 1,
|
||||||
paddingVertical: 10,
|
borderBottomColor: '#eeeeee',
|
||||||
paddingHorizontal: 12,
|
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
},
|
},
|
||||||
colPos: { width: '8%' },
|
colPos: { width: '6%' },
|
||||||
colDesc: { width: '62%' },
|
colDesc: { width: '64%' },
|
||||||
colQty: { width: '10%', textAlign: 'center' },
|
colQty: { width: '10%', textAlign: 'center' },
|
||||||
colPrice: { width: '20%', textAlign: 'right' },
|
colPrice: { width: '20%', textAlign: 'right' },
|
||||||
|
|
||||||
headerText: {
|
headerText: {
|
||||||
fontSize: 8,
|
fontSize: 7,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#64748b',
|
color: '#000000',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 1,
|
||||||
},
|
},
|
||||||
posText: { fontSize: 9, color: '#94a3b8' },
|
posText: { fontSize: 8, color: '#999999' },
|
||||||
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 2 },
|
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4, color: '#000000' },
|
||||||
itemDesc: { fontSize: 8, color: '#64748b', lineHeight: 1.4 },
|
itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 },
|
||||||
priceText: { fontSize: 10, fontWeight: 'bold' },
|
priceText: { fontSize: 10, fontWeight: 'bold', color: '#000000' },
|
||||||
|
|
||||||
summarySection: {
|
summaryContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
marginTop: 10,
|
marginTop: 32,
|
||||||
},
|
},
|
||||||
summaryTable: {
|
summaryCard: {
|
||||||
width: '40%',
|
width: '40%',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 20,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000000',
|
||||||
},
|
},
|
||||||
summaryRow: {
|
summaryRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
},
|
},
|
||||||
|
summaryLabel: { fontSize: 8, color: '#666666' },
|
||||||
|
summaryValue: { fontSize: 9, fontWeight: 'bold', color: '#000000' },
|
||||||
|
|
||||||
totalRow: {
|
totalRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 12,
|
paddingTop: 12,
|
||||||
borderTop: 1,
|
|
||||||
borderTopColor: '#000000',
|
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#eeeeee',
|
||||||
},
|
},
|
||||||
totalLabel: { fontSize: 12, fontWeight: 'bold' },
|
totalLabel: { fontSize: 10, fontWeight: 'bold', color: '#000000' },
|
||||||
totalValue: { fontSize: 16, fontWeight: 'bold' },
|
totalValue: { fontSize: 14, fontWeight: 'bold', color: '#000000' },
|
||||||
|
|
||||||
configSection: {
|
hostingBox: {
|
||||||
marginTop: 20,
|
marginTop: 24,
|
||||||
padding: 20,
|
padding: 16,
|
||||||
backgroundColor: '#f8fafc',
|
borderWidth: 1,
|
||||||
borderRadius: 8,
|
borderColor: '#eeeeee',
|
||||||
},
|
borderRadius: 12,
|
||||||
configTitle: {
|
flexDirection: 'row',
|
||||||
fontSize: 10,
|
justifyContent: 'space-between',
|
||||||
fontWeight: 'bold',
|
alignItems: 'center',
|
||||||
marginBottom: 10,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
color: '#475569',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
configGrid: {
|
configGrid: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 20,
|
gap: 24,
|
||||||
},
|
},
|
||||||
configItem: {
|
configItem: {
|
||||||
width: '45%',
|
width: '30%',
|
||||||
marginBottom: 10,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
configLabel: { fontSize: 8, color: '#94a3b8', marginBottom: 2 },
|
configLabel: { fontSize: 7, color: '#999999', marginBottom: 4, textTransform: 'uppercase', fontWeight: 'bold' },
|
||||||
configValue: { fontSize: 9, color: '#1e293b' },
|
configValue: { fontSize: 8, color: '#000000', fontWeight: 'bold' },
|
||||||
|
|
||||||
qrSection: {
|
colorGrid: {
|
||||||
marginTop: 30,
|
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',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
qrImage: {
|
qrImage: {
|
||||||
width: 80,
|
width: 60,
|
||||||
height: 80,
|
height: 60,
|
||||||
},
|
},
|
||||||
qrText: {
|
qrText: {
|
||||||
fontSize: 7,
|
fontSize: 6,
|
||||||
color: '#94a3b8',
|
color: '#999999',
|
||||||
marginTop: 5,
|
marginTop: 8,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
|
||||||
footer: {
|
footer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 30,
|
bottom: 48,
|
||||||
left: 40,
|
left: 48,
|
||||||
right: 40,
|
right: 48,
|
||||||
borderTop: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#f1f5f9',
|
borderTopColor: '#f1f5f9',
|
||||||
paddingTop: 20,
|
paddingTop: 24,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
footerBrand: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: -1,
|
||||||
|
color: '#000000',
|
||||||
|
textTransform: 'lowercase',
|
||||||
|
},
|
||||||
|
footerRight: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
footerContact: {
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 1,
|
||||||
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
pageNumber: {
|
pageNumber: {
|
||||||
position: 'absolute',
|
fontSize: 7,
|
||||||
bottom: 30,
|
color: '#cbd5e1',
|
||||||
right: 40,
|
fontWeight: 'bold',
|
||||||
fontSize: 8,
|
|
||||||
color: '#94a3b8',
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,408 +294,252 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
});
|
});
|
||||||
|
|
||||||
const vibeLabels: Record<string, string> = {
|
const positions = calculatePositions(state, pricing);
|
||||||
minimal: 'Minimalistisch',
|
|
||||||
bold: 'Mutig & Laut',
|
|
||||||
nature: 'Natürlich',
|
|
||||||
tech: 'Technisch'
|
|
||||||
};
|
|
||||||
|
|
||||||
const deadlineLabels: Record<string, string> = {
|
|
||||||
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<string, string> = {
|
|
||||||
logo: 'Logo',
|
|
||||||
styleguide: 'Styleguide',
|
|
||||||
content_concept: 'Inhalts-Konzept',
|
|
||||||
media: 'Bild/Video-Material',
|
|
||||||
icons: 'Icons',
|
|
||||||
illustrations: 'Illustrationen',
|
|
||||||
fonts: 'Fonts'
|
|
||||||
};
|
|
||||||
|
|
||||||
const featureLabels: Record<string, string> = {
|
|
||||||
blog_news: 'Blog / News',
|
|
||||||
products: 'Produktbereich',
|
|
||||||
jobs: 'Karriere / Jobs',
|
|
||||||
refs: 'Referenzen / Cases',
|
|
||||||
events: 'Events / Termine'
|
|
||||||
};
|
|
||||||
|
|
||||||
const functionLabels: Record<string, string> = {
|
|
||||||
search: 'Suche',
|
|
||||||
filter: 'Filter-Systeme',
|
|
||||||
pdf: 'PDF-Export',
|
|
||||||
forms: 'Erweiterte Formulare'
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiLabels: Record<string, string> = {
|
|
||||||
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<string, string> = {
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<PDFDocument>
|
||||||
<Page size="A4" style={styles.page}>
|
<PDFPage size="A4" style={styles.page}>
|
||||||
<View style={styles.header}>
|
<PDFView style={styles.header}>
|
||||||
<View>
|
<PDFView style={styles.brandIconContainer}>
|
||||||
<Text style={styles.brand}>marc mintel</Text>
|
<PDFText style={styles.brandIconText}>M</PDFText>
|
||||||
<Text style={{ fontSize: 8, color: '#64748b', marginTop: 4 }}>Digital Systems & Design</Text>
|
</PDFView>
|
||||||
</View>
|
<PDFView style={styles.quoteInfo}>
|
||||||
<View style={styles.quoteInfo}>
|
<PDFText style={styles.quoteTitle}>Kostenschätzung</PDFText>
|
||||||
<Text style={styles.quoteTitle}>Kostenschätzung</Text>
|
<PDFText style={styles.quoteDate}>{date}</PDFText>
|
||||||
<Text style={styles.quoteDate}>{date}</Text>
|
<PDFText style={[styles.quoteDate, { marginTop: 4, fontWeight: 'bold', color: '#000000' }]}>
|
||||||
<Text style={[styles.quoteDate, { marginTop: 2 }]}>Projekt: {state.projectType === 'website' ? 'Website' : 'Web App'}</Text>
|
Projekt: {state.projectType === 'website' ? 'Website' : 'Web App'}
|
||||||
</View>
|
</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
|
||||||
<View style={styles.recipientSection}>
|
<PDFView style={styles.section}>
|
||||||
<Text style={styles.recipientLabel}>Ansprechpartner</Text>
|
<PDFText style={styles.sectionTitle}>Ansprechpartner</PDFText>
|
||||||
<Text style={styles.recipientName}>{state.name || 'Interessent'}</Text>
|
<PDFView style={styles.recipientGrid}>
|
||||||
{state.companyName && <Text style={styles.recipientRole}>{state.companyName}</Text>}
|
<PDFView style={styles.recipientItem}>
|
||||||
{state.role && <Text style={styles.recipientRole}>{state.role}</Text>}
|
<PDFText style={styles.recipientLabel}>Name</PDFText>
|
||||||
<Text style={styles.recipientRole}>{state.email}</Text>
|
<PDFText style={styles.recipientValue}>{state.name || 'Interessent'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
|
{state.companyName && (
|
||||||
|
<PDFView style={styles.recipientItem}>
|
||||||
|
<PDFText style={styles.recipientLabel}>Unternehmen</PDFText>
|
||||||
|
<PDFText style={styles.recipientValue}>{state.companyName}</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
)}
|
||||||
|
{state.email && (
|
||||||
|
<PDFView style={styles.recipientItem}>
|
||||||
|
<PDFText style={styles.recipientLabel}>E-Mail</PDFText>
|
||||||
|
<PDFText style={styles.recipientValue}>{state.email}</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
)}
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
|
||||||
<View style={styles.table}>
|
<PDFView style={styles.table}>
|
||||||
<View style={styles.tableHeader}>
|
<PDFView style={styles.tableHeader}>
|
||||||
<Text style={[styles.headerText, styles.colPos]}>Pos</Text>
|
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
|
||||||
<Text style={[styles.headerText, styles.colDesc]}>Beschreibung</Text>
|
<PDFText style={[styles.headerText, styles.colDesc]}>Beschreibung</PDFText>
|
||||||
<Text style={[styles.headerText, styles.colQty]}>Menge</Text>
|
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText>
|
||||||
<Text style={[styles.headerText, styles.colPrice]}>Betrag</Text>
|
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
|
|
||||||
{positions.map((item, i) => (
|
{positions.map((item, i) => (
|
||||||
<View key={i} style={styles.tableRow}>
|
<PDFView key={i} style={styles.tableRow}>
|
||||||
<Text style={[styles.posText, styles.colPos]}>{item.pos}</Text>
|
<PDFText style={[styles.posText, styles.colPos]}>{item.pos.toString().padStart(2, '0')}</PDFText>
|
||||||
<View style={styles.colDesc}>
|
<PDFView style={styles.colDesc}>
|
||||||
<Text style={styles.itemTitle}>{item.title}</Text>
|
<PDFText style={styles.itemTitle}>{item.title}</PDFText>
|
||||||
<Text style={styles.itemDesc}>{item.desc}</Text>
|
<PDFText style={styles.itemDesc}>{item.desc}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<Text style={[styles.posText, styles.colQty]}>{item.qty}</Text>
|
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText>
|
||||||
<Text style={[styles.priceText, styles.colPrice]}>
|
<PDFText style={[styles.priceText, styles.colPrice]}>
|
||||||
{item.price > 0 ? `${item.price.toLocaleString()} €` : 'n. A.'}
|
{item.price > 0 ? `${item.price.toLocaleString()} €` : 'n. A.'}
|
||||||
</Text>
|
</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
))}
|
))}
|
||||||
</View>
|
</PDFView>
|
||||||
|
|
||||||
<View style={styles.summarySection}>
|
<PDFView style={styles.summaryContainer}>
|
||||||
<View style={styles.summaryTable}>
|
<PDFView style={styles.summaryCard}>
|
||||||
<View style={styles.summaryRow}>
|
<PDFView style={styles.summaryRow}>
|
||||||
<Text style={{ color: '#64748b' }}>Zwischensumme (Netto)</Text>
|
<PDFText style={styles.summaryLabel}>Zwischensumme (Netto)</PDFText>
|
||||||
<Text>{totalPrice.toLocaleString()} €</Text>
|
<PDFText style={styles.summaryValue}>{totalPrice.toLocaleString()} €</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.summaryRow}>
|
<PDFView style={styles.totalRow}>
|
||||||
<Text style={{ color: '#64748b' }}>Umsatzsteuer (0%)*</Text>
|
<PDFText style={styles.totalLabel}>Gesamtsumme</PDFText>
|
||||||
<Text>0,00 €</Text>
|
<PDFText style={styles.totalValue}>{totalPrice.toLocaleString()} €</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.totalRow}>
|
</PDFView>
|
||||||
<Text style={styles.totalLabel}>Gesamtsumme</Text>
|
</PDFView>
|
||||||
<Text style={styles.totalValue}>{totalPrice.toLocaleString()} €</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={{ fontSize: 7, color: '#94a3b8', textAlign: 'right', marginTop: -4 }}>
|
|
||||||
*Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.
|
|
||||||
</Text>
|
|
||||||
{state.projectType === 'website' && (
|
|
||||||
<View style={[styles.summaryRow, { marginTop: 15, borderTop: 1, borderTopColor: '#f1f5f9', paddingTop: 10 }]}>
|
|
||||||
<Text style={{ color: '#64748b', fontSize: 9 }}>Betrieb & Hosting</Text>
|
|
||||||
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{monthlyPrice.toLocaleString()} € / Monat</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.footer}>
|
{state.projectType === 'website' && (
|
||||||
<Text>marc@mintel.me</Text>
|
<PDFView style={styles.hostingBox}>
|
||||||
<Text>mintel.me</Text>
|
<PDFText style={{ color: '#666666', fontSize: 8, fontWeight: 'bold', textTransform: 'uppercase' }}>Betrieb & Hosting</PDFText>
|
||||||
<Text>Digital Systems & Design</Text>
|
<PDFText style={{ fontSize: 10, fontWeight: 'bold', color: '#000000' }}>{monthlyPrice.toLocaleString()} € / Monat</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<Text style={styles.pageNumber} render={({ pageNumber, totalPages }) => `Seite ${pageNumber} von ${totalPages}`} fixed />
|
)}
|
||||||
</Page>
|
|
||||||
|
|
||||||
<Page size="A4" style={styles.page}>
|
<PDFView style={styles.footer}>
|
||||||
<View style={styles.header}>
|
<PDFText style={styles.footerBrand}>marc mintel</PDFText>
|
||||||
<View>
|
<PDFView style={styles.footerRight}>
|
||||||
<Text style={styles.brand}>marc mintel</Text>
|
<PDFText style={styles.footerContact}>marc@mintel.me</PDFText>
|
||||||
</View>
|
<PDFText style={styles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />
|
||||||
<View style={styles.quoteInfo}>
|
</PDFView>
|
||||||
<Text style={styles.quoteTitle}>Projektdetails</Text>
|
</PDFView>
|
||||||
</View>
|
</PDFPage>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.configSection}>
|
<PDFPage size="A4" style={styles.page}>
|
||||||
<Text style={styles.configTitle}>Konfiguration & Wünsche</Text>
|
<PDFView style={styles.header}>
|
||||||
<View style={styles.configGrid}>
|
<PDFView style={styles.brandIconContainer}>
|
||||||
|
<PDFText style={styles.brandIconText}>M</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
<PDFView style={styles.quoteInfo}>
|
||||||
|
<PDFText style={styles.quoteTitle}>Projektdetails</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
|
||||||
|
<PDFView style={styles.section}>
|
||||||
|
<PDFText style={styles.sectionTitle}>Konfiguration & Wünsche</PDFText>
|
||||||
|
<PDFView style={styles.configGrid}>
|
||||||
{state.projectType === 'website' ? (
|
{state.projectType === 'website' ? (
|
||||||
<>
|
<>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Thema</Text>
|
<PDFText style={styles.configLabel}>Thema</PDFText>
|
||||||
<Text style={styles.configValue}>{state.websiteTopic || 'Nicht angegeben'}</Text>
|
<PDFText style={styles.configValue}>{state.websiteTopic || 'Nicht angegeben'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Design-Vibe</Text>
|
<PDFText style={styles.configLabel}>Design-Vibe</PDFText>
|
||||||
<Text style={styles.configValue}>{vibeLabels[state.designVibe] || state.designVibe}</Text>
|
<PDFText style={styles.configValue}>{VIBE_LABELS[state.designVibe] || state.designVibe}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={[styles.configItem, { width: '100%' }]}>
|
||||||
<Text style={styles.configLabel}>Farbschema</Text>
|
<PDFText style={styles.configLabel}>Farbschema</PDFText>
|
||||||
<Text style={styles.configValue}>{state.colorScheme.join(', ')}</Text>
|
<PDFView style={styles.colorGrid}>
|
||||||
</View>
|
{state.colorScheme.map((color: string, i: number) => (
|
||||||
|
<PDFView key={i} style={{ alignItems: 'center' }}>
|
||||||
|
<PDFView style={[styles.colorSwatch, { backgroundColor: color }]} />
|
||||||
|
<PDFText style={styles.colorHex}>{color.toUpperCase()}</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
))}
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Zielgruppe</Text>
|
<PDFText style={styles.configLabel}>Zielgruppe</PDFText>
|
||||||
<Text style={styles.configValue}>{state.targetAudience === 'internal' ? 'Internes Tool' : 'Kunden-Portal'}</Text>
|
<PDFText style={styles.configValue}>{state.targetAudience === 'internal' ? 'Internes Tool' : 'Kunden-Portal'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Plattform</Text>
|
<PDFText style={styles.configLabel}>Plattform</PDFText>
|
||||||
<Text style={styles.configValue}>{state.platformType.toUpperCase()}</Text>
|
<PDFText style={styles.configValue}>{state.platformType.toUpperCase()}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Sicherheit</Text>
|
<PDFText style={styles.configLabel}>Sicherheit</PDFText>
|
||||||
<Text style={styles.configValue}>{state.dataSensitivity === 'high' ? 'Sensibel' : 'Standard'}</Text>
|
<PDFText style={styles.configValue}>{state.dataSensitivity === 'high' ? 'Sensibel' : 'Standard'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Rollen</Text>
|
<PDFText style={styles.configLabel}>Rollen</PDFText>
|
||||||
<Text style={styles.configValue}>{state.userRoles.join(', ') || 'Keine'}</Text>
|
<PDFText style={styles.configValue}>{state.userRoles.join(', ') || 'Keine'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Mitarbeiter</Text>
|
<PDFText style={styles.configLabel}>Mitarbeiter</PDFText>
|
||||||
<Text style={styles.configValue}>{state.employeeCount || 'Nicht angegeben'}</Text>
|
<PDFText style={styles.configValue}>{state.employeeCount || 'Nicht angegeben'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Bestehende Website</Text>
|
<PDFText style={styles.configLabel}>Bestehende Website</PDFText>
|
||||||
<Text style={styles.configValue}>{state.existingWebsite || 'Keine'}</Text>
|
<PDFText style={styles.configValue}>{state.existingWebsite || 'Keine'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Bestehende Domain</Text>
|
<PDFText style={styles.configLabel}>Bestehende Domain</PDFText>
|
||||||
<Text style={styles.configValue}>{state.existingDomain || 'Keine'}</Text>
|
<PDFText style={styles.configValue}>{state.existingDomain || 'Keine'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Wunsch-Domain</Text>
|
<PDFText style={styles.configLabel}>Wunsch-Domain</PDFText>
|
||||||
<Text style={styles.configValue}>{state.wishedDomain || 'Keine'}</Text>
|
<PDFText style={styles.configValue}>{state.wishedDomain || 'Keine'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Zeitplan</Text>
|
<PDFText style={styles.configLabel}>Zeitplan</PDFText>
|
||||||
<Text style={styles.configValue}>{deadlineLabels[state.deadline] || state.deadline}</Text>
|
<PDFText style={styles.configValue}>{DEADLINE_LABELS[state.deadline] || state.deadline}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Assets vorhanden</Text>
|
<PDFText style={styles.configLabel}>Assets vorhanden</PDFText>
|
||||||
<Text style={styles.configValue}>{state.assets.map((a: string) => assetLabels[a] || a).join(', ') || 'Keine angegeben'}</Text>
|
<PDFText style={styles.configValue}>{state.assets.map((a: string) => ASSET_LABELS[a] || a).join(', ') || 'Keine angegeben'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
{state.otherAssets.length > 0 && (
|
{state.otherAssets.length > 0 && (
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Weitere Assets</Text>
|
<PDFText style={styles.configLabel}>Weitere Assets</PDFText>
|
||||||
<Text style={styles.configValue}>{state.otherAssets.join(', ')}</Text>
|
<PDFText style={styles.configValue}>{state.otherAssets.join(', ')}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
)}
|
)}
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Sprachen</Text>
|
<PDFText style={styles.configLabel}>Sprachen</PDFText>
|
||||||
<Text style={styles.configValue}>{state.languagesCount} ({state.languagesList.join(', ')})</Text>
|
<PDFText style={styles.configValue}>{state.languagesList.length} ({state.languagesList.join(', ')})</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
{state.projectType === 'website' && (
|
{state.projectType === 'website' && (
|
||||||
<>
|
<>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>CMS (Inhaltsverwaltung)</Text>
|
<PDFText style={styles.configLabel}>CMS (Inhaltsverwaltung)</PDFText>
|
||||||
<Text style={styles.configValue}>{state.cmsSetup ? 'Ja' : 'Nein'}</Text>
|
<PDFText style={styles.configValue}>{state.cmsSetup ? 'Ja' : 'Nein'}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
<View style={styles.configItem}>
|
<PDFView style={styles.configItem}>
|
||||||
<Text style={styles.configLabel}>Änderungsfrequenz</Text>
|
<PDFText style={styles.configLabel}>Änderungsfrequenz</PDFText>
|
||||||
<Text style={styles.configValue}>
|
<PDFText style={styles.configValue}>
|
||||||
{state.expectedAdjustments === 'low' ? 'Selten' :
|
{state.expectedAdjustments === 'low' ? 'Selten' :
|
||||||
state.expectedAdjustments === 'medium' ? 'Regelmäßig' : 'Häufig'}
|
state.expectedAdjustments === 'medium' ? 'Regelmäßig' : 'Häufig'}
|
||||||
</Text>
|
</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</PDFView>
|
||||||
|
|
||||||
{state.socialMedia.length > 0 && (
|
{state.socialMedia.length > 0 && (
|
||||||
<View style={{ marginTop: 15 }}>
|
<PDFView style={{ marginTop: 24, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#eeeeee' }}>
|
||||||
<Text style={styles.configLabel}>Social Media Accounts</Text>
|
<PDFText style={styles.configLabel}>Social Media Accounts</PDFText>
|
||||||
{state.socialMedia.map((id: string) => (
|
{state.socialMedia.map((id: string) => (
|
||||||
<Text key={id} style={[styles.configValue, { lineHeight: 1.4 }]}>
|
<PDFText key={id} style={[styles.configValue, { lineHeight: 1.6, color: '#666666', fontWeight: 'normal' }]}>
|
||||||
{socialLabels[id] || id}: {state.socialMediaUrls[id] || 'Keine URL angegeben'}
|
<PDFText style={{ color: '#000000', fontWeight: 'bold' }}>{SOCIAL_LABELS[id] || id}:</PDFText> {state.socialMediaUrls[id] || 'Keine URL angegeben'}
|
||||||
</Text>
|
</PDFText>
|
||||||
))}
|
))}
|
||||||
</View>
|
</PDFView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.designWishes && (
|
{state.designWishes && (
|
||||||
<View style={{ marginTop: 15 }}>
|
<PDFView style={{ marginTop: 24, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#eeeeee' }}>
|
||||||
<Text style={styles.configLabel}>Design-Vorstellungen</Text>
|
<PDFText style={styles.configLabel}>Design-Vorstellungen</PDFText>
|
||||||
<Text style={[styles.configValue, { lineHeight: 1.4 }]}>{state.designWishes}</Text>
|
<PDFText style={[styles.configValue, { lineHeight: 1.6, fontWeight: 'normal', color: '#666666' }]}>{state.designWishes}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.references.length > 0 && (
|
{state.references.length > 0 && (
|
||||||
<View style={{ marginTop: 15 }}>
|
<PDFView style={{ marginTop: 24, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#eeeeee' }}>
|
||||||
<Text style={styles.configLabel}>Referenzen</Text>
|
<PDFText style={styles.configLabel}>Referenzen</PDFText>
|
||||||
<Text style={[styles.configValue, { lineHeight: 1.4 }]}>{state.references.join('\n')}</Text>
|
<PDFText style={[styles.configValue, { lineHeight: 1.6, fontWeight: 'normal', color: '#666666' }]}>{state.references.join('\n')}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.message && (
|
{state.message && (
|
||||||
<View style={{ marginTop: 15 }}>
|
<PDFView style={{ marginTop: 24, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#eeeeee' }}>
|
||||||
<Text style={styles.configLabel}>Nachricht / Anmerkungen</Text>
|
<PDFText style={styles.configLabel}>Nachricht / Anmerkungen</PDFText>
|
||||||
<Text style={[styles.configValue, { lineHeight: 1.4 }]}>{state.message}</Text>
|
<PDFText style={[styles.configValue, { lineHeight: 1.6, fontWeight: 'normal', color: '#666666' }]}>{state.message}</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
)}
|
)}
|
||||||
</View>
|
</PDFView>
|
||||||
|
|
||||||
{qrCodeData && (
|
{qrCodeData && (
|
||||||
<View style={styles.qrSection}>
|
<PDFView style={styles.qrContainer}>
|
||||||
<Image src={qrCodeData} style={styles.qrImage} />
|
<PDFImage src={qrCodeData} style={styles.qrImage} />
|
||||||
<Text style={styles.qrText}>QR-Code scannen, um Konfiguration online zu öffnen</Text>
|
<PDFText style={styles.qrText}>Online öffnen</PDFText>
|
||||||
</View>
|
</PDFView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<PDFView style={styles.footer}>
|
||||||
<Text>marc@mintel.me</Text>
|
<PDFText style={styles.footerBrand}>marc mintel</PDFText>
|
||||||
<Text>mintel.me</Text>
|
<PDFView style={styles.footerRight}>
|
||||||
<Text>Digital Systems & Design</Text>
|
<PDFText style={styles.footerContact}>marc@mintel.me</PDFText>
|
||||||
</View>
|
<PDFText style={styles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />
|
||||||
<Text style={styles.pageNumber} render={({ pageNumber, totalPages }) => `Seite ${pageNumber} von ${totalPages}`} fixed />
|
</PDFView>
|
||||||
</Page>
|
</PDFView>
|
||||||
</Document>
|
</PDFPage>
|
||||||
|
</PDFDocument>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user