From 3e70b00abc18d28951c49675638b01fb2801e560 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 3 Feb 2026 19:25:07 +0100 Subject: [PATCH] feat: introduce new PDF layouts and modules, enhance shared UI components, and add wording guidelines. --- docs/PRICING.md | 3 +- docs/WORDING.md | 56 ++ scripts/ai-estimate.ts | 174 +++-- scripts/generate-quote.ts | 12 +- src/components/AgbsPDF.tsx | 130 ++-- src/components/CombinedQuotePDF.tsx | 9 +- src/components/EstimationPDF.tsx | 654 ++---------------- src/components/pdf/DINLayout.tsx | 55 ++ src/components/pdf/SharedUI.tsx | 93 ++- src/components/pdf/SimpleLayout.tsx | 66 ++ .../pdf/modules/BrandingModules.tsx | 99 +++ src/components/pdf/modules/BriefingModule.tsx | 42 ++ src/components/pdf/modules/CommonModules.tsx | 100 +++ .../pdf/modules/EstimationModule.tsx | 55 ++ .../pdf/modules/FrontPageModule.tsx | 73 ++ src/components/pdf/modules/SitemapModule.tsx | 52 ++ src/logic/pricing/calculator.ts | 2 +- src/logic/pricing/constants.ts | 4 +- .../default/SDK_CRAWLER_STATISTICS_0.json | 28 +- .../default/SDK_SESSION_POOL_STATE.json | 400 ++--------- 20 files changed, 1057 insertions(+), 1050 deletions(-) create mode 100644 docs/WORDING.md create mode 100644 src/components/pdf/DINLayout.tsx create mode 100644 src/components/pdf/SimpleLayout.tsx create mode 100644 src/components/pdf/modules/BrandingModules.tsx create mode 100644 src/components/pdf/modules/BriefingModule.tsx create mode 100644 src/components/pdf/modules/CommonModules.tsx create mode 100644 src/components/pdf/modules/EstimationModule.tsx create mode 100644 src/components/pdf/modules/FrontPageModule.tsx create mode 100644 src/components/pdf/modules/SitemapModule.tsx diff --git a/docs/PRICING.md b/docs/PRICING.md index 4d7b54d..df0acee 100644 --- a/docs/PRICING.md +++ b/docs/PRICING.md @@ -12,6 +12,7 @@ Die Grundlage für jede Website: • Grundstruktur & Design-Vorlage • technisches SEO-Basics • Analytics (mit automatischem Mail-Report) + • Testing, Staging, Production Umgebung • Livegang Enthält keine Seiten, Inhalte oder Funktionen. @@ -125,7 +126,7 @@ Datensatz anpassen Hosting & Betrieb -120 € / Monat +12 Monate = 1440 € Sichert: • Webhosting & Verfügbarkeit diff --git a/docs/WORDING.md b/docs/WORDING.md new file mode 100644 index 0000000..2c1dafa --- /dev/null +++ b/docs/WORDING.md @@ -0,0 +1,56 @@ +1. Aktiv statt passiv + +Sätze werden aktiv formuliert. +Keine unpersönlichen Konstruktionen, kein „es wird“, „man sollte“, „könnte“. + +2. Kurz und eindeutig + +Sätze sind so kurz wie möglich, so lang wie nötig. +Ein Gedanke pro Satz. Keine Schachtelsätze. + +3. Keine Weichmacher + +Keine Wörter wie: + • eventuell + • möglicherweise + • grundsätzlich + • in der Regel + • normalerweise + +Wenn etwas gilt, wird es gesagt. Wenn nicht, wird es ausgeschlossen. + +4. Keine Marketingbegriffe + +Keine Buzzwords, Superlative oder leeren Versprechen. +Keine emotional aufgeladenen Begriffe. Keine Werbesprache. + +5. Konkrete Aussagen + +Keine abstrakten Formulierungen. +Aussagen beziehen sich auf konkrete Ergebnisse, Zustände oder Abläufe. + +6. Ich-Form + +Kommunikation erfolgt konsequent in der Ich-Form. +Kein „wir“, kein „unser Team“, keine künstliche Vergrößerung. + +7. Keine Rechtfertigungen + +Keine erklärenden Absicherungen im Satz. +Aussagen stehen für sich und werden nicht relativiert. + +8. Neutraler Ton + +Keine Umgangssprache. +Keine Ironie. +Keine Emojis. + +9. Verbindliche Sprache + +Keine offenen Enden ohne Grund. +Wenn etwas nicht garantiert wird, wird das klar benannt – ohne Abschwächung. + +10. Technisch präzise, sprachlich einfach + +Technische Inhalte werden präzise beschrieben, sprachlich jedoch simpel gehalten. +Kein unnötiger Jargon. diff --git a/scripts/ai-estimate.ts b/scripts/ai-estimate.ts index 700cbf4..3fc28d4 100644 --- a/scripts/ai-estimate.ts +++ b/scripts/ai-estimate.ts @@ -89,7 +89,19 @@ async function main() { } } - // 2. AI Prompting + // 2. Distill Crawl Context (Context Filtering) + let distilledCrawl = ''; + if (crawlContext) { + const cachedDistilled = await cache.get(`distilled_${targetUrl}`); + if (cachedDistilled && !clearCache) { + distilledCrawl = cachedDistilled; + } else { + distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY); + await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400); + } + } + + // 3. AI Prompting console.log('🤖 Consultating Gemini 3 Flash...'); const cachedAi = !clearCache ? await cache.get(finalCacheKey) : null; let formState: any; @@ -108,7 +120,7 @@ async function main() { console.log('📦 Using cached AI response.'); formState = cachedAi; } else { - const result = await getAiEstimation(briefing, crawlContext, comments, OPENROUTER_KEY, principles, techStandards, tone); + const result = await getAiEstimation(briefing, distilledCrawl, comments, OPENROUTER_KEY, principles, techStandards, tone); formState = result.state; usage = result.usage; await cache.set(finalCacheKey, formState); @@ -146,6 +158,32 @@ async function main() { } } +async function distillCrawlContext(rawCrawl: string, apiKey: string): Promise { + if (!rawCrawl || rawCrawl.trim().length === 0) return "Keine Crawl-Daten vorhanden."; + + console.log(' ↳ Distilling Crawl Context (Noise Filtering)...'); + const systemPrompt = ` +You are a context distiller. Your goal is to strip away HTML noise, legal footers, and generic fluff from a website crawl. +Extract the "Company DNA" in 5-8 bullet points (GERMAN). + +### FOCUS ON: +1. Core Business / Services. +2. Unique Selling Points (USPs). +3. Target Audience (if clear). +4. Tech Stack or industry-specific equipment mentioned. +5. Brand tone (e.g. "industrial", "friendly", "technical"). + +### OUTPUT: +Return ONLY the bullet points. No intro/outro. +`; + const resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { + model: 'google/gemini-3-flash-preview', + messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: `RAW_CRAWL:\n${rawCrawl.substring(0, 30000)}` }], + }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); + + return resp.data.choices[0].message.content; +} + async function performCrawl(url: string): Promise { const pages: { url: string, content: string, type: string }[] = []; const origin = new URL(url).origin; @@ -205,7 +243,7 @@ async function performCrawl(url: string): Promise { return summary + pages.map(p => `--- PAGE: ${p.url} ---\n${p.content}`).join('\n\n'); } -async function getAiEstimation(briefing: string, crawlContext: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) { +async function getAiEstimation(briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) { let usage = { prompt: 0, completion: 0, cost: 0 }; const addUsage = (data: any) => { if (data?.usage) { @@ -222,22 +260,25 @@ async function getAiEstimation(briefing: string, crawlContext: string, comments: } }; - // 1. PASS 1: Fact Extraction - console.log(' ↳ Pass 1: Fact Extraction...'); + // 1. PASS 1: Fact Extraction (Briefing Sensor) + console.log(' ↳ Pass 1: Fact Extraction (Briefing Sensor)...'); const pass1SystemPrompt = ` -You are a precision extraction engine. Analyze the briefing and extract ONLY the raw facts. +You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts. Tone: Literal, non-interpretive. Output language: GERMAN (Strict). +### MISSION: +Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL only as background context for terms or company details. If there is a conflict, the BRIEFING is the absolute source of truth. + ### OBJECTIVES: - Extract companyName (Strictly the name, no descriptors). - Extract companyAddress (Full address if found). - Extract personName (Primary contact if found). -- Extract **websiteTopic**: This MUST be a single, short branch name (e.g., "Kabeltiefbau", "Logistik", "Anwaltskanzlei"). ABSOLUTELY NO SENTENCES. If the briefing says "Group-Homepage for X", extract ONLY "X". +- Extract **websiteTopic**: This MUST be a single, short branch name (e.g., "Kabeltiefbau", "Logistik", "Anwaltskanzlei"). ABSOLUTELY NO SENTENCES. - Map to internal IDs for selectedPages, features, functions, apiSystems, assets. - Identify if isRelaunch is true (briefing mentions existing site or URL). - For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN. -- **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN). If only one language is mentioned, do NOT use multilang. +- **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN). - **maps**: If "Google Maps" or location maps are mentioned or implicit (Contact page). - **CRITICAL**: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC/POST content to social media. "Existing social media links" are NOT apiSystems. - **CRITICAL**: "Video Player", "Cookie Banner", "Animations" are NOT features. They are visual/base. Do NOT map them to features. @@ -267,7 +308,7 @@ Output language: GERMAN (Strict). "employeeCount": string } `; - const pass1UserPrompt = `BRIEFING:\n${briefing}\n\nCOMMENTS:\n${comments}\n\nCRAWL:\n${crawlContext}`; + const pass1UserPrompt = `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nCOMMENTS:\n${comments}\n\nDISTILLED_CRAWL (CONTEXT ONLY):\n${distilledCrawl}`; const p1Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass1SystemPrompt }, { role: 'user', content: pass1UserPrompt }], @@ -308,8 +349,8 @@ ${JSON.stringify(facts, null, 2)} addUsage(p2Resp.data); const details = JSON.parse(p2Resp.data.choices[0].message.content); - // 3. PASS 3: Strategic Content - console.log(' ↳ Pass 3: Strategic Content...'); + // 3. PASS 3: Strategic Content (Bespoke Strategy) + console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...'); const pass3SystemPrompt = ` You are a high-end Digital Architect. Analyze the BRIEFING. ABSOLUTE RULE: OUTPUT MUST BE 100% GERMAN. @@ -319,14 +360,16 @@ ${tone} ### OBJECTIVE: 1. **briefingSummary**: Summarize the project's essence for the CUSTOMER. - - FOLLOW PRINCIPLE 1 & 5: Clear, direct, no marketing fluff, no "partnership talk". - - Focus purely on the CUSTOMER'S goal: What are they building, why does it matter to their business, and what is the outcome? + - FOLLOW PRINCIPLE 1 & 5: Clear, direct, no marketing fluff. + - **MIRROR TEST**: Capture unique customer "hooks" or personality (e.g., if they mention an Instagram account or a specific hobby/preference that influences the project vibe). + - Focus 100% on the BRIEFING (TRUTH SOURCE). Ignore the CRAWL context for this narrative. - Keep it 2-3 professional, direct sentences. 2. **designVision**: A solid, grounded, and high-quality description of the look & feel. - FOLLOW PRINCIPLE 1 & 3: Fact-based, professional, high density of information. + - **BESPOKE ELEMENTS**: If the client mentions specific layout ideas (e.g., "Timeline", "Grid", "Industrial Tiles"), incorporate these as strategic decisions. - **NO ARROGANCE**: Eliminate all "high-end", "world-class", "dominance" language. Be humble and precise. - **SIMPLE & CLEAR**: Use simple German. No buzzwords. "Solid Industrial Design" instead of "Technocratic Sovereignty". - - 3-4 sentences of deep analysis. + - 3-5 sentences of deep analysis. ### OUTPUT FORMAT (Strict JSON): { @@ -336,7 +379,7 @@ ${tone} `; const p3Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', - messages: [{ role: 'system', content: pass3SystemPrompt }, { role: 'user', content: briefing }], + messages: [{ role: 'system', content: pass3SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}` }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p3Resp.data); @@ -365,7 +408,7 @@ ${JSON.stringify({ facts, strategy }, null, 2)} `; const p4Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', - messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: briefing }], + messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nDISTILLED_CRAWL (CONTEXT):\n${distilledCrawl}` }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p4Resp.data); @@ -388,26 +431,29 @@ Each position in the quote must be perfectly justified and detailed. - **Schnittstellen (API)**: REAL Data Syncs (CRM, ERP). DO NOT include Tracking, Google Maps, or simple Video embedding here. Basic embedding is "Basis Website Setup". - **Sorglos-Betrieb (Hosting)**: Hosting & Maintenance. -### RULES FOR positionDescriptions: +### RULES FOR positionDescriptions (STRICT): 1. **ZERO GENERALIZATION**: Do NOT say "Verschiedene Funktionen". -2. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1. -3. **BREVITY & DENSITY**: Max 1-2 short sentences. Focus on TASKS not RESULTS. -4. **STYLE**: Direct, engineering-grade, no fluff. -5. **LANGUAGE**: 100% GERMAN. -6. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list (e.g. "Umfasst: Startseite, Über uns, Leistungen, Kontakt, Impressum"). -7. **SPECIFIC - API**: Video Uploads, Google Maps, and Tracking are NOT APIs. If video/maps are standard embedding, do NOT put them in "Schnittstellen". -8. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher. Auto-Erweiterung +10€/10GB." -9. **SPECIFIC - LOGIC**: Describe the ACTUAL logic. - - BAD: "Erweiterte Formulare", "Logikfunktionen" - - GOOD: "Anfrage-Strecken mit Validierung", "Filterung nach Kategorie", "Mehrsprachigkeits-Routing". +2. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1. +3. **HARD SPECIFICS**: Preserve technical details from the briefing (e.g., "110 kV", "HDD-Bohrtechnik", specific industry standards). +4. **BREVITY & DENSITY**: Max 1-2 short sentences. Focus on TASKS not RESULTS. +5. **STYLE**: Direct, engineering-grade, no fluff. +6. **LANGUAGE**: 100% GERMAN. +7. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list. +8. **SPECIFIC - API**: Video Uploads, Google Maps, and Tracking are NOT APIs. +9. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher. Auto-Erweiterung +10€/10GB." +10. **SPECIFIC - LOGIC**: Describe the ACTUAL logic. NEVER use generic terms like "Erweiterte Formulare" or "Individuelle Formular-Logik". +11. **STRICT KEYS**: Keys MUST be EXACTLY: "Basis Website Setup", "Individuelle Seiten", "System-Module", "Logik-Funktionen", "Schnittstellen (API)", "Inhaltsverwaltung (CMS)", "Sorglos-Betrieb (Hosting)". -### FORBIDDEN PHRASES (STRICT BLOCKLIST): -- "Erweiterte Formulare" (INSTEAD USE: "Komplexe Anfrage-Logik" or "Valide Formular-Systeme") -- "Verschiedene Funktionen" -- "Allgemeine Logik" -- "Optimierte Darstellung" +### EXAMPLES (FEW-SHOT): +- **BAD**: "Individuelle Seiten für die Unternehmensdarstellung." +- **GOOD**: "Erstellung der Seiten: Startseite (Video-Hero), Über uns (Timeline), Leistungen (110kV Montage), Kontakt." +- **BAD**: "Anbindung von externen Systemen." +- **GOOD**: "Native Integration von Google Maps zur Standortermittlung inkl. individueller Marker-Logik." -10. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK. +### FORBIDDEN PHRASES: +- "Erweiterte Formulare", "Verschiedene Funktionen", "Allgemeine Logik", "Optimierte Darstellung", "Individuelle Formular-Logik". + +11. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK. ### DATA CONTEXT: ${JSON.stringify({ facts, details, strategy, ia }, null, 2)} @@ -425,48 +471,60 @@ ${JSON.stringify({ facts, details, strategy, ia }, null, 2)} addUsage(p5Resp.data); const positionsData = JSON.parse(p5Resp.data.choices[0].message.content); - // 6. PASS 6: Reflection & Hardening - console.log(' ↳ Pass 6: Reflection & Nuance Check...'); + // 6. PASS 6: The Industrial Critic + console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...'); const pass6SystemPrompt = ` -You are a senior supervisor. Compare the CURRENT_STATE against the RAW_BRIEFING. -Your goal is to catch missed nuances, specific customer wishes, and technical details. +You are the "Industrial Critic". Your goal is to catch quality regressions and ensure the document is bespoke, technical, and professional. +Analyze the CURRENT_STATE against the BRIEFING_TRUTH. -### CHECKLIST: -1. **SPECIFICS**: Did we miss names, technical terms (kV, HDD, etc.), or specific vendor refs? -2. **CONSISTENCY**: Do the positionDescriptions match the counts of features/functions in Pass 1? -3. **DEADLINE**: Is there a specific month? (e.g. April/Mai). If yes, set "deadline" field. -4. **LANGUAGE**: ABSOLUTE RULE: EVERYTHING MUST BE GERMAN. -5. **CONFLICT CHECK**: If 'languagesList' has only 1 item, REMOVE 'multilang' from 'functions'. -6. Refactor 'dontKnows' into a 'gridDontKnows' object for missing technical facts. +### CRITICAL ERROR CHECKLIST (FAIL IF FOUND): +1. **Placeholder Leakage**: Catch "null", "undefined", or generic strings like "Verschiedene Funktionen", "Erweiterte Formulare", "Individuelle Formular-Logik". +2. **Detail Loss**: The user mentioned specific terms (e.g., "110 kV", "HDD", "Timeline", "Instagram"). Are they present in the positionDescriptions or sitemap? If not, ADD THEM. +3. **Consistency**: Ensure the count of pages in "Individuelle Seiten" matches the sitemap pages. +4. **Deadlines**: Ensure relative dates (e.g., "April / Mai") are resolved to the year 2026. +5. **Tone Drift**: Remove any marketing "fluff" or "sales-y" language. Maintain the "Industrial Design" persona. -### CURRENT_STATE: +### MISSION: +Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', and 'briefingSummary'. + +### DATA CONTEXT: ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} `; const p6Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', - messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `RAW_BRIEFING:\n${briefing}\n\nEnhance the state. Return ONLY the delta or the corrected fields.` }], + messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `BRIEFING_TRUTH:\n${briefing}` }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p6Resp.data); const reflection = JSON.parse(p6Resp.data.choices[0].message.content); - let finalState = { + // 6. Reflection Merge Utility + const mergeReflection = (state: any, reflection: any) => { + let result = { ...state }; + const unwrap = (obj: any): any => { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj; + if (obj["0"]) return unwrap(obj["0"]); + if (obj.state) return unwrap(obj.state); + if (obj.facts) return unwrap(obj.facts); + if (obj.strategy) return unwrap(obj.strategy); + if (obj.ia) return unwrap(obj.ia); + if (obj.positionsData) return unwrap(obj.positionsData); + return obj; + }; + + const cleanedReflection = unwrap(reflection); + return { ...result, ...cleanedReflection }; + }; + + let finalState = mergeReflection({ ...initialState, ...facts, ...strategy, ...ia, - ...positionsData, - ...reflection, - statusQuo: facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung' - }; + ...positionsData + }, reflection); - // Flatten if AI nested everything under "0", "state" or "state.0" - if (finalState["0"]) finalState = { ...finalState, ...finalState["0"] }; - if ((finalState as any).state) { - const nestedState = (finalState as any).state; - finalState = { ...finalState, ...nestedState }; - if (nestedState["0"]) finalState = { ...finalState, ...nestedState["0"] }; - } + finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung'; // Normalization Layer: Map hallucinated German keys back to internal keys const normalizationMap: Record = { diff --git a/scripts/generate-quote.ts b/scripts/generate-quote.ts index 9201d85..6ed0937 100644 --- a/scripts/generate-quote.ts +++ b/scripts/generate-quote.ts @@ -15,8 +15,8 @@ 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 outputPath = args.find((_, i) => args[i - 1] === '--output' || args[i - 1] === '-o'); let state = { ...initialState }; @@ -39,7 +39,7 @@ async function main() { const monthlyPrice = calculateMonthly(state); const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0); - const finalOutputPath = outputPath || generateDefaultPath(state); + const finalOutputPath = generateDefaultPath(state); const outputDir = path.dirname(finalOutputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); @@ -66,7 +66,9 @@ async function main() { createElement(CombinedQuotePDF as any, { estimationProps, techDetails: getTechDetails(), - principles: getPrinciples() + principles: getPrinciples(), + mode: isEstimationOnly ? 'estimation' : 'full', + showAgbs: !isEstimationOnly // AGBS only for full quotes }) as any, finalOutputPath ); @@ -144,8 +146,10 @@ 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}_${company}_${state.projectType}.pdf`); + return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${time}_${company}_${state.projectType}.pdf`); } main().catch(err => { diff --git a/src/components/AgbsPDF.tsx b/src/components/AgbsPDF.tsx index 22b272a..75211dc 100644 --- a/src/components/AgbsPDF.tsx +++ b/src/components/AgbsPDF.tsx @@ -8,6 +8,7 @@ import { StyleSheet as PDFStyleSheet, } from '@react-pdf/renderer'; import { pdfStyles, Header, Footer, FoldingMarks, DocumentTitle } from './pdf/SharedUI'; +import { SimpleLayout } from './pdf/SimpleLayout'; const localStyles = PDFStyleSheet.create({ sectionContainer: { @@ -52,9 +53,10 @@ interface AgbsPDFProps { state: any; headerIcon?: string; footerLogo?: string; + mode?: 'estimation' | 'full'; } -export const AgbsPDF = ({ state, headerIcon, footerLogo }: AgbsPDFProps) => { +export const AgbsPDF = ({ state, headerIcon, footerLogo, mode = 'full' }: AgbsPDFProps) => { const date = new Date().toLocaleDateString('de-DE', { year: 'numeric', month: 'long', @@ -74,59 +76,79 @@ export const AgbsPDF = ({ state, headerIcon, footerLogo }: AgbsPDFProps) => { iban: "DE50 1001 1001 2620 4328 65" }; + const content = ( + <> + + + + Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend „Auftragnehmer“) und dem jeweiligen Kunden (nachfolgend „Auftraggeber“). Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch wenn ihrer Geltung nicht ausdrücklich widersprochen wird. + + + + Der Auftragnehmer erbringt Dienstleistungen im Bereich: Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen, Schnittstellen und Automatisierungen sowie Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbard. Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch einen wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten, Suchmaschinen-Rankings oder rechtliche Ergebnisse. + + + + Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback, Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum, DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen aller Termine ohne Schadensersatzanspruch. + + + + Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als verbindlich vereinbart wurden. + + + + Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar. + + + + Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für entgangenen Gewinn, Umsatzausfälle, Datenverlust, Betriebsunterbrechungen, mittelbare oder Folgeschäden ist ausgeschlossen, soweit gesetzlich zulässig. + + + + Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine permanente Verfügbarkeit. Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen Einschränkungen führen und begründen keine Haftungsansprüche. + + + + Die Betriebs- und Pflegeleistung umfasst ausschließlich die Sicherstellung des technischen Betriebs, Wartung, Updates, Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender Datensätze ohne Strukturänderung. Nicht Bestandteil sind die Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle Tätigkeiten, strategische Planung oder der Aufbau neuer Features/Datenmodelle. Leistungen darüber hinaus gelten als Neuentwicklung. + + + + Der Auftragnehmer übernimmt keine Verantwortung für Leistungen, Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden. + + + + Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten. Der Auftragnehmer übernimmt keine rechtliche Prüfung. + + + + Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt, Leistungen auszusetzen, Systeme offline zu nehmen oder laufende Arbeiten zu stoppen. + + + + Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes vereinbart ist. + + + + Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt. + + + + ); + + if (mode === 'full') { + return ( + + {content} + + ); + } + return ( -
- - Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend „Auftragnehmer“) und dem jeweiligen Kunden (nachfolgend „Auftraggeber“). Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch wenn ihrer Geltung nicht ausdrücklich widersprochen wird. - - - - Der Auftragnehmer erbringt Dienstleistungen im Bereich: Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen, Schnittstellen und Automatisierungen sowie Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbart. Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch einen wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten, Suchmaschinen-Rankings oder rechtliche Ergebnisse. - - - - Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback, Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum, DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen aller Termine ohne Schadensersatzanspruch. - - - - Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als verbindlich vereinbart wurden. - - - - Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar. - - - - Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für entgangenen Gewinn, Umsatzausfälle, Datenverlust, Betriebsunterbrechungen, mittelbare oder Folgeschäden ist ausgeschlossen, soweit gesetzlich zulässig. - - - - Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine permanente Verfügbarkeit. Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen Einschränkungen führen und begründen keine Haftungsansprüche. - - - - Die Betriebs- und Pflegeleistung umfasst ausschließlich die Sicherstellung des technischen Betriebs, Wartung, Updates, Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender Datensätze ohne Strukturänderung. Nicht Bestandteil sind die Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle Tätigkeiten, strategische Planung oder der Aufbau neuer Features/Datenmodelle. Leistungen darüber hinaus gelten als Neuentwicklung. - - - - Der Auftragnehmer übernimmt keine Verantwortung für Leistungen, Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden. - - - - Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten. Der Auftragnehmer übernimmt keine rechtliche Prüfung. - - - - Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt, Leistungen auszusetzen, Systeme offline zu nehmen oder laufende Arbeiten zu stoppen. - - - - Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes vereinbart ist. - - - - Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt. - -