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

@@ -0,0 +1,35 @@
# Sorglos-Betrieb Damit Sie sich auf Ihr Geschäft konzentrieren können
Eine Website ist ein lebendiger Teil Ihres Unternehmens. Sie soll funktionieren, sicher sein und gut aussehen ohne dass Sie sich ständig darum kümmern müssen.
Genau dafür gibt es meinen Sorglos-Betrieb.
Das ist keine automatische Server-Überwachung und kein reiner Wartungsvertrag. Es ist meine persönliche Verantwortung, Ihre Website so stabil und aktuell wie möglich zu halten. Ich schaue regelmäßig vorbei, behebe Probleme frühzeitig und reagiere schnell, wenn doch einmal etwas nicht rund läuft.
## Was Sie erwarten können:
### Ich kümmere mich proaktiv
Regelmäßige Checks, Updates von Systemen und Plugins, Sicherheitsüberprüfungen alles, was typische Ausfälle und Schwachstellen verhindert oder minimiert.
### Schnelle Reaktion bei Problemen
Websites können mal down gehen durch Hoster-Störungen, plötzliche Updates von Drittanbietern oder andere unvorhergesehene Dinge. In solchen Fällen bin ich für Sie da: Ich analysiere, behebe und informiere Sie transparent. Sie müssen nicht selbst recherchieren oder panisch Support-Tickets schreiben.
### Sicherheit und Aktualität bleiben im Blick
Ich halte die bestehende Technik auf dem neuesten Stand, korrigiere kleine Fehler und passe Inhalte an, wenn nötig alles innerhalb des vereinbarten Rahmens. So bleibt Ihre Website vertrauenswürdig und nutzerfreundlich.
### Klare Grenzen faire Erwartungen
Der Sorglos-Betrieb deckt die Instandhaltung und Pflege der bestehenden Website ab: Technischer Betrieb, Sicherheit, kleine Korrekturen und Aktualisierungen.
Neue Inhalte erstellen, große Umstrukturierungen, neue Features oder umfangreiche Redaktionsarbeit gehören nicht dazu das besprechen und bepreisen wir separat und transparent.
### Kurz gesagt:
Ich nehme Ihnen so viel wie möglich vom technischen Alltag ab, damit Sie sich auf das konzentrieren können, was Sie am besten können Ihr Geschäft führen.
Ich kann nicht jede Störung im Voraus verhindern (das kann niemand), aber ich sorge dafür, dass solche Momente selten bleiben und schnell wieder behoben sind.
### Sorglos-Betrieb bedeutet für mich:
Ich kümmere mich verlässlich, ehrlich und mit dem gleichen Anspruch, mit dem ich Ihre Website gebaut habe.

View File

@@ -0,0 +1,36 @@
# Meine Standards
Ich entwickle Websites nach Prinzipien, die über das reine Funktionieren hinausgehen.
Das Ziel ist eine Seite, die nicht nur heute überzeugt, sondern in den nächsten Jahren stabil, kosteneffizient, sicher und verantwortungsvoll bleibt ohne dass Sie ständig nachbessern, abmahnen lassen oder sich für unnötigen Ressourcenverbrauch rechtfertigen müssen.
## Was das für Sie konkret bedeutet:
### Deutlich geringerer Energie- und CO₂-Verbrauch
Durch konsequente Optimierung von Code, Bildern, Schriften und Ladeverhalten entsteht eine schlanke Website.
→ Ihre Besucher laden die Seite spürbar schneller, Ihre Hosting-Kosten bleiben niedrig, und der CO₂-Fußabdruck pro Aufruf liegt oft um 7090 % unter dem Durchschnitt vergleichbarer Projekte. Das ist heute für viele Unternehmen ein echter Wettbewerbs- und Imagevorteil ohne dass Sie Kompromisse bei Design oder Funktionalität eingehen müssen.
### Technologische Souveränität & Unabhängigkeit
Keine Abhängigkeit von Big Tech, geschlossenen Baukasten-Systemen oder undurchsichtigen Cloud-Plattformen. Wir setzen konsequent auf Self-Hosting und Open-Source-Kerntechnologien.
→ Alles, was für Ihre Website entwickelt wird, gehört Ihnen. Der Code ist custom-coded, die Infrastruktur ist unabhängig. Wenn ein großer Anbieter seine Preise erhöht, Bedingungen ändert oder Dienste einstellt, bleibt Ihre Website davon unberührt. Sie behalten die volle Kontrolle über Ihre digitale Identität dauerhaft und ohne "Lock-in"-Effekte.
### Vertrauen bei Ihren Besuchern
Kein Cookie-Banner, kein heimliches Tracking, keine versteckten Datenabfragen.
→ Viele Menschen erkennen intuitiv, dass hier mitgedacht wurde. Das schafft ein deutlich besseres Gefühl besonders bei Kunden, die selbst Wert auf Datenschutz legen, in regulierten Branchen tätig sind oder einfach ein seriöses Unternehmen erwarten.
### Sicherheit von Grund auf eingebaut
Sichere Formulare, Schutz vor typischen Angriffsvektoren, keine veralteten oder riskanten Bibliotheken, lokale Ressourcen wo immer möglich.
→ Die Website wird nicht schon nach wenigen Monaten zum Sicherheitsrisiko. Sie sparen sich spätere teure Sicherheits-Updates, Penetrationstests oder im Worst Case den Umgang mit einem Datenleck. Mehr Ruhe und weniger unvorhergesehene Kosten.
### DSGVO-Konformität ohne Grauzonen oder Tricks
Es wird nur verarbeitet, was technisch unbedingt erforderlich ist und ohne aktive Einwilligung erlaubt bleibt.
→ Sie können das Thema Datenschutz mit gutem Gewissen abhaken. Keine Abmahn-Risiken, keine nervösen Kundenanfragen, keine teuren Nachbesserungen.
### Langfristig wartungsarm und zukunftssicher
Weil Abhängigkeiten minimiert und bewährte, schlanke Techniken bevorzugt werden, altert die Website deutlich langsamer.
→ Sie zahlen weniger für regelmäßige Updates, haben seltener böse Überraschungen und können Ihr Budget gezielt in Inhalte, Marketing oder neue Funktionen investieren statt in Notfall-Reparaturen.

File diff suppressed because it is too large Load Diff

View File

@@ -1,131 +1,165 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import * as path from 'node:path'; import * as path from "node:path";
import * as readline from 'node:readline/promises'; import * as readline from "node:readline/promises";
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from "node:url";
import { createElement } from 'react'; import { createElement } from "react";
import { renderToFile } from '@react-pdf/renderer'; import { renderToFile } from "@react-pdf/renderer";
import { calculatePositions, calculateTotals } from '../src/logic/pricing/calculator.js'; import {
import { CombinedQuotePDF } from '../src/components/CombinedQuotePDF.js'; calculatePositions,
import { initialState, PRICING } from '../src/logic/pricing/constants.js'; calculateTotals,
import { getTechDetails, getPrinciples } from '../src/logic/content-provider.js'; } from "../src/logic/pricing/calculator.js";
import { CombinedQuotePDF } from "../src/components/CombinedQuotePDF.js";
import { initialState, PRICING } from "../src/logic/pricing/constants.js";
import {
getTechDetails,
getPrinciples,
getMaintenanceDetails,
getStandardsDetails,
} from "../src/logic/content-provider.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
async function main() { async function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
const isInteractive = args.includes('--interactive') || args.includes('-I'); const isInteractive = args.includes("--interactive") || args.includes("-I");
const isEstimationOnly = args.includes('--estimation') || args.includes('-E'); const isEstimationOnly = args.includes("--estimation") || args.includes("-E");
const inputPath = args.find((_, i) => args[i - 1] === '--input' || args[i - 1] === '-i'); const inputPath = args.find(
(_, i) => args[i - 1] === "--input" || args[i - 1] === "-i",
);
let state = { ...initialState }; let state = { ...initialState };
if (inputPath) { if (inputPath) {
const rawData = fs.readFileSync(path.resolve(process.cwd(), inputPath), 'utf8'); const rawData = fs.readFileSync(
const diskState = JSON.parse(rawData); path.resolve(process.cwd(), inputPath),
state = { ...state, ...diskState }; "utf8",
}
if (isInteractive) {
state = await runWizard(state);
}
// Final confirmation of data needed for PDF
if (!state.name || !state.email) {
console.warn('⚠️ Missing recipient name or email. Document might look incomplete.');
}
const totals = calculateTotals(state, PRICING);
const { totalPrice, monthlyPrice, totalPagesCount } = totals;
const finalOutputPath = generateDefaultPath(state);
const outputDir = path.dirname(finalOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Resolve assets for the PDF
const assetsDir = path.resolve(process.cwd(), 'src/assets');
const headerIcon = path.join(assetsDir, 'logo/Icon White Transparent.png');
const footerLogo = path.join(assetsDir, 'logo/Logo Black Transparent.png');
console.log(`🚀 Generating PDF: ${finalOutputPath}`);
const estimationProps = {
state,
totalPrice,
monthlyPrice,
totalPagesCount,
pricing: PRICING,
headerIcon,
footerLogo
};
await renderToFile(
createElement(CombinedQuotePDF as any, {
estimationProps,
techDetails: getTechDetails(),
principles: getPrinciples(),
mode: isEstimationOnly ? 'estimation' : 'full',
showAgbs: !isEstimationOnly // AGBS only for full quotes
}) as any,
finalOutputPath
); );
const diskState = JSON.parse(rawData);
state = { ...state, ...diskState };
}
console.log('✅ Done!'); if (isInteractive) {
state = await runWizard(state);
}
// Final confirmation of data needed for PDF
if (!state.name || !state.email) {
console.warn(
"⚠️ Missing recipient name or email. Document might look incomplete.",
);
}
const totals = calculateTotals(state, PRICING);
const { totalPrice, monthlyPrice, totalPagesCount } = totals;
const finalOutputPath = generateDefaultPath(state);
const outputDir = path.dirname(finalOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Resolve assets for the PDF
const assetsDir = path.resolve(process.cwd(), "src/assets");
const headerIcon = path.join(assetsDir, "logo/Icon White Transparent.png");
const footerLogo = path.join(assetsDir, "logo/Logo Black Transparent.png");
console.log(`🚀 Generating PDF: ${finalOutputPath}`);
const estimationProps = {
state,
totalPrice,
monthlyPrice,
totalPagesCount,
pricing: PRICING,
headerIcon,
footerLogo,
};
await renderToFile(
createElement(CombinedQuotePDF as any, {
estimationProps,
techDetails: getTechDetails(),
principles: getPrinciples(),
maintenanceDetails: getMaintenanceDetails(),
standardsDetails: getStandardsDetails(),
mode: isEstimationOnly ? "estimation" : "full",
showAgbs: !isEstimationOnly, // AGBS only for full quotes
}) as any,
finalOutputPath,
);
console.log("✅ Done!");
} }
async function runWizard(state: any) { async function runWizard(state: any) {
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout,
}); });
console.log('\n--- Mintel Quote Generator Wizard ---\n'); console.log("\n--- Mintel Quote Generator Wizard ---\n");
const ask = async (q: string, def?: string) => { const ask = async (q: string, def?: string) => {
const answer = await rl.question(`${q}${def ? ` [${def}]` : ''}: `); const answer = await rl.question(`${q}${def ? ` [${def}]` : ""}: `);
return answer || def || ''; return answer || def || "";
}; };
const selectOne = async (q: string, options: { id: string, label: string }[]) => { const selectOne = async (
console.log(`\n${q}:`); q: string,
options.forEach((opt, i) => console.log(`${i + 1}) ${opt.label}`)); options: { id: string; label: string }[],
const answer = await rl.question('Selection (number): '); ) => {
const idx = parseInt(answer) - 1; console.log(`\n${q}:`);
return options[idx]?.id || options[0].id; options.forEach((opt, i) => console.log(`${i + 1}) ${opt.label}`));
}; const answer = await rl.question("Selection (number): ");
const idx = parseInt(answer) - 1;
return options[idx]?.id || options[0].id;
};
state.name = await ask('Recipient Name', state.name); state.name = await ask("Recipient Name", state.name);
state.email = await ask('Recipient Email', state.email); state.email = await ask("Recipient Email", state.email);
state.companyName = await ask('Company Name', state.companyName); state.companyName = await ask("Company Name", state.companyName);
state.projectType = await selectOne('Project Type', [ state.projectType = await selectOne("Project Type", [
{ id: 'website', label: 'Website' }, { id: "website", label: "Website" },
{ id: 'web-app', label: 'Web App' } { id: "web-app", label: "Web App" },
]); ]);
if (state.projectType === 'website') { if (state.projectType === "website") {
state.websiteTopic = await ask('Website Topic', state.websiteTopic); state.websiteTopic = await ask("Website Topic", state.websiteTopic);
// Simplified for now, in a real tool we'd loop through all options // Simplified for now, in a real tool we'd loop through all options
} }
rl.close(); rl.close();
return state; return state;
} }
function generateDefaultPath(state: any) { function generateDefaultPath(state: any) {
const now = new Date(); const now = new Date();
const month = now.toISOString().slice(0, 7); const month = now.toISOString().slice(0, 7);
const day = now.toISOString().slice(0, 10); const day = now.toISOString().slice(0, 10);
// Add seconds and minutes for 100% unique names without collision // Add seconds and minutes for 100% unique names without collision
const time = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/:/g, '-'); const time = now
const company = (state.companyName || state.name || 'Unknown').replace(/[^a-z0-9]/gi, '_'); .toLocaleTimeString("de-DE", {
return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${time}_${company}_${state.projectType}.pdf`); hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
.replace(/:/g, "-");
const company = (state.companyName || state.name || "Unknown").replace(
/[^a-z0-9]/gi,
"_",
);
return path.join(
process.cwd(),
"out",
"estimations",
month,
`${day}_${time}_${company}_${state.projectType}.pdf`,
);
} }
main().catch(err => { main().catch((err) => {
console.error('❌ Error:', err); console.error("❌ Error:", err);
process.exit(1); process.exit(1);
}); });

View File

@@ -1,24 +1,80 @@
'use client'; "use client";
import * as React from 'react'; import * as React from "react";
import { Document as PDFDocument } from '@react-pdf/renderer'; import { Document as PDFDocument } from "@react-pdf/renderer";
import { EstimationPDF } from './EstimationPDF'; import { EstimationPDF } from "./EstimationPDF";
import { AgbsPDF } from './AgbsPDF'; import { AgbsPDF } from "./AgbsPDF";
import { ClosingModule } from "./pdf/modules/CommonModules";
import { SimpleLayout } from "./pdf/SimpleLayout";
interface CombinedProps { interface CombinedProps {
estimationProps: any; estimationProps: any;
showAgbs?: boolean; showAgbs?: boolean;
techDetails?: any[]; techDetails?: any[];
principles?: any[]; principles?: any[];
maintenanceDetails?: any[];
standardsDetails?: any[];
} }
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true, techDetails, principles, mode = 'full' }: CombinedProps & { mode?: 'estimation' | 'full' }) => { export const CombinedQuotePDF = ({
return ( estimationProps,
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}> showAgbs = true,
<EstimationPDF {...estimationProps} mode={mode} techDetails={techDetails} principles={principles} /> techDetails,
{showAgbs && ( principles,
<AgbsPDF mode={mode} state={estimationProps.state} headerIcon={estimationProps.headerIcon} footerLogo={estimationProps.footerLogo} /> maintenanceDetails,
)} standardsDetails,
</PDFDocument> 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 * as React from "react";
import { FormState, Totals } from '../types'; import { FormState, Totals } from "../types";
import { PRICING } from '../constants'; import { PRICING } from "../constants";
import { AnimatedNumber } from './AnimatedNumber'; import { AnimatedNumber } from "./AnimatedNumber";
import { ConceptPrice, ConceptAutomation } from '../../Landing/ConceptIllustrations'; import {
import { Info, Download, Share2, RefreshCw } from 'lucide-react'; ConceptPrice,
import { motion, AnimatePresence } from 'framer-motion'; ConceptAutomation,
import dynamic from 'next/dynamic'; } 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 // EstimationPDF will be imported dynamically where used or inside the and client-side block
import IconWhite from '../../../assets/logo/Icon White Transparent.png'; import IconWhite from "../../../assets/logo/Icon White Transparent.png";
import LogoBlack from '../../../assets/logo/Logo Black 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 // 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, totals,
isClient, isClient,
qrCodeData, qrCodeData,
onShare onShare,
}: PriceCalculationProps) { }: PriceCalculationProps) {
const { totalPrice, monthlyPrice, totalPagesCount, totalFeatures, totalFunctions, totalApis, languagesCount } = totals; const {
totalPrice,
monthlyPrice,
totalPagesCount,
totalFeatures,
totalFunctions,
totalApis,
languagesCount,
} = totals;
const totalPages = totalPagesCount; const totalPages = totalPagesCount;
const [pdfLoading, setPdfLoading] = React.useState(false); const [pdfLoading, setPdfLoading] = React.useState(false);
@@ -40,35 +51,41 @@ export function PriceCalculation({
setPdfLoading(true); setPdfLoading(true);
try { try {
const { EstimationPDF } = await import('../../EstimationPDF'); const { EstimationPDF } = await import("../../EstimationPDF");
const doc = <EstimationPDF const doc = (
state={state} <EstimationPDF
totalPrice={totalPrice} state={state}
monthlyPrice={monthlyPrice} totalPrice={totalPrice}
totalPagesCount={totalPagesCount} monthlyPrice={monthlyPrice}
pricing={PRICING} totalPagesCount={totalPagesCount}
headerIcon={typeof IconWhite === 'string' ? IconWhite : (IconWhite as any).src} pricing={PRICING}
footerLogo={typeof LogoBlack === 'string' ? LogoBlack : (LogoBlack as any).src} 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 // Minimum loading time of 2 seconds for better UX
const [blob] = await Promise.all([ const [blob] = await Promise.all([
pdf(doc).toBlob(), pdf(doc).toBlob(),
new Promise(resolve => setTimeout(resolve, 2000)) new Promise((resolve) => setTimeout(resolve, 2000)),
]); ]);
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement("a");
link.href = url; 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); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} catch (error) { } catch (error) {
console.error('PDF generation failed:', error); console.error("PDF generation failed:", error);
} finally { } finally {
setPdfLoading(false); 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="lg:col-span-4 lg:sticky lg:top-32 self-start z-30">
<div className="bg-slate-50 border border-slate-100 rounded-[3rem] p-6 space-y-6"> <div className="bg-slate-50 border border-slate-100 rounded-[3rem] p-6 space-y-6">
<div className="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]"> <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>)} {totalPages > 0 && (
{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>)} <div className="flex justify-between items-center text-sm">
{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>)} <span className="text-slate-500">{totalPages}x Seite</span>
{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>)} <span className="font-medium text-slate-900">
{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>)} {(totalPages * PRICING.PAGE).toLocaleString()}
{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>)} </span>
{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>
)}
{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>
<div className="pt-4 space-y-2"> <div className="pt-4 space-y-2">
<div className="flex justify-between items-end"> <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-right">
<div className="text-2xl font-bold tracking-tighter text-slate-900"> <div className="text-2xl font-bold tracking-tighter text-slate-900">
<AnimatedNumber value={totalPrice} /> <AnimatedNumber value={totalPrice} />
@@ -101,8 +191,12 @@ export function PriceCalculation({
</div> </div>
<div className="pt-4 border-t border-slate-200 space-y-4"> <div className="pt-4 border-t border-slate-200 space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-slate-600 font-medium text-sm">Sorglos-Paket</span> <span className="text-slate-600 font-medium text-sm">
<span className="text-base font-bold text-slate-900">{monthlyPrice.toLocaleString()} / Monat</span> Sorglos Betrieb (Hosting + Support)
</span>
<span className="text-base font-bold text-slate-900">
{(monthlyPrice * 12).toLocaleString()} / Jahr
</span>
</div> </div>
</div> </div>
@@ -169,8 +263,15 @@ export function PriceCalculation({
<ConceptAutomation className="w-10 h-10 text-black" /> <ConceptAutomation className="w-10 h-10 text-black" />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-slate-600 text-xs leading-relaxed">Web Apps werden nach Aufwand abgerechnet.</p> <p className="text-slate-600 text-xs leading-relaxed">
<p className="text-2xl font-bold text-slate-900">{PRICING.APP_HOURLY} <span className="text-base text-slate-400 font-normal">/ Std.</span></p> 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>
</div> </div>
{onShare && ( {onShare && (
@@ -186,7 +287,10 @@ export function PriceCalculation({
</div> </div>
)} )}
</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>
</div> </div>
); );

View File

@@ -1,19 +1,24 @@
'use client'; "use client";
import * as React from 'react'; import * as React from "react";
import { calculatePositions } from '../logic/pricing'; import { calculatePositions } from "../logic/pricing";
import { Page as PDFPage } from '@react-pdf/renderer'; import { Page as PDFPage } from "@react-pdf/renderer";
import { pdfStyles } from './pdf/SharedUI'; import { pdfStyles } from "./pdf/SharedUI";
import { DINLayout } from './pdf/DINLayout'; import { DINLayout } from "./pdf/DINLayout";
import { SimpleLayout } from './pdf/SimpleLayout'; import { SimpleLayout } from "./pdf/SimpleLayout";
// Modules // Modules
import { FrontPageModule } from './pdf/modules/FrontPageModule'; import { FrontPageModule } from "./pdf/modules/FrontPageModule";
import { BriefingModule } from './pdf/modules/BriefingModule'; import { BriefingModule } from "./pdf/modules/BriefingModule";
import { SitemapModule } from './pdf/modules/SitemapModule'; import { SitemapModule } from "./pdf/modules/SitemapModule";
import { EstimationModule } from './pdf/modules/EstimationModule'; import { EstimationModule } from "./pdf/modules/EstimationModule";
import { TransparenzModule, techPageModule as TechPageModule, PrinciplesModule } from './pdf/modules/CommonModules'; import {
import { AboutModule, CrossSellModule } from './pdf/modules/BrandingModules'; TransparenzModule,
techPageModule as TechPageModule,
MaintenanceModule,
StandardsModule,
} from "./pdf/modules/CommonModules";
import { AboutModule, CrossSellModule } from "./pdf/modules/BrandingModules";
interface PDFProps { interface PDFProps {
state: any; state: any;
@@ -21,28 +26,32 @@ interface PDFProps {
monthlyPrice: number; monthlyPrice: number;
totalPagesCount: number; totalPagesCount: number;
pricing: any; pricing: any;
mode?: 'estimation' | 'full'; mode?: "estimation" | "full";
headerIcon?: string; headerIcon?: string;
footerLogo?: string; footerLogo?: string;
techDetails?: { t: string, d: string }[]; techDetails?: { t: string; d: string }[];
principles?: { t: string, d: string }[]; principles?: { t: string; d: string }[];
maintenanceDetails?: { t: string; d: string }[];
standardsDetails?: { t: string; d: string }[];
} }
export const EstimationPDF = ({ export const EstimationPDF = ({
state, state,
totalPrice, totalPrice,
pricing, pricing,
mode = 'full', mode = "full",
headerIcon, headerIcon,
footerLogo, footerLogo,
techDetails, techDetails,
principles, principles,
maintenanceDetails,
standardsDetails,
...props ...props
}: PDFProps) => { }: PDFProps) => {
const date = new Date().toLocaleDateString('de-DE', { const date = new Date().toLocaleDateString("de-DE", {
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric', day: "numeric",
}); });
const positions = calculatePositions(state, pricing); const positions = calculatePositions(state, pricing);
@@ -51,13 +60,13 @@ export const EstimationPDF = ({
name: "Marc Mintel", name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7", address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller", address2: "54586 Schüller",
ustId: "DE367588065" ustId: "DE367588065",
}; };
const bankData = { const bankData = {
name: "N26", name: "N26",
bic: "NTSBDEB1XXX", bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65" iban: "DE50 1001 1001 2620 4328 65",
}; };
const commonProps = { const commonProps = {
@@ -66,20 +75,25 @@ export const EstimationPDF = ({
icon: headerIcon, icon: headerIcon,
footerLogo, footerLogo,
companyData, companyData,
bankData bankData,
}; };
if (mode === 'estimation') { if (mode === "estimation") {
return ( return (
<DINLayout {...commonProps} showAddress={true} showFooterDetails={true}> <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> </DINLayout>
); );
} }
// Full Portfolio Mode // Full Portfolio Mode
let pageCounter = 1; let pageCounter = 1;
const getPageNum = () => (pageCounter++).toString().padStart(2, '0'); const getPageNum = () => (pageCounter++).toString().padStart(2, "0");
return ( return (
<> <>
@@ -98,22 +112,36 @@ export const EstimationPDF = ({
)} )}
<SimpleLayout {...commonProps} pageNumber={getPageNum()}> <SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<EstimationModule state={state} positions={positions} totalPrice={totalPrice} date={date} /> <EstimationModule
state={state}
positions={positions}
totalPrice={totalPrice}
date={date}
/>
</SimpleLayout> </SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}> <SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TransparenzModule pricing={pricing} /> <TransparenzModule pricing={pricing} />
</SimpleLayout> </SimpleLayout>
{standardsDetails && standardsDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<StandardsModule
standardsDetails={standardsDetails}
principles={principles}
/>
</SimpleLayout>
)}
{techDetails && techDetails.length > 0 && ( {techDetails && techDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}> <SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TechPageModule techDetails={techDetails} headerIcon={headerIcon} /> <TechPageModule techDetails={techDetails} headerIcon={headerIcon} />
</SimpleLayout> </SimpleLayout>
)} )}
{principles && principles.length > 0 && ( {maintenanceDetails && maintenanceDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}> <SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<PrinciplesModule principles={principles} /> <MaintenanceModule maintenanceDetails={maintenanceDetails} />
</SimpleLayout> </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 * as React from "react";
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; import {
import { IndustrialListItem, IndustrialCard, Divider, COLORS, FONT_SIZES } from '../SharedUI'; 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({ const styles = StyleSheet.create({
industrialTitle: { fontSize: FONT_SIZES.H1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 6, letterSpacing: -1 }, industrialTextLead: {
industrialSubtitle: { fontSize: FONT_SIZES.SUB, fontWeight: 'bold', color: COLORS.TEXT_LIGHT, marginBottom: 16, letterSpacing: 0.5 }, fontSize: FONT_SIZES.BODY,
industrialTextLead: { fontSize: FONT_SIZES.H3, color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 16 }, color: COLORS.TEXT_MAIN,
industrialText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 12 }, lineHeight: 1.4,
industrialGrid2: { flexDirection: 'row' }, marginBottom: 16,
industrialCol: { width: '46%' }, },
industrialBulletBox: { industrialText: {
width: 6, fontSize: FONT_SIZES.BODY,
height: 6, color: COLORS.TEXT_DIM,
backgroundColor: COLORS.DIVIDER, lineHeight: 1.4,
marginRight: 8, marginBottom: 12,
marginTop: 5, },
}, industrialGrid2: { flexDirection: "row" },
industrialCol: { width: "46%" },
}); });
export const AboutModule = () => ( export const AboutModule = () => (
<> <>
<PDFText style={styles.industrialTitle}>Expertise & Profil</PDFText> <DocumentTitle
<PDFText style={styles.industrialSubtitle}>Entwicklung & Technischer Partner für den Mittelstand</PDFText> title="Expertise & Profil"
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} /> subLines={["Entwicklung & Technischer Partner für den Mittelstand"]}
isHero={true}
/>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={{ marginTop: 24 }}> <PDFView style={{ marginTop: 24 }}>
<PDFText style={styles.industrialTextLead}> <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. Begleitung mittelständischer Unternehmen und Agenturen bei der
</PDFText> 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.industrialGrid2, { marginTop: 20 }]}>
<PDFView style={[styles.industrialCol, { marginRight: '8%' }]}> <PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Erfahrung & Substanz</PDFText> <PDFText
<PDFText style={styles.industrialText}> style={[
Der Werdegang umfasst alle Ebenen der Webentwicklung: von der Teamleitung in Kreativagenturen bis zur Softwareentwicklung für internationale Konzerne. styles.industrialText,
</PDFText> { fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
<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> Erfahrung & Substanz
</PDFView> </PDFText>
<PDFText style={styles.industrialText}>
<PDFView style={styles.industrialCol}> Der Werdegang umfasst alle Ebenen der Webentwicklung: von der
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Fokus Einzelentwicklung</PDFText> Teamleitung in Kreativagenturen bis zur Softwareentwicklung für
<PDFText style={styles.industrialText}> internationale Konzerne.
Die Umsetzung erfolgt bewusst als spezialisierter Einzelentwickler. Dies garantiert maximale Geschwindigkeit, direkte Kommunikationswege und volle technologische Verantwortung. </PDFText>
</PDFText> <PDFText style={styles.industrialText}>
<PDFText style={styles.industrialText}> Die Kenntnis komplexer Enterprise-Systeme wird mit der Agilität
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. kombiniert, die im Mittelstand gefordert ist. Dieses Wissen
</PDFText> ermöglicht den Bau von Lösungen, die technologisch auf Augenhöhe mit
</PDFView> Konzern-Standards sind, jedoch ohne unnötigen bürokratischen
</PDFView> Overhead auskommen.
</PDFText>
<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> </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) => { export const CrossSellModule = ({ state }: any) => {
const isWebsite = state.projectType === 'website'; const isWebsite = state.projectType === "website";
const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme"; const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
const subtitle = isWebsite ? "Automatisierung und Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse"; const subtitle = isWebsite
? "Automatisierung und Prozessoptimierung"
: "Technische Infrastruktur ohne Kompromisse";
return ( return (
<> <>
<PDFText style={styles.industrialTitle}>{title}</PDFText> <DocumentTitle title={title} subLines={[subtitle]} isHero={true} />
<PDFText style={styles.industrialSubtitle}>{subtitle}</PDFText> <Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} /> <PDFView style={[styles.industrialGrid2, { marginTop: 16 }]}>
<PDFView style={[styles.industrialGrid2, { marginTop: 16 }]} > {isWebsite ? (
{isWebsite ? ( <>
<> <PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFView style={[styles.industrialCol, { marginRight: '8%' }]}> <PDFText style={styles.industrialTextLead}>
<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> Über die klassische Webpräsenz hinaus werden maßgeschneiderte
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Keine Abos. Keine komplexen neuen Systeme. Gezielte Zeitersparnis.</PDFText> Lösungen zur Automatisierung von Routine-Prozessen angeboten.
<PDFView style={{ marginTop: 24, padding: 16, backgroundColor: '#f8fafc', borderLeftWidth: 2, borderLeftColor: COLORS.GRID }}> Dies ermöglicht eine signifikante Effizienzsteigerung im
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Individuelle Analyse</PDFText> Tagesgeschäft.
<PDFText style={styles.industrialText}>Spezifische Prozesse werden auf technisches Automatisierungspotenzial untersucht. Das Ergebnis liefert Klarheit über die wirtschaftliche Sinnhaftigkeit einer Umsetzung.</PDFText> </PDFText>
</PDFView> <PDFText style={[styles.industrialText, { fontWeight: "bold" }]}>
</PDFView> Keine Abos. Keine komplexen neuen Systeme. Gezielte
<PDFView style={styles.industrialCol}> Zeitersparnis.
<IndustrialCard title="DOKUMENT-AUTOMATION"> </PDFText>
<PDFText style={styles.industrialText}>Erstellung von PDF-Angeboten, Berichten oder Protokollen in Sekunden statt Stunden.</PDFText> <PDFView
</IndustrialCard> style={{
<IndustrialCard title="EXCEL-LOGIK"> marginTop: 24,
<PDFText style={styles.industrialText}>Intelligente Tabellen und automatisierte Auswertungen bestehender Datensätze.</PDFText> padding: 16,
</IndustrialCard> backgroundColor: "#f8fafc",
<IndustrialCard title="KI-ASSISTENZ"> borderLeftWidth: 2,
<PDFText style={styles.industrialText}>Effiziente Verarbeitung von analogen Dokumenten oder handschriftlichen Notizen mittels KI.</PDFText> borderLeftColor: COLORS.GRID,
</IndustrialCard> }}
</PDFView> >
</> <PDFText
) : ( style={[
<PDFView style={{ width: '100%' }}> styles.industrialText,
<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> fontWeight: "bold",
</PDFView> 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>
</> <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 * as React from "react";
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; import {
import { DocumentTitle, COLORS, FONT_SIZES } from '../SharedUI'; View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: { marginBottom: 24 }, section: { marginBottom: 24 },
sectionTitle: { fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', marginBottom: 8, color: COLORS.CHARCOAL }, sectionTitle: {
visionText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.8, textAlign: 'justify' }, 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) => ( export const BriefingModule = ({ state }: any) => (
<> <>
<DocumentTitle title="Projektdetails" /> <DocumentTitle title="Projektdetails" isHero={true} />
{state.briefingSummary && ( {state.briefingSummary && (
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText> <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> <PDFText
</PDFView> style={{
)} fontSize: FONT_SIZES.BODY,
{state.designVision && ( color: COLORS.TEXT_MAIN,
<PDFView style={[styles.section, { padding: 12, borderLeftWidth: 2, borderLeftColor: COLORS.DIVIDER, backgroundColor: COLORS.GRID }]}> lineHeight: 1.6,
<PDFText style={[styles.sectionTitle, { color: COLORS.CHARCOAL, marginBottom: 4 }]}>Strategische Vision</PDFText> textAlign: "justify",
<PDFText style={[styles.visionText, { lineHeight: 1.6 }]}>{state.designVision}</PDFText> }}
</PDFView> >
)} {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 * as React from "react";
import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from '@react-pdf/renderer'; import {
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI'; 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({ const styles = StyleSheet.create({
section: { marginBottom: 16 }, section: { marginBottom: 24 },
pricingGrid: { marginTop: 12 }, moduleLabel: {
pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 12, alignItems: 'flex-start' }, fontSize: FONT_SIZES.LABEL,
pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, paddingRight: 15 }, fontWeight: "bold",
pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.5, paddingRight: 10 }, color: COLORS.CHARCOAL,
pricingTag: { width: '15%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', textAlign: 'right', color: COLORS.CHARCOAL }, letterSpacing: 0.5,
configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 8 }, 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" /> <DocumentTitle title="Technische Umsetzung" isHero={true} />
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFView style={styles.pricingGrid}> {/* FEATURED SPECS - Editorial focus */}
{techDetails?.map((item: any, i: number) => ( {featured.map((item: any, i: number) => (
<PDFView key={i} style={styles.pricingRow}> <PDFView key={i} style={{ marginBottom: 24 }}>
<PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText> <PDFText
<PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText> style={[
</PDFView> 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> }
/>
</PDFView>
</> </>
); );
};
export const TransparenzModule = ({ pricing }: any) => ( export const TransparenzModule = ({ pricing }: any) => {
const sorglosPrice = (pricing.HOSTING_MONTHLY || 250) * 12;
return (
<> <>
<DocumentTitle title="Preis-Transparenz & Modell" /> <DocumentTitle title="Preis-Transparenz & Modell" isHero={true} />
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFView style={styles.pricingGrid}> <PDFView style={{ borderTopWidth: 1, borderTopColor: COLORS.CHARCOAL }}>
<PDFView style={styles.pricingRow}> {[
<PDFText style={styles.pricingTitle}>Fundament</PDFText> {
<PDFText style={styles.pricingDesc}>Setup, Infrastruktur, Hosting, SEO-Basics, Staging & Live-Umgebungen.</PDFText> l: "Fundament",
<PDFText style={styles.pricingTag}>{pricing.BASE_WEBSITE?.toLocaleString('de-DE')} </PDFText> d: "Bereitstellung der techn. Infrastruktur & System-Umgebung.",
</PDFView> p: pricing.BASE_WEBSITE,
<PDFView style={styles.pricingRow}> },
<PDFText style={styles.pricingTitle}>Seiten</PDFText> {
<PDFText style={styles.pricingDesc}>Layout & Umsetzung individueller Seiten. Responsive Design / Cross-Browser.</PDFText> l: "Einzelseiten",
<PDFText style={styles.pricingTag}>{pricing.PAGE?.toLocaleString('de-DE')} / Stk</PDFText> d: "Individuelle Gestaltung, Layout & responsive Struktur.",
</PDFView> p: pricing.PAGE,
<PDFView style={styles.pricingRow}> unit: "/ Stk",
<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> l: "Core Features",
</PDFView> d: "Geschlossene Datensysteme mit eigener Datenstruktur.",
<PDFView style={styles.pricingRow}> p: pricing.FEATURE,
<PDFText style={styles.pricingTitle}>Funktionen</PDFText> unit: "/ Stk",
<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> l: "Logik & Funktionen",
<PDFView style={styles.pricingRow}> d: "Interaktive Funktions-Bausteine & Prozess-Logik.",
<PDFText style={styles.pricingTitle}>Schnittstellen</PDFText> p: pricing.FUNCTION,
<PDFText style={styles.pricingDesc}>Anbindung externer Systeme (CRM, ERP, Payment) zur Synchronisation.</PDFText> unit: "/ Stk",
<PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} / Stk</PDFText> },
</PDFView> {
<PDFView style={styles.pricingRow}> l: "Schnittstellen",
<PDFText style={styles.pricingTitle}>CMS Setup</PDFText> d: "Synchronisation mit externen Zielsystemen.",
<PDFText style={styles.pricingDesc}>Konfiguration Headless CMS zur unabhängigen Datenpflege aller Module.</PDFText> p: pricing.API_INTEGRATION,
<PDFText style={pricing.CMS_SETUP ? styles.pricingTag : [styles.pricingTag, { color: COLORS.TEXT_LIGHT }]}>{pricing.CMS_SETUP?.toLocaleString('de-DE')} </PDFText> unit: "/ Stk",
</PDFView> },
<PDFView style={styles.pricingRow}> {
<PDFText style={styles.pricingTitle}>Inszenierung</PDFText> l: "Inhalts-Verwaltung",
<PDFText style={styles.pricingDesc}>Interaktions-Mechanismen, Konfiguratoren oder visuelles Storytelling.</PDFText> d: "Schnittstelle zur eigenständigen Daten-Pflege (optional).",
<PDFText style={styles.pricingTag}>ab {pricing.VISUAL_STAGING?.toLocaleString('de-DE')} </PDFText> p: pricing.CMS_CONNECTION_PER_FEATURE,
</PDFView> unit: "/ Stk",
<PDFView style={styles.pricingRow}> },
<PDFText style={styles.pricingTitle}>Sprachen</PDFText> {
<PDFText style={styles.pricingDesc}>Skalierung der System-Architektur auf zusätzliche Sprachversionen.</PDFText> l: "Sprachversionen",
<PDFText style={styles.pricingTag}>+20% / Sprache</PDFText> d: "Skalierung der System-Architektur auf Zweit-Sprachen.",
</PDFView> p: "+20%",
<PDFView style={styles.pricingRow}> isLang: true,
<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> l: "Initial-Pflege",
</PDFView> d: "Konvertierung & Aufbereitung von Bestandsdaten.",
<PDFView style={styles.pricingRow}> p: pricing.NEW_DATASET,
<PDFText style={styles.pricingTitle}>Sorglos-Paket</PDFText> unit: "/ Stk",
<PDFText style={styles.pricingDesc}>Betrieb, Hosting, Updates & Monitoring gemäß AGB Punkt 7a.</PDFText> },
<PDFText style={styles.pricingTag}>Inklusive 1 Jahr</PDFText> {
</PDFView> 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> </PDFView>
</PDFView>
</> </>
); );
};
export const PrinciplesModule = ({ principles }: any) => ( export const ClosingModule = () => (
<> <>
<DocumentTitle title="Prinzipien & Standards" /> <DocumentTitle title="Abschluss & Kontakt" isHero={true} />
<PDFView style={[styles.pricingGrid, { marginTop: 8 }]}> <PDFView style={styles.section}>
{principles?.map((item: any, i: number) => ( <PDFText
<PDFView key={i} style={styles.pricingRow}> style={[
<PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText> styles.moduleLabel,
<PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText> { fontSize: FONT_SIZES.HEADING, marginBottom: 12 },
</PDFView> ]}
))} >
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>
</PDFView>
</>
); );

View File

@@ -1,55 +1,159 @@
'use client'; "use client";
import * as React from 'react'; import * as React from "react";
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; import {
import { DocumentTitle } from '../SharedUI'; View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({ const styles = StyleSheet.create({
table: { marginTop: 12 }, table: { marginTop: 12 },
tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#334155', marginBottom: 12 }, tableHeader: {
tableRow: { flexDirection: 'row', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f8fafc', alignItems: 'flex-start' }, flexDirection: "row",
colPos: { width: '8%' }, paddingBottom: 8,
colDesc: { width: '62%' }, borderBottomWidth: 1,
colQty: { width: '10%', textAlign: 'center' }, borderBottomColor: COLORS.CHARCOAL,
colPrice: { width: '20%', textAlign: 'right' }, marginBottom: 12,
headerText: { fontSize: 7, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 }, },
posText: { fontSize: 8, color: '#999999' }, tableRow: {
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4 }, flexDirection: "row",
itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 }, paddingVertical: 10,
priceText: { fontSize: 10, fontWeight: 'bold' }, borderBottomWidth: 1,
summaryContainer: { borderTopWidth: 1, borderTopColor: '#334155', paddingTop: 8 }, borderBottomColor: COLORS.GRID,
summaryRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingVertical: 4, alignItems: 'baseline' }, alignItems: "flex-start",
summaryLabel: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, fontWeight: 'bold', marginRight: 12 }, },
summaryValue: { fontSize: 9, fontWeight: 'bold', width: 100, textAlign: 'right' }, colPos: { width: "8%" },
totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#334155', alignItems: 'baseline' }, 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) => ( export const EstimationModule = ({
<> state,
<DocumentTitle title="Kostenschätzung" subLines={[`Datum: ${date}`, `Projekt: ${state.projectType === 'website' ? 'Website' : 'Web App'}`]} /> positions,
<PDFView style={styles.table}> totalPrice,
<PDFView style={styles.tableHeader}> date,
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText> }: any) => (
<PDFText style={[styles.headerText, styles.colDesc]}>Beschreibung</PDFText> <>
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText> <DocumentTitle
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText> title="Kostenschätzung"
</PDFView> subLines={[
{positions.map((item: any, i: number) => ( `Datum: ${date}`,
<PDFView key={i} style={styles.tableRow} wrap={false}> `Projekt: ${state.projectType === "website" ? "Website" : "Web App"}`,
<PDFText style={[styles.posText, styles.colPos]}>{item.pos.toString().padStart(2, '0')}</PDFText> ]}
<PDFView style={styles.colDesc}> isHero={true}
<PDFText style={styles.itemTitle}>{item.title}</PDFText> />
<PDFText style={styles.itemDesc}>{state.positionDescriptions?.[item.title] || item.desc}</PDFText> <PDFView style={styles.table}>
</PDFView> <PDFView style={styles.tableHeader}>
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText> <PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
<PDFText style={[styles.priceText, styles.colPrice]}>{item.price > 0 ? `${item.price.toLocaleString('de-DE')}` : 'n. A.'}</PDFText> <PDFText style={[styles.headerText, styles.colDesc]}>
</PDFView> 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>
<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>
<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.summaryContainer} wrap={false}>
<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 style={styles.summaryRow}>
</PDFView> <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 * as React from "react";
import { View as PDFView, Text as PDFText, Image as PDFImage, StyleSheet } from '@react-pdf/renderer'; import {
import { COLORS, FONT_SIZES } from '../SharedUI'; View as PDFView,
Text as PDFText,
Image as PDFImage,
StyleSheet,
} from "@react-pdf/renderer";
import { COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({ const styles = StyleSheet.create({
titlePage: { titlePage: {
flex: 1, // Fill the whole page flex: 1, // Fill the whole page
padding: 60, padding: 60,
justifyContent: 'center', justifyContent: "center",
alignItems: 'center', alignItems: "center",
backgroundColor: COLORS.WHITE, backgroundColor: COLORS.WHITE,
}, },
titleBrandIcon: { titleBrandIcon: {
width: 80, width: 80,
height: 80, height: 80,
backgroundColor: COLORS.CHARCOAL, backgroundColor: COLORS.CHARCOAL,
borderRadius: 16, borderRadius: 16,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
marginBottom: 40, marginBottom: 40,
}, },
brandIconText: { brandIconText: {
fontSize: 40, fontSize: 40,
color: COLORS.WHITE, color: COLORS.WHITE,
fontWeight: 'bold' fontWeight: "bold",
}, },
titleProjectName: { titleProjectName: {
fontSize: FONT_SIZES.H1, fontSize: FONT_SIZES.HERO,
fontWeight: 'bold', fontWeight: "bold",
color: COLORS.CHARCOAL, color: COLORS.CHARCOAL,
marginBottom: 16, marginBottom: 16,
textAlign: 'center', textAlign: "center",
maxWidth: '85%', maxWidth: "85%",
lineHeight: 1.2, lineHeight: 1.2,
}, },
titleCustomerName: { titleCustomerName: {
fontSize: FONT_SIZES.H3, fontSize: FONT_SIZES.HEADING,
color: COLORS.TEXT_DIM, color: COLORS.TEXT_DIM,
marginBottom: 40, marginBottom: 40,
textAlign: 'center', textAlign: "center",
maxWidth: '80%', maxWidth: "80%",
}, },
titleDocumentType: { titleDocumentType: {
fontSize: FONT_SIZES.BODY + 1, // ~10 fontSize: FONT_SIZES.BODY + 1, // ~10
color: COLORS.TEXT_LIGHT, color: COLORS.TEXT_LIGHT,
textTransform: 'uppercase', textTransform: "uppercase",
letterSpacing: 4, letterSpacing: 4,
marginBottom: 12, marginBottom: 12,
}, },
titleDivider: { titleDivider: {
width: 40, width: 40,
height: 2, height: 2,
backgroundColor: COLORS.CHARCOAL, backgroundColor: COLORS.CHARCOAL,
marginBottom: 40, marginBottom: 40,
}, },
titleDate: { titleDate: {
fontSize: FONT_SIZES.BODY, fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_LIGHT, color: COLORS.TEXT_LIGHT,
marginTop: 40, marginTop: 40,
}, },
}); });
export const FrontPageModule = ({ state, headerIcon, date }: any) => { 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 // Responsive font size based on length
const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22; const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22;
return ( return (
<PDFView style={styles.titlePage}> <PDFView style={styles.titlePage}>
<PDFView style={styles.titleBrandIcon}> <PDFView style={styles.titleBrandIcon}>
{headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>} {headerIcon ? (
</PDFView> <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} />
<PDFText style={[styles.titleProjectName, { fontSize }]}>{fullTitle}</PDFText> ) : (
<PDFView style={{ marginBottom: 40 }} /> <PDFText style={styles.brandIconText}>M</PDFText>
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText> )}
</PDFView> </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 * as React from "react";
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; import {
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI'; View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: { marginBottom: 32 }, section: { marginBottom: 32 },
intro: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 24, textAlign: 'justify' }, intro: {
sitemapTree: { marginTop: 8 }, fontSize: FONT_SIZES.BODY,
rootNode: { color: COLORS.TEXT_DIM,
padding: 12, lineHeight: 1.4,
backgroundColor: COLORS.GRID, marginBottom: 24,
marginBottom: 20, textAlign: "justify",
borderLeftWidth: 2, },
borderLeftColor: COLORS.CHARCOAL sitemapTree: { marginTop: 8 },
}, rootNode: {
rootTitle: { fontSize: FONT_SIZES.H3, fontWeight: 'bold', color: COLORS.CHARCOAL, letterSpacing: 0.5 }, padding: 12,
categorySection: { marginBottom: 20 }, backgroundColor: COLORS.GRID,
categoryHeader: { marginBottom: 20,
flexDirection: 'row', borderLeftWidth: 2,
alignItems: 'center', borderLeftColor: COLORS.CHARCOAL,
paddingBottom: 6, },
borderBottomWidth: 1, rootTitle: {
borderBottomColor: COLORS.BLUEPRINT, fontSize: FONT_SIZES.HEADING,
marginBottom: 10 fontWeight: "bold",
}, color: COLORS.CHARCOAL,
categoryIcon: { width: 8, height: 8, backgroundColor: COLORS.GRID, borderInlineWidth: 1, borderColor: COLORS.DIVIDER, marginRight: 10 }, letterSpacing: 0.5,
categoryTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, textTransform: 'uppercase', letterSpacing: 1 }, },
pagesGrid: { flexDirection: 'row', flexWrap: 'wrap' }, categorySection: { marginBottom: 20 },
pageCard: { categoryHeader: {
width: '48%', flexDirection: "row",
marginRight: '2%', alignItems: "center",
marginBottom: 12, paddingBottom: 6,
padding: 10, borderBottomWidth: 1,
borderWidth: 1, borderBottomColor: COLORS.BLUEPRINT,
borderColor: COLORS.GRID, marginBottom: 10,
backgroundColor: '#fafafa' },
}, categoryIcon: {
pageTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN, marginBottom: 2 }, width: 8,
pageDesc: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_DIM, lineHeight: 1.4 }, 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) => ( export const SitemapModule = ({ state }: any) => (
<> <>
<DocumentTitle title="Informationsarchitektur" /> <DocumentTitle title="Informationsarchitektur" isHero={true} />
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFText style={styles.intro}> <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. Die folgende Struktur definiert die logische Hierarchie und
</PDFText> 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.sitemapTree}>
<PDFView style={styles.rootNode}> <PDFView style={styles.rootNode}>
<PDFText style={styles.rootTitle}>Seitenstruktur</PDFText> <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> </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 fs from "fs";
import path from 'path'; import path from "path";
const DOCS_DIR = path.join(process.cwd(), 'docs'); const DOCS_DIR = path.join(process.cwd(), "docs");
export function getTechDetails() { export function getTechDetails() {
try { try {
const content = fs.readFileSync(path.join(DOCS_DIR, 'TECH.md'), 'utf-8'); const content = fs.readFileSync(path.join(DOCS_DIR, "TECH.md"), "utf-8");
const sections = content.split('⸻').map(s => s.trim()); const sections = content.split("⸻").map((s) => s.trim());
// Extract items (Speed, Responsive, Stability, etc.) // Extract items (Speed, Responsive, Stability, etc.)
// Logic: Look for section headers and their summaries // Logic: Look for section headers and their summaries
const items = [ 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: "Maximale Ladegeschwindigkeit & SEO-Vorsprung",
{ t: 'Stabilität & Betriebssicherheit', d: 'Im Hintergrund laufen Überwachungs- und Kontrollmechanismen, die technische Probleme automatisch erkennen, bevor sie zum Risiko werden.' }, 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: '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.' } 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; return items;
} catch (e) { } catch (e) {
console.error('Failed to read TECH.md', e); console.error("Failed to read TECH.md", e);
return []; return [];
} }
} }
export function getPrinciples() { export function getPrinciples() {
try { try {
const content = fs.readFileSync(path.join(DOCS_DIR, 'PRINCIPLES.md'), 'utf-8'); const content = fs.readFileSync(
// Simplified extraction for now, mirroring the structure in the PDF path.join(DOCS_DIR, "PRINCIPLES.md"),
const principles = [ "utf-8",
{ 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.' }, // Simplified extraction for now, mirroring the structure in the PDF
{ 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.' }, const principles = [
{ 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: "1. Volle Preis-Transparenz",
{ t: '6. Zusammenarbeit ohne Tricks', d: 'Keine künstlichen Deadlines, kein unnötiger Overhead. Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.' } 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.",
]; },
return principles; {
} catch (e) { t: "2. Quellcode & Projektzugang",
console.error('Failed to read PRINCIPLES.md', e); d: "Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code. Damit kann jeder andere Entwickler problemlos weiterarbeiten.",
return []; },
} {
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 { FormState, Position, Totals } from "./types";
import { FEATURE_LABELS, FUNCTION_LABELS, API_LABELS, PAGE_LABELS } from './constants'; import {
FEATURE_LABELS,
FUNCTION_LABELS,
API_LABELS,
PAGE_LABELS,
} from "./constants";
export function calculateTotals(state: FormState, pricing: any): Totals { export function calculateTotals(state: FormState, pricing: any): Totals {
if (state.projectType !== 'website') { 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);
return { return {
totalPrice: Math.round(total), totalPrice: 0,
monthlyPrice: Math.round(monthlyPrice), monthlyPrice: 0,
totalPagesCount, totalPagesCount: 0,
totalFeatures, totalFeatures: 0,
totalFunctions, totalFunctions: 0,
totalApis, totalApis: 0,
languagesCount 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[] { export function calculatePositions(state: FormState, pricing: any): Position[] {
const positions: Position[] = []; const positions: Position[] = [];
let pos = 1; let pos = 1;
if (state.projectType === 'website') { if (state.projectType === "website") {
positions.push({ positions.push({
pos: pos++, pos: pos++,
title: 'Das technische Fundament', title: "Das technische Fundament",
desc: 'Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.', desc: "Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.",
qty: 1, qty: 1,
price: pricing.BASE_WEBSITE price: pricing.BASE_WEBSITE,
}); });
const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0; const sitemapPagesCount =
const totalPagesCount = Math.max( state.sitemap?.reduce(
(state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0), (acc: number, cat: any) => acc + (cat.pages?.length || 0),
sitemapPagesCount 0,
); ) || 0;
const totalPagesCount = Math.max(
(state.selectedPages?.length || 0) +
(state.otherPages?.length || 0) +
(state.otherPagesCount || 0),
sitemapPagesCount,
);
const allPages = [ const allPages = [
...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p), ...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p),
...(state.otherPages || []), ...(state.otherPages || []),
...(state.sitemap?.flatMap((cat: any) => cat.pages?.map((p: any) => p.title)) || []) ...(state.sitemap?.flatMap((cat: any) =>
]; cat.pages?.map((p: any) => p.title),
) || []),
];
// Deduplicate labels // Deduplicate labels
const uniquePages = Array.from(new Set(allPages)); const uniquePages = Array.from(new Set(allPages));
positions.push({ positions.push({
pos: pos++, pos: pos++,
title: 'Individuelle Seiten', title: "Individuelle Seiten",
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(', ')}).`, desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(", ")}).`,
qty: totalPagesCount, qty: totalPagesCount,
price: totalPagesCount * pricing.PAGE price: totalPagesCount * pricing.PAGE,
}); });
if (state.features.length > 0 || (state.otherFeatures?.length || 0) > 0) { if (state.features.length > 0 || (state.otherFeatures?.length || 0) > 0) {
const allFeatures = [...state.features.map((f: string) => FEATURE_LABELS[f] || f), ...(state.otherFeatures || [])]; const allFeatures = [
positions.push({ ...state.features.map((f: string) => FEATURE_LABELS[f] || f),
pos: pos++, ...(state.otherFeatures || []),
title: 'System-Module (Features)', ];
desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(', ')}. Inklusive Datenstruktur und Darstellung.`, positions.push({
qty: allFeatures.length, pos: pos++,
price: allFeatures.length * pricing.FEATURE 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
});
} }
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 = { export const PRICING = {
BASE_WEBSITE: 4000, BASE_WEBSITE: 4000,
PAGE: 600, PAGE: 600,
FEATURE: 1500, FEATURE: 1500,
FUNCTION: 800, FUNCTION: 800,
NEW_DATASET: 200, NEW_DATASET: 450,
HOSTING_MONTHLY: 250, HOSTING_MONTHLY: 250,
STORAGE_EXPANSION_MONTHLY: 10, STORAGE_EXPANSION_MONTHLY: 10,
CMS_SETUP: 1500, CMS_SETUP: 1500,
CMS_CONNECTION_PER_FEATURE: 800, CMS_CONNECTION_PER_FEATURE: 1500,
API_INTEGRATION: 800, API_INTEGRATION: 800,
APP_HOURLY: 120, APP_HOURLY: 120,
VISUAL_STAGING: 2000,
COMPLEX_INTERACTION: 1500,
}; };
export const initialState: FormState = { export const initialState: FormState = {
projectType: 'website', projectType: "website",
// Company // Company
companyName: '', companyName: "",
employeeCount: '', employeeCount: "",
// Existing Presence // Existing Presence
existingWebsite: '', existingWebsite: "",
socialMedia: [], socialMedia: [],
socialMediaUrls: {}, socialMediaUrls: {},
existingDomain: '', existingDomain: "",
wishedDomain: '', wishedDomain: "",
// Project // Project
websiteTopic: '', websiteTopic: "",
selectedPages: ['Home'], selectedPages: ["Home"],
otherPages: [], otherPages: [],
otherPagesCount: 0, otherPagesCount: 0,
features: [], features: [],
otherFeatures: [], otherFeatures: [],
otherFeaturesCount: 0, otherFeaturesCount: 0,
functions: [], functions: [],
otherFunctions: [], otherFunctions: [],
otherFunctionsCount: 0, otherFunctionsCount: 0,
apiSystems: [], apiSystems: [],
otherTech: [], otherTech: [],
otherTechCount: 0, otherTechCount: 0,
assets: [], assets: [],
otherAssets: [], otherAssets: [],
otherAssetsCount: 0, otherAssetsCount: 0,
newDatasets: 0, newDatasets: 0,
cmsSetup: false, cmsSetup: false,
storageExpansion: 0, storageExpansion: 0,
name: '', name: "",
email: '', email: "",
role: '', role: "",
message: '', message: "",
sitemapFile: null, sitemapFile: null,
contactFiles: [], contactFiles: [],
// Design // Design
designVibe: 'minimal', designVibe: "minimal",
colorScheme: ['#ffffff', '#f8fafc', '#0f172a'], colorScheme: ["#ffffff", "#f8fafc", "#0f172a"],
references: [], references: [],
designWishes: '', designWishes: "",
// Maintenance // Maintenance
expectedAdjustments: 'low', expectedAdjustments: "low",
languagesList: ['Deutsch'], languagesList: ["Deutsch"],
personName: '', personName: "",
// Timeline // Timeline
deadline: 'flexible', deadline: "flexible",
// Web App specific // Web App specific
targetAudience: 'internal', targetAudience: "internal",
userRoles: [], userRoles: [],
dataSensitivity: 'standard', dataSensitivity: "standard",
platformType: 'web-only', platformType: "web-only",
// Meta // Meta
dontKnows: [], dontKnows: [],
visualStaging: 'standard', visualStaging: "standard",
complexInteractions: 'standard', complexInteractions: "standard",
// AI generated / Post-processed // AI generated / Post-processed
briefingSummary: '', briefingSummary: "",
designVision: '', designVision: "",
positionDescriptions: {}, positionDescriptions: {},
taxId: '', taxId: "",
sitemap: [], sitemap: [],
}; };
export const PAGE_SAMPLES = [ export const PAGE_SAMPLES = [
{ id: 'Home', label: 'Startseite', desc: 'Der erste Eindruck Ihrer Marke.' }, { id: "Home", label: "Startseite", desc: "Der erste Eindruck Ihrer Marke." },
{ id: 'About', label: 'Über uns', desc: 'Ihre Geschichte und Ihr Team.' }, { id: "About", label: "Über uns", desc: "Ihre Geschichte und Ihr Team." },
{ id: 'Services', label: 'Leistungen', desc: 'Übersicht Ihres Angebots.' }, { id: "Services", label: "Leistungen", desc: "Übersicht Ihres Angebots." },
{ id: 'Contact', label: 'Kontakt', desc: 'Anlaufstelle für Ihre Kunden.' }, { 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: "Landing",
label: "Landingpage",
desc: "Optimiert für Marketing-Kampagnen.",
},
{ id: "Legal", label: "Rechtliches", desc: "Impressum & Datenschutz." },
]; ];
export const FEATURE_OPTIONS = [ 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: "blog_news",
{ id: 'jobs', label: 'Karriere / Jobs', desc: 'Stellenanzeigen und Bewerbungsoptionen.' }, label: "Blog / News",
{ id: 'refs', label: 'Referenzen / Cases', desc: 'Präsentation Ihrer Projekte.' }, desc: "Ein Bereich für aktuelle Beiträge und Neuigkeiten.",
{ id: 'events', label: 'Events / Termine', desc: 'Veranstaltungskalender.' }, },
{
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 = [ export const FUNCTION_OPTIONS = [
{ id: 'search', label: 'Suche', desc: 'Volltextsuche über alle Inhalte.' }, { 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: "filter",
{ id: 'forms', label: 'Individuelle Formular-Logik', desc: 'Smarte Validierung & mehrstufige Prozesse.' }, 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 = [ 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: "crm",
{ id: 'stripe', label: 'Stripe / Payment', desc: 'Zahlungsabwicklung und Abonnements.' }, label: "CRM System",
{ id: 'newsletter', label: 'Newsletter / Marketing', desc: 'Mailchimp, Brevo, ActiveCampaign etc.' }, desc: "HubSpot, Salesforce, Pipedrive 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: "erp",
{ id: 'calendar', label: 'Termine / Booking', desc: 'Calendly, Shore, Doctolib etc.' }, label: "ERP / Warenwirtschaft",
{ id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' }, desc: "SAP, Microsoft Dynamics, Xentral etc.",
{ id: 'maps', label: 'Google Maps / Places', desc: 'Standortsuche und Kartenintegration.' }, },
{ id: 'analytics', label: 'Custom Analytics', desc: 'Anbindung an spezialisierte Tracking-Tools.' }, {
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 = [ 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: "existing_website",
{ id: 'styleguide', label: 'Styleguide', desc: 'Farben, Schriften, Design-Vorgaben.' }, label: "Bestehende Website",
{ id: 'content_concept', label: 'Inhalts-Konzept', desc: 'Struktur und Texte sind bereits geplant.' }, desc: "Inhalte oder Struktur können übernommen werden.",
{ id: 'media', label: 'Bild/Video-Material', desc: 'Professionelles Bildmaterial vorhanden.' }, },
{ id: 'icons', label: 'Icons', desc: 'Eigene Icon-Sets vorhanden.' }, { id: "logo", label: "Logo", desc: "Vektordatei Ihres Logos." },
{ id: 'illustrations', label: 'Illustrationen', desc: 'Eigene Illustrationen vorhanden.' }, {
{ id: 'fonts', label: 'Fonts', desc: 'Lizenzen für Hausschriften vorhanden.' }, 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 = [ 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: "minimal",
{ id: 'nature', label: 'Natürlich', desc: 'Sanfte Erdtöne, organische Formen.' }, label: "Minimalistisch",
{ id: 'tech', label: 'Technisch', desc: 'Präzise Linien, dunkle Akzente.' }, 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 = [ export const EMPLOYEE_OPTIONS = [
{ id: '1-5', label: '1-5 Mitarbeiter' }, { id: "1-5", label: "1-5 Mitarbeiter" },
{ id: '6-20', label: '6-20 Mitarbeiter' }, { id: "6-20", label: "6-20 Mitarbeiter" },
{ id: '21-100', label: '21-100 Mitarbeiter' }, { id: "21-100", label: "21-100 Mitarbeiter" },
{ id: '100+', label: '100+ Mitarbeiter' }, { id: "100+", label: "100+ Mitarbeiter" },
]; ];
export const SOCIAL_MEDIA_OPTIONS = [ export const SOCIAL_MEDIA_OPTIONS = [
{ id: 'instagram', label: 'Instagram' }, { id: "instagram", label: "Instagram" },
{ id: 'linkedin', label: 'LinkedIn' }, { id: "linkedin", label: "LinkedIn" },
{ id: 'facebook', label: 'Facebook' }, { id: "facebook", label: "Facebook" },
{ id: 'twitter', label: 'Twitter / X' }, { id: "twitter", label: "Twitter / X" },
{ id: 'tiktok', label: 'TikTok' }, { id: "tiktok", label: "TikTok" },
{ id: 'youtube', label: 'YouTube' }, { id: "youtube", label: "YouTube" },
]; ];
export const VIBE_LABELS: Record<string, string> = { export const VIBE_LABELS: Record<string, string> = {
minimal: 'Minimalistisch', minimal: "Minimalistisch",
bold: 'Mutig & Laut', bold: "Mutig & Laut",
nature: 'Natürlich', nature: "Natürlich",
tech: 'Technisch' tech: "Technisch",
}; };
export const DEADLINE_LABELS: Record<string, string> = { export const DEADLINE_LABELS: Record<string, string> = {
asap: 'So schnell wie möglich', asap: "So schnell wie möglich",
'2-3-months': 'In 2-3 Monaten', "2-3-months": "In 2-3 Monaten",
'3-6-months': 'In 3-6 Monaten', "3-6-months": "In 3-6 Monaten",
flexible: 'Flexibel' flexible: "Flexibel",
}; };
export const ASSET_LABELS: Record<string, string> = { export const ASSET_LABELS: Record<string, string> = {
existing_website: 'Bestehende Website', existing_website: "Bestehende Website",
logo: 'Logo', logo: "Logo",
styleguide: 'Styleguide', styleguide: "Styleguide",
content_concept: 'Inhalts-Konzept', content_concept: "Inhalts-Konzept",
media: 'Bild/Video-Material', media: "Bild/Video-Material",
icons: 'Icons', icons: "Icons",
illustrations: 'Illustrationen', illustrations: "Illustrationen",
fonts: 'Fonts' fonts: "Fonts",
}; };
export const FEATURE_LABELS: Record<string, string> = { export const FEATURE_LABELS: Record<string, string> = {
blog_news: 'Blog / News', blog_news: "Blog / News",
products: 'Produktbereich', products: "Produktbereich",
jobs: 'Karriere / Jobs', jobs: "Karriere / Jobs",
refs: 'Referenzen / Cases', refs: "Referenzen / Cases",
events: 'Events / Termine' events: "Events / Termine",
}; };
export const FUNCTION_LABELS: Record<string, string> = { export const FUNCTION_LABELS: Record<string, string> = {
search: 'Suche', search: "Suche",
filter: 'Filter-Systeme', filter: "Filter-Systeme",
pdf: 'PDF-Export', pdf: "PDF-Export",
forms: 'Individuelle Formular-Logik', forms: "Individuelle Formular-Logik",
members: 'Mitgliederbereich', members: "Mitgliederbereich",
calendar: 'Event-Kalender', calendar: "Event-Kalender",
multilang: 'Mehrsprachigkeit', multilang: "Mehrsprachigkeit",
chat: 'Echtzeit-Chat' chat: "Echtzeit-Chat",
}; };
export const API_LABELS: Record<string, string> = { export const API_LABELS: Record<string, string> = {
crm_erp: 'CRM / ERP', crm_erp: "CRM / ERP",
payment: 'Payment', payment: "Payment",
marketing: 'Marketing', marketing: "Marketing",
ecommerce: 'E-Commerce', ecommerce: "E-Commerce",
maps: 'Google Maps / Places', maps: "Google Maps / Places",
social: 'Social Media Sync', social: "Social Media Sync",
analytics: 'Custom Analytics' analytics: "Custom Analytics",
}; };
export const SOCIAL_LABELS: Record<string, string> = { export const SOCIAL_LABELS: Record<string, string> = {
instagram: 'Instagram', instagram: "Instagram",
linkedin: 'LinkedIn', linkedin: "LinkedIn",
facebook: 'Facebook', facebook: "Facebook",
twitter: 'Twitter / X', twitter: "Twitter / X",
tiktok: 'TikTok', tiktok: "TikTok",
youtube: 'YouTube' youtube: "YouTube",
}; };
export const PAGE_LABELS: Record<string, string> = { export const PAGE_LABELS: Record<string, string> = {
Home: 'Startseite', Home: "Startseite",
About: 'Über uns', About: "Über uns",
Services: 'Leistungen', Services: "Leistungen",
Contact: 'Kontakt', Contact: "Kontakt",
Landing: 'Landingpage', Landing: "Landingpage",
Legal: 'Impressum & Datenschutz' Legal: "Impressum & Datenschutz",
}; };