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
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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user