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