refactor: estimation generation
All checks were successful
Build & Deploy / 🔍 Prepare Environment (push) Successful in 32s
Build & Deploy / 🏗️ Build (push) Successful in 4m41s
Build & Deploy / 🧪 QA (push) Successful in 5m56s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🔔 Notifications (push) Successful in 1s
Build & Deploy / ⚡ PageSpeed (push) Successful in 55s
All checks were successful
Build & Deploy / 🔍 Prepare Environment (push) Successful in 32s
Build & Deploy / 🏗️ Build (push) Successful in 4m41s
Build & Deploy / 🧪 QA (push) Successful in 5m56s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🔔 Notifications (push) Successful in 1s
Build & Deploy / ⚡ PageSpeed (push) Successful in 55s
This commit is contained in:
35
apps/web/docs/MAINTENANCE.md
Normal file
35
apps/web/docs/MAINTENANCE.md
Normal 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.
|
||||
36
apps/web/docs/STANDARDS.md
Normal file
36
apps/web/docs/STANDARDS.md
Normal 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 70–90 % 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
@@ -1,131 +1,165 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as readline from 'node:readline/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createElement } from 'react';
|
||||
import { renderToFile } from '@react-pdf/renderer';
|
||||
import { calculatePositions, calculateTotals } 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 } from '../src/logic/content-provider.js';
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as readline from "node:readline/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createElement } from "react";
|
||||
import { renderToFile } from "@react-pdf/renderer";
|
||||
import {
|
||||
calculatePositions,
|
||||
calculateTotals,
|
||||
} 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 __dirname = path.dirname(__filename);
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const isInteractive = args.includes('--interactive') || args.includes('-I');
|
||||
const isEstimationOnly = args.includes('--estimation') || args.includes('-E');
|
||||
const inputPath = args.find((_, i) => args[i - 1] === '--input' || args[i - 1] === '-i');
|
||||
const args = process.argv.slice(2);
|
||||
const isInteractive = args.includes("--interactive") || args.includes("-I");
|
||||
const isEstimationOnly = args.includes("--estimation") || args.includes("-E");
|
||||
const inputPath = args.find(
|
||||
(_, i) => args[i - 1] === "--input" || args[i - 1] === "-i",
|
||||
);
|
||||
|
||||
let state = { ...initialState };
|
||||
let state = { ...initialState };
|
||||
|
||||
if (inputPath) {
|
||||
const rawData = fs.readFileSync(path.resolve(process.cwd(), inputPath), 'utf8');
|
||||
const diskState = JSON.parse(rawData);
|
||||
state = { ...state, ...diskState };
|
||||
}
|
||||
|
||||
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
|
||||
if (inputPath) {
|
||||
const rawData = fs.readFileSync(
|
||||
path.resolve(process.cwd(), inputPath),
|
||||
"utf8",
|
||||
);
|
||||
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) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
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 answer = await rl.question(`${q}${def ? ` [${def}]` : ''}: `);
|
||||
return answer || def || '';
|
||||
};
|
||||
const ask = async (q: string, def?: string) => {
|
||||
const answer = await rl.question(`${q}${def ? ` [${def}]` : ""}: `);
|
||||
return answer || def || "";
|
||||
};
|
||||
|
||||
const selectOne = async (q: string, options: { id: string, label: string }[]) => {
|
||||
console.log(`\n${q}:`);
|
||||
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;
|
||||
};
|
||||
const selectOne = async (
|
||||
q: string,
|
||||
options: { id: string; label: string }[],
|
||||
) => {
|
||||
console.log(`\n${q}:`);
|
||||
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.email = await ask('Recipient Email', state.email);
|
||||
state.companyName = await ask('Company Name', state.companyName);
|
||||
state.name = await ask("Recipient Name", state.name);
|
||||
state.email = await ask("Recipient Email", state.email);
|
||||
state.companyName = await ask("Company Name", state.companyName);
|
||||
|
||||
state.projectType = await selectOne('Project Type', [
|
||||
{ id: 'website', label: 'Website' },
|
||||
{ id: 'web-app', label: 'Web App' }
|
||||
]);
|
||||
state.projectType = await selectOne("Project Type", [
|
||||
{ id: "website", label: "Website" },
|
||||
{ id: "web-app", label: "Web App" },
|
||||
]);
|
||||
|
||||
if (state.projectType === 'website') {
|
||||
state.websiteTopic = await ask('Website Topic', state.websiteTopic);
|
||||
// Simplified for now, in a real tool we'd loop through all options
|
||||
}
|
||||
if (state.projectType === "website") {
|
||||
state.websiteTopic = await ask("Website Topic", state.websiteTopic);
|
||||
// Simplified for now, in a real tool we'd loop through all options
|
||||
}
|
||||
|
||||
rl.close();
|
||||
return state;
|
||||
rl.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
function generateDefaultPath(state: any) {
|
||||
const now = new Date();
|
||||
const month = now.toISOString().slice(0, 7);
|
||||
const day = now.toISOString().slice(0, 10);
|
||||
// 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 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`);
|
||||
const now = new Date();
|
||||
const month = now.toISOString().slice(0, 7);
|
||||
const day = now.toISOString().slice(0, 10);
|
||||
// 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 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 => {
|
||||
console.error('❌ Error:', err);
|
||||
process.exit(1);
|
||||
main().catch((err) => {
|
||||
console.error("❌ Error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,24 +1,80 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { Document as PDFDocument } from '@react-pdf/renderer';
|
||||
import { EstimationPDF } from './EstimationPDF';
|
||||
import { AgbsPDF } from './AgbsPDF';
|
||||
import * as React from "react";
|
||||
import { Document as PDFDocument } from "@react-pdf/renderer";
|
||||
import { EstimationPDF } from "./EstimationPDF";
|
||||
import { AgbsPDF } from "./AgbsPDF";
|
||||
import { ClosingModule } from "./pdf/modules/CommonModules";
|
||||
import { SimpleLayout } from "./pdf/SimpleLayout";
|
||||
|
||||
interface CombinedProps {
|
||||
estimationProps: any;
|
||||
showAgbs?: boolean;
|
||||
techDetails?: any[];
|
||||
principles?: any[];
|
||||
estimationProps: any;
|
||||
showAgbs?: boolean;
|
||||
techDetails?: any[];
|
||||
principles?: any[];
|
||||
maintenanceDetails?: any[];
|
||||
standardsDetails?: any[];
|
||||
}
|
||||
|
||||
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true, techDetails, principles, mode = 'full' }: CombinedProps & { mode?: 'estimation' | 'full' }) => {
|
||||
return (
|
||||
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}>
|
||||
<EstimationPDF {...estimationProps} mode={mode} techDetails={techDetails} principles={principles} />
|
||||
{showAgbs && (
|
||||
<AgbsPDF mode={mode} state={estimationProps.state} headerIcon={estimationProps.headerIcon} footerLogo={estimationProps.footerLogo} />
|
||||
)}
|
||||
</PDFDocument>
|
||||
);
|
||||
export const CombinedQuotePDF = ({
|
||||
estimationProps,
|
||||
showAgbs = true,
|
||||
techDetails,
|
||||
principles,
|
||||
maintenanceDetails,
|
||||
standardsDetails,
|
||||
mode = "full",
|
||||
}: CombinedProps & { mode?: "estimation" | "full" }) => {
|
||||
const date = new Date().toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const companyData = {
|
||||
name: "Marc Mintel",
|
||||
address1: "Georg-Meistermann-Straße 7",
|
||||
address2: "54586 Schüller",
|
||||
ustId: "DE367588065",
|
||||
};
|
||||
|
||||
const bankData = {
|
||||
name: "N26",
|
||||
bic: "NTSBDEB1XXX",
|
||||
iban: "DE50 1001 1001 2620 4328 65",
|
||||
};
|
||||
|
||||
const layoutProps = {
|
||||
date,
|
||||
icon: estimationProps.headerIcon,
|
||||
footerLogo: estimationProps.footerLogo,
|
||||
companyData,
|
||||
bankData,
|
||||
};
|
||||
|
||||
return (
|
||||
<PDFDocument
|
||||
title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}
|
||||
>
|
||||
<EstimationPDF
|
||||
{...estimationProps}
|
||||
mode={mode}
|
||||
techDetails={techDetails}
|
||||
principles={principles}
|
||||
maintenanceDetails={maintenanceDetails}
|
||||
standardsDetails={standardsDetails}
|
||||
/>
|
||||
{showAgbs && (
|
||||
<AgbsPDF
|
||||
mode={mode}
|
||||
state={estimationProps.state}
|
||||
headerIcon={estimationProps.headerIcon}
|
||||
footerLogo={estimationProps.footerLogo}
|
||||
/>
|
||||
)}
|
||||
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
|
||||
<ClosingModule />
|
||||
</SimpleLayout>
|
||||
</PDFDocument>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState, Totals } from '../types';
|
||||
import { PRICING } from '../constants';
|
||||
import { AnimatedNumber } from './AnimatedNumber';
|
||||
import { ConceptPrice, ConceptAutomation } from '../../Landing/ConceptIllustrations';
|
||||
import { Info, Download, Share2, RefreshCw } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import dynamic from 'next/dynamic';
|
||||
import * as React from "react";
|
||||
import { FormState, Totals } from "../types";
|
||||
import { PRICING } from "../constants";
|
||||
import { AnimatedNumber } from "./AnimatedNumber";
|
||||
import {
|
||||
ConceptPrice,
|
||||
ConceptAutomation,
|
||||
} from "../../Landing/ConceptIllustrations";
|
||||
import { Info, Download, Share2, RefreshCw } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import dynamic from "next/dynamic";
|
||||
// EstimationPDF will be imported dynamically where used or inside the and client-side block
|
||||
import IconWhite from '../../../assets/logo/Icon White Transparent.png';
|
||||
import LogoBlack from '../../../assets/logo/Logo Black Transparent.png';
|
||||
import IconWhite from "../../../assets/logo/Icon White Transparent.png";
|
||||
import LogoBlack from "../../../assets/logo/Logo Black Transparent.png";
|
||||
|
||||
// PDF components removed from top-level dynamic import to fix ESM resolution issues in Next.js 16/Webpack
|
||||
|
||||
@@ -27,9 +30,17 @@ export function PriceCalculation({
|
||||
totals,
|
||||
isClient,
|
||||
qrCodeData,
|
||||
onShare
|
||||
onShare,
|
||||
}: PriceCalculationProps) {
|
||||
const { totalPrice, monthlyPrice, totalPagesCount, totalFeatures, totalFunctions, totalApis, languagesCount } = totals;
|
||||
const {
|
||||
totalPrice,
|
||||
monthlyPrice,
|
||||
totalPagesCount,
|
||||
totalFeatures,
|
||||
totalFunctions,
|
||||
totalApis,
|
||||
languagesCount,
|
||||
} = totals;
|
||||
const totalPages = totalPagesCount;
|
||||
|
||||
const [pdfLoading, setPdfLoading] = React.useState(false);
|
||||
@@ -40,35 +51,41 @@ export function PriceCalculation({
|
||||
setPdfLoading(true);
|
||||
|
||||
try {
|
||||
const { EstimationPDF } = await import('../../EstimationPDF');
|
||||
const doc = <EstimationPDF
|
||||
state={state}
|
||||
totalPrice={totalPrice}
|
||||
monthlyPrice={monthlyPrice}
|
||||
totalPagesCount={totalPagesCount}
|
||||
pricing={PRICING}
|
||||
headerIcon={typeof IconWhite === 'string' ? IconWhite : (IconWhite as any).src}
|
||||
footerLogo={typeof LogoBlack === 'string' ? LogoBlack : (LogoBlack as any).src}
|
||||
/>;
|
||||
const { EstimationPDF } = await import("../../EstimationPDF");
|
||||
const doc = (
|
||||
<EstimationPDF
|
||||
state={state}
|
||||
totalPrice={totalPrice}
|
||||
monthlyPrice={monthlyPrice}
|
||||
totalPagesCount={totalPagesCount}
|
||||
pricing={PRICING}
|
||||
headerIcon={
|
||||
typeof IconWhite === "string" ? IconWhite : (IconWhite as any).src
|
||||
}
|
||||
footerLogo={
|
||||
typeof LogoBlack === "string" ? LogoBlack : (LogoBlack as any).src
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const { pdf } = await import('@react-pdf/renderer');
|
||||
const { pdf } = await import("@react-pdf/renderer");
|
||||
|
||||
// Minimum loading time of 2 seconds for better UX
|
||||
const [blob] = await Promise.all([
|
||||
pdf(doc).toBlob(),
|
||||
new Promise(resolve => setTimeout(resolve, 2000))
|
||||
new Promise((resolve) => setTimeout(resolve, 2000)),
|
||||
]);
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `kalkulation-${state.name.toLowerCase().replace(/\s+/g, '-') || 'projekt'}.pdf`;
|
||||
link.download = `kalkulation-${state.name.toLowerCase().replace(/\s+/g, "-") || "projekt"}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('PDF generation failed:', error);
|
||||
console.error("PDF generation failed:", error);
|
||||
} finally {
|
||||
setPdfLoading(false);
|
||||
}
|
||||
@@ -78,20 +95,93 @@ export function PriceCalculation({
|
||||
<div className="lg:col-span-4 lg:sticky lg:top-32 self-start z-30">
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-[3rem] p-6 space-y-6">
|
||||
<div className="space-y-6">
|
||||
{state.projectType === 'website' ? (
|
||||
{state.projectType === "website" ? (
|
||||
<>
|
||||
<div className="space-y-4 overflow-y-auto pr-2 hide-scrollbar max-h-[120px]">
|
||||
{totalPages > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalPages}x Seite</span><span className="font-medium text-slate-900">{(totalPages * PRICING.PAGE).toLocaleString()} €</span></div>)}
|
||||
{totalFeatures > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalFeatures}x System-Modul</span><span className="font-medium text-slate-900">{(totalFeatures * PRICING.FEATURE).toLocaleString()} €</span></div>)}
|
||||
{totalFunctions > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalFunctions}x Logik-Funktion</span><span className="font-medium text-slate-900">{(totalFunctions * PRICING.FUNCTION).toLocaleString()} €</span></div>)}
|
||||
{totalApis > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalApis}x API Sync</span><span className="font-medium text-slate-900">{(totalApis * PRICING.API_INTEGRATION).toLocaleString()} €</span></div>)}
|
||||
{state.cmsSetup && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">CMS Setup & Anbindung</span><span className="font-medium text-slate-900">{(PRICING.CMS_SETUP + totalFeatures * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} €</span></div>)}
|
||||
{state.newDatasets > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.newDatasets}x Inhalte einpflegen</span><span className="font-medium text-slate-900">{(state.newDatasets * PRICING.NEW_DATASET).toLocaleString()} €</span></div>)}
|
||||
{languagesCount > 1 && (<div className="flex justify-between items-center text-sm text-slate-900 font-bold pt-2 border-t border-slate-100"><span className="text-slate-500">Mehrsprachigkeit ({languagesCount}x)</span><span>+{(totalPrice - (totalPrice / (1 + (languagesCount - 1) * 0.2))).toLocaleString()} €</span></div>)}
|
||||
{totalPages > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">{totalPages}x Seite</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{(totalPages * PRICING.PAGE).toLocaleString()} €
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{totalFeatures > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">
|
||||
{totalFeatures}x System-Modul
|
||||
</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{(totalFeatures * PRICING.FEATURE).toLocaleString()} €
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{totalFunctions > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">
|
||||
{totalFunctions}x Logik-Funktion
|
||||
</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{(totalFunctions * PRICING.FUNCTION).toLocaleString()} €
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{totalApis > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">
|
||||
{totalApis}x API Sync
|
||||
</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{(totalApis * PRICING.API_INTEGRATION).toLocaleString()} €
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{state.cmsSetup && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">Inhalts-Verwaltung</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{(
|
||||
Math.max(1, totalFeatures) *
|
||||
PRICING.CMS_CONNECTION_PER_FEATURE
|
||||
).toLocaleString()}{" "}
|
||||
€
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{state.newDatasets > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">
|
||||
{state.newDatasets}x Inhalte einpflegen
|
||||
</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{(
|
||||
state.newDatasets * PRICING.NEW_DATASET
|
||||
).toLocaleString()}{" "}
|
||||
€
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{languagesCount > 1 && (
|
||||
<div className="flex justify-between items-center text-sm text-slate-900 font-bold pt-2 border-t border-slate-100">
|
||||
<span className="text-slate-500">
|
||||
Mehrsprachigkeit ({languagesCount}x)
|
||||
</span>
|
||||
<span>
|
||||
+
|
||||
{(
|
||||
totalPrice -
|
||||
totalPrice / (1 + (languagesCount - 1) * 0.2)
|
||||
).toLocaleString()}{" "}
|
||||
€
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4 space-y-2">
|
||||
<div className="flex justify-between items-end">
|
||||
<span className="text-lg font-bold text-slate-900">Gesamt</span>
|
||||
<span className="text-lg font-bold text-slate-900">
|
||||
Gesamt
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold tracking-tighter text-slate-900">
|
||||
<AnimatedNumber value={totalPrice} /> €
|
||||
@@ -101,8 +191,12 @@ export function PriceCalculation({
|
||||
</div>
|
||||
<div className="pt-4 border-t border-slate-200 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600 font-medium text-sm">Sorglos-Paket</span>
|
||||
<span className="text-base font-bold text-slate-900">{monthlyPrice.toLocaleString()} € / Monat</span>
|
||||
<span className="text-slate-600 font-medium text-sm">
|
||||
Sorglos Betrieb (Hosting + Support)
|
||||
</span>
|
||||
<span className="text-base font-bold text-slate-900">
|
||||
{(monthlyPrice * 12).toLocaleString()} € / Jahr
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,8 +263,15 @@ export function PriceCalculation({
|
||||
<ConceptAutomation className="w-10 h-10 text-black" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-slate-600 text-xs leading-relaxed">Web Apps werden nach Aufwand abgerechnet.</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{PRICING.APP_HOURLY} € <span className="text-base text-slate-400 font-normal">/ Std.</span></p>
|
||||
<p className="text-slate-600 text-xs leading-relaxed">
|
||||
Web Apps werden nach Aufwand abgerechnet.
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-slate-900">
|
||||
{PRICING.APP_HOURLY} €{" "}
|
||||
<span className="text-base text-slate-400 font-normal">
|
||||
/ Std.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{onShare && (
|
||||
@@ -186,7 +287,10 @@ export function PriceCalculation({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] leading-relaxed text-slate-400 italic text-center">Ein verbindliches Angebot erstelle ich nach einem persönlichen Gespräch.</p>
|
||||
<p className="text-[10px] leading-relaxed text-slate-400 italic text-center">
|
||||
Ein verbindliches Angebot erstelle ich nach einem persönlichen
|
||||
Gespräch.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { calculatePositions } from '../logic/pricing';
|
||||
import { Page as PDFPage } from '@react-pdf/renderer';
|
||||
import { pdfStyles } from './pdf/SharedUI';
|
||||
import { DINLayout } from './pdf/DINLayout';
|
||||
import { SimpleLayout } from './pdf/SimpleLayout';
|
||||
import * as React from "react";
|
||||
import { calculatePositions } from "../logic/pricing";
|
||||
import { Page as PDFPage } from "@react-pdf/renderer";
|
||||
import { pdfStyles } from "./pdf/SharedUI";
|
||||
import { DINLayout } from "./pdf/DINLayout";
|
||||
import { SimpleLayout } from "./pdf/SimpleLayout";
|
||||
|
||||
// Modules
|
||||
import { FrontPageModule } from './pdf/modules/FrontPageModule';
|
||||
import { BriefingModule } from './pdf/modules/BriefingModule';
|
||||
import { SitemapModule } from './pdf/modules/SitemapModule';
|
||||
import { EstimationModule } from './pdf/modules/EstimationModule';
|
||||
import { TransparenzModule, techPageModule as TechPageModule, PrinciplesModule } from './pdf/modules/CommonModules';
|
||||
import { AboutModule, CrossSellModule } from './pdf/modules/BrandingModules';
|
||||
import { FrontPageModule } from "./pdf/modules/FrontPageModule";
|
||||
import { BriefingModule } from "./pdf/modules/BriefingModule";
|
||||
import { SitemapModule } from "./pdf/modules/SitemapModule";
|
||||
import { EstimationModule } from "./pdf/modules/EstimationModule";
|
||||
import {
|
||||
TransparenzModule,
|
||||
techPageModule as TechPageModule,
|
||||
MaintenanceModule,
|
||||
StandardsModule,
|
||||
} from "./pdf/modules/CommonModules";
|
||||
import { AboutModule, CrossSellModule } from "./pdf/modules/BrandingModules";
|
||||
|
||||
interface PDFProps {
|
||||
state: any;
|
||||
@@ -21,28 +26,32 @@ interface PDFProps {
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
pricing: any;
|
||||
mode?: 'estimation' | 'full';
|
||||
mode?: "estimation" | "full";
|
||||
headerIcon?: string;
|
||||
footerLogo?: string;
|
||||
techDetails?: { t: string, d: string }[];
|
||||
principles?: { t: string, d: string }[];
|
||||
techDetails?: { t: string; d: string }[];
|
||||
principles?: { t: string; d: string }[];
|
||||
maintenanceDetails?: { t: string; d: string }[];
|
||||
standardsDetails?: { t: string; d: string }[];
|
||||
}
|
||||
|
||||
export const EstimationPDF = ({
|
||||
state,
|
||||
totalPrice,
|
||||
pricing,
|
||||
mode = 'full',
|
||||
mode = "full",
|
||||
headerIcon,
|
||||
footerLogo,
|
||||
techDetails,
|
||||
principles,
|
||||
maintenanceDetails,
|
||||
standardsDetails,
|
||||
...props
|
||||
}: PDFProps) => {
|
||||
const date = new Date().toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
const date = new Date().toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const positions = calculatePositions(state, pricing);
|
||||
@@ -51,13 +60,13 @@ export const EstimationPDF = ({
|
||||
name: "Marc Mintel",
|
||||
address1: "Georg-Meistermann-Straße 7",
|
||||
address2: "54586 Schüller",
|
||||
ustId: "DE367588065"
|
||||
ustId: "DE367588065",
|
||||
};
|
||||
|
||||
const bankData = {
|
||||
name: "N26",
|
||||
bic: "NTSBDEB1XXX",
|
||||
iban: "DE50 1001 1001 2620 4328 65"
|
||||
iban: "DE50 1001 1001 2620 4328 65",
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
@@ -66,20 +75,25 @@ export const EstimationPDF = ({
|
||||
icon: headerIcon,
|
||||
footerLogo,
|
||||
companyData,
|
||||
bankData
|
||||
bankData,
|
||||
};
|
||||
|
||||
if (mode === 'estimation') {
|
||||
if (mode === "estimation") {
|
||||
return (
|
||||
<DINLayout {...commonProps} showAddress={true} showFooterDetails={true}>
|
||||
<EstimationModule state={state} positions={positions} totalPrice={totalPrice} date={date} />
|
||||
<EstimationModule
|
||||
state={state}
|
||||
positions={positions}
|
||||
totalPrice={totalPrice}
|
||||
date={date}
|
||||
/>
|
||||
</DINLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Full Portfolio Mode
|
||||
let pageCounter = 1;
|
||||
const getPageNum = () => (pageCounter++).toString().padStart(2, '0');
|
||||
const getPageNum = () => (pageCounter++).toString().padStart(2, "0");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -98,22 +112,36 @@ export const EstimationPDF = ({
|
||||
)}
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<EstimationModule state={state} positions={positions} totalPrice={totalPrice} date={date} />
|
||||
<EstimationModule
|
||||
state={state}
|
||||
positions={positions}
|
||||
totalPrice={totalPrice}
|
||||
date={date}
|
||||
/>
|
||||
</SimpleLayout>
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<TransparenzModule pricing={pricing} />
|
||||
</SimpleLayout>
|
||||
|
||||
{standardsDetails && standardsDetails.length > 0 && (
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<StandardsModule
|
||||
standardsDetails={standardsDetails}
|
||||
principles={principles}
|
||||
/>
|
||||
</SimpleLayout>
|
||||
)}
|
||||
|
||||
{techDetails && techDetails.length > 0 && (
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<TechPageModule techDetails={techDetails} headerIcon={headerIcon} />
|
||||
</SimpleLayout>
|
||||
)}
|
||||
|
||||
{principles && principles.length > 0 && (
|
||||
{maintenanceDetails && maintenanceDetails.length > 0 && (
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<PrinciplesModule principles={principles} />
|
||||
<MaintenanceModule maintenanceDetails={maintenanceDetails} />
|
||||
</SimpleLayout>
|
||||
)}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,108 +1,218 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { IndustrialListItem, IndustrialCard, Divider, COLORS, FONT_SIZES } from '../SharedUI';
|
||||
import * as React from "react";
|
||||
import {
|
||||
View as PDFView,
|
||||
Text as PDFText,
|
||||
StyleSheet,
|
||||
} from "@react-pdf/renderer";
|
||||
import {
|
||||
DocumentTitle,
|
||||
IndustrialListItem,
|
||||
IndustrialCard,
|
||||
Divider,
|
||||
COLORS,
|
||||
FONT_SIZES,
|
||||
} from "../SharedUI";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
industrialTitle: { fontSize: FONT_SIZES.H1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 6, letterSpacing: -1 },
|
||||
industrialSubtitle: { fontSize: FONT_SIZES.SUB, fontWeight: 'bold', color: COLORS.TEXT_LIGHT, marginBottom: 16, letterSpacing: 0.5 },
|
||||
industrialTextLead: { fontSize: FONT_SIZES.H3, color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 16 },
|
||||
industrialText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 12 },
|
||||
industrialGrid2: { flexDirection: 'row' },
|
||||
industrialCol: { width: '46%' },
|
||||
industrialBulletBox: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
backgroundColor: COLORS.DIVIDER,
|
||||
marginRight: 8,
|
||||
marginTop: 5,
|
||||
},
|
||||
industrialTextLead: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.TEXT_MAIN,
|
||||
lineHeight: 1.4,
|
||||
marginBottom: 16,
|
||||
},
|
||||
industrialText: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.TEXT_DIM,
|
||||
lineHeight: 1.4,
|
||||
marginBottom: 12,
|
||||
},
|
||||
industrialGrid2: { flexDirection: "row" },
|
||||
industrialCol: { width: "46%" },
|
||||
});
|
||||
|
||||
export const AboutModule = () => (
|
||||
<>
|
||||
<PDFText style={styles.industrialTitle}>Expertise & Profil</PDFText>
|
||||
<PDFText style={styles.industrialSubtitle}>Entwicklung & Technischer Partner für den Mittelstand</PDFText>
|
||||
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
|
||||
<>
|
||||
<DocumentTitle
|
||||
title="Expertise & Profil"
|
||||
subLines={["Entwicklung & Technischer Partner für den Mittelstand"]}
|
||||
isHero={true}
|
||||
/>
|
||||
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
|
||||
|
||||
<PDFView style={{ marginTop: 24 }}>
|
||||
<PDFText style={styles.industrialTextLead}>
|
||||
Begleitung mittelständischer Unternehmen und Agenturen bei der Realisierung anspruchsvoller Web-Projekte. Als Senior Software Developer mit über 15 Jahren Erfahrung wird das gesamte technische Spektrum abgedeckt – von der Architektur bis zum fertigen Produkt.
|
||||
</PDFText>
|
||||
<PDFView style={{ marginTop: 24 }}>
|
||||
<PDFText style={styles.industrialTextLead}>
|
||||
Begleitung mittelständischer Unternehmen und Agenturen bei der
|
||||
Realisierung anspruchsvoller Web-Projekte. Als Senior Software Developer
|
||||
mit over 15 Jahren Erfahrung wird das gesamte technische Spektrum
|
||||
abgedeckt – von der Architektur bis zum fertigen Produkt.
|
||||
</PDFText>
|
||||
|
||||
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
|
||||
<PDFView style={[styles.industrialCol, { marginRight: '8%' }]}>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Erfahrung & Substanz</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Der Werdegang umfasst alle Ebenen der Webentwicklung: von der Teamleitung in Kreativagenturen bis zur Softwareentwicklung für internationale Konzerne.
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Die Kenntnis komplexer Enterprise-Systeme wird mit der Agilität kombiniert, die im Mittelstand gefordert ist. Dieses Wissen ermöglicht den Bau von Lösungen, die technologisch auf Augenhöhe mit Konzern-Standards sind, jedoch ohne unnötigen bürokratischen Overhead auskommen.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Fokus Einzelentwicklung</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Die Umsetzung erfolgt bewusst als spezialisierter Einzelentwickler. Dies garantiert maximale Geschwindigkeit, direkte Kommunikationswege und volle technologische Verantwortung.
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Als direkter technischer Sparringspartner bleibt die Codebasis von der ersten bis zur letzten Zeile transparent und wartbar. Diese Unmittelbarkeit stellt sicher, dass Ergebnisse sowohl technisch sauber als auch wirtschaftlich sinnvoll realisiert werden.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
|
||||
<PDFView style={{ marginTop: 32, paddingVertical: 16, borderTopWidth: 1, borderTopColor: COLORS.GRID }}>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Infrastruktur & Souveränität</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Es wird keine instabile Prototyp-Software geliefert, sondern produktionsreife Systeme, die technisch skalierbar bleiben. Die Codebasis folgt modernen Standards – bei wachsenden Ansprüchen oder dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos übernommen und weitergeführt werden.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
|
||||
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.industrialText,
|
||||
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
|
||||
]}
|
||||
>
|
||||
Erfahrung & Substanz
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Der Werdegang umfasst alle Ebenen der Webentwicklung: von der
|
||||
Teamleitung in Kreativagenturen bis zur Softwareentwicklung für
|
||||
internationale Konzerne.
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Die Kenntnis komplexer Enterprise-Systeme wird mit der Agilität
|
||||
kombiniert, die im Mittelstand gefordert ist. Dieses Wissen
|
||||
ermöglicht den Bau von Lösungen, die technologisch auf Augenhöhe mit
|
||||
Konzern-Standards sind, jedoch ohne unnötigen bürokratischen
|
||||
Overhead auskommen.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
</>
|
||||
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.industrialText,
|
||||
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
|
||||
]}
|
||||
>
|
||||
Fokus Einzelentwicklung
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Die Umsetzung erfolgt bewusst als spezialisierter Einzelentwickler.
|
||||
Dies garantiert maximale Geschwindigkeit, direkte Kommunikationswege
|
||||
und volle technologische Verantwortung.
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Als direkter technischer Sparringspartner bleibt die Codebasis von
|
||||
der ersten bis zur letzten Zeile transparent und wartbar. Diese
|
||||
Unmittelbarkeit stellt sicher, dass Ergebnisse sowohl technisch
|
||||
sauber als auch wirtschaftlich sinnvoll realisiert werden.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
|
||||
<PDFView
|
||||
style={{
|
||||
marginTop: 32,
|
||||
paddingVertical: 16,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: COLORS.GRID,
|
||||
}}
|
||||
>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.industrialText,
|
||||
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 4 },
|
||||
]}
|
||||
>
|
||||
Infrastruktur & Souveränität
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Es wird keine instabile Prototyp-Software geliefert, sondern
|
||||
produktionsreife Systeme, die technisch skalierbar bleiben. Die
|
||||
Codebasis folgt modernen Standards – bei wachsenden Ansprüchen oder
|
||||
dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos
|
||||
übernommen und weitergeführt werden.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
export const CrossSellModule = ({ state }: any) => {
|
||||
const isWebsite = state.projectType === 'website';
|
||||
const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
|
||||
const subtitle = isWebsite ? "Automatisierung und Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse";
|
||||
const isWebsite = state.projectType === "website";
|
||||
const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
|
||||
const subtitle = isWebsite
|
||||
? "Automatisierung und Prozessoptimierung"
|
||||
: "Technische Infrastruktur ohne Kompromisse";
|
||||
|
||||
return (
|
||||
<>
|
||||
<PDFText style={styles.industrialTitle}>{title}</PDFText>
|
||||
<PDFText style={styles.industrialSubtitle}>{subtitle}</PDFText>
|
||||
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
|
||||
<PDFView style={[styles.industrialGrid2, { marginTop: 16 }]} >
|
||||
{isWebsite ? (
|
||||
<>
|
||||
<PDFView style={[styles.industrialCol, { marginRight: '8%' }]}>
|
||||
<PDFText style={styles.industrialTextLead}>Über die klassische Webpräsenz hinaus werden maßgeschneiderte Lösungen zur Automatisierung von Routine-Prozessen angeboten. Dies ermöglicht eine signifikante Effizienzsteigerung im Tagesgeschäft.</PDFText>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Keine Abos. Keine komplexen neuen Systeme. Gezielte Zeitersparnis.</PDFText>
|
||||
<PDFView style={{ marginTop: 24, padding: 16, backgroundColor: '#f8fafc', borderLeftWidth: 2, borderLeftColor: COLORS.GRID }}>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Individuelle Analyse</PDFText>
|
||||
<PDFText style={styles.industrialText}>Spezifische Prozesse werden auf technisches Automatisierungspotenzial untersucht. Das Ergebnis liefert Klarheit über die wirtschaftliche Sinnhaftigkeit einer Umsetzung.</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<IndustrialCard title="DOKUMENT-AUTOMATION">
|
||||
<PDFText style={styles.industrialText}>Erstellung von PDF-Angeboten, Berichten oder Protokollen in Sekunden statt Stunden.</PDFText>
|
||||
</IndustrialCard>
|
||||
<IndustrialCard title="EXCEL-LOGIK">
|
||||
<PDFText style={styles.industrialText}>Intelligente Tabellen und automatisierte Auswertungen bestehender Datensätze.</PDFText>
|
||||
</IndustrialCard>
|
||||
<IndustrialCard title="KI-ASSISTENZ">
|
||||
<PDFText style={styles.industrialText}>Effiziente Verarbeitung von analogen Dokumenten oder handschriftlichen Notizen mittels KI.</PDFText>
|
||||
</IndustrialCard>
|
||||
</PDFView>
|
||||
</>
|
||||
) : (
|
||||
<PDFView style={{ width: '100%' }}>
|
||||
<PDFText style={styles.industrialTextLead}>Bereitstellung einer stabilen technischen Basis ohne Abhängigkeiten von Baukasten-Systemen oder Agenturen.</PDFText>
|
||||
<PDFText style={styles.industrialText}>Entwicklung performanter Frontends und skalierbarer Backends. Die Auslieferung erfolgt als kontrollierbarer und nachhaltiger Quellcode.</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
return (
|
||||
<>
|
||||
<DocumentTitle title={title} subLines={[subtitle]} isHero={true} />
|
||||
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
|
||||
<PDFView style={[styles.industrialGrid2, { marginTop: 16 }]}>
|
||||
{isWebsite ? (
|
||||
<>
|
||||
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
|
||||
<PDFText style={styles.industrialTextLead}>
|
||||
Über die klassische Webpräsenz hinaus werden maßgeschneiderte
|
||||
Lösungen zur Automatisierung von Routine-Prozessen angeboten.
|
||||
Dies ermöglicht eine signifikante Effizienzsteigerung im
|
||||
Tagesgeschäft.
|
||||
</PDFText>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: "bold" }]}>
|
||||
Keine Abos. Keine komplexen neuen Systeme. Gezielte
|
||||
Zeitersparnis.
|
||||
</PDFText>
|
||||
<PDFView
|
||||
style={{
|
||||
marginTop: 24,
|
||||
padding: 16,
|
||||
backgroundColor: "#f8fafc",
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: COLORS.GRID,
|
||||
}}
|
||||
>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.industrialText,
|
||||
{
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
marginBottom: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
Individuelle Analyse
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Spezifische Prozesse werden auf technisches
|
||||
Automatisierungspotenzial untersucht. Das Ergebnis liefert
|
||||
Klarheit über die wirtschaftliche Sinnhaftigkeit einer
|
||||
Umsetzung.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<IndustrialCard title="DOKUMENT-AUTOMATION">
|
||||
<PDFText style={styles.industrialText}>
|
||||
Erstellung von PDF-Angeboten, Berichten oder Protokollen in
|
||||
Sekunden statt Stunden.
|
||||
</PDFText>
|
||||
</IndustrialCard>
|
||||
<IndustrialCard title="EXCEL-LOGIK">
|
||||
<PDFText style={styles.industrialText}>
|
||||
Intelligente Tabellen und automatisierte Auswertungen
|
||||
bestehender Datensätze.
|
||||
</PDFText>
|
||||
</IndustrialCard>
|
||||
<IndustrialCard title="KI-ASSISTENZ">
|
||||
<PDFText style={styles.industrialText}>
|
||||
Effiziente Verarbeitung von analogen Dokumenten oder
|
||||
handschriftlichen Notizen mittels KI.
|
||||
</PDFText>
|
||||
</IndustrialCard>
|
||||
</PDFView>
|
||||
</>
|
||||
) : (
|
||||
<PDFView style={{ width: "100%" }}>
|
||||
<PDFText style={styles.industrialTextLead}>
|
||||
Bereitstellung einer stabilen technischen Basis ohne
|
||||
Abhängigkeiten von Baukasten-Systemen oder Agenturen.
|
||||
</PDFText>
|
||||
<PDFText style={styles.industrialText}>
|
||||
Entwicklung performanter Frontends und skalierbarer Backends. Die
|
||||
Auslieferung erfolgt als kontrollierbarer und nachhaltiger
|
||||
Quellcode.
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,69 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { DocumentTitle, COLORS, FONT_SIZES } from '../SharedUI';
|
||||
import * as React from "react";
|
||||
import {
|
||||
View as PDFView,
|
||||
Text as PDFText,
|
||||
StyleSheet,
|
||||
} from "@react-pdf/renderer";
|
||||
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: { marginBottom: 24 },
|
||||
sectionTitle: { fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', marginBottom: 8, color: COLORS.CHARCOAL },
|
||||
visionText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.8, textAlign: 'justify' },
|
||||
section: { marginBottom: 24 },
|
||||
sectionTitle: {
|
||||
fontSize: FONT_SIZES.LABEL,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 8,
|
||||
color: COLORS.CHARCOAL,
|
||||
},
|
||||
visionText: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.TEXT_MAIN,
|
||||
lineHeight: 1.4,
|
||||
textAlign: "justify",
|
||||
},
|
||||
});
|
||||
|
||||
export const BriefingModule = ({ state }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Projektdetails" />
|
||||
{state.briefingSummary && (
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText>
|
||||
<PDFText style={{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.6, textAlign: 'justify' }}>{state.briefingSummary}</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
{state.designVision && (
|
||||
<PDFView style={[styles.section, { padding: 12, borderLeftWidth: 2, borderLeftColor: COLORS.DIVIDER, backgroundColor: COLORS.GRID }]}>
|
||||
<PDFText style={[styles.sectionTitle, { color: COLORS.CHARCOAL, marginBottom: 4 }]}>Strategische Vision</PDFText>
|
||||
<PDFText style={[styles.visionText, { lineHeight: 1.6 }]}>{state.designVision}</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
<DocumentTitle title="Projektdetails" isHero={true} />
|
||||
{state.briefingSummary && (
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText>
|
||||
<PDFText
|
||||
style={{
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.TEXT_MAIN,
|
||||
lineHeight: 1.6,
|
||||
textAlign: "justify",
|
||||
}}
|
||||
>
|
||||
{state.briefingSummary}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
{state.designVision && (
|
||||
<PDFView
|
||||
style={[
|
||||
styles.section,
|
||||
{
|
||||
padding: 12,
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: COLORS.DIVIDER,
|
||||
backgroundColor: COLORS.GRID,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{ color: COLORS.CHARCOAL, marginBottom: 4 },
|
||||
]}
|
||||
>
|
||||
Strategische Vision
|
||||
</PDFText>
|
||||
<PDFText style={styles.visionText}>{state.designVision}</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,106 +1,443 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from '@react-pdf/renderer';
|
||||
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
|
||||
import * as React from "react";
|
||||
import {
|
||||
View as PDFView,
|
||||
Text as PDFText,
|
||||
StyleSheet,
|
||||
Image as PDFImage,
|
||||
} from "@react-pdf/renderer";
|
||||
import {
|
||||
DocumentTitle,
|
||||
Divider,
|
||||
COLORS,
|
||||
FONT_SIZES,
|
||||
TechnicalSpec,
|
||||
AsymmetryView,
|
||||
} from "../SharedUI";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: { marginBottom: 16 },
|
||||
pricingGrid: { marginTop: 12 },
|
||||
pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 12, alignItems: 'flex-start' },
|
||||
pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, paddingRight: 15 },
|
||||
pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.5, paddingRight: 10 },
|
||||
pricingTag: { width: '15%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', textAlign: 'right', color: COLORS.CHARCOAL },
|
||||
configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 8 },
|
||||
section: { marginBottom: 24 },
|
||||
moduleLabel: {
|
||||
fontSize: FONT_SIZES.LABEL,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 4,
|
||||
},
|
||||
moduleDesc: {
|
||||
fontSize: FONT_SIZES.SMALL,
|
||||
color: COLORS.TEXT_DIM,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
ledgerRow: {
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.GRID,
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
ledgerPrice: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
},
|
||||
ledgerUnit: {
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
marginLeft: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const techPageModule = ({ techDetails, headerIcon }: any) => (
|
||||
export const techPageModule = ({ techDetails }: any) => {
|
||||
// Focus on the first 3 items as "Featured Specs", the rest as high-density grid
|
||||
const featured = techDetails?.slice(0, 3) || [];
|
||||
const rest = techDetails?.slice(3) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentTitle title="Technische Umsetzung" />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFView style={styles.pricingGrid}>
|
||||
{techDetails?.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.pricingRow}>
|
||||
<PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText>
|
||||
<PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText>
|
||||
</PDFView>
|
||||
<DocumentTitle title="Technische Umsetzung" isHero={true} />
|
||||
<PDFView style={styles.section}>
|
||||
{/* FEATURED SPECS - Editorial focus */}
|
||||
{featured.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={{ marginBottom: 24 }}>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleLabel,
|
||||
{ color: COLORS.BLUEPRINT, fontSize: FONT_SIZES.TINY },
|
||||
]}
|
||||
>
|
||||
FOKUS_{i + 1}
|
||||
</PDFText>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleLabel,
|
||||
{ fontSize: FONT_SIZES.HEADING, marginBottom: 8 },
|
||||
]}
|
||||
>
|
||||
{item.t}
|
||||
</PDFText>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleDesc,
|
||||
{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN },
|
||||
]}
|
||||
>
|
||||
{item.d}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
|
||||
<Divider style={{ marginVertical: 24, backgroundColor: COLORS.GRID }} />
|
||||
|
||||
{/* TECHNICAL GRID - High density */}
|
||||
<PDFView style={{ flexDirection: "row", flexWrap: "wrap", gap: 12 }}>
|
||||
{rest.map((item: any, i: number) => (
|
||||
<PDFView
|
||||
key={i}
|
||||
style={{
|
||||
width: "31%",
|
||||
padding: 10,
|
||||
backgroundColor: "#fdfdfd",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.BLUEPRINT,
|
||||
}}
|
||||
>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleLabel,
|
||||
{
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
marginBottom: 4,
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.t.toUpperCase()}
|
||||
</PDFText>
|
||||
<PDFText
|
||||
style={[styles.moduleDesc, { fontSize: FONT_SIZES.TINY }]}
|
||||
>
|
||||
{item.d}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MaintenanceModule = ({ maintenanceDetails }: any) => (
|
||||
<>
|
||||
<DocumentTitle
|
||||
title="Monitoring, Security & techn. Support"
|
||||
isHero={true}
|
||||
/>
|
||||
<PDFView style={styles.section}>
|
||||
<PDFView style={{ flexDirection: "row", flexWrap: "wrap", gap: 16 }}>
|
||||
{maintenanceDetails?.map((item: any, i: number) => (
|
||||
<PDFView
|
||||
key={i}
|
||||
style={{
|
||||
width: "48%",
|
||||
padding: 16,
|
||||
backgroundColor: COLORS.GRID,
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: COLORS.DIVIDER,
|
||||
}}
|
||||
>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleLabel,
|
||||
{
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
marginBottom: 8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.t.toUpperCase()}
|
||||
</PDFText>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleDesc,
|
||||
{
|
||||
fontSize: FONT_SIZES.SMALL,
|
||||
color: COLORS.CHARCOAL,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.d}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
export const StandardsModule = ({ standardsDetails, principles }: any) => {
|
||||
const independence = standardsDetails?.find(
|
||||
(s: any) => s.t === "Unabhängigkeit",
|
||||
);
|
||||
const others =
|
||||
standardsDetails?.filter((s: any) => s.t !== "Unabhängigkeit") || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentTitle title="Meine Standards" isHero={true} />
|
||||
|
||||
<PDFView style={styles.section}>
|
||||
{/* FOCUS: UNABHÄNGIGKEIT & PRINCIPLES */}
|
||||
<AsymmetryView
|
||||
left={
|
||||
<PDFView>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleLabel,
|
||||
{
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
marginBottom: 12,
|
||||
},
|
||||
]}
|
||||
>
|
||||
MODERNE PRINZIPIEN
|
||||
</PDFText>
|
||||
{principles?.map((p: any, i: number) => (
|
||||
<PDFView key={i} style={{ marginBottom: 12 }}>
|
||||
<PDFText
|
||||
style={[styles.moduleLabel, { fontSize: FONT_SIZES.TINY }]}
|
||||
>
|
||||
{p.t.toUpperCase()}
|
||||
</PDFText>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleDesc,
|
||||
{ fontSize: FONT_SIZES.TINY, opacity: 0.8 },
|
||||
]}
|
||||
>
|
||||
{p.d}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
}
|
||||
right={
|
||||
<PDFView>
|
||||
{/* HERO BOX: UNABHÄNGIGKEIT */}
|
||||
{independence && (
|
||||
<PDFView
|
||||
style={{
|
||||
backgroundColor: COLORS.CHARCOAL,
|
||||
padding: 24,
|
||||
marginBottom: 32,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: COLORS.TEXT_LIGHT,
|
||||
}}
|
||||
>
|
||||
<PDFText
|
||||
style={{
|
||||
fontSize: FONT_SIZES.HEADING,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.WHITE,
|
||||
letterSpacing: 1,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{independence.t.toUpperCase()}
|
||||
</PDFText>
|
||||
<PDFText
|
||||
style={{
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.WHITE,
|
||||
lineHeight: 1.4,
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
{independence.d}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
|
||||
{/* VERTICAL STACK OF OTHER STANDARDS */}
|
||||
<PDFView>
|
||||
{others.map((item: any, i: number) => (
|
||||
<PDFView
|
||||
key={i}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.GRID,
|
||||
paddingBottom: 12,
|
||||
}}
|
||||
>
|
||||
<PDFText style={styles.moduleLabel}>{item.t}</PDFText>
|
||||
<PDFText style={styles.moduleDesc}>{item.d}</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
}
|
||||
/>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export const TransparenzModule = ({ pricing }: any) => (
|
||||
export const TransparenzModule = ({ pricing }: any) => {
|
||||
const sorglosPrice = (pricing.HOSTING_MONTHLY || 250) * 12;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentTitle title="Preis-Transparenz & Modell" />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFView style={styles.pricingGrid}>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Fundament</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Setup, Infrastruktur, Hosting, SEO-Basics, Staging & Live-Umgebungen.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.BASE_WEBSITE?.toLocaleString('de-DE')} €</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Seiten</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Layout & Umsetzung individueller Seiten. Responsive Design / Cross-Browser.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.PAGE?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Features</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Abgeschlossene Systeme (z. B. Blog, Jobs, Produkte) inkl. Datenstruktur.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.FEATURE?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Funktionen</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Logik-Einheiten wie Filter, Suchen oder Kontakt-Schnittstellen.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.FUNCTION?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Schnittstellen</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Anbindung externer Systeme (CRM, ERP, Payment) zur Synchronisation.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>CMS Setup</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Konfiguration Headless CMS zur unabhängigen Datenpflege aller Module.</PDFText>
|
||||
<PDFText style={pricing.CMS_SETUP ? styles.pricingTag : [styles.pricingTag, { color: COLORS.TEXT_LIGHT }]}>{pricing.CMS_SETUP?.toLocaleString('de-DE')} €</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Inszenierung</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Interaktions-Mechanismen, Konfiguratoren oder visuelles Storytelling.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>ab {pricing.VISUAL_STAGING?.toLocaleString('de-DE')} €</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Sprachen</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Skalierung der System-Architektur auf zusätzliche Sprachversionen.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>+20% / Sprache</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Initial-Pflege</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Manuelle Aufbereitung & Übernahme von Datensätzen in das Zielsystem.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.NEW_DATASET?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>Sorglos-Paket</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Betrieb, Hosting, Updates & Monitoring gemäß AGB Punkt 7a.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>Inklusive 1 Jahr</PDFText>
|
||||
</PDFView>
|
||||
<DocumentTitle title="Preis-Transparenz & Modell" isHero={true} />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFView style={{ borderTopWidth: 1, borderTopColor: COLORS.CHARCOAL }}>
|
||||
{[
|
||||
{
|
||||
l: "Fundament",
|
||||
d: "Bereitstellung der techn. Infrastruktur & System-Umgebung.",
|
||||
p: pricing.BASE_WEBSITE,
|
||||
},
|
||||
{
|
||||
l: "Einzelseiten",
|
||||
d: "Individuelle Gestaltung, Layout & responsive Struktur.",
|
||||
p: pricing.PAGE,
|
||||
unit: "/ Stk",
|
||||
},
|
||||
{
|
||||
l: "Core Features",
|
||||
d: "Geschlossene Datensysteme mit eigener Datenstruktur.",
|
||||
p: pricing.FEATURE,
|
||||
unit: "/ Stk",
|
||||
},
|
||||
{
|
||||
l: "Logik & Funktionen",
|
||||
d: "Interaktive Funktions-Bausteine & Prozess-Logik.",
|
||||
p: pricing.FUNCTION,
|
||||
unit: "/ Stk",
|
||||
},
|
||||
{
|
||||
l: "Schnittstellen",
|
||||
d: "Synchronisation mit externen Zielsystemen.",
|
||||
p: pricing.API_INTEGRATION,
|
||||
unit: "/ Stk",
|
||||
},
|
||||
{
|
||||
l: "Inhalts-Verwaltung",
|
||||
d: "Schnittstelle zur eigenständigen Daten-Pflege (optional).",
|
||||
p: pricing.CMS_CONNECTION_PER_FEATURE,
|
||||
unit: "/ Stk",
|
||||
},
|
||||
{
|
||||
l: "Sprachversionen",
|
||||
d: "Skalierung der System-Architektur auf Zweit-Sprachen.",
|
||||
p: "+20%",
|
||||
isLang: true,
|
||||
},
|
||||
{
|
||||
l: "Initial-Pflege",
|
||||
d: "Konvertierung & Aufbereitung von Bestandsdaten.",
|
||||
p: pricing.NEW_DATASET,
|
||||
unit: "/ Stk",
|
||||
},
|
||||
{
|
||||
l: "Sorglos Betrieb",
|
||||
d: "Hosting, Instandhaltung, Security & techn. Support.",
|
||||
p: sorglosPrice,
|
||||
unit: "/ Jahr",
|
||||
},
|
||||
].map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.ledgerRow}>
|
||||
<PDFView style={{ width: "25%" }}>
|
||||
<PDFText style={styles.moduleLabel}>
|
||||
{item.l.toUpperCase()}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={{ width: "50%" }}>
|
||||
<PDFText style={styles.moduleDesc}>{item.d}</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={{ width: "25%", alignItems: "flex-end" }}>
|
||||
<PDFText style={styles.ledgerPrice}>
|
||||
{typeof item.p === "number"
|
||||
? `${item.p.toLocaleString("de-DE")} €`
|
||||
: item.p}
|
||||
{item.unit && (
|
||||
<PDFText style={styles.ledgerUnit}> {item.unit}</PDFText>
|
||||
)}
|
||||
</PDFText>
|
||||
{item.sub && (
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleDesc,
|
||||
{
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
marginTop: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.sub}
|
||||
</PDFText>
|
||||
)}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export const PrinciplesModule = ({ principles }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Prinzipien & Standards" />
|
||||
<PDFView style={[styles.pricingGrid, { marginTop: 8 }]}>
|
||||
{principles?.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.pricingRow}>
|
||||
<PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText>
|
||||
<PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
export const ClosingModule = () => (
|
||||
<>
|
||||
<DocumentTitle title="Abschluss & Kontakt" isHero={true} />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleLabel,
|
||||
{ fontSize: FONT_SIZES.HEADING, marginBottom: 12 },
|
||||
]}
|
||||
>
|
||||
Vielen Dank für Ihr Interesse!
|
||||
</PDFText>
|
||||
<PDFText style={styles.moduleDesc}>
|
||||
Die aufgeführten Positionen stellen eine detaillierte Schätzung auf
|
||||
Basis unseres aktuellen Stands dar. Sollten sich Anforderungen ändern
|
||||
oder Sie Fragen zu einzelnen Details haben, lassen Sie uns die
|
||||
Positionen gerne gemeinsam besprechen.
|
||||
</PDFText>
|
||||
<PDFView
|
||||
style={{
|
||||
marginTop: 24,
|
||||
padding: 16,
|
||||
backgroundColor: COLORS.GRID,
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: COLORS.DIVIDER,
|
||||
}}
|
||||
>
|
||||
<PDFText style={[styles.moduleLabel, { marginBottom: 8 }]}>
|
||||
Haben Sie Fragen?
|
||||
</PDFText>
|
||||
<PDFText style={styles.moduleDesc}>
|
||||
Ich erkläre Ihnen gerne noch einmal persönlich, was die technische
|
||||
Umsetzung für Ihr Projekt bedeutet und wie wir die nächsten Schritte
|
||||
gemeinsam gehen können.
|
||||
</PDFText>
|
||||
<PDFView style={{ marginTop: 16 }}>
|
||||
<PDFText style={styles.moduleLabel}>Kontakt:</PDFText>
|
||||
<PDFText
|
||||
style={[
|
||||
styles.moduleDesc,
|
||||
{ color: COLORS.CHARCOAL, fontWeight: "bold" },
|
||||
]}
|
||||
>
|
||||
Marc Mintel – marc@mintel.me
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
</>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,55 +1,159 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { DocumentTitle } from '../SharedUI';
|
||||
import * as React from "react";
|
||||
import {
|
||||
View as PDFView,
|
||||
Text as PDFText,
|
||||
StyleSheet,
|
||||
} from "@react-pdf/renderer";
|
||||
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
table: { marginTop: 12 },
|
||||
tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#334155', marginBottom: 12 },
|
||||
tableRow: { flexDirection: 'row', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f8fafc', alignItems: 'flex-start' },
|
||||
colPos: { width: '8%' },
|
||||
colDesc: { width: '62%' },
|
||||
colQty: { width: '10%', textAlign: 'center' },
|
||||
colPrice: { width: '20%', textAlign: 'right' },
|
||||
headerText: { fontSize: 7, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
posText: { fontSize: 8, color: '#999999' },
|
||||
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4 },
|
||||
itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 },
|
||||
priceText: { fontSize: 10, fontWeight: 'bold' },
|
||||
summaryContainer: { borderTopWidth: 1, borderTopColor: '#334155', paddingTop: 8 },
|
||||
summaryRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingVertical: 4, alignItems: 'baseline' },
|
||||
summaryLabel: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, fontWeight: 'bold', marginRight: 12 },
|
||||
summaryValue: { fontSize: 9, fontWeight: 'bold', width: 100, textAlign: 'right' },
|
||||
totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#334155', alignItems: 'baseline' },
|
||||
table: { marginTop: 12 },
|
||||
tableHeader: {
|
||||
flexDirection: "row",
|
||||
paddingBottom: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.CHARCOAL,
|
||||
marginBottom: 12,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: "row",
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.GRID,
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
colPos: { width: "8%" },
|
||||
colDesc: { width: "62%" },
|
||||
colQty: { width: "10%", textAlign: "center" },
|
||||
colPrice: { width: "20%", textAlign: "right" },
|
||||
headerText: {
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
fontWeight: "bold",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
color: COLORS.TEXT_DIM,
|
||||
},
|
||||
posText: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_LIGHT },
|
||||
itemTitle: {
|
||||
fontSize: FONT_SIZES.LABEL,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
marginBottom: 4,
|
||||
},
|
||||
itemDesc: {
|
||||
fontSize: FONT_SIZES.SMALL,
|
||||
color: COLORS.TEXT_DIM,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
priceText: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
},
|
||||
summaryContainer: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: COLORS.CHARCOAL,
|
||||
paddingTop: 8,
|
||||
},
|
||||
summaryRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
paddingVertical: 4,
|
||||
alignItems: "baseline",
|
||||
},
|
||||
summaryLabel: {
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
color: COLORS.TEXT_DIM,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
fontWeight: "bold",
|
||||
marginRight: 12,
|
||||
},
|
||||
summaryValue: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
fontWeight: "bold",
|
||||
width: 100,
|
||||
textAlign: "right",
|
||||
color: COLORS.CHARCOAL,
|
||||
},
|
||||
totalRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
paddingTop: 12,
|
||||
marginTop: 8,
|
||||
borderTopWidth: 2,
|
||||
borderTopColor: COLORS.CHARCOAL,
|
||||
alignItems: "baseline",
|
||||
},
|
||||
});
|
||||
|
||||
export const EstimationModule = ({ state, positions, totalPrice, date }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Kostenschätzung" subLines={[`Datum: ${date}`, `Projekt: ${state.projectType === 'website' ? 'Website' : 'Web App'}`]} />
|
||||
<PDFView style={styles.table}>
|
||||
<PDFView style={styles.tableHeader}>
|
||||
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colDesc]}>Beschreibung</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText>
|
||||
</PDFView>
|
||||
{positions.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.tableRow} wrap={false}>
|
||||
<PDFText style={[styles.posText, styles.colPos]}>{item.pos.toString().padStart(2, '0')}</PDFText>
|
||||
<PDFView style={styles.colDesc}>
|
||||
<PDFText style={styles.itemTitle}>{item.title}</PDFText>
|
||||
<PDFText style={styles.itemDesc}>{state.positionDescriptions?.[item.title] || item.desc}</PDFText>
|
||||
</PDFView>
|
||||
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText>
|
||||
<PDFText style={[styles.priceText, styles.colPrice]}>{item.price > 0 ? `${item.price.toLocaleString('de-DE')} €` : 'n. A.'}</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
export const EstimationModule = ({
|
||||
state,
|
||||
positions,
|
||||
totalPrice,
|
||||
date,
|
||||
}: any) => (
|
||||
<>
|
||||
<DocumentTitle
|
||||
title="Kostenschätzung"
|
||||
subLines={[
|
||||
`Datum: ${date}`,
|
||||
`Projekt: ${state.projectType === "website" ? "Website" : "Web App"}`,
|
||||
]}
|
||||
isHero={true}
|
||||
/>
|
||||
<PDFView style={styles.table}>
|
||||
<PDFView style={styles.tableHeader}>
|
||||
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colDesc]}>
|
||||
Beschreibung
|
||||
</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText>
|
||||
</PDFView>
|
||||
{positions.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.tableRow} wrap={false}>
|
||||
<PDFText style={[styles.posText, styles.colPos]}>
|
||||
{item.pos.toString().padStart(2, "0")}
|
||||
</PDFText>
|
||||
<PDFView style={styles.colDesc}>
|
||||
<PDFText style={styles.itemTitle}>{item.title}</PDFText>
|
||||
<PDFText style={styles.itemDesc}>
|
||||
{state.positionDescriptions?.[item.title] || item.desc}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText>
|
||||
<PDFText style={[styles.priceText, styles.colPrice]}>
|
||||
{item.price > 0
|
||||
? `${item.price.toLocaleString("de-DE")} €`
|
||||
: "n. A."}
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.summaryContainer} wrap={false}>
|
||||
<PDFView style={styles.summaryRow}><PDFText style={styles.summaryLabel}>Nettobetrag</PDFText><PDFText style={styles.summaryValue}>{totalPrice.toLocaleString('de-DE')} €</PDFText></PDFView>
|
||||
<PDFView style={styles.summaryRow}><PDFText style={styles.summaryLabel}>Umsatzsteuer (19%)</PDFText><PDFText style={styles.summaryValue}>{(totalPrice * 0.19).toLocaleString('de-DE')} €</PDFText></PDFView>
|
||||
<PDFView style={styles.totalRow}><PDFText style={styles.summaryLabel}>Gesamtbetrag (Brutto)</PDFText><PDFText style={[styles.summaryValue, { fontSize: 14 }]}>{(totalPrice * 1.19).toLocaleString('de-DE')} €</PDFText></PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
))}
|
||||
</PDFView>
|
||||
<PDFView style={styles.summaryContainer} wrap={false}>
|
||||
<PDFView style={styles.summaryRow}>
|
||||
<PDFText style={styles.summaryLabel}>Nettobetrag</PDFText>
|
||||
<PDFText style={styles.summaryValue}>
|
||||
{totalPrice.toLocaleString("de-DE")} €
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.summaryRow}>
|
||||
<PDFText style={styles.summaryLabel}>Umsatzsteuer (19%)</PDFText>
|
||||
<PDFText style={styles.summaryValue}>
|
||||
{(totalPrice * 0.19).toLocaleString("de-DE")} €
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.totalRow}>
|
||||
<PDFText style={styles.summaryLabel}>Gesamtbetrag (Brutto)</PDFText>
|
||||
<PDFText
|
||||
style={[styles.summaryValue, { fontSize: FONT_SIZES.HEADING }]}
|
||||
>
|
||||
{(totalPrice * 1.19).toLocaleString("de-DE")} €
|
||||
</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,81 +1,92 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, Image as PDFImage, StyleSheet } from '@react-pdf/renderer';
|
||||
import { COLORS, FONT_SIZES } from '../SharedUI';
|
||||
import * as React from "react";
|
||||
import {
|
||||
View as PDFView,
|
||||
Text as PDFText,
|
||||
Image as PDFImage,
|
||||
StyleSheet,
|
||||
} from "@react-pdf/renderer";
|
||||
import { COLORS, FONT_SIZES } from "../SharedUI";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titlePage: {
|
||||
flex: 1, // Fill the whole page
|
||||
padding: 60,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: COLORS.WHITE,
|
||||
},
|
||||
titleBrandIcon: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
backgroundColor: COLORS.CHARCOAL,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 40,
|
||||
},
|
||||
brandIconText: {
|
||||
fontSize: 40,
|
||||
color: COLORS.WHITE,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
titleProjectName: {
|
||||
fontSize: FONT_SIZES.H1,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.CHARCOAL,
|
||||
marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
maxWidth: '85%',
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
titleCustomerName: {
|
||||
fontSize: FONT_SIZES.H3,
|
||||
color: COLORS.TEXT_DIM,
|
||||
marginBottom: 40,
|
||||
textAlign: 'center',
|
||||
maxWidth: '80%',
|
||||
},
|
||||
titleDocumentType: {
|
||||
fontSize: FONT_SIZES.BODY + 1, // ~10
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 4,
|
||||
marginBottom: 12,
|
||||
},
|
||||
titleDivider: {
|
||||
width: 40,
|
||||
height: 2,
|
||||
backgroundColor: COLORS.CHARCOAL,
|
||||
marginBottom: 40,
|
||||
},
|
||||
titleDate: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
marginTop: 40,
|
||||
},
|
||||
titlePage: {
|
||||
flex: 1, // Fill the whole page
|
||||
padding: 60,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: COLORS.WHITE,
|
||||
},
|
||||
titleBrandIcon: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
backgroundColor: COLORS.CHARCOAL,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 40,
|
||||
},
|
||||
brandIconText: {
|
||||
fontSize: 40,
|
||||
color: COLORS.WHITE,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
titleProjectName: {
|
||||
fontSize: FONT_SIZES.HERO,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
marginBottom: 16,
|
||||
textAlign: "center",
|
||||
maxWidth: "85%",
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
titleCustomerName: {
|
||||
fontSize: FONT_SIZES.HEADING,
|
||||
color: COLORS.TEXT_DIM,
|
||||
marginBottom: 40,
|
||||
textAlign: "center",
|
||||
maxWidth: "80%",
|
||||
},
|
||||
titleDocumentType: {
|
||||
fontSize: FONT_SIZES.BODY + 1, // ~10
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 4,
|
||||
marginBottom: 12,
|
||||
},
|
||||
titleDivider: {
|
||||
width: 40,
|
||||
height: 2,
|
||||
backgroundColor: COLORS.CHARCOAL,
|
||||
marginBottom: 40,
|
||||
},
|
||||
titleDate: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
marginTop: 40,
|
||||
},
|
||||
});
|
||||
|
||||
export const FrontPageModule = ({ state, headerIcon, date }: any) => {
|
||||
const fullTitle = `Digitale Webpräsenz für\n${state.companyName || "Ihr Projekt"}`;
|
||||
const fullTitle = `Digitale Webpräsenz für\n${state.companyName || "Ihr Projekt"}`;
|
||||
|
||||
// Responsive font size based on length
|
||||
const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22;
|
||||
// Responsive font size based on length
|
||||
const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22;
|
||||
|
||||
return (
|
||||
<PDFView style={styles.titlePage}>
|
||||
<PDFView style={styles.titleBrandIcon}>
|
||||
{headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>}
|
||||
</PDFView>
|
||||
<PDFText style={[styles.titleProjectName, { fontSize }]}>{fullTitle}</PDFText>
|
||||
<PDFView style={{ marginBottom: 40 }} />
|
||||
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
|
||||
</PDFView>
|
||||
);
|
||||
return (
|
||||
<PDFView style={styles.titlePage}>
|
||||
<PDFView style={styles.titleBrandIcon}>
|
||||
{headerIcon ? (
|
||||
<PDFImage src={headerIcon} style={{ width: 40, height: 40 }} />
|
||||
) : (
|
||||
<PDFText style={styles.brandIconText}>M</PDFText>
|
||||
)}
|
||||
</PDFView>
|
||||
<PDFText style={[styles.titleProjectName, { fontSize }]}>
|
||||
{fullTitle}
|
||||
</PDFText>
|
||||
<PDFView style={{ marginBottom: 40 }} />
|
||||
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
|
||||
</PDFView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,77 +1,125 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
|
||||
import * as React from "react";
|
||||
import {
|
||||
View as PDFView,
|
||||
Text as PDFText,
|
||||
StyleSheet,
|
||||
} from "@react-pdf/renderer";
|
||||
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from "../SharedUI";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: { marginBottom: 32 },
|
||||
intro: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 24, textAlign: 'justify' },
|
||||
sitemapTree: { marginTop: 8 },
|
||||
rootNode: {
|
||||
padding: 12,
|
||||
backgroundColor: COLORS.GRID,
|
||||
marginBottom: 20,
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: COLORS.CHARCOAL
|
||||
},
|
||||
rootTitle: { fontSize: FONT_SIZES.H3, fontWeight: 'bold', color: COLORS.CHARCOAL, letterSpacing: 0.5 },
|
||||
categorySection: { marginBottom: 20 },
|
||||
categoryHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 6,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.BLUEPRINT,
|
||||
marginBottom: 10
|
||||
},
|
||||
categoryIcon: { width: 8, height: 8, backgroundColor: COLORS.GRID, borderInlineWidth: 1, borderColor: COLORS.DIVIDER, marginRight: 10 },
|
||||
categoryTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, textTransform: 'uppercase', letterSpacing: 1 },
|
||||
pagesGrid: { flexDirection: 'row', flexWrap: 'wrap' },
|
||||
pageCard: {
|
||||
width: '48%',
|
||||
marginRight: '2%',
|
||||
marginBottom: 12,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.GRID,
|
||||
backgroundColor: '#fafafa'
|
||||
},
|
||||
pageTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN, marginBottom: 2 },
|
||||
pageDesc: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_DIM, lineHeight: 1.4 },
|
||||
section: { marginBottom: 32 },
|
||||
intro: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
color: COLORS.TEXT_DIM,
|
||||
lineHeight: 1.4,
|
||||
marginBottom: 24,
|
||||
textAlign: "justify",
|
||||
},
|
||||
sitemapTree: { marginTop: 8 },
|
||||
rootNode: {
|
||||
padding: 12,
|
||||
backgroundColor: COLORS.GRID,
|
||||
marginBottom: 20,
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: COLORS.CHARCOAL,
|
||||
},
|
||||
rootTitle: {
|
||||
fontSize: FONT_SIZES.HEADING,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
categorySection: { marginBottom: 20 },
|
||||
categoryHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingBottom: 6,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.BLUEPRINT,
|
||||
marginBottom: 10,
|
||||
},
|
||||
categoryIcon: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: COLORS.GRID,
|
||||
borderInlineWidth: 1,
|
||||
borderColor: COLORS.DIVIDER,
|
||||
marginRight: 10,
|
||||
},
|
||||
categoryTitle: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.CHARCOAL,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
},
|
||||
pagesGrid: { flexDirection: "row", flexWrap: "wrap" },
|
||||
pageCard: {
|
||||
width: "48%",
|
||||
marginRight: "2%",
|
||||
marginBottom: 12,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.GRID,
|
||||
backgroundColor: "#fafafa",
|
||||
},
|
||||
pageTitle: {
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.TEXT_MAIN,
|
||||
marginBottom: 4,
|
||||
},
|
||||
pageDesc: {
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
color: COLORS.TEXT_DIM,
|
||||
lineHeight: 1.3,
|
||||
},
|
||||
});
|
||||
|
||||
export const SitemapModule = ({ state }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Informationsarchitektur" />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={styles.intro}>
|
||||
Die folgende Struktur definiert die logische Hierarchie und Benutzerführung. Sie dient als Bauplan für die technische Umsetzung und stellt sicher, dass alle relevanten Geschäftsbereiche intuitiv auffindbar sind.
|
||||
</PDFText>
|
||||
<>
|
||||
<DocumentTitle title="Informationsarchitektur" isHero={true} />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={styles.intro}>
|
||||
Die folgende Struktur definiert die logische Hierarchie und
|
||||
Benutzerführung. Sie dient als Bauplan für die technische Umsetzung und
|
||||
stellt sicher, dass alle relevanten Geschäftsbereiche intuitiv
|
||||
auffindbar sind.
|
||||
</PDFText>
|
||||
|
||||
<PDFView style={styles.sitemapTree}>
|
||||
<PDFView style={styles.rootNode}>
|
||||
<PDFText style={styles.rootTitle}>Seitenstruktur</PDFText>
|
||||
</PDFView>
|
||||
|
||||
{state.sitemap?.map((cat: any, i: number) => (
|
||||
<PDFView key={i} style={styles.categorySection} wrap={false}>
|
||||
<PDFView style={styles.categoryHeader}>
|
||||
<PDFView style={styles.categoryIcon} />
|
||||
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
|
||||
</PDFView>
|
||||
|
||||
<PDFView style={styles.pagesGrid}>
|
||||
{cat.pages.map((p: any, j: number) => (
|
||||
<PDFView key={j} style={[styles.pageCard, j % 2 === 1 ? { marginRight: 0 } : {}]}>
|
||||
<PDFText style={styles.pageTitle}>{p.title}</PDFText>
|
||||
{p.desc && <PDFText style={styles.pageDesc}>{p.desc}</PDFText>}
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
<PDFView style={styles.sitemapTree}>
|
||||
<PDFView style={styles.rootNode}>
|
||||
<PDFText style={styles.rootTitle}>Seitenstruktur</PDFText>
|
||||
</PDFView>
|
||||
</>
|
||||
|
||||
{state.sitemap?.map((cat: any, i: number) => (
|
||||
<PDFView key={i} style={styles.categorySection} wrap={false}>
|
||||
<PDFView style={styles.categoryHeader}>
|
||||
<PDFView style={styles.categoryIcon} />
|
||||
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
|
||||
</PDFView>
|
||||
|
||||
<PDFView style={styles.pagesGrid}>
|
||||
{cat.pages.map((p: any, j: number) => (
|
||||
<PDFView
|
||||
key={j}
|
||||
style={[
|
||||
styles.pageCard,
|
||||
j % 2 === 1 ? { marginRight: 0 } : {},
|
||||
]}
|
||||
>
|
||||
<PDFText style={styles.pageTitle}>{p.title}</PDFText>
|
||||
{p.desc && (
|
||||
<PDFText style={styles.pageDesc}>{p.desc}</PDFText>
|
||||
)}
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,46 +1,161 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const DOCS_DIR = path.join(process.cwd(), 'docs');
|
||||
const DOCS_DIR = path.join(process.cwd(), "docs");
|
||||
|
||||
export function getTechDetails() {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(DOCS_DIR, 'TECH.md'), 'utf-8');
|
||||
const sections = content.split('⸻').map(s => s.trim());
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(DOCS_DIR, "TECH.md"), "utf-8");
|
||||
const sections = content.split("⸻").map((s) => s.trim());
|
||||
|
||||
// Extract items (Speed, Responsive, Stability, etc.)
|
||||
// Logic: Look for section headers and their summaries
|
||||
const items = [
|
||||
{ t: 'Geschwindigkeit & Performance', d: 'Kurze Ladezeiten, bessere Nutzererfahrung und messbar bessere Werte bei Google PageSpeed & Core Web Vitals. Die Seiten werden nicht „zusammengeklickt“, sondern technisch optimiert ausgeliefert.' },
|
||||
{ t: 'Responsives Design', d: 'Jede Website ist von Grund auf responsiv. Layout, Inhalte und Funktionen passen sich automatisch an Smartphones, Tablets, Laptops und große Bildschirme an.' },
|
||||
{ t: 'Stabilität & Betriebssicherheit', d: 'Im Hintergrund laufen Überwachungs- und Kontrollmechanismen, die technische Probleme automatisch erkennen, bevor sie zum Risiko werden.' },
|
||||
{ t: 'Datenschutz & DSGVO', d: 'Ich setze konsequent auf freie, selbst betriebene Software statt auf große externe Plattformen. Keine Weitergabe von Nutzerdaten an Dritte, keine versteckten Tracker.' },
|
||||
{ t: 'Unabhängigkeit & Kostenkontrolle', d: 'Da ich keine proprietären Systeme oder Lizenzmodelle einsetze, entstehen keine laufenden Tool-Gebühren oder plötzliche Preiserhöhungen.' },
|
||||
{ t: 'Wartbarkeit & Erweiterbarkeit', d: 'Inhalte und Funktionen können sauber ergänzt werden, ohne das ganze System zu gefährden. Das schützt Ihre Investition langfristig.' }
|
||||
];
|
||||
// Extract items (Speed, Responsive, Stability, etc.)
|
||||
// Logic: Look for section headers and their summaries
|
||||
const items = [
|
||||
{
|
||||
t: "Maximale Ladegeschwindigkeit & SEO-Vorsprung",
|
||||
d: "Durch modernste Auslieferungstechnologien laden Ihre Seiten nahezu verzögerungsfrei. Das sorgt für ein flüssiges Nutzererlebnis, reduziert Absprungraten und sichert Ihnen eine bevorzugte Platzierung bei Google.",
|
||||
},
|
||||
{
|
||||
t: "Investitionsschutz durch Fehlertoleranz",
|
||||
d: "Eine strikte technische Prüfung des Quellcodes bereits während der Entwicklung verhindert Fehler, bevor sie entstehen. Das garantiert eine extrem stabile, sichere und über Jahre hinweg wartbare Codebasis.",
|
||||
},
|
||||
{
|
||||
t: "Intuitive System-Kontrolle & Flexibilität",
|
||||
d: "Die saubere Trennung von Design und Inhalten erlaubt es Ihnen, alle Texte und Medien flexibel und ohne technisches Vorwissen selbst zu verwalten, während das System im Hintergrund stabil und skalierbar bleibt.",
|
||||
},
|
||||
{
|
||||
t: "Globale Performance & Skalierbarkeit",
|
||||
d: "Fortgeschrittene Zwischenspeicher-Technologien garantieren, dass Ihre Website auch bei hohen Besucherzahlen stets blitzschnell reagiert und weltweit ohne Verzögerung verfügbar ist.",
|
||||
},
|
||||
{
|
||||
t: "Infrastruktur-Souveränität & Portabilität",
|
||||
d: "Dank einer standardisierten technischen Umgebung ist Ihre Website vollständig portabel. Sie sind an keinen Anbieter gebunden und behalten die volle Kontrolle über Ihre digitale Infrastruktur.",
|
||||
},
|
||||
{
|
||||
t: "Präzision auf allen Endgeräten",
|
||||
d: "Eine hocheffiziente Architektur minimiert die zu übertragenden Datenmengen und sorgt für eine perfekte, fehlerfreie Darstellung auf jedem Smartphone, Tablet oder Desktop-Rechner.",
|
||||
},
|
||||
];
|
||||
|
||||
return items;
|
||||
} catch (e) {
|
||||
console.error('Failed to read TECH.md', e);
|
||||
return [];
|
||||
}
|
||||
return items;
|
||||
} catch (e) {
|
||||
console.error("Failed to read TECH.md", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getPrinciples() {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(DOCS_DIR, 'PRINCIPLES.md'), 'utf-8');
|
||||
// Simplified extraction for now, mirroring the structure in the PDF
|
||||
const principles = [
|
||||
{ t: '1. Volle Preis-Transparenz', d: 'Alle Kosten sind offen und nachvollziehbar. Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins. Jeder Kunde sieht genau, wofür er bezahlt.' },
|
||||
{ t: '2. Quellcode & Projektzugang', d: 'Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code. Damit kann jeder andere Entwickler problemlos weiterarbeiten.' },
|
||||
{ t: '3. Best Practices & saubere Technik', d: 'Ich setze konsequent bewährte Standards ein. Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben.' },
|
||||
{ t: '4. Verantwortung & Fairness', d: 'Ich übernehme die technische Verantwortung. Ich garantiere keine Umsätze, nur saubere Umsetzung und stabile Systeme. Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.' },
|
||||
{ t: '5. Langfristiger Wert', d: 'Eine Website ist ein Investment. Ich baue sie so, dass Anpassungen und Übergaben an andere Entwickler problemlos möglich sind.' },
|
||||
{ t: '6. Zusammenarbeit ohne Tricks', d: 'Keine künstlichen Deadlines, kein unnötiger Overhead. Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.' }
|
||||
];
|
||||
return principles;
|
||||
} catch (e) {
|
||||
console.error('Failed to read PRINCIPLES.md', e);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(
|
||||
path.join(DOCS_DIR, "PRINCIPLES.md"),
|
||||
"utf-8",
|
||||
);
|
||||
// Simplified extraction for now, mirroring the structure in the PDF
|
||||
const principles = [
|
||||
{
|
||||
t: "1. Volle Preis-Transparenz",
|
||||
d: "Alle Kosten sind offen und nachvollziehbar. Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins. Jeder Kunde sieht genau, wofür er bezahlt.",
|
||||
},
|
||||
{
|
||||
t: "2. Quellcode & Projektzugang",
|
||||
d: "Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code. Damit kann jeder andere Entwickler problemlos weiterarbeiten.",
|
||||
},
|
||||
{
|
||||
t: "3. Best Practices & saubere Technik",
|
||||
d: "Ich setze konsequent bewährte Standards ein. Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben.",
|
||||
},
|
||||
{
|
||||
t: "4. Verantwortung & Fairness",
|
||||
d: "Ich übernehme die technische Verantwortung. Ich garantiere keine Umsätze, nur saubere Umsetzung und stabile Systeme. Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.",
|
||||
},
|
||||
{
|
||||
t: "5. Langfristiger Wert",
|
||||
d: "Eine Website ist ein Investment. Ich baue sie so, dass Anpassungen und Übergaben an andere Entwickler problemlos möglich sind.",
|
||||
},
|
||||
{
|
||||
t: "6. Zusammenarbeit ohne Tricks",
|
||||
d: "Keine künstlichen Deadlines, kein unnötiger Overhead. Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.",
|
||||
},
|
||||
];
|
||||
return principles;
|
||||
} catch (e) {
|
||||
console.error("Failed to read PRINCIPLES.md", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getMaintenanceDetails() {
|
||||
try {
|
||||
const content = fs.readFileSync(
|
||||
path.join(DOCS_DIR, "MAINTENANCE.md"),
|
||||
"utf-8",
|
||||
);
|
||||
// Extracting key points based on the structure of MAINTENANCE.md
|
||||
const items = [
|
||||
{
|
||||
t: "Proaktive Betreuung",
|
||||
d: "Regelmäßige Checks, Updates von Systemen und Plugins sowie Sicherheitsüberprüfungen zur Vermeidung von Ausfällen.",
|
||||
},
|
||||
{
|
||||
t: "Schnelle Reaktion",
|
||||
d: "Analyse und Behebung bei Hoster-Störungen oder unvorhergesehenen Updates von Drittanbietern ohne Eigenaufwand für Sie.",
|
||||
},
|
||||
{
|
||||
t: "Sicherheit & Aktualität",
|
||||
d: "Bestehende Technik wird auf dem neuesten Stand gehalten, kleine Fehler werden korrigiert und Inhalte bei Bedarf angepasst.",
|
||||
},
|
||||
{
|
||||
t: "Betriebs- & Pflegeleistung",
|
||||
d: "Fortlaufende Instandhaltung und Pflege der bestehenden Seite gemäß AGB Punkt 7a.",
|
||||
},
|
||||
{
|
||||
t: "Verlässlichkeit",
|
||||
d: "Persönliche Verantwortung für einen stabilen Betrieb mit dem gleichen Anspruch, mit dem die Website gebaut wurde.",
|
||||
},
|
||||
];
|
||||
return items;
|
||||
} catch (e) {
|
||||
console.error("Failed to read MAINTENANCE.md", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getStandardsDetails() {
|
||||
try {
|
||||
const content = fs.readFileSync(
|
||||
path.join(DOCS_DIR, "STANDARDS.md"),
|
||||
"utf-8",
|
||||
);
|
||||
// Extracting key points based on the structure of STANDARDS.md
|
||||
const items = [
|
||||
{
|
||||
t: "Geringer CO₂-Verbrauch",
|
||||
d: "Schlanke Website durch optimierten Code und Bilder – oft 70–90% 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,202 +1,224 @@
|
||||
import { FormState, Position, Totals } from './types';
|
||||
import { FEATURE_LABELS, FUNCTION_LABELS, API_LABELS, PAGE_LABELS } from './constants';
|
||||
import { FormState, Position, Totals } from "./types";
|
||||
import {
|
||||
FEATURE_LABELS,
|
||||
FUNCTION_LABELS,
|
||||
API_LABELS,
|
||||
PAGE_LABELS,
|
||||
} from "./constants";
|
||||
|
||||
export function calculateTotals(state: FormState, pricing: any): Totals {
|
||||
if (state.projectType !== 'website') {
|
||||
return {
|
||||
totalPrice: 0,
|
||||
monthlyPrice: 0,
|
||||
totalPagesCount: 0,
|
||||
totalFeatures: 0,
|
||||
totalFunctions: 0,
|
||||
totalApis: 0,
|
||||
languagesCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0;
|
||||
const totalPagesCount = Math.max(
|
||||
(state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0),
|
||||
sitemapPagesCount
|
||||
);
|
||||
|
||||
const totalFeatures = (state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0);
|
||||
const totalFunctions = (state.functions?.length || 0) + (state.otherFunctions?.length || 0) + (state.otherFunctionsCount || 0);
|
||||
const totalApis = (state.apiSystems?.length || 0) + (state.otherTech?.length || 0) + (state.otherTechCount || 0);
|
||||
|
||||
let total = pricing.BASE_WEBSITE;
|
||||
total += totalPagesCount * pricing.PAGE;
|
||||
total += totalFeatures * pricing.FEATURE;
|
||||
total += totalFunctions * pricing.FUNCTION;
|
||||
total += totalApis * pricing.API_INTEGRATION;
|
||||
total += (state.newDatasets || 0) * pricing.NEW_DATASET;
|
||||
|
||||
if (state.cmsSetup) {
|
||||
total += pricing.CMS_SETUP;
|
||||
total += totalFeatures * pricing.CMS_CONNECTION_PER_FEATURE;
|
||||
}
|
||||
|
||||
// Optional visual/complexity boosters (from calculator.ts logic)
|
||||
if (state.visualStaging && !isNaN(Number(state.visualStaging)) && Number(state.visualStaging) > 0) {
|
||||
total += Number(state.visualStaging) * pricing.VISUAL_STAGING;
|
||||
}
|
||||
if (state.complexInteractions && !isNaN(Number(state.complexInteractions)) && Number(state.complexInteractions) > 0) {
|
||||
total += Number(state.complexInteractions) * pricing.COMPLEX_INTERACTION;
|
||||
}
|
||||
|
||||
const languagesCount = state.languagesList?.length || 1;
|
||||
if (languagesCount > 1) {
|
||||
total *= (1 + (languagesCount - 1) * 0.2);
|
||||
}
|
||||
|
||||
const monthlyPrice = pricing.HOSTING_MONTHLY + ((state.storageExpansion || 0) * pricing.STORAGE_EXPANSION_MONTHLY);
|
||||
|
||||
if (state.projectType !== "website") {
|
||||
return {
|
||||
totalPrice: Math.round(total),
|
||||
monthlyPrice: Math.round(monthlyPrice),
|
||||
totalPagesCount,
|
||||
totalFeatures,
|
||||
totalFunctions,
|
||||
totalApis,
|
||||
languagesCount
|
||||
totalPrice: 0,
|
||||
monthlyPrice: 0,
|
||||
totalPagesCount: 0,
|
||||
totalFeatures: 0,
|
||||
totalFunctions: 0,
|
||||
totalApis: 0,
|
||||
languagesCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const sitemapPagesCount =
|
||||
state.sitemap?.reduce(
|
||||
(acc: number, cat: any) => acc + (cat.pages?.length || 0),
|
||||
0,
|
||||
) || 0;
|
||||
const totalPagesCount = Math.max(
|
||||
(state.selectedPages?.length || 0) +
|
||||
(state.otherPages?.length || 0) +
|
||||
(state.otherPagesCount || 0),
|
||||
sitemapPagesCount,
|
||||
);
|
||||
|
||||
const totalFeatures =
|
||||
(state.features?.length || 0) +
|
||||
(state.otherFeatures?.length || 0) +
|
||||
(state.otherFeaturesCount || 0);
|
||||
const totalFunctions =
|
||||
(state.functions?.length || 0) +
|
||||
(state.otherFunctions?.length || 0) +
|
||||
(state.otherFunctionsCount || 0);
|
||||
const totalApis =
|
||||
(state.apiSystems?.length || 0) +
|
||||
(state.otherTech?.length || 0) +
|
||||
(state.otherTechCount || 0);
|
||||
|
||||
let total = pricing.BASE_WEBSITE;
|
||||
total += totalPagesCount * pricing.PAGE;
|
||||
total += totalFeatures * pricing.FEATURE;
|
||||
total += totalFunctions * pricing.FUNCTION;
|
||||
total += totalApis * pricing.API_INTEGRATION;
|
||||
total += (state.newDatasets || 0) * pricing.NEW_DATASET;
|
||||
|
||||
if (state.cmsSetup) {
|
||||
total += Math.max(1, totalFeatures) * pricing.CMS_CONNECTION_PER_FEATURE;
|
||||
}
|
||||
|
||||
const languagesCount = state.languagesList?.length || 1;
|
||||
if (languagesCount > 1) {
|
||||
total *= 1 + (languagesCount - 1) * 0.2;
|
||||
}
|
||||
|
||||
const monthlyPrice =
|
||||
pricing.HOSTING_MONTHLY +
|
||||
(state.storageExpansion || 0) * pricing.STORAGE_EXPANSION_MONTHLY;
|
||||
|
||||
return {
|
||||
totalPrice: Math.round(total),
|
||||
monthlyPrice: Math.round(monthlyPrice),
|
||||
totalPagesCount,
|
||||
totalFeatures,
|
||||
totalFunctions,
|
||||
totalApis,
|
||||
languagesCount,
|
||||
};
|
||||
}
|
||||
|
||||
export function calculatePositions(state: FormState, pricing: any): Position[] {
|
||||
const positions: Position[] = [];
|
||||
let pos = 1;
|
||||
const positions: Position[] = [];
|
||||
let pos = 1;
|
||||
|
||||
if (state.projectType === 'website') {
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Das technische Fundament',
|
||||
desc: 'Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.',
|
||||
qty: 1,
|
||||
price: pricing.BASE_WEBSITE
|
||||
});
|
||||
if (state.projectType === "website") {
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Das technische Fundament",
|
||||
desc: "Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.",
|
||||
qty: 1,
|
||||
price: pricing.BASE_WEBSITE,
|
||||
});
|
||||
|
||||
const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0;
|
||||
const totalPagesCount = Math.max(
|
||||
(state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0),
|
||||
sitemapPagesCount
|
||||
);
|
||||
const sitemapPagesCount =
|
||||
state.sitemap?.reduce(
|
||||
(acc: number, cat: any) => acc + (cat.pages?.length || 0),
|
||||
0,
|
||||
) || 0;
|
||||
const totalPagesCount = Math.max(
|
||||
(state.selectedPages?.length || 0) +
|
||||
(state.otherPages?.length || 0) +
|
||||
(state.otherPagesCount || 0),
|
||||
sitemapPagesCount,
|
||||
);
|
||||
|
||||
const allPages = [
|
||||
...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p),
|
||||
...(state.otherPages || []),
|
||||
...(state.sitemap?.flatMap((cat: any) => cat.pages?.map((p: any) => p.title)) || [])
|
||||
];
|
||||
const allPages = [
|
||||
...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p),
|
||||
...(state.otherPages || []),
|
||||
...(state.sitemap?.flatMap((cat: any) =>
|
||||
cat.pages?.map((p: any) => p.title),
|
||||
) || []),
|
||||
];
|
||||
|
||||
// Deduplicate labels
|
||||
const uniquePages = Array.from(new Set(allPages));
|
||||
// Deduplicate labels
|
||||
const uniquePages = Array.from(new Set(allPages));
|
||||
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Individuelle Seiten',
|
||||
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(', ')}).`,
|
||||
qty: totalPagesCount,
|
||||
price: totalPagesCount * pricing.PAGE
|
||||
});
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Individuelle Seiten",
|
||||
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(", ")}).`,
|
||||
qty: totalPagesCount,
|
||||
price: totalPagesCount * pricing.PAGE,
|
||||
});
|
||||
|
||||
if (state.features.length > 0 || (state.otherFeatures?.length || 0) > 0) {
|
||||
const allFeatures = [...state.features.map((f: string) => FEATURE_LABELS[f] || f), ...(state.otherFeatures || [])];
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'System-Module (Features)',
|
||||
desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(', ')}. Inklusive Datenstruktur und Darstellung.`,
|
||||
qty: allFeatures.length,
|
||||
price: allFeatures.length * pricing.FEATURE
|
||||
});
|
||||
}
|
||||
|
||||
if (state.functions.length > 0 || (state.otherFunctions?.length || 0) > 0) {
|
||||
const allFunctions = [...state.functions.map((f: string) => FUNCTION_LABELS[f] || f), ...(state.otherFunctions || [])];
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Logik-Funktionen',
|
||||
desc: `Implementierung technischer Logik: ${allFunctions.join(', ')}.`,
|
||||
qty: allFunctions.length,
|
||||
price: allFunctions.length * pricing.FUNCTION
|
||||
});
|
||||
}
|
||||
|
||||
if (state.apiSystems.length > 0 || (state.otherTech?.length || 0) > 0) {
|
||||
const allApis = [...state.apiSystems.map((a: string) => API_LABELS[a] || a), ...(state.otherTech || [])];
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Schnittstellen (API)',
|
||||
desc: `Anbindung externer Systeme zur Datensynchronisation: ${allApis.join(', ')}.`,
|
||||
qty: allApis.length,
|
||||
price: allApis.length * pricing.API_INTEGRATION
|
||||
});
|
||||
}
|
||||
|
||||
if (state.cmsSetup) {
|
||||
const totalFeatures = state.features.length + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0);
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Inhaltsverwaltung (CMS)',
|
||||
desc: 'Einrichtung eines Systems zur eigenständigen Pflege von Inhalten und Datensätzen.',
|
||||
qty: 1,
|
||||
price: pricing.CMS_SETUP + totalFeatures * pricing.CMS_CONNECTION_PER_FEATURE
|
||||
});
|
||||
}
|
||||
|
||||
if (state.newDatasets > 0) {
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Inhaltliche Initial-Pflege',
|
||||
desc: `Manuelle Übernahme und Aufbereitung von ${state.newDatasets} Datensätzen (Produkte, Artikel) in das Zielsystem.`,
|
||||
qty: state.newDatasets,
|
||||
price: state.newDatasets * pricing.NEW_DATASET
|
||||
});
|
||||
}
|
||||
|
||||
if ((state.visualStaging && Number(state.visualStaging) > 0) || (state.complexInteractions && Number(state.complexInteractions) > 0)) {
|
||||
const vsCount = Number(state.visualStaging || 0);
|
||||
const ciCount = Number(state.complexInteractions || 0);
|
||||
const totalCount = vsCount + ciCount;
|
||||
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Inszenierung & Interaktion',
|
||||
desc: `Umsetzung von ${totalCount} speziellen Sektionen, Hero-Stories oder Konfiguratoren zur Steigerung der Conversion.`,
|
||||
qty: totalCount,
|
||||
price: (vsCount * pricing.VISUAL_STAGING) + (ciCount * pricing.COMPLEX_INTERACTION)
|
||||
});
|
||||
}
|
||||
|
||||
const languagesCount = state.languagesList.length || 1;
|
||||
if (languagesCount > 1) {
|
||||
const subtotal = positions.reduce((sum, p) => sum + p.price, 0);
|
||||
const factorPrice = subtotal * ((languagesCount - 1) * 0.2);
|
||||
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Mehrsprachigkeit',
|
||||
desc: `Erweiterung des Systems auf ${languagesCount} Sprachen (Struktur & Logik).`,
|
||||
qty: languagesCount,
|
||||
price: Math.round(factorPrice)
|
||||
});
|
||||
}
|
||||
|
||||
const monthlyRate = pricing.HOSTING_MONTHLY + (state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY);
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Sorglos-Paket (Betrieb & Pflege)',
|
||||
desc: `1 Jahr Sicherung des technischen Betriebs, Instandhaltung, Sicherheits-Updates und Inhalts-Aktualisierungen gemäß AGB Punkt 7a.`,
|
||||
qty: 1,
|
||||
price: monthlyRate * 12
|
||||
});
|
||||
} else {
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Web App / Software Entwicklung',
|
||||
desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.',
|
||||
qty: 1,
|
||||
price: 0
|
||||
});
|
||||
if (state.features.length > 0 || (state.otherFeatures?.length || 0) > 0) {
|
||||
const allFeatures = [
|
||||
...state.features.map((f: string) => FEATURE_LABELS[f] || f),
|
||||
...(state.otherFeatures || []),
|
||||
];
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "System-Module (Features)",
|
||||
desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(", ")}. Inklusive Datenstruktur und Darstellung.`,
|
||||
qty: allFeatures.length,
|
||||
price: allFeatures.length * pricing.FEATURE,
|
||||
});
|
||||
}
|
||||
|
||||
return positions;
|
||||
if (state.functions.length > 0 || (state.otherFunctions?.length || 0) > 0) {
|
||||
const allFunctions = [
|
||||
...state.functions.map((f: string) => FUNCTION_LABELS[f] || f),
|
||||
...(state.otherFunctions || []),
|
||||
];
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Logik-Funktionen",
|
||||
desc: `Implementierung technischer Logik: ${allFunctions.join(", ")}.`,
|
||||
qty: allFunctions.length,
|
||||
price: allFunctions.length * pricing.FUNCTION,
|
||||
});
|
||||
}
|
||||
|
||||
if (state.apiSystems.length > 0 || (state.otherTech?.length || 0) > 0) {
|
||||
const allApis = [
|
||||
...state.apiSystems.map((a: string) => API_LABELS[a] || a),
|
||||
...(state.otherTech || []),
|
||||
];
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Schnittstellen (API)",
|
||||
desc: `Anbindung externer Systeme zur Datensynchronisation: ${allApis.join(", ")}.`,
|
||||
qty: allApis.length,
|
||||
price: allApis.length * pricing.API_INTEGRATION,
|
||||
});
|
||||
}
|
||||
|
||||
if (state.cmsSetup) {
|
||||
const totalFeatures =
|
||||
state.features.length +
|
||||
(state.otherFeatures?.length || 0) +
|
||||
(state.otherFeaturesCount || 0);
|
||||
const qty = Math.max(1, totalFeatures);
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Inhalts-Verwaltung",
|
||||
desc: "Anbindung der System-Module an das Redaktions-System zur eigenständigen Pflege von Inhalten.",
|
||||
qty: qty,
|
||||
price: qty * pricing.CMS_CONNECTION_PER_FEATURE,
|
||||
});
|
||||
}
|
||||
|
||||
if (state.newDatasets > 0) {
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Inhaltliche Initial-Pflege",
|
||||
desc: `Manuelle Übernahme und Aufbereitung von ${state.newDatasets} Datensätzen (Produkte, Artikel) in das Zielsystem.`,
|
||||
qty: state.newDatasets,
|
||||
price: state.newDatasets * pricing.NEW_DATASET,
|
||||
});
|
||||
}
|
||||
|
||||
const languagesCount = state.languagesList.length || 1;
|
||||
if (languagesCount > 1) {
|
||||
const subtotal = positions.reduce((sum, p) => sum + p.price, 0);
|
||||
const factorPrice = subtotal * ((languagesCount - 1) * 0.2);
|
||||
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Mehrsprachigkeit",
|
||||
desc: `Erweiterung des Systems auf ${languagesCount} Sprachen (Struktur & Logik).`,
|
||||
qty: languagesCount,
|
||||
price: Math.round(factorPrice),
|
||||
});
|
||||
}
|
||||
|
||||
const monthlyRate =
|
||||
pricing.HOSTING_MONTHLY +
|
||||
state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY;
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Sorglos Betrieb (1 Jahr)",
|
||||
desc: `Inklusive 1 Jahr Sicherung des technischen Betriebs, Hosting, Instandhaltung, Sicherheits-Updates und techn. Support gemäß AGB Punkt 7a.`,
|
||||
qty: 1,
|
||||
price: monthlyRate * 12,
|
||||
});
|
||||
} else {
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: "Web App / Software Entwicklung",
|
||||
desc: "Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.",
|
||||
qty: 1,
|
||||
price: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
@@ -1,226 +1,332 @@
|
||||
import { FormState } from './types';
|
||||
import { FormState } from "./types";
|
||||
|
||||
export const PRICING = {
|
||||
BASE_WEBSITE: 4000,
|
||||
PAGE: 600,
|
||||
FEATURE: 1500,
|
||||
FUNCTION: 800,
|
||||
NEW_DATASET: 200,
|
||||
HOSTING_MONTHLY: 250,
|
||||
STORAGE_EXPANSION_MONTHLY: 10,
|
||||
CMS_SETUP: 1500,
|
||||
CMS_CONNECTION_PER_FEATURE: 800,
|
||||
API_INTEGRATION: 800,
|
||||
APP_HOURLY: 120,
|
||||
VISUAL_STAGING: 2000,
|
||||
COMPLEX_INTERACTION: 1500,
|
||||
BASE_WEBSITE: 4000,
|
||||
PAGE: 600,
|
||||
FEATURE: 1500,
|
||||
FUNCTION: 800,
|
||||
NEW_DATASET: 450,
|
||||
HOSTING_MONTHLY: 250,
|
||||
STORAGE_EXPANSION_MONTHLY: 10,
|
||||
CMS_SETUP: 1500,
|
||||
CMS_CONNECTION_PER_FEATURE: 1500,
|
||||
API_INTEGRATION: 800,
|
||||
APP_HOURLY: 120,
|
||||
};
|
||||
|
||||
export const initialState: FormState = {
|
||||
projectType: 'website',
|
||||
// Company
|
||||
companyName: '',
|
||||
employeeCount: '',
|
||||
// Existing Presence
|
||||
existingWebsite: '',
|
||||
socialMedia: [],
|
||||
socialMediaUrls: {},
|
||||
existingDomain: '',
|
||||
wishedDomain: '',
|
||||
// Project
|
||||
websiteTopic: '',
|
||||
selectedPages: ['Home'],
|
||||
otherPages: [],
|
||||
otherPagesCount: 0,
|
||||
features: [],
|
||||
otherFeatures: [],
|
||||
otherFeaturesCount: 0,
|
||||
functions: [],
|
||||
otherFunctions: [],
|
||||
otherFunctionsCount: 0,
|
||||
apiSystems: [],
|
||||
otherTech: [],
|
||||
otherTechCount: 0,
|
||||
assets: [],
|
||||
otherAssets: [],
|
||||
otherAssetsCount: 0,
|
||||
newDatasets: 0,
|
||||
cmsSetup: false,
|
||||
storageExpansion: 0,
|
||||
name: '',
|
||||
email: '',
|
||||
role: '',
|
||||
message: '',
|
||||
sitemapFile: null,
|
||||
contactFiles: [],
|
||||
// Design
|
||||
designVibe: 'minimal',
|
||||
colorScheme: ['#ffffff', '#f8fafc', '#0f172a'],
|
||||
references: [],
|
||||
designWishes: '',
|
||||
// Maintenance
|
||||
expectedAdjustments: 'low',
|
||||
languagesList: ['Deutsch'],
|
||||
personName: '',
|
||||
// Timeline
|
||||
deadline: 'flexible',
|
||||
// Web App specific
|
||||
targetAudience: 'internal',
|
||||
userRoles: [],
|
||||
dataSensitivity: 'standard',
|
||||
platformType: 'web-only',
|
||||
// Meta
|
||||
dontKnows: [],
|
||||
visualStaging: 'standard',
|
||||
complexInteractions: 'standard',
|
||||
// AI generated / Post-processed
|
||||
briefingSummary: '',
|
||||
designVision: '',
|
||||
positionDescriptions: {},
|
||||
taxId: '',
|
||||
sitemap: [],
|
||||
projectType: "website",
|
||||
// Company
|
||||
companyName: "",
|
||||
employeeCount: "",
|
||||
// Existing Presence
|
||||
existingWebsite: "",
|
||||
socialMedia: [],
|
||||
socialMediaUrls: {},
|
||||
existingDomain: "",
|
||||
wishedDomain: "",
|
||||
// Project
|
||||
websiteTopic: "",
|
||||
selectedPages: ["Home"],
|
||||
otherPages: [],
|
||||
otherPagesCount: 0,
|
||||
features: [],
|
||||
otherFeatures: [],
|
||||
otherFeaturesCount: 0,
|
||||
functions: [],
|
||||
otherFunctions: [],
|
||||
otherFunctionsCount: 0,
|
||||
apiSystems: [],
|
||||
otherTech: [],
|
||||
otherTechCount: 0,
|
||||
assets: [],
|
||||
otherAssets: [],
|
||||
otherAssetsCount: 0,
|
||||
newDatasets: 0,
|
||||
cmsSetup: false,
|
||||
storageExpansion: 0,
|
||||
name: "",
|
||||
email: "",
|
||||
role: "",
|
||||
message: "",
|
||||
sitemapFile: null,
|
||||
contactFiles: [],
|
||||
// Design
|
||||
designVibe: "minimal",
|
||||
colorScheme: ["#ffffff", "#f8fafc", "#0f172a"],
|
||||
references: [],
|
||||
designWishes: "",
|
||||
// Maintenance
|
||||
expectedAdjustments: "low",
|
||||
languagesList: ["Deutsch"],
|
||||
personName: "",
|
||||
// Timeline
|
||||
deadline: "flexible",
|
||||
// Web App specific
|
||||
targetAudience: "internal",
|
||||
userRoles: [],
|
||||
dataSensitivity: "standard",
|
||||
platformType: "web-only",
|
||||
// Meta
|
||||
dontKnows: [],
|
||||
visualStaging: "standard",
|
||||
complexInteractions: "standard",
|
||||
// AI generated / Post-processed
|
||||
briefingSummary: "",
|
||||
designVision: "",
|
||||
positionDescriptions: {},
|
||||
taxId: "",
|
||||
sitemap: [],
|
||||
};
|
||||
|
||||
export const PAGE_SAMPLES = [
|
||||
{ id: 'Home', label: 'Startseite', desc: 'Der erste Eindruck Ihrer Marke.' },
|
||||
{ id: 'About', label: 'Über uns', desc: 'Ihre Geschichte und Ihr Team.' },
|
||||
{ id: 'Services', label: 'Leistungen', desc: 'Übersicht Ihres Angebots.' },
|
||||
{ id: 'Contact', label: 'Kontakt', desc: 'Anlaufstelle für Ihre Kunden.' },
|
||||
{ id: 'Landing', label: 'Landingpage', desc: 'Optimiert für Marketing-Kampagnen.' },
|
||||
{ id: 'Legal', label: 'Rechtliches', desc: 'Impressum & Datenschutz.' },
|
||||
{ id: "Home", label: "Startseite", desc: "Der erste Eindruck Ihrer Marke." },
|
||||
{ id: "About", label: "Über uns", desc: "Ihre Geschichte und Ihr Team." },
|
||||
{ id: "Services", label: "Leistungen", desc: "Übersicht Ihres Angebots." },
|
||||
{ id: "Contact", label: "Kontakt", desc: "Anlaufstelle für Ihre Kunden." },
|
||||
{
|
||||
id: "Landing",
|
||||
label: "Landingpage",
|
||||
desc: "Optimiert für Marketing-Kampagnen.",
|
||||
},
|
||||
{ id: "Legal", label: "Rechtliches", desc: "Impressum & Datenschutz." },
|
||||
];
|
||||
|
||||
export const FEATURE_OPTIONS = [
|
||||
{ id: 'blog_news', label: 'Blog / News', desc: 'Ein Bereich für aktuelle Beiträge und Neuigkeiten.' },
|
||||
{ id: 'products', label: 'Produktbereich', desc: 'Katalog Ihrer Leistungen oder Produkte.' },
|
||||
{ id: 'jobs', label: 'Karriere / Jobs', desc: 'Stellenanzeigen und Bewerbungsoptionen.' },
|
||||
{ id: 'refs', label: 'Referenzen / Cases', desc: 'Präsentation Ihrer Projekte.' },
|
||||
{ id: 'events', label: 'Events / Termine', desc: 'Veranstaltungskalender.' },
|
||||
{
|
||||
id: "blog_news",
|
||||
label: "Blog / News",
|
||||
desc: "Ein Bereich für aktuelle Beiträge und Neuigkeiten.",
|
||||
},
|
||||
{
|
||||
id: "products",
|
||||
label: "Produktbereich",
|
||||
desc: "Katalog Ihrer Leistungen oder Produkte.",
|
||||
},
|
||||
{
|
||||
id: "jobs",
|
||||
label: "Karriere / Jobs",
|
||||
desc: "Stellenanzeigen und Bewerbungsoptionen.",
|
||||
},
|
||||
{
|
||||
id: "refs",
|
||||
label: "Referenzen / Cases",
|
||||
desc: "Präsentation Ihrer Projekte.",
|
||||
},
|
||||
{ id: "events", label: "Events / Termine", desc: "Veranstaltungskalender." },
|
||||
];
|
||||
|
||||
export const FUNCTION_OPTIONS = [
|
||||
{ id: 'search', label: 'Suche', desc: 'Volltextsuche über alle Inhalte.' },
|
||||
{ id: 'filter', label: 'Filter-Systeme', desc: 'Kategorisierung und Sortierung.' },
|
||||
{ id: 'pdf', label: 'PDF-Export', desc: 'Automatisierte PDF-Erstellung.' },
|
||||
{ id: 'forms', label: 'Individuelle Formular-Logik', desc: 'Smarte Validierung & mehrstufige Prozesse.' },
|
||||
{ id: "search", label: "Suche", desc: "Volltextsuche über alle Inhalte." },
|
||||
{
|
||||
id: "filter",
|
||||
label: "Filter-Systeme",
|
||||
desc: "Kategorisierung und Sortierung.",
|
||||
},
|
||||
{ id: "pdf", label: "PDF-Export", desc: "Automatisierte PDF-Erstellung." },
|
||||
{
|
||||
id: "forms",
|
||||
label: "Individuelle Formular-Logik",
|
||||
desc: "Smarte Validierung & mehrstufige Prozesse.",
|
||||
},
|
||||
];
|
||||
|
||||
export const API_OPTIONS = [
|
||||
{ id: 'crm', label: 'CRM System', desc: 'HubSpot, Salesforce, Pipedrive etc.' },
|
||||
{ id: 'erp', label: 'ERP / Warenwirtschaft', desc: 'SAP, Microsoft Dynamics, Xentral etc.' },
|
||||
{ id: 'stripe', label: 'Stripe / Payment', desc: 'Zahlungsabwicklung und Abonnements.' },
|
||||
{ id: 'newsletter', label: 'Newsletter / Marketing', desc: 'Mailchimp, Brevo, ActiveCampaign etc.' },
|
||||
{ id: 'ecommerce', label: 'E-Commerce / Shop', desc: 'Shopify, WooCommerce, Shopware Sync.' },
|
||||
{ id: 'hr', label: 'HR / Recruiting', desc: 'Personio, Workday, Recruitee etc.' },
|
||||
{ id: 'realestate', label: 'Immobilien', desc: 'OpenImmo, FlowFact, Immowelt Sync.' },
|
||||
{ id: 'calendar', label: 'Termine / Booking', desc: 'Calendly, Shore, Doctolib etc.' },
|
||||
{ id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' },
|
||||
{ id: 'maps', label: 'Google Maps / Places', desc: 'Standortsuche und Kartenintegration.' },
|
||||
{ id: 'analytics', label: 'Custom Analytics', desc: 'Anbindung an spezialisierte Tracking-Tools.' },
|
||||
{
|
||||
id: "crm",
|
||||
label: "CRM System",
|
||||
desc: "HubSpot, Salesforce, Pipedrive etc.",
|
||||
},
|
||||
{
|
||||
id: "erp",
|
||||
label: "ERP / Warenwirtschaft",
|
||||
desc: "SAP, Microsoft Dynamics, Xentral etc.",
|
||||
},
|
||||
{
|
||||
id: "stripe",
|
||||
label: "Stripe / Payment",
|
||||
desc: "Zahlungsabwicklung und Abonnements.",
|
||||
},
|
||||
{
|
||||
id: "newsletter",
|
||||
label: "Newsletter / Marketing",
|
||||
desc: "Mailchimp, Brevo, ActiveCampaign etc.",
|
||||
},
|
||||
{
|
||||
id: "ecommerce",
|
||||
label: "E-Commerce / Shop",
|
||||
desc: "Shopify, WooCommerce, Shopware Sync.",
|
||||
},
|
||||
{
|
||||
id: "hr",
|
||||
label: "HR / Recruiting",
|
||||
desc: "Personio, Workday, Recruitee etc.",
|
||||
},
|
||||
{
|
||||
id: "realestate",
|
||||
label: "Immobilien",
|
||||
desc: "OpenImmo, FlowFact, Immowelt Sync.",
|
||||
},
|
||||
{
|
||||
id: "calendar",
|
||||
label: "Termine / Booking",
|
||||
desc: "Calendly, Shore, Doctolib etc.",
|
||||
},
|
||||
{
|
||||
id: "social",
|
||||
label: "Social Media Sync",
|
||||
desc: "Automatisierte Posts oder Feeds.",
|
||||
},
|
||||
{
|
||||
id: "maps",
|
||||
label: "Google Maps / Places",
|
||||
desc: "Standortsuche und Kartenintegration.",
|
||||
},
|
||||
{
|
||||
id: "analytics",
|
||||
label: "Custom Analytics",
|
||||
desc: "Anbindung an spezialisierte Tracking-Tools.",
|
||||
},
|
||||
];
|
||||
|
||||
export const ASSET_OPTIONS = [
|
||||
{ id: 'existing_website', label: 'Bestehende Website', desc: 'Inhalte oder Struktur können übernommen werden.' },
|
||||
{ id: 'logo', label: 'Logo', desc: 'Vektordatei Ihres Logos.' },
|
||||
{ id: 'styleguide', label: 'Styleguide', desc: 'Farben, Schriften, Design-Vorgaben.' },
|
||||
{ id: 'content_concept', label: 'Inhalts-Konzept', desc: 'Struktur und Texte sind bereits geplant.' },
|
||||
{ id: 'media', label: 'Bild/Video-Material', desc: 'Professionelles Bildmaterial vorhanden.' },
|
||||
{ id: 'icons', label: 'Icons', desc: 'Eigene Icon-Sets vorhanden.' },
|
||||
{ id: 'illustrations', label: 'Illustrationen', desc: 'Eigene Illustrationen vorhanden.' },
|
||||
{ id: 'fonts', label: 'Fonts', desc: 'Lizenzen für Hausschriften vorhanden.' },
|
||||
{
|
||||
id: "existing_website",
|
||||
label: "Bestehende Website",
|
||||
desc: "Inhalte oder Struktur können übernommen werden.",
|
||||
},
|
||||
{ id: "logo", label: "Logo", desc: "Vektordatei Ihres Logos." },
|
||||
{
|
||||
id: "styleguide",
|
||||
label: "Styleguide",
|
||||
desc: "Farben, Schriften, Design-Vorgaben.",
|
||||
},
|
||||
{
|
||||
id: "content_concept",
|
||||
label: "Inhalts-Konzept",
|
||||
desc: "Struktur und Texte sind bereits geplant.",
|
||||
},
|
||||
{
|
||||
id: "media",
|
||||
label: "Bild/Video-Material",
|
||||
desc: "Professionelles Bildmaterial vorhanden.",
|
||||
},
|
||||
{ id: "icons", label: "Icons", desc: "Eigene Icon-Sets vorhanden." },
|
||||
{
|
||||
id: "illustrations",
|
||||
label: "Illustrationen",
|
||||
desc: "Eigene Illustrationen vorhanden.",
|
||||
},
|
||||
{
|
||||
id: "fonts",
|
||||
label: "Fonts",
|
||||
desc: "Lizenzen für Hausschriften vorhanden.",
|
||||
},
|
||||
];
|
||||
|
||||
export const DESIGN_OPTIONS = [
|
||||
{ id: 'minimal', label: 'Minimalistisch', desc: 'Viel Weißraum, klare Typografie.' },
|
||||
{ id: 'bold', label: 'Mutig & Laut', desc: 'Starke Kontraste, große Schriften.' },
|
||||
{ id: 'nature', label: 'Natürlich', desc: 'Sanfte Erdtöne, organische Formen.' },
|
||||
{ id: 'tech', label: 'Technisch', desc: 'Präzise Linien, dunkle Akzente.' },
|
||||
{
|
||||
id: "minimal",
|
||||
label: "Minimalistisch",
|
||||
desc: "Viel Weißraum, klare Typografie.",
|
||||
},
|
||||
{
|
||||
id: "bold",
|
||||
label: "Mutig & Laut",
|
||||
desc: "Starke Kontraste, große Schriften.",
|
||||
},
|
||||
{
|
||||
id: "nature",
|
||||
label: "Natürlich",
|
||||
desc: "Sanfte Erdtöne, organische Formen.",
|
||||
},
|
||||
{ id: "tech", label: "Technisch", desc: "Präzise Linien, dunkle Akzente." },
|
||||
];
|
||||
|
||||
export const EMPLOYEE_OPTIONS = [
|
||||
{ id: '1-5', label: '1-5 Mitarbeiter' },
|
||||
{ id: '6-20', label: '6-20 Mitarbeiter' },
|
||||
{ id: '21-100', label: '21-100 Mitarbeiter' },
|
||||
{ id: '100+', label: '100+ Mitarbeiter' },
|
||||
{ id: "1-5", label: "1-5 Mitarbeiter" },
|
||||
{ id: "6-20", label: "6-20 Mitarbeiter" },
|
||||
{ id: "21-100", label: "21-100 Mitarbeiter" },
|
||||
{ id: "100+", label: "100+ Mitarbeiter" },
|
||||
];
|
||||
|
||||
export const SOCIAL_MEDIA_OPTIONS = [
|
||||
{ id: 'instagram', label: 'Instagram' },
|
||||
{ id: 'linkedin', label: 'LinkedIn' },
|
||||
{ id: 'facebook', label: 'Facebook' },
|
||||
{ id: 'twitter', label: 'Twitter / X' },
|
||||
{ id: 'tiktok', label: 'TikTok' },
|
||||
{ id: 'youtube', label: 'YouTube' },
|
||||
{ id: "instagram", label: "Instagram" },
|
||||
{ id: "linkedin", label: "LinkedIn" },
|
||||
{ id: "facebook", label: "Facebook" },
|
||||
{ id: "twitter", label: "Twitter / X" },
|
||||
{ id: "tiktok", label: "TikTok" },
|
||||
{ id: "youtube", label: "YouTube" },
|
||||
];
|
||||
|
||||
export const VIBE_LABELS: Record<string, string> = {
|
||||
minimal: 'Minimalistisch',
|
||||
bold: 'Mutig & Laut',
|
||||
nature: 'Natürlich',
|
||||
tech: 'Technisch'
|
||||
minimal: "Minimalistisch",
|
||||
bold: "Mutig & Laut",
|
||||
nature: "Natürlich",
|
||||
tech: "Technisch",
|
||||
};
|
||||
|
||||
export const DEADLINE_LABELS: Record<string, string> = {
|
||||
asap: 'So schnell wie möglich',
|
||||
'2-3-months': 'In 2-3 Monaten',
|
||||
'3-6-months': 'In 3-6 Monaten',
|
||||
flexible: 'Flexibel'
|
||||
asap: "So schnell wie möglich",
|
||||
"2-3-months": "In 2-3 Monaten",
|
||||
"3-6-months": "In 3-6 Monaten",
|
||||
flexible: "Flexibel",
|
||||
};
|
||||
|
||||
export const ASSET_LABELS: Record<string, string> = {
|
||||
existing_website: 'Bestehende Website',
|
||||
logo: 'Logo',
|
||||
styleguide: 'Styleguide',
|
||||
content_concept: 'Inhalts-Konzept',
|
||||
media: 'Bild/Video-Material',
|
||||
icons: 'Icons',
|
||||
illustrations: 'Illustrationen',
|
||||
fonts: 'Fonts'
|
||||
existing_website: "Bestehende Website",
|
||||
logo: "Logo",
|
||||
styleguide: "Styleguide",
|
||||
content_concept: "Inhalts-Konzept",
|
||||
media: "Bild/Video-Material",
|
||||
icons: "Icons",
|
||||
illustrations: "Illustrationen",
|
||||
fonts: "Fonts",
|
||||
};
|
||||
|
||||
export const FEATURE_LABELS: Record<string, string> = {
|
||||
blog_news: 'Blog / News',
|
||||
products: 'Produktbereich',
|
||||
jobs: 'Karriere / Jobs',
|
||||
refs: 'Referenzen / Cases',
|
||||
events: 'Events / Termine'
|
||||
blog_news: "Blog / News",
|
||||
products: "Produktbereich",
|
||||
jobs: "Karriere / Jobs",
|
||||
refs: "Referenzen / Cases",
|
||||
events: "Events / Termine",
|
||||
};
|
||||
|
||||
export const FUNCTION_LABELS: Record<string, string> = {
|
||||
search: 'Suche',
|
||||
filter: 'Filter-Systeme',
|
||||
pdf: 'PDF-Export',
|
||||
forms: 'Individuelle Formular-Logik',
|
||||
members: 'Mitgliederbereich',
|
||||
calendar: 'Event-Kalender',
|
||||
multilang: 'Mehrsprachigkeit',
|
||||
chat: 'Echtzeit-Chat'
|
||||
search: "Suche",
|
||||
filter: "Filter-Systeme",
|
||||
pdf: "PDF-Export",
|
||||
forms: "Individuelle Formular-Logik",
|
||||
members: "Mitgliederbereich",
|
||||
calendar: "Event-Kalender",
|
||||
multilang: "Mehrsprachigkeit",
|
||||
chat: "Echtzeit-Chat",
|
||||
};
|
||||
|
||||
export const API_LABELS: Record<string, string> = {
|
||||
crm_erp: 'CRM / ERP',
|
||||
payment: 'Payment',
|
||||
marketing: 'Marketing',
|
||||
ecommerce: 'E-Commerce',
|
||||
maps: 'Google Maps / Places',
|
||||
social: 'Social Media Sync',
|
||||
analytics: 'Custom Analytics'
|
||||
crm_erp: "CRM / ERP",
|
||||
payment: "Payment",
|
||||
marketing: "Marketing",
|
||||
ecommerce: "E-Commerce",
|
||||
maps: "Google Maps / Places",
|
||||
social: "Social Media Sync",
|
||||
analytics: "Custom Analytics",
|
||||
};
|
||||
|
||||
export const SOCIAL_LABELS: Record<string, string> = {
|
||||
instagram: 'Instagram',
|
||||
linkedin: 'LinkedIn',
|
||||
facebook: 'Facebook',
|
||||
twitter: 'Twitter / X',
|
||||
tiktok: 'TikTok',
|
||||
youtube: 'YouTube'
|
||||
instagram: "Instagram",
|
||||
linkedin: "LinkedIn",
|
||||
facebook: "Facebook",
|
||||
twitter: "Twitter / X",
|
||||
tiktok: "TikTok",
|
||||
youtube: "YouTube",
|
||||
};
|
||||
|
||||
export const PAGE_LABELS: Record<string, string> = {
|
||||
Home: 'Startseite',
|
||||
About: 'Über uns',
|
||||
Services: 'Leistungen',
|
||||
Contact: 'Kontakt',
|
||||
Landing: 'Landingpage',
|
||||
Legal: 'Impressum & Datenschutz'
|
||||
Home: "Startseite",
|
||||
About: "Über uns",
|
||||
Services: "Leistungen",
|
||||
Contact: "Kontakt",
|
||||
Landing: "Landingpage",
|
||||
Legal: "Impressum & Datenschutz",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user