refactor: estimation generation
All checks were successful
Build & Deploy / 🔍 Prepare Environment (push) Successful in 32s
Build & Deploy / 🏗️ Build (push) Successful in 4m41s
Build & Deploy / 🧪 QA (push) Successful in 5m56s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🔔 Notifications (push) Successful in 1s
Build & Deploy / ⚡ PageSpeed (push) Successful in 55s

This commit is contained in:
2026-02-09 19:11:02 +01:00
parent f4ba861b4d
commit e6809a6d64
17 changed files with 3788 additions and 1805 deletions

View File

@@ -1,24 +1,80 @@
'use client';
"use client";
import * as React from 'react';
import { Document as PDFDocument } from '@react-pdf/renderer';
import { EstimationPDF } from './EstimationPDF';
import { AgbsPDF } from './AgbsPDF';
import * as React from "react";
import { Document as PDFDocument } from "@react-pdf/renderer";
import { EstimationPDF } from "./EstimationPDF";
import { AgbsPDF } from "./AgbsPDF";
import { ClosingModule } from "./pdf/modules/CommonModules";
import { SimpleLayout } from "./pdf/SimpleLayout";
interface CombinedProps {
estimationProps: any;
showAgbs?: boolean;
techDetails?: any[];
principles?: any[];
estimationProps: any;
showAgbs?: boolean;
techDetails?: any[];
principles?: any[];
maintenanceDetails?: any[];
standardsDetails?: any[];
}
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true, techDetails, principles, mode = 'full' }: CombinedProps & { mode?: 'estimation' | 'full' }) => {
return (
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}>
<EstimationPDF {...estimationProps} mode={mode} techDetails={techDetails} principles={principles} />
{showAgbs && (
<AgbsPDF mode={mode} state={estimationProps.state} headerIcon={estimationProps.headerIcon} footerLogo={estimationProps.footerLogo} />
)}
</PDFDocument>
);
export const CombinedQuotePDF = ({
estimationProps,
showAgbs = true,
techDetails,
principles,
maintenanceDetails,
standardsDetails,
mode = "full",
}: CombinedProps & { mode?: "estimation" | "full" }) => {
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065",
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65",
};
const layoutProps = {
date,
icon: estimationProps.headerIcon,
footerLogo: estimationProps.footerLogo,
companyData,
bankData,
};
return (
<PDFDocument
title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}
>
<EstimationPDF
{...estimationProps}
mode={mode}
techDetails={techDetails}
principles={principles}
maintenanceDetails={maintenanceDetails}
standardsDetails={standardsDetails}
/>
{showAgbs && (
<AgbsPDF
mode={mode}
state={estimationProps.state}
headerIcon={estimationProps.headerIcon}
footerLogo={estimationProps.footerLogo}
/>
)}
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>
);
};

View File

@@ -1,16 +1,19 @@
'use client';
"use client";
import * as React from 'react';
import { FormState, Totals } from '../types';
import { PRICING } from '../constants';
import { AnimatedNumber } from './AnimatedNumber';
import { ConceptPrice, ConceptAutomation } from '../../Landing/ConceptIllustrations';
import { Info, Download, Share2, RefreshCw } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import dynamic from 'next/dynamic';
import * as React from "react";
import { FormState, Totals } from "../types";
import { PRICING } from "../constants";
import { AnimatedNumber } from "./AnimatedNumber";
import {
ConceptPrice,
ConceptAutomation,
} from "../../Landing/ConceptIllustrations";
import { Info, Download, Share2, RefreshCw } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import dynamic from "next/dynamic";
// EstimationPDF will be imported dynamically where used or inside the and client-side block
import IconWhite from '../../../assets/logo/Icon White Transparent.png';
import LogoBlack from '../../../assets/logo/Logo Black Transparent.png';
import IconWhite from "../../../assets/logo/Icon White Transparent.png";
import LogoBlack from "../../../assets/logo/Logo Black Transparent.png";
// PDF components removed from top-level dynamic import to fix ESM resolution issues in Next.js 16/Webpack
@@ -27,9 +30,17 @@ export function PriceCalculation({
totals,
isClient,
qrCodeData,
onShare
onShare,
}: PriceCalculationProps) {
const { totalPrice, monthlyPrice, totalPagesCount, totalFeatures, totalFunctions, totalApis, languagesCount } = totals;
const {
totalPrice,
monthlyPrice,
totalPagesCount,
totalFeatures,
totalFunctions,
totalApis,
languagesCount,
} = totals;
const totalPages = totalPagesCount;
const [pdfLoading, setPdfLoading] = React.useState(false);
@@ -40,35 +51,41 @@ export function PriceCalculation({
setPdfLoading(true);
try {
const { EstimationPDF } = await import('../../EstimationPDF');
const doc = <EstimationPDF
state={state}
totalPrice={totalPrice}
monthlyPrice={monthlyPrice}
totalPagesCount={totalPagesCount}
pricing={PRICING}
headerIcon={typeof IconWhite === 'string' ? IconWhite : (IconWhite as any).src}
footerLogo={typeof LogoBlack === 'string' ? LogoBlack : (LogoBlack as any).src}
/>;
const { EstimationPDF } = await import("../../EstimationPDF");
const doc = (
<EstimationPDF
state={state}
totalPrice={totalPrice}
monthlyPrice={monthlyPrice}
totalPagesCount={totalPagesCount}
pricing={PRICING}
headerIcon={
typeof IconWhite === "string" ? IconWhite : (IconWhite as any).src
}
footerLogo={
typeof LogoBlack === "string" ? LogoBlack : (LogoBlack as any).src
}
/>
);
const { pdf } = await import('@react-pdf/renderer');
const { pdf } = await import("@react-pdf/renderer");
// Minimum loading time of 2 seconds for better UX
const [blob] = await Promise.all([
pdf(doc).toBlob(),
new Promise(resolve => setTimeout(resolve, 2000))
new Promise((resolve) => setTimeout(resolve, 2000)),
]);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = `kalkulation-${state.name.toLowerCase().replace(/\s+/g, '-') || 'projekt'}.pdf`;
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);
console.error("PDF generation failed:", error);
} finally {
setPdfLoading(false);
}
@@ -78,20 +95,93 @@ export function PriceCalculation({
<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="space-y-6">
{state.projectType === 'website' ? (
{state.projectType === "website" ? (
<>
<div className="space-y-4 overflow-y-auto pr-2 hide-scrollbar max-h-[120px]">
{totalPages > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalPages}x Seite</span><span className="font-medium text-slate-900">{(totalPages * PRICING.PAGE).toLocaleString()} </span></div>)}
{totalFeatures > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalFeatures}x System-Modul</span><span className="font-medium text-slate-900">{(totalFeatures * PRICING.FEATURE).toLocaleString()} </span></div>)}
{totalFunctions > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalFunctions}x Logik-Funktion</span><span className="font-medium text-slate-900">{(totalFunctions * PRICING.FUNCTION).toLocaleString()} </span></div>)}
{totalApis > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalApis}x API Sync</span><span className="font-medium text-slate-900">{(totalApis * PRICING.API_INTEGRATION).toLocaleString()} </span></div>)}
{state.cmsSetup && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">CMS Setup & Anbindung</span><span className="font-medium text-slate-900">{(PRICING.CMS_SETUP + totalFeatures * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} </span></div>)}
{state.newDatasets > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.newDatasets}x Inhalte einpflegen</span><span className="font-medium text-slate-900">{(state.newDatasets * PRICING.NEW_DATASET).toLocaleString()} </span></div>)}
{languagesCount > 1 && (<div className="flex justify-between items-center text-sm text-slate-900 font-bold pt-2 border-t border-slate-100"><span className="text-slate-500">Mehrsprachigkeit ({languagesCount}x)</span><span>+{(totalPrice - (totalPrice / (1 + (languagesCount - 1) * 0.2))).toLocaleString()} </span></div>)}
{totalPages > 0 && (
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500">{totalPages}x Seite</span>
<span className="font-medium text-slate-900">
{(totalPages * PRICING.PAGE).toLocaleString()}
</span>
</div>
)}
{totalFeatures > 0 && (
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500">
{totalFeatures}x System-Modul
</span>
<span className="font-medium text-slate-900">
{(totalFeatures * PRICING.FEATURE).toLocaleString()}
</span>
</div>
)}
{totalFunctions > 0 && (
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500">
{totalFunctions}x Logik-Funktion
</span>
<span className="font-medium text-slate-900">
{(totalFunctions * PRICING.FUNCTION).toLocaleString()}
</span>
</div>
)}
{totalApis > 0 && (
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500">
{totalApis}x API Sync
</span>
<span className="font-medium text-slate-900">
{(totalApis * PRICING.API_INTEGRATION).toLocaleString()}
</span>
</div>
)}
{state.cmsSetup && (
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500">Inhalts-Verwaltung</span>
<span className="font-medium text-slate-900">
{(
Math.max(1, totalFeatures) *
PRICING.CMS_CONNECTION_PER_FEATURE
).toLocaleString()}{" "}
</span>
</div>
)}
{state.newDatasets > 0 && (
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500">
{state.newDatasets}x Inhalte einpflegen
</span>
<span className="font-medium text-slate-900">
{(
state.newDatasets * PRICING.NEW_DATASET
).toLocaleString()}{" "}
</span>
</div>
)}
{languagesCount > 1 && (
<div className="flex justify-between items-center text-sm text-slate-900 font-bold pt-2 border-t border-slate-100">
<span className="text-slate-500">
Mehrsprachigkeit ({languagesCount}x)
</span>
<span>
+
{(
totalPrice -
totalPrice / (1 + (languagesCount - 1) * 0.2)
).toLocaleString()}{" "}
</span>
</div>
)}
</div>
<div className="pt-4 space-y-2">
<div className="flex justify-between items-end">
<span className="text-lg font-bold text-slate-900">Gesamt</span>
<span className="text-lg font-bold text-slate-900">
Gesamt
</span>
<div className="text-right">
<div className="text-2xl font-bold tracking-tighter text-slate-900">
<AnimatedNumber value={totalPrice} />
@@ -101,8 +191,12 @@ export function PriceCalculation({
</div>
<div className="pt-4 border-t border-slate-200 space-y-4">
<div className="flex justify-between items-center">
<span className="text-slate-600 font-medium text-sm">Sorglos-Paket</span>
<span className="text-base font-bold text-slate-900">{monthlyPrice.toLocaleString()} / Monat</span>
<span className="text-slate-600 font-medium text-sm">
Sorglos Betrieb (Hosting + Support)
</span>
<span className="text-base font-bold text-slate-900">
{(monthlyPrice * 12).toLocaleString()} / Jahr
</span>
</div>
</div>
@@ -169,8 +263,15 @@ export function PriceCalculation({
<ConceptAutomation className="w-10 h-10 text-black" />
</div>
<div className="space-y-1">
<p className="text-slate-600 text-xs leading-relaxed">Web Apps werden nach Aufwand abgerechnet.</p>
<p className="text-2xl font-bold text-slate-900">{PRICING.APP_HOURLY} <span className="text-base text-slate-400 font-normal">/ Std.</span></p>
<p className="text-slate-600 text-xs leading-relaxed">
Web Apps werden nach Aufwand abgerechnet.
</p>
<p className="text-2xl font-bold text-slate-900">
{PRICING.APP_HOURLY} {" "}
<span className="text-base text-slate-400 font-normal">
/ Std.
</span>
</p>
</div>
</div>
{onShare && (
@@ -186,7 +287,10 @@ export function PriceCalculation({
</div>
)}
</div>
<p className="text-[10px] leading-relaxed text-slate-400 italic text-center">Ein verbindliches Angebot erstelle ich nach einem persönlichen Gespräch.</p>
<p className="text-[10px] leading-relaxed text-slate-400 italic text-center">
Ein verbindliches Angebot erstelle ich nach einem persönlichen
Gespräch.
</p>
</div>
</div>
);

View File

@@ -1,19 +1,24 @@
'use client';
"use client";
import * as React from 'react';
import { calculatePositions } from '../logic/pricing';
import { Page as PDFPage } from '@react-pdf/renderer';
import { pdfStyles } from './pdf/SharedUI';
import { DINLayout } from './pdf/DINLayout';
import { SimpleLayout } from './pdf/SimpleLayout';
import * as React from "react";
import { calculatePositions } from "../logic/pricing";
import { Page as PDFPage } from "@react-pdf/renderer";
import { pdfStyles } from "./pdf/SharedUI";
import { DINLayout } from "./pdf/DINLayout";
import { SimpleLayout } from "./pdf/SimpleLayout";
// Modules
import { FrontPageModule } from './pdf/modules/FrontPageModule';
import { BriefingModule } from './pdf/modules/BriefingModule';
import { SitemapModule } from './pdf/modules/SitemapModule';
import { EstimationModule } from './pdf/modules/EstimationModule';
import { TransparenzModule, techPageModule as TechPageModule, PrinciplesModule } from './pdf/modules/CommonModules';
import { AboutModule, CrossSellModule } from './pdf/modules/BrandingModules';
import { FrontPageModule } from "./pdf/modules/FrontPageModule";
import { BriefingModule } from "./pdf/modules/BriefingModule";
import { SitemapModule } from "./pdf/modules/SitemapModule";
import { EstimationModule } from "./pdf/modules/EstimationModule";
import {
TransparenzModule,
techPageModule as TechPageModule,
MaintenanceModule,
StandardsModule,
} from "./pdf/modules/CommonModules";
import { AboutModule, CrossSellModule } from "./pdf/modules/BrandingModules";
interface PDFProps {
state: any;
@@ -21,28 +26,32 @@ interface PDFProps {
monthlyPrice: number;
totalPagesCount: number;
pricing: any;
mode?: 'estimation' | 'full';
mode?: "estimation" | "full";
headerIcon?: string;
footerLogo?: string;
techDetails?: { t: string, d: string }[];
principles?: { t: string, d: string }[];
techDetails?: { t: string; d: string }[];
principles?: { t: string; d: string }[];
maintenanceDetails?: { t: string; d: string }[];
standardsDetails?: { t: string; d: string }[];
}
export const EstimationPDF = ({
state,
totalPrice,
pricing,
mode = 'full',
mode = "full",
headerIcon,
footerLogo,
techDetails,
principles,
maintenanceDetails,
standardsDetails,
...props
}: PDFProps) => {
const date = new Date().toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const positions = calculatePositions(state, pricing);
@@ -51,13 +60,13 @@ export const EstimationPDF = ({
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065"
ustId: "DE367588065",
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65"
iban: "DE50 1001 1001 2620 4328 65",
};
const commonProps = {
@@ -66,20 +75,25 @@ export const EstimationPDF = ({
icon: headerIcon,
footerLogo,
companyData,
bankData
bankData,
};
if (mode === 'estimation') {
if (mode === "estimation") {
return (
<DINLayout {...commonProps} showAddress={true} showFooterDetails={true}>
<EstimationModule state={state} positions={positions} totalPrice={totalPrice} date={date} />
<EstimationModule
state={state}
positions={positions}
totalPrice={totalPrice}
date={date}
/>
</DINLayout>
);
}
// Full Portfolio Mode
let pageCounter = 1;
const getPageNum = () => (pageCounter++).toString().padStart(2, '0');
const getPageNum = () => (pageCounter++).toString().padStart(2, "0");
return (
<>
@@ -98,22 +112,36 @@ export const EstimationPDF = ({
)}
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<EstimationModule state={state} positions={positions} totalPrice={totalPrice} date={date} />
<EstimationModule
state={state}
positions={positions}
totalPrice={totalPrice}
date={date}
/>
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TransparenzModule pricing={pricing} />
</SimpleLayout>
{standardsDetails && standardsDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<StandardsModule
standardsDetails={standardsDetails}
principles={principles}
/>
</SimpleLayout>
)}
{techDetails && techDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TechPageModule techDetails={techDetails} headerIcon={headerIcon} />
</SimpleLayout>
)}
{principles && principles.length > 0 && (
{maintenanceDetails && maintenanceDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<PrinciplesModule principles={principles} />
<MaintenanceModule maintenanceDetails={maintenanceDetails} />
</SimpleLayout>
)}

File diff suppressed because it is too large Load Diff

View File

@@ -1,108 +1,218 @@
'use client';
"use client";
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { IndustrialListItem, IndustrialCard, Divider, COLORS, FONT_SIZES } from '../SharedUI';
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import {
DocumentTitle,
IndustrialListItem,
IndustrialCard,
Divider,
COLORS,
FONT_SIZES,
} from "../SharedUI";
const styles = StyleSheet.create({
industrialTitle: { fontSize: FONT_SIZES.H1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 6, letterSpacing: -1 },
industrialSubtitle: { fontSize: FONT_SIZES.SUB, fontWeight: 'bold', color: COLORS.TEXT_LIGHT, marginBottom: 16, letterSpacing: 0.5 },
industrialTextLead: { fontSize: FONT_SIZES.H3, color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 16 },
industrialText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 12 },
industrialGrid2: { flexDirection: 'row' },
industrialCol: { width: '46%' },
industrialBulletBox: {
width: 6,
height: 6,
backgroundColor: COLORS.DIVIDER,
marginRight: 8,
marginTop: 5,
},
industrialTextLead: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.4,
marginBottom: 16,
},
industrialText: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
marginBottom: 12,
},
industrialGrid2: { flexDirection: "row" },
industrialCol: { width: "46%" },
});
export const AboutModule = () => (
<>
<PDFText style={styles.industrialTitle}>Expertise & Profil</PDFText>
<PDFText style={styles.industrialSubtitle}>Entwicklung & Technischer Partner für den Mittelstand</PDFText>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<>
<DocumentTitle
title="Expertise & Profil"
subLines={["Entwicklung & Technischer Partner für den Mittelstand"]}
isHero={true}
/>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={{ marginTop: 24 }}>
<PDFText style={styles.industrialTextLead}>
Begleitung mittelständischer Unternehmen und Agenturen bei der Realisierung anspruchsvoller Web-Projekte. Als Senior Software Developer mit über 15 Jahren Erfahrung wird das gesamte technische Spektrum abgedeckt von der Architektur bis zum fertigen Produkt.
</PDFText>
<PDFView style={{ marginTop: 24 }}>
<PDFText style={styles.industrialTextLead}>
Begleitung mittelständischer Unternehmen und Agenturen bei der
Realisierung anspruchsvoller Web-Projekte. Als Senior Software Developer
mit over 15 Jahren Erfahrung wird das gesamte technische Spektrum
abgedeckt von der Architektur bis zum fertigen Produkt.
</PDFText>
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
<PDFView style={[styles.industrialCol, { marginRight: '8%' }]}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Erfahrung & Substanz</PDFText>
<PDFText style={styles.industrialText}>
Der Werdegang umfasst alle Ebenen der Webentwicklung: von der Teamleitung in Kreativagenturen bis zur Softwareentwicklung für internationale Konzerne.
</PDFText>
<PDFText style={styles.industrialText}>
Die Kenntnis komplexer Enterprise-Systeme wird mit der Agilität kombiniert, die im Mittelstand gefordert ist. Dieses Wissen ermöglicht den Bau von Lösungen, die technologisch auf Augenhöhe mit Konzern-Standards sind, jedoch ohne unnötigen bürokratischen Overhead auskommen.
</PDFText>
</PDFView>
<PDFView style={styles.industrialCol}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Fokus Einzelentwicklung</PDFText>
<PDFText style={styles.industrialText}>
Die Umsetzung erfolgt bewusst als spezialisierter Einzelentwickler. Dies garantiert maximale Geschwindigkeit, direkte Kommunikationswege und volle technologische Verantwortung.
</PDFText>
<PDFText style={styles.industrialText}>
Als direkter technischer Sparringspartner bleibt die Codebasis von der ersten bis zur letzten Zeile transparent und wartbar. Diese Unmittelbarkeit stellt sicher, dass Ergebnisse sowohl technisch sauber als auch wirtschaftlich sinnvoll realisiert werden.
</PDFText>
</PDFView>
</PDFView>
<PDFView style={{ marginTop: 32, paddingVertical: 16, borderTopWidth: 1, borderTopColor: COLORS.GRID }}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Infrastruktur & Souveränität</PDFText>
<PDFText style={styles.industrialText}>
Es wird keine instabile Prototyp-Software geliefert, sondern produktionsreife Systeme, die technisch skalierbar bleiben. Die Codebasis folgt modernen Standards bei wachsenden Ansprüchen oder dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos übernommen und weitergeführt werden.
</PDFText>
</PDFView>
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
]}
>
Erfahrung & Substanz
</PDFText>
<PDFText style={styles.industrialText}>
Der Werdegang umfasst alle Ebenen der Webentwicklung: von der
Teamleitung in Kreativagenturen bis zur Softwareentwicklung für
internationale Konzerne.
</PDFText>
<PDFText style={styles.industrialText}>
Die Kenntnis komplexer Enterprise-Systeme wird mit der Agilität
kombiniert, die im Mittelstand gefordert ist. Dieses Wissen
ermöglicht den Bau von Lösungen, die technologisch auf Augenhöhe mit
Konzern-Standards sind, jedoch ohne unnötigen bürokratischen
Overhead auskommen.
</PDFText>
</PDFView>
</>
<PDFView style={styles.industrialCol}>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
]}
>
Fokus Einzelentwicklung
</PDFText>
<PDFText style={styles.industrialText}>
Die Umsetzung erfolgt bewusst als spezialisierter Einzelentwickler.
Dies garantiert maximale Geschwindigkeit, direkte Kommunikationswege
und volle technologische Verantwortung.
</PDFText>
<PDFText style={styles.industrialText}>
Als direkter technischer Sparringspartner bleibt die Codebasis von
der ersten bis zur letzten Zeile transparent und wartbar. Diese
Unmittelbarkeit stellt sicher, dass Ergebnisse sowohl technisch
sauber als auch wirtschaftlich sinnvoll realisiert werden.
</PDFText>
</PDFView>
</PDFView>
<PDFView
style={{
marginTop: 32,
paddingVertical: 16,
borderTopWidth: 1,
borderTopColor: COLORS.GRID,
}}
>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 4 },
]}
>
Infrastruktur & Souveränität
</PDFText>
<PDFText style={styles.industrialText}>
Es wird keine instabile Prototyp-Software geliefert, sondern
produktionsreife Systeme, die technisch skalierbar bleiben. Die
Codebasis folgt modernen Standards bei wachsenden Ansprüchen oder
dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos
übernommen und weitergeführt werden.
</PDFText>
</PDFView>
</PDFView>
</>
);
export const CrossSellModule = ({ state }: any) => {
const isWebsite = state.projectType === 'website';
const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
const subtitle = isWebsite ? "Automatisierung und Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse";
const isWebsite = state.projectType === "website";
const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
const subtitle = isWebsite
? "Automatisierung und Prozessoptimierung"
: "Technische Infrastruktur ohne Kompromisse";
return (
<>
<PDFText style={styles.industrialTitle}>{title}</PDFText>
<PDFText style={styles.industrialSubtitle}>{subtitle}</PDFText>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 16 }]} >
{isWebsite ? (
<>
<PDFView style={[styles.industrialCol, { marginRight: '8%' }]}>
<PDFText style={styles.industrialTextLead}>Über die klassische Webpräsenz hinaus werden maßgeschneiderte Lösungen zur Automatisierung von Routine-Prozessen angeboten. Dies ermöglicht eine signifikante Effizienzsteigerung im Tagesgeschäft.</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Keine Abos. Keine komplexen neuen Systeme. Gezielte Zeitersparnis.</PDFText>
<PDFView style={{ marginTop: 24, padding: 16, backgroundColor: '#f8fafc', borderLeftWidth: 2, borderLeftColor: COLORS.GRID }}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Individuelle Analyse</PDFText>
<PDFText style={styles.industrialText}>Spezifische Prozesse werden auf technisches Automatisierungspotenzial untersucht. Das Ergebnis liefert Klarheit über die wirtschaftliche Sinnhaftigkeit einer Umsetzung.</PDFText>
</PDFView>
</PDFView>
<PDFView style={styles.industrialCol}>
<IndustrialCard title="DOKUMENT-AUTOMATION">
<PDFText style={styles.industrialText}>Erstellung von PDF-Angeboten, Berichten oder Protokollen in Sekunden statt Stunden.</PDFText>
</IndustrialCard>
<IndustrialCard title="EXCEL-LOGIK">
<PDFText style={styles.industrialText}>Intelligente Tabellen und automatisierte Auswertungen bestehender Datensätze.</PDFText>
</IndustrialCard>
<IndustrialCard title="KI-ASSISTENZ">
<PDFText style={styles.industrialText}>Effiziente Verarbeitung von analogen Dokumenten oder handschriftlichen Notizen mittels KI.</PDFText>
</IndustrialCard>
</PDFView>
</>
) : (
<PDFView style={{ width: '100%' }}>
<PDFText style={styles.industrialTextLead}>Bereitstellung einer stabilen technischen Basis ohne Abhängigkeiten von Baukasten-Systemen oder Agenturen.</PDFText>
<PDFText style={styles.industrialText}>Entwicklung performanter Frontends und skalierbarer Backends. Die Auslieferung erfolgt als kontrollierbarer und nachhaltiger Quellcode.</PDFText>
</PDFView>
)}
return (
<>
<DocumentTitle title={title} subLines={[subtitle]} isHero={true} />
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 16 }]}>
{isWebsite ? (
<>
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFText style={styles.industrialTextLead}>
Über die klassische Webpräsenz hinaus werden maßgeschneiderte
Lösungen zur Automatisierung von Routine-Prozessen angeboten.
Dies ermöglicht eine signifikante Effizienzsteigerung im
Tagesgeschäft.
</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: "bold" }]}>
Keine Abos. Keine komplexen neuen Systeme. Gezielte
Zeitersparnis.
</PDFText>
<PDFView
style={{
marginTop: 24,
padding: 16,
backgroundColor: "#f8fafc",
borderLeftWidth: 2,
borderLeftColor: COLORS.GRID,
}}
>
<PDFText
style={[
styles.industrialText,
{
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 4,
},
]}
>
Individuelle Analyse
</PDFText>
<PDFText style={styles.industrialText}>
Spezifische Prozesse werden auf technisches
Automatisierungspotenzial untersucht. Das Ergebnis liefert
Klarheit über die wirtschaftliche Sinnhaftigkeit einer
Umsetzung.
</PDFText>
</PDFView>
</PDFView>
</>
);
<PDFView style={styles.industrialCol}>
<IndustrialCard title="DOKUMENT-AUTOMATION">
<PDFText style={styles.industrialText}>
Erstellung von PDF-Angeboten, Berichten oder Protokollen in
Sekunden statt Stunden.
</PDFText>
</IndustrialCard>
<IndustrialCard title="EXCEL-LOGIK">
<PDFText style={styles.industrialText}>
Intelligente Tabellen und automatisierte Auswertungen
bestehender Datensätze.
</PDFText>
</IndustrialCard>
<IndustrialCard title="KI-ASSISTENZ">
<PDFText style={styles.industrialText}>
Effiziente Verarbeitung von analogen Dokumenten oder
handschriftlichen Notizen mittels KI.
</PDFText>
</IndustrialCard>
</PDFView>
</>
) : (
<PDFView style={{ width: "100%" }}>
<PDFText style={styles.industrialTextLead}>
Bereitstellung einer stabilen technischen Basis ohne
Abhängigkeiten von Baukasten-Systemen oder Agenturen.
</PDFText>
<PDFText style={styles.industrialText}>
Entwicklung performanter Frontends und skalierbarer Backends. Die
Auslieferung erfolgt als kontrollierbarer und nachhaltiger
Quellcode.
</PDFText>
</PDFView>
)}
</PDFView>
</>
);
};

View File

@@ -1,29 +1,69 @@
'use client';
"use client";
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { DocumentTitle, COLORS, FONT_SIZES } from '../SharedUI';
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({
section: { marginBottom: 24 },
sectionTitle: { fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', marginBottom: 8, color: COLORS.CHARCOAL },
visionText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.8, textAlign: 'justify' },
section: { marginBottom: 24 },
sectionTitle: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
marginBottom: 8,
color: COLORS.CHARCOAL,
},
visionText: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.4,
textAlign: "justify",
},
});
export const BriefingModule = ({ state }: any) => (
<>
<DocumentTitle title="Projektdetails" />
{state.briefingSummary && (
<PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText>
<PDFText style={{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.6, textAlign: 'justify' }}>{state.briefingSummary}</PDFText>
</PDFView>
)}
{state.designVision && (
<PDFView style={[styles.section, { padding: 12, borderLeftWidth: 2, borderLeftColor: COLORS.DIVIDER, backgroundColor: COLORS.GRID }]}>
<PDFText style={[styles.sectionTitle, { color: COLORS.CHARCOAL, marginBottom: 4 }]}>Strategische Vision</PDFText>
<PDFText style={[styles.visionText, { lineHeight: 1.6 }]}>{state.designVision}</PDFText>
</PDFView>
)}
</>
<>
<DocumentTitle title="Projektdetails" isHero={true} />
{state.briefingSummary && (
<PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText>
<PDFText
style={{
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.6,
textAlign: "justify",
}}
>
{state.briefingSummary}
</PDFText>
</PDFView>
)}
{state.designVision && (
<PDFView
style={[
styles.section,
{
padding: 12,
borderLeftWidth: 2,
borderLeftColor: COLORS.DIVIDER,
backgroundColor: COLORS.GRID,
},
]}
>
<PDFText
style={[
styles.sectionTitle,
{ color: COLORS.CHARCOAL, marginBottom: 4 },
]}
>
Strategische Vision
</PDFText>
<PDFText style={styles.visionText}>{state.designVision}</PDFText>
</PDFView>
)}
</>
);

View File

@@ -1,106 +1,443 @@
'use client';
"use client";
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from '@react-pdf/renderer';
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
Image as PDFImage,
} from "@react-pdf/renderer";
import {
DocumentTitle,
Divider,
COLORS,
FONT_SIZES,
TechnicalSpec,
AsymmetryView,
} from "../SharedUI";
const styles = StyleSheet.create({
section: { marginBottom: 16 },
pricingGrid: { marginTop: 12 },
pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 12, alignItems: 'flex-start' },
pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, paddingRight: 15 },
pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.5, paddingRight: 10 },
pricingTag: { width: '15%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', textAlign: 'right', color: COLORS.CHARCOAL },
configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 8 },
section: { marginBottom: 24 },
moduleLabel: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
color: COLORS.CHARCOAL,
letterSpacing: 0.5,
marginBottom: 4,
},
moduleDesc: {
fontSize: FONT_SIZES.SMALL,
color: COLORS.TEXT_DIM,
lineHeight: 1.5,
},
ledgerRow: {
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
flexDirection: "row",
alignItems: "flex-start",
},
ledgerPrice: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.CHARCOAL,
},
ledgerUnit: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginLeft: 2,
},
});
export const techPageModule = ({ techDetails, headerIcon }: any) => (
export const techPageModule = ({ techDetails }: any) => {
// Focus on the first 3 items as "Featured Specs", the rest as high-density grid
const featured = techDetails?.slice(0, 3) || [];
const rest = techDetails?.slice(3) || [];
return (
<>
<DocumentTitle title="Technische Umsetzung" />
<PDFView style={styles.section}>
<PDFView style={styles.pricingGrid}>
{techDetails?.map((item: any, i: number) => (
<PDFView key={i} style={styles.pricingRow}>
<PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText>
<PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText>
</PDFView>
<DocumentTitle title="Technische Umsetzung" isHero={true} />
<PDFView style={styles.section}>
{/* FEATURED SPECS - Editorial focus */}
{featured.map((item: any, i: number) => (
<PDFView key={i} style={{ marginBottom: 24 }}>
<PDFText
style={[
styles.moduleLabel,
{ color: COLORS.BLUEPRINT, fontSize: FONT_SIZES.TINY },
]}
>
FOKUS_{i + 1}
</PDFText>
<PDFText
style={[
styles.moduleLabel,
{ fontSize: FONT_SIZES.HEADING, marginBottom: 8 },
]}
>
{item.t}
</PDFText>
<PDFText
style={[
styles.moduleDesc,
{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN },
]}
>
{item.d}
</PDFText>
</PDFView>
))}
<Divider style={{ marginVertical: 24, backgroundColor: COLORS.GRID }} />
{/* TECHNICAL GRID - High density */}
<PDFView style={{ flexDirection: "row", flexWrap: "wrap", gap: 12 }}>
{rest.map((item: any, i: number) => (
<PDFView
key={i}
style={{
width: "31%",
padding: 10,
backgroundColor: "#fdfdfd",
borderBottomWidth: 1,
borderBottomColor: COLORS.BLUEPRINT,
}}
>
<PDFText
style={[
styles.moduleLabel,
{
fontSize: FONT_SIZES.TINY,
marginBottom: 4,
color: COLORS.TEXT_LIGHT,
},
]}
>
{item.t.toUpperCase()}
</PDFText>
<PDFText
style={[styles.moduleDesc, { fontSize: FONT_SIZES.TINY }]}
>
{item.d}
</PDFText>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);
};
export const MaintenanceModule = ({ maintenanceDetails }: any) => (
<>
<DocumentTitle
title="Monitoring, Security & techn. Support"
isHero={true}
/>
<PDFView style={styles.section}>
<PDFView style={{ flexDirection: "row", flexWrap: "wrap", gap: 16 }}>
{maintenanceDetails?.map((item: any, i: number) => (
<PDFView
key={i}
style={{
width: "48%",
padding: 16,
backgroundColor: COLORS.GRID,
borderLeftWidth: 2,
borderLeftColor: COLORS.DIVIDER,
}}
>
<PDFText
style={[
styles.moduleLabel,
{
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginBottom: 8,
},
]}
>
{item.t.toUpperCase()}
</PDFText>
<PDFText
style={[
styles.moduleDesc,
{
fontSize: FONT_SIZES.SMALL,
color: COLORS.CHARCOAL,
lineHeight: 1.4,
},
]}
>
{item.d}
</PDFText>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);
export const StandardsModule = ({ standardsDetails, principles }: any) => {
const independence = standardsDetails?.find(
(s: any) => s.t === "Unabhängigkeit",
);
const others =
standardsDetails?.filter((s: any) => s.t !== "Unabhängigkeit") || [];
return (
<>
<DocumentTitle title="Meine Standards" isHero={true} />
<PDFView style={styles.section}>
{/* FOCUS: UNABHÄNGIGKEIT & PRINCIPLES */}
<AsymmetryView
left={
<PDFView>
<PDFText
style={[
styles.moduleLabel,
{
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginBottom: 12,
},
]}
>
MODERNE PRINZIPIEN
</PDFText>
{principles?.map((p: any, i: number) => (
<PDFView key={i} style={{ marginBottom: 12 }}>
<PDFText
style={[styles.moduleLabel, { fontSize: FONT_SIZES.TINY }]}
>
{p.t.toUpperCase()}
</PDFText>
<PDFText
style={[
styles.moduleDesc,
{ fontSize: FONT_SIZES.TINY, opacity: 0.8 },
]}
>
{p.d}
</PDFText>
</PDFView>
))}
</PDFView>
}
right={
<PDFView>
{/* HERO BOX: UNABHÄNGIGKEIT */}
{independence && (
<PDFView
style={{
backgroundColor: COLORS.CHARCOAL,
padding: 24,
marginBottom: 32,
borderLeftWidth: 4,
borderLeftColor: COLORS.TEXT_LIGHT,
}}
>
<PDFText
style={{
fontSize: FONT_SIZES.HEADING,
fontWeight: "bold",
color: COLORS.WHITE,
letterSpacing: 1,
marginBottom: 12,
}}
>
{independence.t.toUpperCase()}
</PDFText>
<PDFText
style={{
fontSize: FONT_SIZES.BODY,
color: COLORS.WHITE,
lineHeight: 1.4,
opacity: 0.9,
}}
>
{independence.d}
</PDFText>
</PDFView>
)}
{/* VERTICAL STACK OF OTHER STANDARDS */}
<PDFView>
{others.map((item: any, i: number) => (
<PDFView
key={i}
style={{
marginBottom: 16,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
paddingBottom: 12,
}}
>
<PDFText style={styles.moduleLabel}>{item.t}</PDFText>
<PDFText style={styles.moduleDesc}>{item.d}</PDFText>
</PDFView>
))}
</PDFView>
</PDFView>
</PDFView>
}
/>
</PDFView>
</>
);
);
};
export const TransparenzModule = ({ pricing }: any) => (
export const TransparenzModule = ({ pricing }: any) => {
const sorglosPrice = (pricing.HOSTING_MONTHLY || 250) * 12;
return (
<>
<DocumentTitle title="Preis-Transparenz & Modell" />
<PDFView style={styles.section}>
<PDFView style={styles.pricingGrid}>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Fundament</PDFText>
<PDFText style={styles.pricingDesc}>Setup, Infrastruktur, Hosting, SEO-Basics, Staging & Live-Umgebungen.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.BASE_WEBSITE?.toLocaleString('de-DE')} </PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Seiten</PDFText>
<PDFText style={styles.pricingDesc}>Layout & Umsetzung individueller Seiten. Responsive Design / Cross-Browser.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.PAGE?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Features</PDFText>
<PDFText style={styles.pricingDesc}>Abgeschlossene Systeme (z. B. Blog, Jobs, Produkte) inkl. Datenstruktur.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.FEATURE?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Funktionen</PDFText>
<PDFText style={styles.pricingDesc}>Logik-Einheiten wie Filter, Suchen oder Kontakt-Schnittstellen.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.FUNCTION?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Schnittstellen</PDFText>
<PDFText style={styles.pricingDesc}>Anbindung externer Systeme (CRM, ERP, Payment) zur Synchronisation.</PDFText>
<PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>CMS Setup</PDFText>
<PDFText style={styles.pricingDesc}>Konfiguration Headless CMS zur unabhängigen Datenpflege aller Module.</PDFText>
<PDFText style={pricing.CMS_SETUP ? styles.pricingTag : [styles.pricingTag, { color: COLORS.TEXT_LIGHT }]}>{pricing.CMS_SETUP?.toLocaleString('de-DE')} </PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Inszenierung</PDFText>
<PDFText style={styles.pricingDesc}>Interaktions-Mechanismen, Konfiguratoren oder visuelles Storytelling.</PDFText>
<PDFText style={styles.pricingTag}>ab {pricing.VISUAL_STAGING?.toLocaleString('de-DE')} </PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Sprachen</PDFText>
<PDFText style={styles.pricingDesc}>Skalierung der System-Architektur auf zusätzliche Sprachversionen.</PDFText>
<PDFText style={styles.pricingTag}>+20% / Sprache</PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Initial-Pflege</PDFText>
<PDFText style={styles.pricingDesc}>Manuelle Aufbereitung & Übernahme von Datensätzen in das Zielsystem.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.NEW_DATASET?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>Sorglos-Paket</PDFText>
<PDFText style={styles.pricingDesc}>Betrieb, Hosting, Updates & Monitoring gemäß AGB Punkt 7a.</PDFText>
<PDFText style={styles.pricingTag}>Inklusive 1 Jahr</PDFText>
</PDFView>
<DocumentTitle title="Preis-Transparenz & Modell" isHero={true} />
<PDFView style={styles.section}>
<PDFView style={{ borderTopWidth: 1, borderTopColor: COLORS.CHARCOAL }}>
{[
{
l: "Fundament",
d: "Bereitstellung der techn. Infrastruktur & System-Umgebung.",
p: pricing.BASE_WEBSITE,
},
{
l: "Einzelseiten",
d: "Individuelle Gestaltung, Layout & responsive Struktur.",
p: pricing.PAGE,
unit: "/ Stk",
},
{
l: "Core Features",
d: "Geschlossene Datensysteme mit eigener Datenstruktur.",
p: pricing.FEATURE,
unit: "/ Stk",
},
{
l: "Logik & Funktionen",
d: "Interaktive Funktions-Bausteine & Prozess-Logik.",
p: pricing.FUNCTION,
unit: "/ Stk",
},
{
l: "Schnittstellen",
d: "Synchronisation mit externen Zielsystemen.",
p: pricing.API_INTEGRATION,
unit: "/ Stk",
},
{
l: "Inhalts-Verwaltung",
d: "Schnittstelle zur eigenständigen Daten-Pflege (optional).",
p: pricing.CMS_CONNECTION_PER_FEATURE,
unit: "/ Stk",
},
{
l: "Sprachversionen",
d: "Skalierung der System-Architektur auf Zweit-Sprachen.",
p: "+20%",
isLang: true,
},
{
l: "Initial-Pflege",
d: "Konvertierung & Aufbereitung von Bestandsdaten.",
p: pricing.NEW_DATASET,
unit: "/ Stk",
},
{
l: "Sorglos Betrieb",
d: "Hosting, Instandhaltung, Security & techn. Support.",
p: sorglosPrice,
unit: "/ Jahr",
},
].map((item: any, i: number) => (
<PDFView key={i} style={styles.ledgerRow}>
<PDFView style={{ width: "25%" }}>
<PDFText style={styles.moduleLabel}>
{item.l.toUpperCase()}
</PDFText>
</PDFView>
<PDFView style={{ width: "50%" }}>
<PDFText style={styles.moduleDesc}>{item.d}</PDFText>
</PDFView>
<PDFView style={{ width: "25%", alignItems: "flex-end" }}>
<PDFText style={styles.ledgerPrice}>
{typeof item.p === "number"
? `${item.p.toLocaleString("de-DE")}`
: item.p}
{item.unit && (
<PDFText style={styles.ledgerUnit}> {item.unit}</PDFText>
)}
</PDFText>
{item.sub && (
<PDFText
style={[
styles.moduleDesc,
{
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginTop: 2,
},
]}
>
{item.sub}
</PDFText>
)}
</PDFView>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);
);
};
export const PrinciplesModule = ({ principles }: any) => (
<>
<DocumentTitle title="Prinzipien & Standards" />
<PDFView style={[styles.pricingGrid, { marginTop: 8 }]}>
{principles?.map((item: any, i: number) => (
<PDFView key={i} style={styles.pricingRow}>
<PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText>
<PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText>
</PDFView>
))}
export const ClosingModule = () => (
<>
<DocumentTitle title="Abschluss & Kontakt" isHero={true} />
<PDFView style={styles.section}>
<PDFText
style={[
styles.moduleLabel,
{ fontSize: FONT_SIZES.HEADING, marginBottom: 12 },
]}
>
Vielen Dank für Ihr Interesse!
</PDFText>
<PDFText style={styles.moduleDesc}>
Die aufgeführten Positionen stellen eine detaillierte Schätzung auf
Basis unseres aktuellen Stands dar. Sollten sich Anforderungen ändern
oder Sie Fragen zu einzelnen Details haben, lassen Sie uns die
Positionen gerne gemeinsam besprechen.
</PDFText>
<PDFView
style={{
marginTop: 24,
padding: 16,
backgroundColor: COLORS.GRID,
borderLeftWidth: 2,
borderLeftColor: COLORS.DIVIDER,
}}
>
<PDFText style={[styles.moduleLabel, { marginBottom: 8 }]}>
Haben Sie Fragen?
</PDFText>
<PDFText style={styles.moduleDesc}>
Ich erkläre Ihnen gerne noch einmal persönlich, was die technische
Umsetzung für Ihr Projekt bedeutet und wie wir die nächsten Schritte
gemeinsam gehen können.
</PDFText>
<PDFView style={{ marginTop: 16 }}>
<PDFText style={styles.moduleLabel}>Kontakt:</PDFText>
<PDFText
style={[
styles.moduleDesc,
{ color: COLORS.CHARCOAL, fontWeight: "bold" },
]}
>
Marc Mintel marc@mintel.me
</PDFText>
</PDFView>
</>
</PDFView>
</PDFView>
</>
);

View File

@@ -1,55 +1,159 @@
'use client';
"use client";
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { DocumentTitle } from '../SharedUI';
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({
table: { marginTop: 12 },
tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#334155', marginBottom: 12 },
tableRow: { flexDirection: 'row', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f8fafc', alignItems: 'flex-start' },
colPos: { width: '8%' },
colDesc: { width: '62%' },
colQty: { width: '10%', textAlign: 'center' },
colPrice: { width: '20%', textAlign: 'right' },
headerText: { fontSize: 7, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 },
posText: { fontSize: 8, color: '#999999' },
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4 },
itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 },
priceText: { fontSize: 10, fontWeight: 'bold' },
summaryContainer: { borderTopWidth: 1, borderTopColor: '#334155', paddingTop: 8 },
summaryRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingVertical: 4, alignItems: 'baseline' },
summaryLabel: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, fontWeight: 'bold', marginRight: 12 },
summaryValue: { fontSize: 9, fontWeight: 'bold', width: 100, textAlign: 'right' },
totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#334155', alignItems: 'baseline' },
table: { marginTop: 12 },
tableHeader: {
flexDirection: "row",
paddingBottom: 8,
borderBottomWidth: 1,
borderBottomColor: COLORS.CHARCOAL,
marginBottom: 12,
},
tableRow: {
flexDirection: "row",
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
alignItems: "flex-start",
},
colPos: { width: "8%" },
colDesc: { width: "62%" },
colQty: { width: "10%", textAlign: "center" },
colPrice: { width: "20%", textAlign: "right" },
headerText: {
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
textTransform: "uppercase",
letterSpacing: 1,
color: COLORS.TEXT_DIM,
},
posText: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_LIGHT },
itemTitle: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 4,
},
itemDesc: {
fontSize: FONT_SIZES.SMALL,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
},
priceText: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.CHARCOAL,
},
summaryContainer: {
borderTopWidth: 1,
borderTopColor: COLORS.CHARCOAL,
paddingTop: 8,
},
summaryRow: {
flexDirection: "row",
justifyContent: "flex-end",
paddingVertical: 4,
alignItems: "baseline",
},
summaryLabel: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_DIM,
textTransform: "uppercase",
letterSpacing: 1,
fontWeight: "bold",
marginRight: 12,
},
summaryValue: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
width: 100,
textAlign: "right",
color: COLORS.CHARCOAL,
},
totalRow: {
flexDirection: "row",
justifyContent: "flex-end",
paddingTop: 12,
marginTop: 8,
borderTopWidth: 2,
borderTopColor: COLORS.CHARCOAL,
alignItems: "baseline",
},
});
export const EstimationModule = ({ state, positions, totalPrice, date }: any) => (
<>
<DocumentTitle title="Kostenschätzung" subLines={[`Datum: ${date}`, `Projekt: ${state.projectType === 'website' ? 'Website' : 'Web App'}`]} />
<PDFView style={styles.table}>
<PDFView style={styles.tableHeader}>
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
<PDFText style={[styles.headerText, styles.colDesc]}>Beschreibung</PDFText>
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText>
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText>
</PDFView>
{positions.map((item: any, i: number) => (
<PDFView key={i} style={styles.tableRow} wrap={false}>
<PDFText style={[styles.posText, styles.colPos]}>{item.pos.toString().padStart(2, '0')}</PDFText>
<PDFView style={styles.colDesc}>
<PDFText style={styles.itemTitle}>{item.title}</PDFText>
<PDFText style={styles.itemDesc}>{state.positionDescriptions?.[item.title] || item.desc}</PDFText>
</PDFView>
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText>
<PDFText style={[styles.priceText, styles.colPrice]}>{item.price > 0 ? `${item.price.toLocaleString('de-DE')}` : 'n. A.'}</PDFText>
</PDFView>
))}
export const EstimationModule = ({
state,
positions,
totalPrice,
date,
}: any) => (
<>
<DocumentTitle
title="Kostenschätzung"
subLines={[
`Datum: ${date}`,
`Projekt: ${state.projectType === "website" ? "Website" : "Web App"}`,
]}
isHero={true}
/>
<PDFView style={styles.table}>
<PDFView style={styles.tableHeader}>
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
<PDFText style={[styles.headerText, styles.colDesc]}>
Beschreibung
</PDFText>
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText>
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText>
</PDFView>
{positions.map((item: any, i: number) => (
<PDFView key={i} style={styles.tableRow} wrap={false}>
<PDFText style={[styles.posText, styles.colPos]}>
{item.pos.toString().padStart(2, "0")}
</PDFText>
<PDFView style={styles.colDesc}>
<PDFText style={styles.itemTitle}>{item.title}</PDFText>
<PDFText style={styles.itemDesc}>
{state.positionDescriptions?.[item.title] || item.desc}
</PDFText>
</PDFView>
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText>
<PDFText style={[styles.priceText, styles.colPrice]}>
{item.price > 0
? `${item.price.toLocaleString("de-DE")}`
: "n. A."}
</PDFText>
</PDFView>
<PDFView style={styles.summaryContainer} wrap={false}>
<PDFView style={styles.summaryRow}><PDFText style={styles.summaryLabel}>Nettobetrag</PDFText><PDFText style={styles.summaryValue}>{totalPrice.toLocaleString('de-DE')} </PDFText></PDFView>
<PDFView style={styles.summaryRow}><PDFText style={styles.summaryLabel}>Umsatzsteuer (19%)</PDFText><PDFText style={styles.summaryValue}>{(totalPrice * 0.19).toLocaleString('de-DE')} </PDFText></PDFView>
<PDFView style={styles.totalRow}><PDFText style={styles.summaryLabel}>Gesamtbetrag (Brutto)</PDFText><PDFText style={[styles.summaryValue, { fontSize: 14 }]}>{(totalPrice * 1.19).toLocaleString('de-DE')} </PDFText></PDFView>
</PDFView>
</>
))}
</PDFView>
<PDFView style={styles.summaryContainer} wrap={false}>
<PDFView style={styles.summaryRow}>
<PDFText style={styles.summaryLabel}>Nettobetrag</PDFText>
<PDFText style={styles.summaryValue}>
{totalPrice.toLocaleString("de-DE")}
</PDFText>
</PDFView>
<PDFView style={styles.summaryRow}>
<PDFText style={styles.summaryLabel}>Umsatzsteuer (19%)</PDFText>
<PDFText style={styles.summaryValue}>
{(totalPrice * 0.19).toLocaleString("de-DE")}
</PDFText>
</PDFView>
<PDFView style={styles.totalRow}>
<PDFText style={styles.summaryLabel}>Gesamtbetrag (Brutto)</PDFText>
<PDFText
style={[styles.summaryValue, { fontSize: FONT_SIZES.HEADING }]}
>
{(totalPrice * 1.19).toLocaleString("de-DE")}
</PDFText>
</PDFView>
</PDFView>
</>
);

View File

@@ -1,81 +1,92 @@
'use client';
"use client";
import * as React from 'react';
import { View as PDFView, Text as PDFText, Image as PDFImage, StyleSheet } from '@react-pdf/renderer';
import { COLORS, FONT_SIZES } from '../SharedUI';
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
Image as PDFImage,
StyleSheet,
} from "@react-pdf/renderer";
import { COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({
titlePage: {
flex: 1, // Fill the whole page
padding: 60,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: COLORS.WHITE,
},
titleBrandIcon: {
width: 80,
height: 80,
backgroundColor: COLORS.CHARCOAL,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 40,
},
brandIconText: {
fontSize: 40,
color: COLORS.WHITE,
fontWeight: 'bold'
},
titleProjectName: {
fontSize: FONT_SIZES.H1,
fontWeight: 'bold',
color: COLORS.CHARCOAL,
marginBottom: 16,
textAlign: 'center',
maxWidth: '85%',
lineHeight: 1.2,
},
titleCustomerName: {
fontSize: FONT_SIZES.H3,
color: COLORS.TEXT_DIM,
marginBottom: 40,
textAlign: 'center',
maxWidth: '80%',
},
titleDocumentType: {
fontSize: FONT_SIZES.BODY + 1, // ~10
color: COLORS.TEXT_LIGHT,
textTransform: 'uppercase',
letterSpacing: 4,
marginBottom: 12,
},
titleDivider: {
width: 40,
height: 2,
backgroundColor: COLORS.CHARCOAL,
marginBottom: 40,
},
titleDate: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_LIGHT,
marginTop: 40,
},
titlePage: {
flex: 1, // Fill the whole page
padding: 60,
justifyContent: "center",
alignItems: "center",
backgroundColor: COLORS.WHITE,
},
titleBrandIcon: {
width: 80,
height: 80,
backgroundColor: COLORS.CHARCOAL,
borderRadius: 16,
alignItems: "center",
justifyContent: "center",
marginBottom: 40,
},
brandIconText: {
fontSize: 40,
color: COLORS.WHITE,
fontWeight: "bold",
},
titleProjectName: {
fontSize: FONT_SIZES.HERO,
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 16,
textAlign: "center",
maxWidth: "85%",
lineHeight: 1.2,
},
titleCustomerName: {
fontSize: FONT_SIZES.HEADING,
color: COLORS.TEXT_DIM,
marginBottom: 40,
textAlign: "center",
maxWidth: "80%",
},
titleDocumentType: {
fontSize: FONT_SIZES.BODY + 1, // ~10
color: COLORS.TEXT_LIGHT,
textTransform: "uppercase",
letterSpacing: 4,
marginBottom: 12,
},
titleDivider: {
width: 40,
height: 2,
backgroundColor: COLORS.CHARCOAL,
marginBottom: 40,
},
titleDate: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_LIGHT,
marginTop: 40,
},
});
export const FrontPageModule = ({ state, headerIcon, date }: any) => {
const fullTitle = `Digitale Webpräsenz für\n${state.companyName || "Ihr Projekt"}`;
const fullTitle = `Digitale Webpräsenz für\n${state.companyName || "Ihr Projekt"}`;
// Responsive font size based on length
const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22;
// Responsive font size based on length
const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22;
return (
<PDFView style={styles.titlePage}>
<PDFView style={styles.titleBrandIcon}>
{headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>}
</PDFView>
<PDFText style={[styles.titleProjectName, { fontSize }]}>{fullTitle}</PDFText>
<PDFView style={{ marginBottom: 40 }} />
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
</PDFView>
);
return (
<PDFView style={styles.titlePage}>
<PDFView style={styles.titleBrandIcon}>
{headerIcon ? (
<PDFImage src={headerIcon} style={{ width: 40, height: 40 }} />
) : (
<PDFText style={styles.brandIconText}>M</PDFText>
)}
</PDFView>
<PDFText style={[styles.titleProjectName, { fontSize }]}>
{fullTitle}
</PDFText>
<PDFView style={{ marginBottom: 40 }} />
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
</PDFView>
);
};

View File

@@ -1,77 +1,125 @@
'use client';
"use client";
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({
section: { marginBottom: 32 },
intro: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 24, textAlign: 'justify' },
sitemapTree: { marginTop: 8 },
rootNode: {
padding: 12,
backgroundColor: COLORS.GRID,
marginBottom: 20,
borderLeftWidth: 2,
borderLeftColor: COLORS.CHARCOAL
},
rootTitle: { fontSize: FONT_SIZES.H3, fontWeight: 'bold', color: COLORS.CHARCOAL, letterSpacing: 0.5 },
categorySection: { marginBottom: 20 },
categoryHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: COLORS.BLUEPRINT,
marginBottom: 10
},
categoryIcon: { width: 8, height: 8, backgroundColor: COLORS.GRID, borderInlineWidth: 1, borderColor: COLORS.DIVIDER, marginRight: 10 },
categoryTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, textTransform: 'uppercase', letterSpacing: 1 },
pagesGrid: { flexDirection: 'row', flexWrap: 'wrap' },
pageCard: {
width: '48%',
marginRight: '2%',
marginBottom: 12,
padding: 10,
borderWidth: 1,
borderColor: COLORS.GRID,
backgroundColor: '#fafafa'
},
pageTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN, marginBottom: 2 },
pageDesc: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_DIM, lineHeight: 1.4 },
section: { marginBottom: 32 },
intro: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
marginBottom: 24,
textAlign: "justify",
},
sitemapTree: { marginTop: 8 },
rootNode: {
padding: 12,
backgroundColor: COLORS.GRID,
marginBottom: 20,
borderLeftWidth: 2,
borderLeftColor: COLORS.CHARCOAL,
},
rootTitle: {
fontSize: FONT_SIZES.HEADING,
fontWeight: "bold",
color: COLORS.CHARCOAL,
letterSpacing: 0.5,
},
categorySection: { marginBottom: 20 },
categoryHeader: {
flexDirection: "row",
alignItems: "center",
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: COLORS.BLUEPRINT,
marginBottom: 10,
},
categoryIcon: {
width: 8,
height: 8,
backgroundColor: COLORS.GRID,
borderInlineWidth: 1,
borderColor: COLORS.DIVIDER,
marginRight: 10,
},
categoryTitle: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.CHARCOAL,
textTransform: "uppercase",
letterSpacing: 1,
},
pagesGrid: { flexDirection: "row", flexWrap: "wrap" },
pageCard: {
width: "48%",
marginRight: "2%",
marginBottom: 12,
padding: 10,
borderWidth: 1,
borderColor: COLORS.GRID,
backgroundColor: "#fafafa",
},
pageTitle: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.TEXT_MAIN,
marginBottom: 4,
},
pageDesc: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_DIM,
lineHeight: 1.3,
},
});
export const SitemapModule = ({ state }: any) => (
<>
<DocumentTitle title="Informationsarchitektur" />
<PDFView style={styles.section}>
<PDFText style={styles.intro}>
Die folgende Struktur definiert die logische Hierarchie und Benutzerführung. Sie dient als Bauplan für die technische Umsetzung und stellt sicher, dass alle relevanten Geschäftsbereiche intuitiv auffindbar sind.
</PDFText>
<>
<DocumentTitle title="Informationsarchitektur" isHero={true} />
<PDFView style={styles.section}>
<PDFText style={styles.intro}>
Die folgende Struktur definiert die logische Hierarchie und
Benutzerführung. Sie dient als Bauplan für die technische Umsetzung und
stellt sicher, dass alle relevanten Geschäftsbereiche intuitiv
auffindbar sind.
</PDFText>
<PDFView style={styles.sitemapTree}>
<PDFView style={styles.rootNode}>
<PDFText style={styles.rootTitle}>Seitenstruktur</PDFText>
</PDFView>
{state.sitemap?.map((cat: any, i: number) => (
<PDFView key={i} style={styles.categorySection} wrap={false}>
<PDFView style={styles.categoryHeader}>
<PDFView style={styles.categoryIcon} />
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
</PDFView>
<PDFView style={styles.pagesGrid}>
{cat.pages.map((p: any, j: number) => (
<PDFView key={j} style={[styles.pageCard, j % 2 === 1 ? { marginRight: 0 } : {}]}>
<PDFText style={styles.pageTitle}>{p.title}</PDFText>
{p.desc && <PDFText style={styles.pageDesc}>{p.desc}</PDFText>}
</PDFView>
))}
</PDFView>
</PDFView>
))}
</PDFView>
<PDFView style={styles.sitemapTree}>
<PDFView style={styles.rootNode}>
<PDFText style={styles.rootTitle}>Seitenstruktur</PDFText>
</PDFView>
</>
{state.sitemap?.map((cat: any, i: number) => (
<PDFView key={i} style={styles.categorySection} wrap={false}>
<PDFView style={styles.categoryHeader}>
<PDFView style={styles.categoryIcon} />
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
</PDFView>
<PDFView style={styles.pagesGrid}>
{cat.pages.map((p: any, j: number) => (
<PDFView
key={j}
style={[
styles.pageCard,
j % 2 === 1 ? { marginRight: 0 } : {},
]}
>
<PDFText style={styles.pageTitle}>{p.title}</PDFText>
{p.desc && (
<PDFText style={styles.pageDesc}>{p.desc}</PDFText>
)}
</PDFView>
))}
</PDFView>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);

View File

@@ -1,46 +1,161 @@
import fs from 'fs';
import path from 'path';
import fs from "fs";
import path from "path";
const DOCS_DIR = path.join(process.cwd(), 'docs');
const DOCS_DIR = path.join(process.cwd(), "docs");
export function getTechDetails() {
try {
const content = fs.readFileSync(path.join(DOCS_DIR, 'TECH.md'), 'utf-8');
const sections = content.split('⸻').map(s => s.trim());
try {
const content = fs.readFileSync(path.join(DOCS_DIR, "TECH.md"), "utf-8");
const sections = content.split("⸻").map((s) => s.trim());
// Extract items (Speed, Responsive, Stability, etc.)
// Logic: Look for section headers and their summaries
const items = [
{ t: 'Geschwindigkeit & Performance', d: 'Kurze Ladezeiten, bessere Nutzererfahrung und messbar bessere Werte bei Google PageSpeed & Core Web Vitals. Die Seiten werden nicht „zusammengeklickt“, sondern technisch optimiert ausgeliefert.' },
{ t: 'Responsives Design', d: 'Jede Website ist von Grund auf responsiv. Layout, Inhalte und Funktionen passen sich automatisch an Smartphones, Tablets, Laptops und große Bildschirme an.' },
{ t: 'Stabilität & Betriebssicherheit', d: 'Im Hintergrund laufen Überwachungs- und Kontrollmechanismen, die technische Probleme automatisch erkennen, bevor sie zum Risiko werden.' },
{ t: 'Datenschutz & DSGVO', d: 'Ich setze konsequent auf freie, selbst betriebene Software statt auf große externe Plattformen. Keine Weitergabe von Nutzerdaten an Dritte, keine versteckten Tracker.' },
{ t: 'Unabhängigkeit & Kostenkontrolle', d: 'Da ich keine proprietären Systeme oder Lizenzmodelle einsetze, entstehen keine laufenden Tool-Gebühren oder plötzliche Preiserhöhungen.' },
{ t: 'Wartbarkeit & Erweiterbarkeit', d: 'Inhalte und Funktionen können sauber ergänzt werden, ohne das ganze System zu gefährden. Das schützt Ihre Investition langfristig.' }
];
// Extract items (Speed, Responsive, Stability, etc.)
// Logic: Look for section headers and their summaries
const items = [
{
t: "Maximale Ladegeschwindigkeit & SEO-Vorsprung",
d: "Durch modernste Auslieferungstechnologien laden Ihre Seiten nahezu verzögerungsfrei. Das sorgt für ein flüssiges Nutzererlebnis, reduziert Absprungraten und sichert Ihnen eine bevorzugte Platzierung bei Google.",
},
{
t: "Investitionsschutz durch Fehlertoleranz",
d: "Eine strikte technische Prüfung des Quellcodes bereits während der Entwicklung verhindert Fehler, bevor sie entstehen. Das garantiert eine extrem stabile, sichere und über Jahre hinweg wartbare Codebasis.",
},
{
t: "Intuitive System-Kontrolle & Flexibilität",
d: "Die saubere Trennung von Design und Inhalten erlaubt es Ihnen, alle Texte und Medien flexibel und ohne technisches Vorwissen selbst zu verwalten, während das System im Hintergrund stabil und skalierbar bleibt.",
},
{
t: "Globale Performance & Skalierbarkeit",
d: "Fortgeschrittene Zwischenspeicher-Technologien garantieren, dass Ihre Website auch bei hohen Besucherzahlen stets blitzschnell reagiert und weltweit ohne Verzögerung verfügbar ist.",
},
{
t: "Infrastruktur-Souveränität & Portabilität",
d: "Dank einer standardisierten technischen Umgebung ist Ihre Website vollständig portabel. Sie sind an keinen Anbieter gebunden und behalten die volle Kontrolle über Ihre digitale Infrastruktur.",
},
{
t: "Präzision auf allen Endgeräten",
d: "Eine hocheffiziente Architektur minimiert die zu übertragenden Datenmengen und sorgt für eine perfekte, fehlerfreie Darstellung auf jedem Smartphone, Tablet oder Desktop-Rechner.",
},
];
return items;
} catch (e) {
console.error('Failed to read TECH.md', e);
return [];
}
return items;
} catch (e) {
console.error("Failed to read TECH.md", e);
return [];
}
}
export function getPrinciples() {
try {
const content = fs.readFileSync(path.join(DOCS_DIR, 'PRINCIPLES.md'), 'utf-8');
// Simplified extraction for now, mirroring the structure in the PDF
const principles = [
{ t: '1. Volle Preis-Transparenz', d: 'Alle Kosten sind offen und nachvollziehbar. Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins. Jeder Kunde sieht genau, wofür er bezahlt.' },
{ t: '2. Quellcode & Projektzugang', d: 'Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code. Damit kann jeder andere Entwickler problemlos weiterarbeiten.' },
{ t: '3. Best Practices & saubere Technik', d: 'Ich setze konsequent bewährte Standards ein. Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben.' },
{ t: '4. Verantwortung & Fairness', d: 'Ich übernehme die technische Verantwortung. Ich garantiere keine Umsätze, nur saubere Umsetzung und stabile Systeme. Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.' },
{ t: '5. Langfristiger Wert', d: 'Eine Website ist ein Investment. Ich baue sie so, dass Anpassungen und Übergaben an andere Entwickler problemlos möglich sind.' },
{ t: '6. Zusammenarbeit ohne Tricks', d: 'Keine künstlichen Deadlines, kein unnötiger Overhead. Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.' }
];
return principles;
} catch (e) {
console.error('Failed to read PRINCIPLES.md', e);
return [];
}
try {
const content = fs.readFileSync(
path.join(DOCS_DIR, "PRINCIPLES.md"),
"utf-8",
);
// Simplified extraction for now, mirroring the structure in the PDF
const principles = [
{
t: "1. Volle Preis-Transparenz",
d: "Alle Kosten sind offen und nachvollziehbar. Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins. Jeder Kunde sieht genau, wofür er bezahlt.",
},
{
t: "2. Quellcode & Projektzugang",
d: "Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code. Damit kann jeder andere Entwickler problemlos weiterarbeiten.",
},
{
t: "3. Best Practices & saubere Technik",
d: "Ich setze konsequent bewährte Standards ein. Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben.",
},
{
t: "4. Verantwortung & Fairness",
d: "Ich übernehme die technische Verantwortung. Ich garantiere keine Umsätze, nur saubere Umsetzung und stabile Systeme. Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.",
},
{
t: "5. Langfristiger Wert",
d: "Eine Website ist ein Investment. Ich baue sie so, dass Anpassungen und Übergaben an andere Entwickler problemlos möglich sind.",
},
{
t: "6. Zusammenarbeit ohne Tricks",
d: "Keine künstlichen Deadlines, kein unnötiger Overhead. Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.",
},
];
return principles;
} catch (e) {
console.error("Failed to read PRINCIPLES.md", e);
return [];
}
}
export function getMaintenanceDetails() {
try {
const content = fs.readFileSync(
path.join(DOCS_DIR, "MAINTENANCE.md"),
"utf-8",
);
// Extracting key points based on the structure of MAINTENANCE.md
const items = [
{
t: "Proaktive Betreuung",
d: "Regelmäßige Checks, Updates von Systemen und Plugins sowie Sicherheitsüberprüfungen zur Vermeidung von Ausfällen.",
},
{
t: "Schnelle Reaktion",
d: "Analyse und Behebung bei Hoster-Störungen oder unvorhergesehenen Updates von Drittanbietern ohne Eigenaufwand für Sie.",
},
{
t: "Sicherheit & Aktualität",
d: "Bestehende Technik wird auf dem neuesten Stand gehalten, kleine Fehler werden korrigiert und Inhalte bei Bedarf angepasst.",
},
{
t: "Betriebs- & Pflegeleistung",
d: "Fortlaufende Instandhaltung und Pflege der bestehenden Seite gemäß AGB Punkt 7a.",
},
{
t: "Verlässlichkeit",
d: "Persönliche Verantwortung für einen stabilen Betrieb mit dem gleichen Anspruch, mit dem die Website gebaut wurde.",
},
];
return items;
} catch (e) {
console.error("Failed to read MAINTENANCE.md", e);
return [];
}
}
export function getStandardsDetails() {
try {
const content = fs.readFileSync(
path.join(DOCS_DIR, "STANDARDS.md"),
"utf-8",
);
// Extracting key points based on the structure of STANDARDS.md
const items = [
{
t: "Geringer CO₂-Verbrauch",
d: "Schlanke Website durch optimierten Code und Bilder oft 7090% unter dem Durchschnitt vergleichbarer Projekte.",
},
{
t: "Unabhängigkeit",
d: 'Kein "Lock-in" durch Big Tech oder Baukasten-Systeme. Alles ist self-hosted oder custom-coded und bleibt dauerhaft in Ihrem Besitz für absolute Souveränität.',
},
{
t: "Besucher-Vertrauen",
d: "Keine Cookie-Banner oder heimliches Tracking. Ein seriöser Auftritt, der Datenschutz intuitiv erlebbar macht.",
},
{
t: "Security by Design",
d: "Schutz vor typischen Angriffsvektoren von Grund auf eingebaut, um teure Sicherheits-Updates oder Datenlecks zu vermeiden.",
},
{
t: "Echte DSGVO-Konformität",
d: "Keine Grauzonen oder Tricks es wird nur verarbeitet, was technisch unbedingt erforderlich ist.",
},
{
t: "Wartungsarmut",
d: "Durch minimierte Abhängigkeiten altert die Website langsamer, was Notfall-Reparaturen reduziert und Budget für Neues lässt.",
},
];
return items;
} catch (e) {
console.error("Failed to read STANDARDS.md", e);
return [];
}
}

View File

@@ -1,202 +1,224 @@
import { FormState, Position, Totals } from './types';
import { FEATURE_LABELS, FUNCTION_LABELS, API_LABELS, PAGE_LABELS } from './constants';
import { FormState, Position, Totals } from "./types";
import {
FEATURE_LABELS,
FUNCTION_LABELS,
API_LABELS,
PAGE_LABELS,
} from "./constants";
export function calculateTotals(state: FormState, pricing: any): Totals {
if (state.projectType !== 'website') {
return {
totalPrice: 0,
monthlyPrice: 0,
totalPagesCount: 0,
totalFeatures: 0,
totalFunctions: 0,
totalApis: 0,
languagesCount: 0
};
}
const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0;
const totalPagesCount = Math.max(
(state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0),
sitemapPagesCount
);
const totalFeatures = (state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0);
const totalFunctions = (state.functions?.length || 0) + (state.otherFunctions?.length || 0) + (state.otherFunctionsCount || 0);
const totalApis = (state.apiSystems?.length || 0) + (state.otherTech?.length || 0) + (state.otherTechCount || 0);
let total = pricing.BASE_WEBSITE;
total += totalPagesCount * pricing.PAGE;
total += totalFeatures * pricing.FEATURE;
total += totalFunctions * pricing.FUNCTION;
total += totalApis * pricing.API_INTEGRATION;
total += (state.newDatasets || 0) * pricing.NEW_DATASET;
if (state.cmsSetup) {
total += pricing.CMS_SETUP;
total += totalFeatures * pricing.CMS_CONNECTION_PER_FEATURE;
}
// Optional visual/complexity boosters (from calculator.ts logic)
if (state.visualStaging && !isNaN(Number(state.visualStaging)) && Number(state.visualStaging) > 0) {
total += Number(state.visualStaging) * pricing.VISUAL_STAGING;
}
if (state.complexInteractions && !isNaN(Number(state.complexInteractions)) && Number(state.complexInteractions) > 0) {
total += Number(state.complexInteractions) * pricing.COMPLEX_INTERACTION;
}
const languagesCount = state.languagesList?.length || 1;
if (languagesCount > 1) {
total *= (1 + (languagesCount - 1) * 0.2);
}
const monthlyPrice = pricing.HOSTING_MONTHLY + ((state.storageExpansion || 0) * pricing.STORAGE_EXPANSION_MONTHLY);
if (state.projectType !== "website") {
return {
totalPrice: Math.round(total),
monthlyPrice: Math.round(monthlyPrice),
totalPagesCount,
totalFeatures,
totalFunctions,
totalApis,
languagesCount
totalPrice: 0,
monthlyPrice: 0,
totalPagesCount: 0,
totalFeatures: 0,
totalFunctions: 0,
totalApis: 0,
languagesCount: 0,
};
}
const sitemapPagesCount =
state.sitemap?.reduce(
(acc: number, cat: any) => acc + (cat.pages?.length || 0),
0,
) || 0;
const totalPagesCount = Math.max(
(state.selectedPages?.length || 0) +
(state.otherPages?.length || 0) +
(state.otherPagesCount || 0),
sitemapPagesCount,
);
const totalFeatures =
(state.features?.length || 0) +
(state.otherFeatures?.length || 0) +
(state.otherFeaturesCount || 0);
const totalFunctions =
(state.functions?.length || 0) +
(state.otherFunctions?.length || 0) +
(state.otherFunctionsCount || 0);
const totalApis =
(state.apiSystems?.length || 0) +
(state.otherTech?.length || 0) +
(state.otherTechCount || 0);
let total = pricing.BASE_WEBSITE;
total += totalPagesCount * pricing.PAGE;
total += totalFeatures * pricing.FEATURE;
total += totalFunctions * pricing.FUNCTION;
total += totalApis * pricing.API_INTEGRATION;
total += (state.newDatasets || 0) * pricing.NEW_DATASET;
if (state.cmsSetup) {
total += Math.max(1, totalFeatures) * pricing.CMS_CONNECTION_PER_FEATURE;
}
const languagesCount = state.languagesList?.length || 1;
if (languagesCount > 1) {
total *= 1 + (languagesCount - 1) * 0.2;
}
const monthlyPrice =
pricing.HOSTING_MONTHLY +
(state.storageExpansion || 0) * pricing.STORAGE_EXPANSION_MONTHLY;
return {
totalPrice: Math.round(total),
monthlyPrice: Math.round(monthlyPrice),
totalPagesCount,
totalFeatures,
totalFunctions,
totalApis,
languagesCount,
};
}
export function calculatePositions(state: FormState, pricing: any): Position[] {
const positions: Position[] = [];
let pos = 1;
const positions: Position[] = [];
let pos = 1;
if (state.projectType === 'website') {
positions.push({
pos: pos++,
title: 'Das technische Fundament',
desc: 'Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.',
qty: 1,
price: pricing.BASE_WEBSITE
});
if (state.projectType === "website") {
positions.push({
pos: pos++,
title: "Das technische Fundament",
desc: "Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.",
qty: 1,
price: pricing.BASE_WEBSITE,
});
const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0;
const totalPagesCount = Math.max(
(state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0),
sitemapPagesCount
);
const sitemapPagesCount =
state.sitemap?.reduce(
(acc: number, cat: any) => acc + (cat.pages?.length || 0),
0,
) || 0;
const totalPagesCount = Math.max(
(state.selectedPages?.length || 0) +
(state.otherPages?.length || 0) +
(state.otherPagesCount || 0),
sitemapPagesCount,
);
const allPages = [
...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p),
...(state.otherPages || []),
...(state.sitemap?.flatMap((cat: any) => cat.pages?.map((p: any) => p.title)) || [])
];
const allPages = [
...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p),
...(state.otherPages || []),
...(state.sitemap?.flatMap((cat: any) =>
cat.pages?.map((p: any) => p.title),
) || []),
];
// Deduplicate labels
const uniquePages = Array.from(new Set(allPages));
// Deduplicate labels
const uniquePages = Array.from(new Set(allPages));
positions.push({
pos: pos++,
title: 'Individuelle Seiten',
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(', ')}).`,
qty: totalPagesCount,
price: totalPagesCount * pricing.PAGE
});
positions.push({
pos: pos++,
title: "Individuelle Seiten",
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(", ")}).`,
qty: totalPagesCount,
price: totalPagesCount * pricing.PAGE,
});
if (state.features.length > 0 || (state.otherFeatures?.length || 0) > 0) {
const allFeatures = [...state.features.map((f: string) => FEATURE_LABELS[f] || f), ...(state.otherFeatures || [])];
positions.push({
pos: pos++,
title: 'System-Module (Features)',
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) > 0) {
const allFunctions = [...state.functions.map((f: string) => FUNCTION_LABELS[f] || f), ...(state.otherFunctions || [])];
positions.push({
pos: pos++,
title: 'Logik-Funktionen',
desc: `Implementierung technischer Logik: ${allFunctions.join(', ')}.`,
qty: allFunctions.length,
price: allFunctions.length * pricing.FUNCTION
});
}
if (state.apiSystems.length > 0 || (state.otherTech?.length || 0) > 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 || 0) + (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: 'Inhaltliche Initial-Pflege',
desc: `Manuelle Übernahme und Aufbereitung von ${state.newDatasets} Datensätzen (Produkte, Artikel) in das Zielsystem.`,
qty: state.newDatasets,
price: state.newDatasets * pricing.NEW_DATASET
});
}
if ((state.visualStaging && Number(state.visualStaging) > 0) || (state.complexInteractions && Number(state.complexInteractions) > 0)) {
const vsCount = Number(state.visualStaging || 0);
const ciCount = Number(state.complexInteractions || 0);
const totalCount = vsCount + ciCount;
positions.push({
pos: pos++,
title: 'Inszenierung & Interaktion',
desc: `Umsetzung von ${totalCount} speziellen Sektionen, Hero-Stories oder Konfiguratoren zur Steigerung der Conversion.`,
qty: totalCount,
price: (vsCount * pricing.VISUAL_STAGING) + (ciCount * pricing.COMPLEX_INTERACTION)
});
}
const languagesCount = state.languagesList.length || 1;
if (languagesCount > 1) {
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)
});
}
const monthlyRate = pricing.HOSTING_MONTHLY + (state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY);
positions.push({
pos: pos++,
title: 'Sorglos-Paket (Betrieb & Pflege)',
desc: `1 Jahr Sicherung des technischen Betriebs, Instandhaltung, Sicherheits-Updates und Inhalts-Aktualisierungen gemäß AGB Punkt 7a.`,
qty: 1,
price: monthlyRate * 12
});
} else {
positions.push({
pos: pos++,
title: 'Web App / Software Entwicklung',
desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.',
qty: 1,
price: 0
});
if (state.features.length > 0 || (state.otherFeatures?.length || 0) > 0) {
const allFeatures = [
...state.features.map((f: string) => FEATURE_LABELS[f] || f),
...(state.otherFeatures || []),
];
positions.push({
pos: pos++,
title: "System-Module (Features)",
desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(", ")}. Inklusive Datenstruktur und Darstellung.`,
qty: allFeatures.length,
price: allFeatures.length * pricing.FEATURE,
});
}
return positions;
if (state.functions.length > 0 || (state.otherFunctions?.length || 0) > 0) {
const allFunctions = [
...state.functions.map((f: string) => FUNCTION_LABELS[f] || f),
...(state.otherFunctions || []),
];
positions.push({
pos: pos++,
title: "Logik-Funktionen",
desc: `Implementierung technischer Logik: ${allFunctions.join(", ")}.`,
qty: allFunctions.length,
price: allFunctions.length * pricing.FUNCTION,
});
}
if (state.apiSystems.length > 0 || (state.otherTech?.length || 0) > 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 || 0) +
(state.otherFeaturesCount || 0);
const qty = Math.max(1, totalFeatures);
positions.push({
pos: pos++,
title: "Inhalts-Verwaltung",
desc: "Anbindung der System-Module an das Redaktions-System zur eigenständigen Pflege von Inhalten.",
qty: qty,
price: qty * pricing.CMS_CONNECTION_PER_FEATURE,
});
}
if (state.newDatasets > 0) {
positions.push({
pos: pos++,
title: "Inhaltliche Initial-Pflege",
desc: `Manuelle Übernahme und Aufbereitung von ${state.newDatasets} Datensätzen (Produkte, Artikel) in das Zielsystem.`,
qty: state.newDatasets,
price: state.newDatasets * pricing.NEW_DATASET,
});
}
const languagesCount = state.languagesList.length || 1;
if (languagesCount > 1) {
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),
});
}
const monthlyRate =
pricing.HOSTING_MONTHLY +
state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY;
positions.push({
pos: pos++,
title: "Sorglos Betrieb (1 Jahr)",
desc: `Inklusive 1 Jahr Sicherung des technischen Betriebs, Hosting, Instandhaltung, Sicherheits-Updates und techn. Support gemäß AGB Punkt 7a.`,
qty: 1,
price: monthlyRate * 12,
});
} 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;
}

View File

@@ -1,226 +1,332 @@
import { FormState } from './types';
import { FormState } from "./types";
export const PRICING = {
BASE_WEBSITE: 4000,
PAGE: 600,
FEATURE: 1500,
FUNCTION: 800,
NEW_DATASET: 200,
HOSTING_MONTHLY: 250,
STORAGE_EXPANSION_MONTHLY: 10,
CMS_SETUP: 1500,
CMS_CONNECTION_PER_FEATURE: 800,
API_INTEGRATION: 800,
APP_HOURLY: 120,
VISUAL_STAGING: 2000,
COMPLEX_INTERACTION: 1500,
BASE_WEBSITE: 4000,
PAGE: 600,
FEATURE: 1500,
FUNCTION: 800,
NEW_DATASET: 450,
HOSTING_MONTHLY: 250,
STORAGE_EXPANSION_MONTHLY: 10,
CMS_SETUP: 1500,
CMS_CONNECTION_PER_FEATURE: 1500,
API_INTEGRATION: 800,
APP_HOURLY: 120,
};
export const initialState: FormState = {
projectType: 'website',
// Company
companyName: '',
employeeCount: '',
// Existing Presence
existingWebsite: '',
socialMedia: [],
socialMediaUrls: {},
existingDomain: '',
wishedDomain: '',
// Project
websiteTopic: '',
selectedPages: ['Home'],
otherPages: [],
otherPagesCount: 0,
features: [],
otherFeatures: [],
otherFeaturesCount: 0,
functions: [],
otherFunctions: [],
otherFunctionsCount: 0,
apiSystems: [],
otherTech: [],
otherTechCount: 0,
assets: [],
otherAssets: [],
otherAssetsCount: 0,
newDatasets: 0,
cmsSetup: false,
storageExpansion: 0,
name: '',
email: '',
role: '',
message: '',
sitemapFile: null,
contactFiles: [],
// Design
designVibe: 'minimal',
colorScheme: ['#ffffff', '#f8fafc', '#0f172a'],
references: [],
designWishes: '',
// Maintenance
expectedAdjustments: 'low',
languagesList: ['Deutsch'],
personName: '',
// Timeline
deadline: 'flexible',
// Web App specific
targetAudience: 'internal',
userRoles: [],
dataSensitivity: 'standard',
platformType: 'web-only',
// Meta
dontKnows: [],
visualStaging: 'standard',
complexInteractions: 'standard',
// AI generated / Post-processed
briefingSummary: '',
designVision: '',
positionDescriptions: {},
taxId: '',
sitemap: [],
projectType: "website",
// Company
companyName: "",
employeeCount: "",
// Existing Presence
existingWebsite: "",
socialMedia: [],
socialMediaUrls: {},
existingDomain: "",
wishedDomain: "",
// Project
websiteTopic: "",
selectedPages: ["Home"],
otherPages: [],
otherPagesCount: 0,
features: [],
otherFeatures: [],
otherFeaturesCount: 0,
functions: [],
otherFunctions: [],
otherFunctionsCount: 0,
apiSystems: [],
otherTech: [],
otherTechCount: 0,
assets: [],
otherAssets: [],
otherAssetsCount: 0,
newDatasets: 0,
cmsSetup: false,
storageExpansion: 0,
name: "",
email: "",
role: "",
message: "",
sitemapFile: null,
contactFiles: [],
// Design
designVibe: "minimal",
colorScheme: ["#ffffff", "#f8fafc", "#0f172a"],
references: [],
designWishes: "",
// Maintenance
expectedAdjustments: "low",
languagesList: ["Deutsch"],
personName: "",
// Timeline
deadline: "flexible",
// Web App specific
targetAudience: "internal",
userRoles: [],
dataSensitivity: "standard",
platformType: "web-only",
// Meta
dontKnows: [],
visualStaging: "standard",
complexInteractions: "standard",
// AI generated / Post-processed
briefingSummary: "",
designVision: "",
positionDescriptions: {},
taxId: "",
sitemap: [],
};
export const PAGE_SAMPLES = [
{ id: 'Home', label: 'Startseite', desc: 'Der erste Eindruck Ihrer Marke.' },
{ id: 'About', label: 'Über uns', desc: 'Ihre Geschichte und Ihr Team.' },
{ id: 'Services', label: 'Leistungen', desc: 'Übersicht Ihres Angebots.' },
{ id: 'Contact', label: 'Kontakt', desc: 'Anlaufstelle für Ihre Kunden.' },
{ id: 'Landing', label: 'Landingpage', desc: 'Optimiert für Marketing-Kampagnen.' },
{ id: 'Legal', label: 'Rechtliches', desc: 'Impressum & Datenschutz.' },
{ id: "Home", label: "Startseite", desc: "Der erste Eindruck Ihrer Marke." },
{ id: "About", label: "Über uns", desc: "Ihre Geschichte und Ihr Team." },
{ id: "Services", label: "Leistungen", desc: "Übersicht Ihres Angebots." },
{ id: "Contact", label: "Kontakt", desc: "Anlaufstelle für Ihre Kunden." },
{
id: "Landing",
label: "Landingpage",
desc: "Optimiert für Marketing-Kampagnen.",
},
{ id: "Legal", label: "Rechtliches", desc: "Impressum & Datenschutz." },
];
export const FEATURE_OPTIONS = [
{ id: 'blog_news', label: 'Blog / News', desc: 'Ein Bereich für aktuelle Beiträge und Neuigkeiten.' },
{ id: 'products', label: 'Produktbereich', desc: 'Katalog Ihrer Leistungen oder Produkte.' },
{ id: 'jobs', label: 'Karriere / Jobs', desc: 'Stellenanzeigen und Bewerbungsoptionen.' },
{ id: 'refs', label: 'Referenzen / Cases', desc: 'Präsentation Ihrer Projekte.' },
{ id: 'events', label: 'Events / Termine', desc: 'Veranstaltungskalender.' },
{
id: "blog_news",
label: "Blog / News",
desc: "Ein Bereich für aktuelle Beiträge und Neuigkeiten.",
},
{
id: "products",
label: "Produktbereich",
desc: "Katalog Ihrer Leistungen oder Produkte.",
},
{
id: "jobs",
label: "Karriere / Jobs",
desc: "Stellenanzeigen und Bewerbungsoptionen.",
},
{
id: "refs",
label: "Referenzen / Cases",
desc: "Präsentation Ihrer Projekte.",
},
{ id: "events", label: "Events / Termine", desc: "Veranstaltungskalender." },
];
export const FUNCTION_OPTIONS = [
{ id: 'search', label: 'Suche', desc: 'Volltextsuche über alle Inhalte.' },
{ id: 'filter', label: 'Filter-Systeme', desc: 'Kategorisierung und Sortierung.' },
{ id: 'pdf', label: 'PDF-Export', desc: 'Automatisierte PDF-Erstellung.' },
{ id: 'forms', label: 'Individuelle Formular-Logik', desc: 'Smarte Validierung & mehrstufige Prozesse.' },
{ id: "search", label: "Suche", desc: "Volltextsuche über alle Inhalte." },
{
id: "filter",
label: "Filter-Systeme",
desc: "Kategorisierung und Sortierung.",
},
{ id: "pdf", label: "PDF-Export", desc: "Automatisierte PDF-Erstellung." },
{
id: "forms",
label: "Individuelle Formular-Logik",
desc: "Smarte Validierung & mehrstufige Prozesse.",
},
];
export const API_OPTIONS = [
{ id: 'crm', label: 'CRM System', desc: 'HubSpot, Salesforce, Pipedrive etc.' },
{ id: 'erp', label: 'ERP / Warenwirtschaft', desc: 'SAP, Microsoft Dynamics, Xentral etc.' },
{ id: 'stripe', label: 'Stripe / Payment', desc: 'Zahlungsabwicklung und Abonnements.' },
{ id: 'newsletter', label: 'Newsletter / Marketing', desc: 'Mailchimp, Brevo, ActiveCampaign etc.' },
{ id: 'ecommerce', label: 'E-Commerce / Shop', desc: 'Shopify, WooCommerce, Shopware Sync.' },
{ id: 'hr', label: 'HR / Recruiting', desc: 'Personio, Workday, Recruitee etc.' },
{ id: 'realestate', label: 'Immobilien', desc: 'OpenImmo, FlowFact, Immowelt Sync.' },
{ id: 'calendar', label: 'Termine / Booking', desc: 'Calendly, Shore, Doctolib etc.' },
{ id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' },
{ id: 'maps', label: 'Google Maps / Places', desc: 'Standortsuche und Kartenintegration.' },
{ id: 'analytics', label: 'Custom Analytics', desc: 'Anbindung an spezialisierte Tracking-Tools.' },
{
id: "crm",
label: "CRM System",
desc: "HubSpot, Salesforce, Pipedrive etc.",
},
{
id: "erp",
label: "ERP / Warenwirtschaft",
desc: "SAP, Microsoft Dynamics, Xentral etc.",
},
{
id: "stripe",
label: "Stripe / Payment",
desc: "Zahlungsabwicklung und Abonnements.",
},
{
id: "newsletter",
label: "Newsletter / Marketing",
desc: "Mailchimp, Brevo, ActiveCampaign etc.",
},
{
id: "ecommerce",
label: "E-Commerce / Shop",
desc: "Shopify, WooCommerce, Shopware Sync.",
},
{
id: "hr",
label: "HR / Recruiting",
desc: "Personio, Workday, Recruitee etc.",
},
{
id: "realestate",
label: "Immobilien",
desc: "OpenImmo, FlowFact, Immowelt Sync.",
},
{
id: "calendar",
label: "Termine / Booking",
desc: "Calendly, Shore, Doctolib etc.",
},
{
id: "social",
label: "Social Media Sync",
desc: "Automatisierte Posts oder Feeds.",
},
{
id: "maps",
label: "Google Maps / Places",
desc: "Standortsuche und Kartenintegration.",
},
{
id: "analytics",
label: "Custom Analytics",
desc: "Anbindung an spezialisierte Tracking-Tools.",
},
];
export const ASSET_OPTIONS = [
{ id: 'existing_website', label: 'Bestehende Website', desc: 'Inhalte oder Struktur können übernommen werden.' },
{ id: 'logo', label: 'Logo', desc: 'Vektordatei Ihres Logos.' },
{ id: 'styleguide', label: 'Styleguide', desc: 'Farben, Schriften, Design-Vorgaben.' },
{ id: 'content_concept', label: 'Inhalts-Konzept', desc: 'Struktur und Texte sind bereits geplant.' },
{ id: 'media', label: 'Bild/Video-Material', desc: 'Professionelles Bildmaterial vorhanden.' },
{ id: 'icons', label: 'Icons', desc: 'Eigene Icon-Sets vorhanden.' },
{ id: 'illustrations', label: 'Illustrationen', desc: 'Eigene Illustrationen vorhanden.' },
{ id: 'fonts', label: 'Fonts', desc: 'Lizenzen für Hausschriften vorhanden.' },
{
id: "existing_website",
label: "Bestehende Website",
desc: "Inhalte oder Struktur können übernommen werden.",
},
{ id: "logo", label: "Logo", desc: "Vektordatei Ihres Logos." },
{
id: "styleguide",
label: "Styleguide",
desc: "Farben, Schriften, Design-Vorgaben.",
},
{
id: "content_concept",
label: "Inhalts-Konzept",
desc: "Struktur und Texte sind bereits geplant.",
},
{
id: "media",
label: "Bild/Video-Material",
desc: "Professionelles Bildmaterial vorhanden.",
},
{ id: "icons", label: "Icons", desc: "Eigene Icon-Sets vorhanden." },
{
id: "illustrations",
label: "Illustrationen",
desc: "Eigene Illustrationen vorhanden.",
},
{
id: "fonts",
label: "Fonts",
desc: "Lizenzen für Hausschriften vorhanden.",
},
];
export const DESIGN_OPTIONS = [
{ id: 'minimal', label: 'Minimalistisch', desc: 'Viel Weißraum, klare Typografie.' },
{ id: 'bold', label: 'Mutig & Laut', desc: 'Starke Kontraste, große Schriften.' },
{ id: 'nature', label: 'Natürlich', desc: 'Sanfte Erdtöne, organische Formen.' },
{ id: 'tech', label: 'Technisch', desc: 'Präzise Linien, dunkle Akzente.' },
{
id: "minimal",
label: "Minimalistisch",
desc: "Viel Weißraum, klare Typografie.",
},
{
id: "bold",
label: "Mutig & Laut",
desc: "Starke Kontraste, große Schriften.",
},
{
id: "nature",
label: "Natürlich",
desc: "Sanfte Erdtöne, organische Formen.",
},
{ id: "tech", label: "Technisch", desc: "Präzise Linien, dunkle Akzente." },
];
export const EMPLOYEE_OPTIONS = [
{ id: '1-5', label: '1-5 Mitarbeiter' },
{ id: '6-20', label: '6-20 Mitarbeiter' },
{ id: '21-100', label: '21-100 Mitarbeiter' },
{ id: '100+', label: '100+ Mitarbeiter' },
{ id: "1-5", label: "1-5 Mitarbeiter" },
{ id: "6-20", label: "6-20 Mitarbeiter" },
{ id: "21-100", label: "21-100 Mitarbeiter" },
{ id: "100+", label: "100+ Mitarbeiter" },
];
export const SOCIAL_MEDIA_OPTIONS = [
{ id: 'instagram', label: 'Instagram' },
{ id: 'linkedin', label: 'LinkedIn' },
{ id: 'facebook', label: 'Facebook' },
{ id: 'twitter', label: 'Twitter / X' },
{ id: 'tiktok', label: 'TikTok' },
{ id: 'youtube', label: 'YouTube' },
{ id: "instagram", label: "Instagram" },
{ id: "linkedin", label: "LinkedIn" },
{ id: "facebook", label: "Facebook" },
{ id: "twitter", label: "Twitter / X" },
{ id: "tiktok", label: "TikTok" },
{ id: "youtube", label: "YouTube" },
];
export const VIBE_LABELS: Record<string, string> = {
minimal: 'Minimalistisch',
bold: 'Mutig & Laut',
nature: 'Natürlich',
tech: 'Technisch'
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'
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> = {
existing_website: 'Bestehende Website',
logo: 'Logo',
styleguide: 'Styleguide',
content_concept: 'Inhalts-Konzept',
media: 'Bild/Video-Material',
icons: 'Icons',
illustrations: 'Illustrationen',
fonts: 'Fonts'
existing_website: "Bestehende Website",
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'
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: 'Individuelle Formular-Logik',
members: 'Mitgliederbereich',
calendar: 'Event-Kalender',
multilang: 'Mehrsprachigkeit',
chat: 'Echtzeit-Chat'
search: "Suche",
filter: "Filter-Systeme",
pdf: "PDF-Export",
forms: "Individuelle Formular-Logik",
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'
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'
instagram: "Instagram",
linkedin: "LinkedIn",
facebook: "Facebook",
twitter: "Twitter / X",
tiktok: "TikTok",
youtube: "YouTube",
};
export const PAGE_LABELS: Record<string, string> = {
Home: 'Startseite',
About: 'Über uns',
Services: 'Leistungen',
Contact: 'Kontakt',
Landing: 'Landingpage',
Legal: 'Impressum & Datenschutz'
Home: "Startseite",
About: "Über uns",
Services: "Leistungen",
Contact: "Kontakt",
Landing: "Landingpage",
Legal: "Impressum & Datenschutz",
};