feat(cms): final restoration of extension logic and monorepo stabilization
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 55s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m32s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m50s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 55s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m32s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m50s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
This commit is contained in:
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
**/index.js
|
||||||
|
**/dist/**
|
||||||
40
build_output.txt
Normal file
40
build_output.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
|
||||||
|
> @mintel/monorepo@1.7.12 build /Users/marcmintel/Projects/at-mintel
|
||||||
|
> pnpm -r build
|
||||||
|
|
||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
Scope: 19 of 20 workspace projects
|
||||||
|
packages/acquisition build$ node build.js
|
||||||
|
packages/acquisition-manager build$ directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)
|
||||||
|
packages/customer-manager build$ directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)
|
||||||
|
packages/feedback-commander build$ directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)
|
||||||
|
packages/acquisition build: Building from /Users/marcmintel/Projects/at-mintel/packages/acquisition/src/index.ts to /Users/marcmintel/Projects/at-mintel/packages/acquisition/index.js...
|
||||||
|
packages/acquisition build: ✘ [ERROR] Could not resolve "@mintel/mail"
|
||||||
|
packages/acquisition build: src/index.ts:4:67:
|
||||||
|
packages/acquisition build: 4 │ ..., SiteAuditTemplate, ProjectEstimateTemplate } from "@mintel/mail";
|
||||||
|
packages/acquisition build: ╵ ~~~~~~~~~~~~~~
|
||||||
|
packages/acquisition build: You can mark the path "@mintel/mail" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle.
|
||||||
|
packages/acquisition build: Build failed: Error: Build failed with 1 error:
|
||||||
|
packages/acquisition build: src/index.ts:4:67: ERROR: Could not resolve "@mintel/mail"
|
||||||
|
packages/acquisition build: at failureErrorWithLog (/Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:1467:15)
|
||||||
|
packages/acquisition build: at /Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:926:25
|
||||||
|
packages/acquisition build: at runOnEndCallbacks (/Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:1307:45)
|
||||||
|
packages/acquisition build: at buildResponseToResult (/Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:924:7)
|
||||||
|
packages/acquisition build: at /Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:951:16
|
||||||
|
packages/acquisition build: at responseCallbacks.<computed> (/Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:603:9)
|
||||||
|
packages/acquisition build: at handleIncomingPacket (/Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:658:12)
|
||||||
|
packages/acquisition build: at Socket.readFromStdout (/Users/marcmintel/Projects/at-mintel/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:581:7)
|
||||||
|
packages/acquisition build: at Socket.emit (node:events:519:28)
|
||||||
|
packages/acquisition build: at addChunk (node:internal/streams/readable:559:12) {
|
||||||
|
packages/acquisition build: errors: [Getter/Setter],
|
||||||
|
packages/acquisition build: warnings: [Getter/Setter]
|
||||||
|
packages/acquisition build: }
|
||||||
|
packages/acquisition build: Failed
|
||||||
|
/Users/marcmintel/Projects/at-mintel/packages/acquisition:
|
||||||
|
ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL acquisition@1.7.12 build: `node build.js`
|
||||||
|
Exit status 1
|
||||||
|
packages/people-manager build$ directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)
|
||||||
|
ELIFECYCLE Command failed with exit code 1.
|
||||||
23
lint_output.txt
Normal file
23
lint_output.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
|
||||||
|
> @mintel/monorepo@1.7.12 lint /Users/marcmintel/Projects/at-mintel
|
||||||
|
> pnpm -r --filter='./packages/**' --filter='./apps/**' lint
|
||||||
|
|
||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
WARN Issue while reading "/Users/marcmintel/Projects/at-mintel/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||||
|
Scope: 19 of 20 workspace projects
|
||||||
|
packages/mail lint$ eslint src
|
||||||
|
packages/next-feedback lint$ eslint src/
|
||||||
|
packages/next-utils lint$ eslint src/
|
||||||
|
packages/observability lint$ eslint src/
|
||||||
|
packages/next-utils lint: Done
|
||||||
|
packages/mail lint: Done
|
||||||
|
packages/observability lint: Done
|
||||||
|
packages/next-feedback lint: Done
|
||||||
|
packages/gatekeeper lint$ eslint src/
|
||||||
|
packages/next-observability lint$ eslint src/
|
||||||
|
packages/next-observability lint: Done
|
||||||
|
packages/gatekeeper lint: Done
|
||||||
|
apps/sample-website lint$ eslint src/
|
||||||
|
apps/sample-website lint: Done
|
||||||
44
packages/acquisition-library/build.js
Normal file
44
packages/acquisition-library/build.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { build } from 'esbuild';
|
||||||
|
import { resolve, dirname } from 'path';
|
||||||
|
import { mkdirSync } from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const entryPoint = resolve(__dirname, 'src/index.ts');
|
||||||
|
const outfile = resolve(__dirname, 'index.js');
|
||||||
|
|
||||||
|
try {
|
||||||
|
mkdirSync(dirname(outfile), { recursive: true });
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
console.log(`Building from ${entryPoint} to ${outfile}...`);
|
||||||
|
|
||||||
|
build({
|
||||||
|
entryPoints: [entryPoint],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: 'node18',
|
||||||
|
outfile: outfile,
|
||||||
|
format: 'esm',
|
||||||
|
external: [],
|
||||||
|
plugins: [{
|
||||||
|
name: 'mock-jquery',
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^jquery$/ }, args => ({ path: args.path, namespace: 'mock-jquery' }));
|
||||||
|
build.onLoad({ filter: /.*/, namespace: 'mock-jquery' }, () => ({ contents: 'export default {};', loader: 'js' }));
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
name: 'mock-canvas',
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^canvas$/ }, args => ({ path: args.path, namespace: 'mock-canvas' }));
|
||||||
|
build.onLoad({ filter: /.*/, namespace: 'mock-canvas' }, () => ({ contents: 'export default {};', loader: 'js' }));
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}).then(() => {
|
||||||
|
console.log("Build succeeded!");
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error("Build failed:", e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
512414
packages/acquisition-library/index.js
Normal file
512414
packages/acquisition-library/index.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "acquisition",
|
"name": "@mintel/acquisition",
|
||||||
"version": "1.7.12",
|
"version": "1.7.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"directus:extension": {
|
|
||||||
"type": "endpoint",
|
|
||||||
"path": "index.js",
|
|
||||||
"source": "src/index.ts",
|
|
||||||
"host": "^11.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
"dev": "node build.js --watch"
|
"dev": "node build.js --watch"
|
||||||
@@ -18,8 +12,9 @@
|
|||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mintel/mail": "workspace:*",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Page as PDFPage, Document as PDFDocument } from "@react-pdf/renderer";
|
||||||
|
import { pdfStyles } from "./pdf/SharedUI.js";
|
||||||
|
import { SimpleLayout } from "./pdf/SimpleLayout.js";
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
import { FrontPageModule } from "./pdf/modules/FrontPageModule.js";
|
||||||
|
import { BriefingModule } from "./pdf/modules/BriefingModule.js";
|
||||||
|
import { SitemapModule } from "./pdf/modules/SitemapModule.js";
|
||||||
|
import { EstimationModule } from "./pdf/modules/EstimationModule.js";
|
||||||
|
import { TransparenzModule } from "./pdf/modules/TransparenzModule.js";
|
||||||
|
import { ClosingModule } from "./pdf/modules/CommonModules.js";
|
||||||
|
|
||||||
|
import { calculatePositions } from "../logic/pricing/calculator.js";
|
||||||
|
|
||||||
|
interface PDFProps {
|
||||||
|
state: any;
|
||||||
|
totalPrice: number;
|
||||||
|
pricing: any;
|
||||||
|
headerIcon?: string;
|
||||||
|
footerLogo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EstimationPDF = ({
|
||||||
|
state,
|
||||||
|
totalPrice,
|
||||||
|
pricing,
|
||||||
|
headerIcon,
|
||||||
|
footerLogo,
|
||||||
|
}: PDFProps) => {
|
||||||
|
const date = new Date().toLocaleDateString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
const positions = calculatePositions(state, pricing);
|
||||||
|
|
||||||
|
const companyData = {
|
||||||
|
name: "Marc Mintel",
|
||||||
|
address1: "Georg-Meistermann-Straße 7",
|
||||||
|
address2: "54586 Schüller",
|
||||||
|
ustId: "DE367588065",
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
state,
|
||||||
|
date,
|
||||||
|
icon: headerIcon,
|
||||||
|
footerLogo,
|
||||||
|
companyData,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pageCounter = 1;
|
||||||
|
const getPageNum = () => (pageCounter++).toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PDFDocument title={`Angebot - ${state.companyName || "Projekt"}`}>
|
||||||
|
<PDFPage size="A4" style={pdfStyles.titlePage}>
|
||||||
|
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
|
||||||
|
</PDFPage>
|
||||||
|
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<BriefingModule state={state} />
|
||||||
|
</SimpleLayout>
|
||||||
|
|
||||||
|
{state.sitemap && state.sitemap.length > 0 && (
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<SitemapModule state={state} />
|
||||||
|
</SimpleLayout>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<EstimationModule
|
||||||
|
state={state}
|
||||||
|
positions={positions}
|
||||||
|
totalPrice={totalPrice}
|
||||||
|
date={date}
|
||||||
|
/>
|
||||||
|
</SimpleLayout>
|
||||||
|
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<TransparenzModule pricing={pricing} />
|
||||||
|
</SimpleLayout>
|
||||||
|
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<ClosingModule />
|
||||||
|
</SimpleLayout>
|
||||||
|
</PDFDocument>
|
||||||
|
);
|
||||||
|
};
|
||||||
401
packages/acquisition-library/src/components/pdf/SharedUI.tsx
Normal file
401
packages/acquisition-library/src/components/pdf/SharedUI.tsx
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
View as PDFView,
|
||||||
|
Text as PDFText,
|
||||||
|
StyleSheet,
|
||||||
|
Image as PDFImage,
|
||||||
|
} from "@react-pdf/renderer";
|
||||||
|
|
||||||
|
// INDUSTRIAL DESIGN SYSTEM TOKENS
|
||||||
|
export const COLORS = {
|
||||||
|
CHARCOAL: "#0f172a", // Slate 900
|
||||||
|
TEXT_MAIN: "#334155", // Slate 700
|
||||||
|
TEXT_DIM: "#64748b", // Slate 500
|
||||||
|
TEXT_LIGHT: "#94a3b8", // Slate 400
|
||||||
|
DIVIDER: "#cbd5e1", // Slate 300
|
||||||
|
GRID: "#f1f5f9", // Slate 100
|
||||||
|
BLUEPRINT: "#e2e8f0", // Slate 200
|
||||||
|
WHITE: "#ffffff",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FONT_SIZES = {
|
||||||
|
HERO: 24, // Main Page Titles
|
||||||
|
HEADING: 14, // Section Headers
|
||||||
|
BODY: 11, // Standard Content
|
||||||
|
LABEL: 10, // Bold Labels / Keys
|
||||||
|
SMALL: 9, // Descriptions / Footnotes
|
||||||
|
TINY: 8, // Metadata / Unit prices
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pdfStyles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
paddingTop: 45, // DIN 5008
|
||||||
|
paddingLeft: 70, // ~25mm
|
||||||
|
paddingRight: 57, // ~20mm
|
||||||
|
paddingBottom: 80, // Safe buffer for absolute footer
|
||||||
|
backgroundColor: COLORS.WHITE,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
|
fontSize: FONT_SIZES.BODY,
|
||||||
|
color: COLORS.CHARCOAL,
|
||||||
|
},
|
||||||
|
titlePage: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: COLORS.WHITE,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
|
color: COLORS.CHARCOAL,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
marginBottom: 20,
|
||||||
|
minHeight: 120,
|
||||||
|
},
|
||||||
|
addressBlock: {
|
||||||
|
width: "55%",
|
||||||
|
marginTop: 45,
|
||||||
|
},
|
||||||
|
senderLine: {
|
||||||
|
fontSize: FONT_SIZES.TINY,
|
||||||
|
textDecoration: "underline",
|
||||||
|
color: COLORS.TEXT_DIM,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
recipientAddress: {
|
||||||
|
fontSize: FONT_SIZES.BODY,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
brandLogoContainer: {
|
||||||
|
width: "40%",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
},
|
||||||
|
brandIconContainer: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
backgroundColor: "#0f172a",
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
brandIconText: {
|
||||||
|
color: COLORS.WHITE,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
titleInfo: {
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
mainTitle: {
|
||||||
|
fontSize: FONT_SIZES.HEADING,
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginBottom: 4,
|
||||||
|
color: COLORS.CHARCOAL,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
subTitle: {
|
||||||
|
fontSize: FONT_SIZES.BODY,
|
||||||
|
color: COLORS.TEXT_DIM,
|
||||||
|
marginTop: 2,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginBottom: 32,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: FONT_SIZES.LABEL,
|
||||||
|
fontWeight: "bold",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 1,
|
||||||
|
color: COLORS.TEXT_LIGHT,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 32,
|
||||||
|
left: 70,
|
||||||
|
right: 57,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: COLORS.GRID,
|
||||||
|
paddingTop: 16,
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
footerColumn: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
footerLogo: {
|
||||||
|
height: 20,
|
||||||
|
width: "auto",
|
||||||
|
objectFit: "contain",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
footerText: {
|
||||||
|
fontSize: FONT_SIZES.TINY,
|
||||||
|
color: COLORS.TEXT_LIGHT,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
asymmetryContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 32,
|
||||||
|
},
|
||||||
|
asymmetryLeft: {
|
||||||
|
width: "32%",
|
||||||
|
},
|
||||||
|
asymmetryRight: {
|
||||||
|
width: "63%",
|
||||||
|
},
|
||||||
|
specRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: COLORS.GRID,
|
||||||
|
},
|
||||||
|
specLabel: {
|
||||||
|
fontSize: FONT_SIZES.TINY,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: COLORS.TEXT_LIGHT,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
specValue: {
|
||||||
|
fontSize: FONT_SIZES.SMALL,
|
||||||
|
color: COLORS.CHARCOAL,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
blueprintBox: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: COLORS.GRID,
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
},
|
||||||
|
footerLabel: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: COLORS.TEXT_DIM,
|
||||||
|
},
|
||||||
|
pageNumber: {
|
||||||
|
fontSize: FONT_SIZES.TINY,
|
||||||
|
color: COLORS.DIVIDER,
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginTop: 8,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
foldingMark: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 20,
|
||||||
|
width: 10,
|
||||||
|
borderTopWidth: 0.5,
|
||||||
|
borderTopColor: COLORS.DIVIDER,
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
width: "100%",
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: COLORS.DIVIDER,
|
||||||
|
marginVertical: 12,
|
||||||
|
},
|
||||||
|
industrialListItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
industrialBulletBox: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: COLORS.DIVIDER,
|
||||||
|
marginRight: 8,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
industrialTitle: {
|
||||||
|
fontSize: FONT_SIZES.HERO,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: COLORS.CHARCOAL,
|
||||||
|
marginBottom: 6,
|
||||||
|
letterSpacing: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IndustrialListItem = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => (
|
||||||
|
<PDFView style={pdfStyles.industrialListItem}>
|
||||||
|
<PDFView style={pdfStyles.industrialBulletBox} />
|
||||||
|
{children}
|
||||||
|
</PDFView>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Divider = ({ style = {} }: { style?: any }) => (
|
||||||
|
<PDFView style={[pdfStyles.divider, style]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Footer = ({
|
||||||
|
logo,
|
||||||
|
companyData,
|
||||||
|
showDetails = true,
|
||||||
|
showPageNumber = true,
|
||||||
|
}: {
|
||||||
|
logo?: string;
|
||||||
|
companyData: any;
|
||||||
|
showDetails?: boolean;
|
||||||
|
showPageNumber?: boolean;
|
||||||
|
}) => (
|
||||||
|
<PDFView style={pdfStyles.footer}>
|
||||||
|
<PDFView style={pdfStyles.footerColumn}>
|
||||||
|
{logo ? (
|
||||||
|
<PDFImage src={logo} style={pdfStyles.footerLogo} />
|
||||||
|
) : (
|
||||||
|
<PDFText style={{ fontSize: 12, fontWeight: "bold", marginBottom: 8 }}>
|
||||||
|
marc mintel
|
||||||
|
</PDFText>
|
||||||
|
)}
|
||||||
|
</PDFView>
|
||||||
|
{showDetails && (
|
||||||
|
<>
|
||||||
|
<PDFView style={pdfStyles.footerColumn}>
|
||||||
|
<PDFText style={pdfStyles.footerText}>
|
||||||
|
<PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>
|
||||||
|
{"\n"}
|
||||||
|
{companyData.address1}
|
||||||
|
{"\n"}
|
||||||
|
{companyData.address2}
|
||||||
|
{"\n"}UST: {companyData.ustId}
|
||||||
|
</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
<PDFView style={[pdfStyles.footerColumn, { alignItems: "flex-end" }]}>
|
||||||
|
{showPageNumber && (
|
||||||
|
<PDFText
|
||||||
|
style={pdfStyles.pageNumber}
|
||||||
|
render={({ pageNumber, totalPages }) =>
|
||||||
|
`${pageNumber} / ${totalPages}`
|
||||||
|
}
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PDFView>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PDFView>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Header = ({
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
icon,
|
||||||
|
showAddress = true,
|
||||||
|
}: {
|
||||||
|
sender?: string;
|
||||||
|
recipient?: {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
email?: string;
|
||||||
|
address?: string;
|
||||||
|
phone?: string;
|
||||||
|
taxId?: string;
|
||||||
|
};
|
||||||
|
icon?: string;
|
||||||
|
showAddress?: boolean;
|
||||||
|
}) => (
|
||||||
|
<PDFView
|
||||||
|
style={[
|
||||||
|
pdfStyles.header,
|
||||||
|
showAddress ? {} : { minHeight: 40, marginBottom: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<PDFView style={pdfStyles.addressBlock}>
|
||||||
|
{showAddress && sender && (
|
||||||
|
<>
|
||||||
|
<PDFText style={pdfStyles.senderLine}>{sender}</PDFText>
|
||||||
|
{recipient && (
|
||||||
|
<PDFView style={pdfStyles.recipientAddress}>
|
||||||
|
<PDFText style={{ fontWeight: "bold" }}>
|
||||||
|
{recipient.title}
|
||||||
|
</PDFText>
|
||||||
|
{recipient.subtitle && <PDFText>{recipient.subtitle}</PDFText>}
|
||||||
|
{recipient.address && <PDFText>{recipient.address}</PDFText>}
|
||||||
|
{recipient.phone && <PDFText>{recipient.phone}</PDFText>}
|
||||||
|
{recipient.email && <PDFText>{recipient.email}</PDFText>}
|
||||||
|
{recipient.taxId && <PDFText>USt-ID: {recipient.taxId}</PDFText>}
|
||||||
|
</PDFView>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PDFView>
|
||||||
|
<PDFView style={pdfStyles.brandLogoContainer}>
|
||||||
|
<PDFView style={pdfStyles.brandIconContainer}>
|
||||||
|
{icon ? (
|
||||||
|
<PDFImage src={icon} style={{ width: 24, height: 24 }} />
|
||||||
|
) : (
|
||||||
|
<PDFText style={pdfStyles.brandIconText}>M</PDFText>
|
||||||
|
)}
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DocumentTitle = ({
|
||||||
|
title,
|
||||||
|
subLines,
|
||||||
|
isHero = false,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subLines?: string[];
|
||||||
|
isHero?: boolean;
|
||||||
|
}) => (
|
||||||
|
<PDFView style={pdfStyles.titleInfo}>
|
||||||
|
<PDFText
|
||||||
|
style={[
|
||||||
|
pdfStyles.mainTitle,
|
||||||
|
{ fontSize: isHero ? FONT_SIZES.HERO : FONT_SIZES.HEADING },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</PDFText>
|
||||||
|
{subLines?.map((line, i) => (
|
||||||
|
<PDFText
|
||||||
|
key={i}
|
||||||
|
style={[
|
||||||
|
pdfStyles.subTitle,
|
||||||
|
i === 1 ? { fontWeight: "bold", color: COLORS.CHARCOAL } : {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</PDFText>
|
||||||
|
))}
|
||||||
|
</PDFView>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TechnicalSpec = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}) => (
|
||||||
|
<PDFView style={pdfStyles.specRow}>
|
||||||
|
<PDFText style={pdfStyles.specLabel}>{label}</PDFText>
|
||||||
|
<PDFText style={pdfStyles.specValue}>{value}</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AsymmetryView = ({
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
style = {},
|
||||||
|
}: {
|
||||||
|
left: React.ReactNode;
|
||||||
|
right: React.ReactNode;
|
||||||
|
style?: any;
|
||||||
|
}) => (
|
||||||
|
<PDFView style={[pdfStyles.asymmetryContainer, style]}>
|
||||||
|
<PDFView style={pdfStyles.asymmetryLeft}>{left}</PDFView>
|
||||||
|
<PDFView style={pdfStyles.asymmetryRight}>{right}</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
);
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Page as PDFPage, View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import { Header, Footer, pdfStyles } from './SharedUI.js';
|
||||||
|
|
||||||
|
const simpleStyles = StyleSheet.create({
|
||||||
|
industrialPage: {
|
||||||
|
padding: 30,
|
||||||
|
paddingTop: 20,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
industrialNumber: {
|
||||||
|
fontSize: 60,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#f1f5f9',
|
||||||
|
position: 'absolute',
|
||||||
|
top: -10,
|
||||||
|
right: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
},
|
||||||
|
industrialSection: {
|
||||||
|
marginTop: 16,
|
||||||
|
paddingTop: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SimpleLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
pageNumber?: string;
|
||||||
|
icon?: string;
|
||||||
|
footerLogo?: string;
|
||||||
|
companyData: any;
|
||||||
|
showPageNumber?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SimpleLayout = ({
|
||||||
|
children,
|
||||||
|
pageNumber,
|
||||||
|
icon,
|
||||||
|
footerLogo,
|
||||||
|
companyData,
|
||||||
|
showPageNumber = true
|
||||||
|
}: SimpleLayoutProps) => {
|
||||||
|
return (
|
||||||
|
<PDFPage size="A4" style={[pdfStyles.page, simpleStyles.industrialPage]}>
|
||||||
|
<Header icon={icon} showAddress={false} />
|
||||||
|
{pageNumber && <PDFText style={simpleStyles.industrialNumber}>{pageNumber}</PDFText>}
|
||||||
|
<PDFView style={simpleStyles.industrialSection}>
|
||||||
|
<PDFView style={{ width: '100%' }}>
|
||||||
|
{children}
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
<Footer
|
||||||
|
logo={footerLogo}
|
||||||
|
companyData={companyData}
|
||||||
|
showDetails={false}
|
||||||
|
showPageNumber={showPageNumber}
|
||||||
|
/>
|
||||||
|
</PDFPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"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.js";
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
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" 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"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.js";
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
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 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"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.js";
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
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"}`,
|
||||||
|
]}
|
||||||
|
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>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"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.js";
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
titlePage: {
|
||||||
|
flex: 1,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { View as PDFView, Text as PDFText, StyleSheet } from "@react-pdf/renderer";
|
||||||
|
import { DocumentTitle, COLORS, FONT_SIZES, IndustrialListItem } from "../SharedUI.js";
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
section: { marginBottom: 24 },
|
||||||
|
categoryBox: {
|
||||||
|
marginBottom: 20,
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: COLORS.GRID,
|
||||||
|
borderLeftWidth: 2,
|
||||||
|
borderLeftColor: COLORS.DIVIDER,
|
||||||
|
},
|
||||||
|
categoryTitle: {
|
||||||
|
fontSize: FONT_SIZES.TINY,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: COLORS.TEXT_LIGHT,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
marginBottom: 10,
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
pageTitle: {
|
||||||
|
fontSize: FONT_SIZES.LABEL,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: COLORS.CHARCOAL,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
pageDesc: {
|
||||||
|
fontSize: FONT_SIZES.TINY,
|
||||||
|
color: COLORS.TEXT_DIM,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SitemapModule = ({ state }: any) => (
|
||||||
|
<>
|
||||||
|
<DocumentTitle title="Informations-Architektur" isHero={true} />
|
||||||
|
<PDFView style={styles.section}>
|
||||||
|
{state.sitemap?.map((cat: any, i: number) => (
|
||||||
|
<PDFView key={i} style={styles.categoryBox}>
|
||||||
|
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
|
||||||
|
{cat.pages?.map((p: any, j: number) => (
|
||||||
|
<IndustrialListItem key={j}>
|
||||||
|
<PDFView style={{ marginBottom: 8 }}>
|
||||||
|
<PDFText style={styles.pageTitle}>{p.title}</PDFText>
|
||||||
|
<PDFText style={styles.pageDesc}>{p.desc}</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
</IndustrialListItem>
|
||||||
|
))}
|
||||||
|
</PDFView>
|
||||||
|
))}
|
||||||
|
</PDFView>
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"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.js";
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
section: { marginBottom: 24 },
|
||||||
|
ledgerRow: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: COLORS.GRID,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
ledgerPrice: {
|
||||||
|
fontSize: FONT_SIZES.BODY,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: COLORS.CHARCOAL,
|
||||||
|
},
|
||||||
|
ledgerUnit: {
|
||||||
|
fontSize: FONT_SIZES.TINY,
|
||||||
|
color: COLORS.TEXT_LIGHT,
|
||||||
|
marginLeft: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TransparenzModule = ({ pricing }: any) => {
|
||||||
|
const sorglosPrice = (pricing.HOSTING_MONTHLY || 250) * 12;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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: "Sprachversionen",
|
||||||
|
d: "Skalierung der System-Architektur auf Zweit-Sprachen.",
|
||||||
|
p: "+20%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
))}
|
||||||
|
</PDFView>
|
||||||
|
</PDFView>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
6
packages/acquisition-library/src/index.ts
Normal file
6
packages/acquisition-library/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from "./logic/pricing/types.js";
|
||||||
|
export * from "./logic/pricing/constants.js";
|
||||||
|
export * from "./logic/pricing/calculator.js";
|
||||||
|
export * from "./services/AcquisitionService.js";
|
||||||
|
export * from "./services/PdfEngine.js";
|
||||||
|
export * from "./components/EstimationPDF.js";
|
||||||
224
packages/acquisition-library/src/logic/pricing/calculator.ts
Normal file
224
packages/acquisition-library/src/logic/pricing/calculator.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { FormState, Position, Totals } from "./types.js";
|
||||||
|
import {
|
||||||
|
FEATURE_LABELS,
|
||||||
|
FUNCTION_LABELS,
|
||||||
|
API_LABELS,
|
||||||
|
PAGE_LABELS,
|
||||||
|
} from "./constants.js";
|
||||||
|
|
||||||
|
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 += 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;
|
||||||
|
|
||||||
|
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 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));
|
||||||
|
|
||||||
|
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) > 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) > 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) > 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 || 0) +
|
||||||
|
(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;
|
||||||
|
}
|
||||||
332
packages/acquisition-library/src/logic/pricing/constants.ts
Normal file
332
packages/acquisition-library/src/logic/pricing/constants.ts
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import { FormState } from "./types.js";
|
||||||
|
|
||||||
|
export const PRICING = {
|
||||||
|
BASE_WEBSITE: 5440, // Updated to match AI prompt requirement in Pass 1
|
||||||
|
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: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
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." },
|
||||||
|
];
|
||||||
|
|
||||||
|
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." },
|
||||||
|
];
|
||||||
|
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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." },
|
||||||
|
];
|
||||||
|
|
||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const VIBE_LABELS: Record<string, string> = {
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FEATURE_LABELS: Record<string, string> = {
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SOCIAL_LABELS: Record<string, string> = {
|
||||||
|
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",
|
||||||
|
};
|
||||||
89
packages/acquisition-library/src/logic/pricing/types.ts
Normal file
89
packages/acquisition-library/src/logic/pricing/types.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
export type ProjectType = 'website' | 'web-app';
|
||||||
|
|
||||||
|
export interface FormState {
|
||||||
|
projectType: ProjectType;
|
||||||
|
// Company
|
||||||
|
companyName: string;
|
||||||
|
employeeCount: string;
|
||||||
|
// Existing Presence
|
||||||
|
existingWebsite: string;
|
||||||
|
socialMedia: string[];
|
||||||
|
socialMediaUrls: Record<string, string>;
|
||||||
|
existingDomain: string;
|
||||||
|
wishedDomain: string;
|
||||||
|
// Project
|
||||||
|
websiteTopic: string;
|
||||||
|
selectedPages: string[];
|
||||||
|
otherPages: string[];
|
||||||
|
otherPagesCount: number;
|
||||||
|
features: string[];
|
||||||
|
otherFeatures: string[];
|
||||||
|
otherFeaturesCount: number;
|
||||||
|
functions: string[];
|
||||||
|
otherFunctions: string[];
|
||||||
|
otherFunctionsCount: number;
|
||||||
|
apiSystems: string[];
|
||||||
|
otherTech: string[];
|
||||||
|
otherTechCount: number;
|
||||||
|
assets: string[];
|
||||||
|
otherAssets: string[];
|
||||||
|
otherAssetsCount: number;
|
||||||
|
newDatasets: number;
|
||||||
|
cmsSetup: boolean;
|
||||||
|
storageExpansion: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
message: string;
|
||||||
|
sitemapFile: any;
|
||||||
|
contactFiles: any[];
|
||||||
|
// Design
|
||||||
|
designVibe: string;
|
||||||
|
colorScheme: string[];
|
||||||
|
references: string[];
|
||||||
|
designWishes: string;
|
||||||
|
// Maintenance
|
||||||
|
expectedAdjustments: string;
|
||||||
|
languagesList: string[];
|
||||||
|
// Timeline
|
||||||
|
deadline: string;
|
||||||
|
// Web App specific
|
||||||
|
targetAudience: string;
|
||||||
|
userRoles: string[];
|
||||||
|
dataSensitivity: string;
|
||||||
|
platformType: string;
|
||||||
|
// Meta
|
||||||
|
dontKnows: string[];
|
||||||
|
visualStaging: string;
|
||||||
|
complexInteractions: string;
|
||||||
|
gridDontKnows?: Record<string, string>;
|
||||||
|
briefingSummary?: string;
|
||||||
|
companyAddress?: string;
|
||||||
|
companyPhone?: string;
|
||||||
|
personName?: string;
|
||||||
|
taxId?: string;
|
||||||
|
designVision?: string;
|
||||||
|
positionDescriptions?: Record<string, string>;
|
||||||
|
sitemap?: {
|
||||||
|
category: string;
|
||||||
|
pages: { title: string; desc: string }[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
pos: number;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
qty: number;
|
||||||
|
price: number;
|
||||||
|
isRecurring?: boolean;
|
||||||
|
}
|
||||||
|
export interface Totals {
|
||||||
|
totalPrice: number;
|
||||||
|
monthlyPrice: number;
|
||||||
|
totalPagesCount: number;
|
||||||
|
totalFeatures: number;
|
||||||
|
totalFunctions: number;
|
||||||
|
totalApis: number;
|
||||||
|
languagesCount: number;
|
||||||
|
}
|
||||||
153
packages/acquisition-library/src/services/AcquisitionService.ts
Normal file
153
packages/acquisition-library/src/services/AcquisitionService.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { CheerioCrawler } from "crawlee";
|
||||||
|
import axios from "axios";
|
||||||
|
import { FileCacheAdapter } from "../utils/cache/FileCacheAdapter.js";
|
||||||
|
import { initialState } from "../logic/pricing/constants.js";
|
||||||
|
import { FormState } from "../logic/pricing/types.js";
|
||||||
|
|
||||||
|
export interface AcquisitionResult {
|
||||||
|
state: FormState;
|
||||||
|
usage: {
|
||||||
|
prompt: number;
|
||||||
|
completion: number;
|
||||||
|
cost: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AcquisitionService {
|
||||||
|
private cache: FileCacheAdapter;
|
||||||
|
private openRouterKey: string;
|
||||||
|
|
||||||
|
constructor(openRouterKey: string) {
|
||||||
|
this.openRouterKey = openRouterKey;
|
||||||
|
this.cache = new FileCacheAdapter({ prefix: "acq_" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async runFullSequence(url: string, briefing: string, comments?: string): Promise<AcquisitionResult> {
|
||||||
|
console.log(`🚀 Starting Acquisition Sequence for: ${url}`);
|
||||||
|
|
||||||
|
// 1. Crawl
|
||||||
|
const crawlData = await this.performCrawl(url);
|
||||||
|
|
||||||
|
// 2. Distill
|
||||||
|
const distilledContext = await this.distillCrawlContext(crawlData);
|
||||||
|
|
||||||
|
// 3. AI Estimation (using parts of the original ai-estimate logic)
|
||||||
|
// For brevity in this initial port, I'll implement a combined prompt strategy
|
||||||
|
// or keep the multi-pass if needed.
|
||||||
|
|
||||||
|
const result = await this.getAiEstimation(briefing, distilledContext, comments || null);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performCrawl(url: string): Promise<string> {
|
||||||
|
const pages: any[] = [];
|
||||||
|
const origin = new URL(url).origin;
|
||||||
|
|
||||||
|
const crawler = new CheerioCrawler({
|
||||||
|
maxRequestsPerCrawl: 15,
|
||||||
|
async requestHandler({ $, request, enqueueLinks }) {
|
||||||
|
const title = $("title").text();
|
||||||
|
const bodyText = $("body").text().replace(/\s+/g, " ").substring(0, 10000);
|
||||||
|
|
||||||
|
pages.push({
|
||||||
|
url: request.url,
|
||||||
|
content: `Title: ${title}\nText: ${bodyText}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await enqueueLinks({
|
||||||
|
limit: 10,
|
||||||
|
transformRequestFunction: (req) => {
|
||||||
|
try {
|
||||||
|
const reqUrl = new URL(req.url);
|
||||||
|
if (reqUrl.origin !== origin) return false;
|
||||||
|
if (reqUrl.pathname.match(/\.(pdf|zip|jpg|png|svg|webp)$/i)) return false;
|
||||||
|
return req;
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignored - malformed URL in enqueueLinks
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await crawler.run([url]);
|
||||||
|
return pages.map((p) => `--- PAGE: ${p.url} ---\n${p.content}`).join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async distillCrawlContext(rawCrawl: string): Promise<string> {
|
||||||
|
const systemPrompt = `
|
||||||
|
You are a context distiller. Extract the "Company DNA" in 5-8 bullet points (GERMAN).
|
||||||
|
Focus on: Services, USPs, Target Audience, Tone.
|
||||||
|
`;
|
||||||
|
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, 20000)}` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${this.openRouterKey}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return resp.data.choices[0].message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAiEstimation(briefing: string, context: string, comments: string | null): Promise<AcquisitionResult> {
|
||||||
|
// Porting a simplified version of Pass 1 and Pass 3 together for the "Audit"
|
||||||
|
const systemPrompt = `
|
||||||
|
You are a Digital Architect. Analyze the briefing and crawl context.
|
||||||
|
Generate a JSON state for a project estimation.
|
||||||
|
Language: GERMAN.
|
||||||
|
Format: ROOT LEVEL JSON matching FormState interface.
|
||||||
|
|
||||||
|
### PRICING RULES:
|
||||||
|
- Base: 5440 €
|
||||||
|
- Page: 600 €
|
||||||
|
- Feature: 1500 €
|
||||||
|
- Function/API: 800 €
|
||||||
|
|
||||||
|
Return ONLY the JSON.
|
||||||
|
`;
|
||||||
|
const userPrompt = `BRIEFING: ${briefing}\n\nCONTEXT: ${context}\n\nCOMMENTS: ${comments}`;
|
||||||
|
|
||||||
|
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: userPrompt },
|
||||||
|
],
|
||||||
|
response_format: { type: "json_object" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${this.openRouterKey}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let state: FormState;
|
||||||
|
try {
|
||||||
|
state = JSON.parse(resp.data.choices[0].message.content);
|
||||||
|
} catch (_error) {
|
||||||
|
console.error("Failed to parse AI estimation JSON, returning initial state.");
|
||||||
|
state = initialState;
|
||||||
|
}
|
||||||
|
// Ensure it matches FormState defaults
|
||||||
|
const finalState = { ...initialState, ...state };
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: finalState,
|
||||||
|
usage: {
|
||||||
|
prompt: resp.data.usage?.prompt_tokens || 0,
|
||||||
|
completion: resp.data.usage?.completion_tokens || 0,
|
||||||
|
cost: resp.data.usage?.cost || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/acquisition-library/src/services/PdfEngine.ts
Normal file
24
packages/acquisition-library/src/services/PdfEngine.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { renderToFile } from "@react-pdf/renderer";
|
||||||
|
import { createElement } from "react";
|
||||||
|
import { EstimationPDF } from "../components/EstimationPDF.js";
|
||||||
|
import { PRICING } from "../logic/pricing/constants.js";
|
||||||
|
import { calculateTotals } from "../logic/pricing/calculator.js";
|
||||||
|
|
||||||
|
export class PdfEngine {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async generateEstimatePdf(state: any, outputPath: string): Promise<string> {
|
||||||
|
const totals = calculateTotals(state, PRICING);
|
||||||
|
|
||||||
|
await renderToFile(
|
||||||
|
createElement(EstimationPDF as any, {
|
||||||
|
state,
|
||||||
|
totalPrice: totals.totalPrice,
|
||||||
|
pricing: PRICING,
|
||||||
|
} as any) as any,
|
||||||
|
outputPath
|
||||||
|
);
|
||||||
|
|
||||||
|
return outputPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/acquisition-library/src/shim.ts
Normal file
22
packages/acquisition-library/src/shim.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { createRequire } from 'module';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = import.meta?.url;
|
||||||
|
// Hardcode fallback path for Directus Docker environment
|
||||||
|
const fallbackPath = '/directus/extensions/acquisition/dist/index.js';
|
||||||
|
const filename = url ? fileURLToPath(url) : fallbackPath;
|
||||||
|
const dir = dirname(filename);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.__filename = filename;
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.__dirname = dir;
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.require = createRequire(url || `file://${fallbackPath}`);
|
||||||
|
|
||||||
|
console.log(`[Shim] Loaded. __dirname: ${dir}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Shim] Failed to shim __dirname/require", e);
|
||||||
|
}
|
||||||
78
packages/acquisition-library/src/utils/cache/FileCacheAdapter.ts
vendored
Normal file
78
packages/acquisition-library/src/utils/cache/FileCacheAdapter.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import * as fs from 'node:fs/promises';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import * as crypto from 'node:crypto';
|
||||||
|
|
||||||
|
export class FileCacheAdapter {
|
||||||
|
private cacheDir: string;
|
||||||
|
private prefix: string;
|
||||||
|
private defaultTTL: number;
|
||||||
|
|
||||||
|
constructor(config?: { cacheDir?: string; prefix?: string; defaultTTL?: number }) {
|
||||||
|
this.cacheDir = config?.cacheDir || path.resolve(process.cwd(), '.cache');
|
||||||
|
this.prefix = config?.prefix || '';
|
||||||
|
this.defaultTTL = config?.defaultTTL || 3600;
|
||||||
|
|
||||||
|
if (!existsSync(this.cacheDir)) {
|
||||||
|
fs.mkdir(this.cacheDir, { recursive: true }).catch(() => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitize(key: string): string {
|
||||||
|
const clean = key.replace(/[^a-z0-9]/gi, '_');
|
||||||
|
if (clean.length > 64) {
|
||||||
|
return crypto.createHash('md5').update(key).digest('hex');
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilePath(key: string): string {
|
||||||
|
const safeKey = this.sanitize(`${this.prefix}${key}`).toLowerCase();
|
||||||
|
return path.join(this.cacheDir, `${safeKey}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(key: string): Promise<T | null> {
|
||||||
|
const filePath = this.getFilePath(key);
|
||||||
|
try {
|
||||||
|
if (!existsSync(filePath)) return null;
|
||||||
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
|
||||||
|
if (data.expiry && Date.now() > data.expiry) {
|
||||||
|
await this.del(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value;
|
||||||
|
} catch (_error) {
|
||||||
|
return null; // Keeping original return type Promise<T | null>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
||||||
|
const filePath = this.getFilePath(key);
|
||||||
|
const effectiveTTL = ttl !== undefined ? ttl : this.defaultTTL;
|
||||||
|
const data = {
|
||||||
|
value,
|
||||||
|
expiry: effectiveTTL > 0 ? Date.now() + effectiveTTL * 1000 : null,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string): Promise<void> {
|
||||||
|
const filePath = this.getFilePath(key);
|
||||||
|
try {
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignored - best effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const entryPoint = resolve(__dirname, 'src/index.ts');
|
const entryPoint = resolve(__dirname, 'src/index.ts');
|
||||||
const outfile = resolve(__dirname, 'index.js');
|
const outfile = resolve(__dirname, 'dist/index.js');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mkdirSync(dirname(outfile), { recursive: true });
|
mkdirSync(dirname(outfile), { recursive: true });
|
||||||
@@ -22,6 +22,7 @@ build({
|
|||||||
target: 'node18',
|
target: 'node18',
|
||||||
outfile: outfile,
|
outfile: outfile,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
|
// Bundle everything, including Directus SDK, to avoid resolution issues in Docker
|
||||||
external: [],
|
external: [],
|
||||||
plugins: [{
|
plugins: [{
|
||||||
name: 'mock-jquery',
|
name: 'mock-jquery',
|
||||||
@@ -35,6 +36,11 @@ build({
|
|||||||
build.onResolve({ filter: /^canvas$/ }, args => ({ path: args.path, namespace: 'mock-canvas' }));
|
build.onResolve({ filter: /^canvas$/ }, args => ({ path: args.path, namespace: 'mock-canvas' }));
|
||||||
build.onLoad({ filter: /.*/, namespace: 'mock-canvas' }, () => ({ contents: 'export default {};', loader: 'js' }));
|
build.onLoad({ filter: /.*/, namespace: 'mock-canvas' }, () => ({ contents: 'export default {};', loader: 'js' }));
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
name: 'mock-jsdom',
|
||||||
|
setup(build) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
console.log("Build succeeded!");
|
console.log("Build succeeded!");
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "acquisition",
|
"name": "acquisition",
|
||||||
"version": "1.7.12",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "endpoint",
|
"type": "endpoint",
|
||||||
"path": "index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "^11.0.0"
|
"host": "^11.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
"dev": "node build.js --watch"
|
"dev": "node build.js --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
"esbuild": "^0.25.0",
|
"@mintel/acquisition": "workspace:*",
|
||||||
"typescript": "^5.6.3"
|
"@mintel/mail": "workspace:*",
|
||||||
},
|
"esbuild": "^0.25.0",
|
||||||
"dependencies": {
|
"typescript": "^5.6.3"
|
||||||
"jquery": "^3.7.1",
|
},
|
||||||
"react": "^19.2.4",
|
"dependencies": {
|
||||||
"react-dom": "^19.2.4"
|
"jquery": "^3.7.1",
|
||||||
}
|
"react": "^19.2.4",
|
||||||
}
|
"react-dom": "^19.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./shim";
|
import "./shim";
|
||||||
import { defineEndpoint } from "@directus/extensions-sdk";
|
import { defineEndpoint } from "@directus/extensions-sdk";
|
||||||
import { AcquisitionService, PdfEngine } from "@mintel/acquisition";
|
import { AcquisitionService, PdfEngine } from "../../acquisition-library/src/index";
|
||||||
import { render, SiteAuditTemplate, ProjectEstimateTemplate } from "@mintel/mail";
|
import { render, SiteAuditTemplate, ProjectEstimateTemplate } from "@mintel/mail";
|
||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,3 +22,6 @@ export * from "./layouts/ClientLayout";
|
|||||||
// Export Templates
|
// Export Templates
|
||||||
export * from "./templates/ContactFormNotification";
|
export * from "./templates/ContactFormNotification";
|
||||||
export * from "./templates/ConfirmationMessage";
|
export * from "./templates/ConfirmationMessage";
|
||||||
|
export * from "./templates/FollowUpTemplate";
|
||||||
|
export * from "./templates/ProjectEstimateTemplate";
|
||||||
|
export * from "./templates/SiteAuditTemplate";
|
||||||
|
|||||||
88
packages/mail/src/templates/FollowUpTemplate.tsx
Normal file
88
packages/mail/src/templates/FollowUpTemplate.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Heading, Text, Button } from "@react-email/components";
|
||||||
|
import { MintelLayout } from "../layouts/MintelLayout";
|
||||||
|
|
||||||
|
export interface FollowUpTemplateProps {
|
||||||
|
companyName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FollowUpTemplate = ({
|
||||||
|
companyName,
|
||||||
|
}: FollowUpTemplateProps) => {
|
||||||
|
const preview = `Kurzes Follow-up: ${companyName}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MintelLayout preview={preview}>
|
||||||
|
<Heading style={h1}>Kurzes Follow-up</Heading>
|
||||||
|
<Text style={intro}>
|
||||||
|
Hallo noch einmal,<br /><br />
|
||||||
|
ich wollte mich nur kurz erkundigen, ob Sie bereits Zeit hatten,
|
||||||
|
einen Blick auf das Audit Ihrer Website zu werfen, das ich Ihnen
|
||||||
|
vor ein paar Tagen gesendet habe.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={bodyText}>
|
||||||
|
Vielleicht passt es ja diese Woche für ein kurzes, unverbindliches
|
||||||
|
Telefonat, um die Punkte gemeinsam durchzugehen?
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style={button}
|
||||||
|
href="https://calendly.com/mintel-me/intro"
|
||||||
|
>
|
||||||
|
Treffen vereinbaren
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Text style={footerText}>
|
||||||
|
Beste Grüße,<br />
|
||||||
|
<strong>Marc Mintel</strong>
|
||||||
|
</Text>
|
||||||
|
</MintelLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FollowUpTemplate;
|
||||||
|
|
||||||
|
const h1 = {
|
||||||
|
fontSize: "28px",
|
||||||
|
fontWeight: "900",
|
||||||
|
margin: "0 0 24px",
|
||||||
|
color: "#ffffff",
|
||||||
|
letterSpacing: "-0.04em",
|
||||||
|
};
|
||||||
|
|
||||||
|
const intro = {
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
color: "#cccccc",
|
||||||
|
margin: "0 0 24px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bodyText = {
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
color: "#888888",
|
||||||
|
margin: "0 0 32px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const button = {
|
||||||
|
backgroundColor: "#333333",
|
||||||
|
borderRadius: "0",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textDecoration: "none",
|
||||||
|
textAlign: "center" as const,
|
||||||
|
display: "inline-block",
|
||||||
|
padding: "12px 24px",
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
letterSpacing: "0.1em",
|
||||||
|
border: "1px solid #444444",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerText = {
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#666666",
|
||||||
|
lineHeight: "20px",
|
||||||
|
marginTop: "48px",
|
||||||
|
};
|
||||||
86
packages/mail/src/templates/ProjectEstimateTemplate.tsx
Normal file
86
packages/mail/src/templates/ProjectEstimateTemplate.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Heading, Text, Section } from "@react-email/components";
|
||||||
|
import { MintelLayout } from "../layouts/MintelLayout";
|
||||||
|
|
||||||
|
export interface ProjectEstimateTemplateProps {
|
||||||
|
companyName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectEstimateTemplate = ({
|
||||||
|
companyName,
|
||||||
|
}: ProjectEstimateTemplateProps) => {
|
||||||
|
const preview = `Ihre personalisierte Projekt-Schätzung: ${companyName}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MintelLayout preview={preview}>
|
||||||
|
<Heading style={h1}>Ihre Projekt-Schätzung</Heading>
|
||||||
|
<Text style={intro}>
|
||||||
|
Hallo {companyName},<br /><br />
|
||||||
|
vielen Dank für unser Gespräch. Wie versprochen sende ich Ihnen hiermit
|
||||||
|
die detaillierte Schätzung für Ihre neue digitale Webpräsenz.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section style={infoBox}>
|
||||||
|
<Text style={infoText}>
|
||||||
|
Im Anhang finden Sie das PDF-Dokument mit allen Positionen,
|
||||||
|
Umfängen und dem strategischen Ausblick.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text style={bodyText}>
|
||||||
|
Ich freue mich auf Ihr Feedback und stehe für Rückfragen jederzeit
|
||||||
|
zur Verfügung.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={footerText}>
|
||||||
|
Beste Grüße,<br />
|
||||||
|
<strong>Marc Mintel</strong>
|
||||||
|
</Text>
|
||||||
|
</MintelLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectEstimateTemplate;
|
||||||
|
|
||||||
|
const h1 = {
|
||||||
|
fontSize: "28px",
|
||||||
|
fontWeight: "900",
|
||||||
|
margin: "0 0 24px",
|
||||||
|
color: "#ffffff",
|
||||||
|
letterSpacing: "-0.04em",
|
||||||
|
};
|
||||||
|
|
||||||
|
const intro = {
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
color: "#cccccc",
|
||||||
|
margin: "0 0 24px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const infoBox = {
|
||||||
|
backgroundColor: "#0f172a",
|
||||||
|
padding: "24px",
|
||||||
|
borderLeft: "4px solid #ffffff",
|
||||||
|
marginBottom: "32px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const infoText = {
|
||||||
|
fontSize: "15px",
|
||||||
|
color: "#ffffff",
|
||||||
|
margin: "0",
|
||||||
|
lineHeight: "1.5",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bodyText = {
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
color: "#888888",
|
||||||
|
margin: "0 0 32px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerText = {
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#666666",
|
||||||
|
lineHeight: "20px",
|
||||||
|
marginTop: "48px",
|
||||||
|
};
|
||||||
146
packages/mail/src/templates/SiteAuditTemplate.tsx
Normal file
146
packages/mail/src/templates/SiteAuditTemplate.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Heading, Section, Text, Button, Link } from "@react-email/components";
|
||||||
|
import { MintelLayout } from "../layouts/MintelLayout";
|
||||||
|
|
||||||
|
export interface SiteAuditTemplateProps {
|
||||||
|
companyName: string;
|
||||||
|
auditHighlights: string[];
|
||||||
|
websiteUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SiteAuditTemplate = ({
|
||||||
|
companyName,
|
||||||
|
auditHighlights,
|
||||||
|
websiteUrl,
|
||||||
|
}: SiteAuditTemplateProps) => {
|
||||||
|
const preview = `Analyse Ihrer Webpräsenz: ${companyName}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MintelLayout preview={preview}>
|
||||||
|
<Heading style={h1}>Analyse Ihrer Webpräsenz</Heading>
|
||||||
|
<Text style={intro}>
|
||||||
|
Hallo {companyName},<br /><br />
|
||||||
|
ich habe mir Ihre aktuelle Website ({websiteUrl}) im Detail angeschaut und
|
||||||
|
einige technische sowie strategische Potenziale identifiziert.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section style={auditContainer}>
|
||||||
|
<Heading as="h2" style={h2}>Audit Highlights</Heading>
|
||||||
|
{auditHighlights.map((highlight, i) => (
|
||||||
|
<div key={i} style={highlightRow}>
|
||||||
|
<div style={bullet} />
|
||||||
|
<Text style={highlightText}>{highlight}</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text style={bodyText}>
|
||||||
|
In der heutigen digitalen Landschaft ist eine performante und strategisch
|
||||||
|
ausgerichtete Website kein Luxus mehr, sondern das Fundament für
|
||||||
|
nachhaltiges Wachstum.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section style={ctaSection}>
|
||||||
|
<Button
|
||||||
|
style={button}
|
||||||
|
href={`mailto:marc@mintel.me?subject=Feedback zum Audit: ${companyName}`}
|
||||||
|
>
|
||||||
|
Audit gemeinsam besprechen
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text style={footerText}>
|
||||||
|
Beste Grüße,<br />
|
||||||
|
<strong>Marc Mintel</strong><br />
|
||||||
|
Digitaler Architekt
|
||||||
|
</Text>
|
||||||
|
</MintelLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SiteAuditTemplate;
|
||||||
|
|
||||||
|
const h1 = {
|
||||||
|
fontSize: "28px",
|
||||||
|
fontWeight: "900",
|
||||||
|
margin: "0 0 24px",
|
||||||
|
color: "#ffffff",
|
||||||
|
letterSpacing: "-0.04em",
|
||||||
|
};
|
||||||
|
|
||||||
|
const h2 = {
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: "900",
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
color: "#444444",
|
||||||
|
margin: "0 0 16px",
|
||||||
|
letterSpacing: "0.1em",
|
||||||
|
};
|
||||||
|
|
||||||
|
const intro = {
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
color: "#cccccc",
|
||||||
|
margin: "0 0 32px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const auditContainer = {
|
||||||
|
backgroundColor: "#151515",
|
||||||
|
padding: "32px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "32px",
|
||||||
|
border: "1px solid #222222",
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightRow = {
|
||||||
|
display: "flex" as const,
|
||||||
|
alignItems: "flex-start" as const,
|
||||||
|
marginBottom: "12px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bullet = {
|
||||||
|
width: "6px",
|
||||||
|
height: "6px",
|
||||||
|
backgroundColor: "#4CAF50",
|
||||||
|
marginTop: "8px",
|
||||||
|
marginRight: "12px",
|
||||||
|
flexShrink: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightText = {
|
||||||
|
fontSize: "15px",
|
||||||
|
color: "#ffffff",
|
||||||
|
margin: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bodyText = {
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
color: "#888888",
|
||||||
|
margin: "0 0 32px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctaSection = {
|
||||||
|
textAlign: "center" as const,
|
||||||
|
marginBottom: "48px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const button = {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
borderRadius: "0",
|
||||||
|
color: "#000000",
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textDecoration: "none",
|
||||||
|
textAlign: "center" as const,
|
||||||
|
display: "inline-block",
|
||||||
|
padding: "16px 32px",
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
letterSpacing: "0.1em",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerText = {
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#666666",
|
||||||
|
lineHeight: "20px",
|
||||||
|
};
|
||||||
141
pnpm-lock.yaml
generated
141
pnpm-lock.yaml
generated
@@ -162,6 +162,37 @@ importers:
|
|||||||
'@directus/extensions-sdk':
|
'@directus/extensions-sdk':
|
||||||
specifier: 11.0.2
|
specifier: 11.0.2
|
||||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||||
|
'@mintel/acquisition':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../acquisition-library
|
||||||
|
'@mintel/mail':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../mail
|
||||||
|
esbuild:
|
||||||
|
specifier: ^0.25.0
|
||||||
|
version: 0.25.12
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.6.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages/acquisition-library:
|
||||||
|
dependencies:
|
||||||
|
'@mintel/mail':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../mail
|
||||||
|
jquery:
|
||||||
|
specifier: ^3.7.1
|
||||||
|
version: 3.7.1
|
||||||
|
react:
|
||||||
|
specifier: ^19.2.4
|
||||||
|
version: 19.2.4
|
||||||
|
react-dom:
|
||||||
|
specifier: ^19.2.4
|
||||||
|
version: 19.2.4(react@19.2.4)
|
||||||
|
devDependencies:
|
||||||
|
'@directus/extensions-sdk':
|
||||||
|
specifier: 11.0.2
|
||||||
|
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.25.0
|
specifier: ^0.25.0
|
||||||
version: 0.25.12
|
version: 0.25.12
|
||||||
@@ -7587,6 +7618,57 @@ snapshots:
|
|||||||
|
|
||||||
'@directus/constants@11.0.3': {}
|
'@directus/constants@11.0.3': {}
|
||||||
|
|
||||||
|
'@directus/extensions-sdk@11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@directus/composables': 10.1.12(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@directus/constants': 11.0.3
|
||||||
|
'@directus/extensions': 1.0.2(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@directus/themes': 0.3.6(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@directus/types': 11.0.8(knex@3.1.0)(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@rollup/plugin-commonjs': 25.0.7(rollup@3.29.4)
|
||||||
|
'@rollup/plugin-json': 6.1.0(rollup@3.29.4)
|
||||||
|
'@rollup/plugin-node-resolve': 15.2.3(rollup@3.29.4)
|
||||||
|
'@rollup/plugin-replace': 5.0.5(rollup@3.29.4)
|
||||||
|
'@rollup/plugin-terser': 0.4.4(rollup@3.29.4)
|
||||||
|
'@rollup/plugin-virtual': 3.0.2(rollup@3.29.4)
|
||||||
|
'@vitejs/plugin-vue': 4.6.2(vite@4.5.2(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))(vue@3.4.21(typescript@5.9.3))
|
||||||
|
chalk: 5.3.0
|
||||||
|
commander: 10.0.1
|
||||||
|
esbuild: 0.17.19
|
||||||
|
execa: 7.2.0
|
||||||
|
fs-extra: 11.2.0
|
||||||
|
inquirer: 9.2.16
|
||||||
|
ora: 6.3.1
|
||||||
|
rollup: 3.29.4
|
||||||
|
rollup-plugin-esbuild: 5.0.0(esbuild@0.17.19)(rollup@3.29.4)
|
||||||
|
rollup-plugin-styles: 4.0.0(rollup@3.29.4)
|
||||||
|
vite: 4.5.2(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||||
|
vue: 3.4.21(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- '@unhead/vue'
|
||||||
|
- better-sqlite3
|
||||||
|
- debug
|
||||||
|
- knex
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- mysql
|
||||||
|
- mysql2
|
||||||
|
- pg
|
||||||
|
- pg-native
|
||||||
|
- pinia
|
||||||
|
- pino
|
||||||
|
- sass
|
||||||
|
- sqlite3
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- tedious
|
||||||
|
- terser
|
||||||
|
- typescript
|
||||||
|
- vue-router
|
||||||
|
|
||||||
'@directus/extensions-sdk@11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)':
|
'@directus/extensions-sdk@11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@directus/composables': 10.1.12(vue@3.4.21(typescript@5.9.3))
|
'@directus/composables': 10.1.12(vue@3.4.21(typescript@5.9.3))
|
||||||
@@ -7638,6 +7720,32 @@ snapshots:
|
|||||||
- typescript
|
- typescript
|
||||||
- vue-router
|
- vue-router
|
||||||
|
|
||||||
|
'@directus/extensions@1.0.2(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(vue@3.4.21(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@directus/constants': 11.0.3
|
||||||
|
'@directus/themes': 0.3.6(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@directus/types': 11.0.8(knex@3.1.0)(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
fs-extra: 11.2.0
|
||||||
|
lodash-es: 4.17.21
|
||||||
|
zod: 3.22.4
|
||||||
|
optionalDependencies:
|
||||||
|
knex: 3.1.0
|
||||||
|
pino: 10.3.1
|
||||||
|
vue: 3.4.21(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@unhead/vue'
|
||||||
|
- better-sqlite3
|
||||||
|
- mysql
|
||||||
|
- mysql2
|
||||||
|
- pg
|
||||||
|
- pg-native
|
||||||
|
- pinia
|
||||||
|
- sqlite3
|
||||||
|
- supports-color
|
||||||
|
- tedious
|
||||||
|
|
||||||
'@directus/extensions@1.0.2(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(vue@3.4.21(typescript@5.9.3))':
|
'@directus/extensions@1.0.2(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(vue@3.4.21(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@directus/constants': 11.0.3
|
'@directus/constants': 11.0.3
|
||||||
@@ -7681,6 +7789,17 @@ snapshots:
|
|||||||
|
|
||||||
'@directus/system-data@1.0.2': {}
|
'@directus/system-data@1.0.2': {}
|
||||||
|
|
||||||
|
'@directus/themes@0.3.6(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||||
|
'@sinclair/typebox': 0.32.15
|
||||||
|
'@unhead/vue': 1.11.20(vue@3.4.21(typescript@5.9.3))
|
||||||
|
decamelize: 6.0.0
|
||||||
|
flat: 6.0.1
|
||||||
|
lodash-es: 4.17.21
|
||||||
|
pinia: 2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3))
|
||||||
|
vue: 3.4.21(typescript@5.9.3)
|
||||||
|
|
||||||
'@directus/themes@0.3.6(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))':
|
'@directus/themes@0.3.6(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||||
@@ -9621,6 +9740,14 @@ snapshots:
|
|||||||
'@unhead/schema': 1.11.20
|
'@unhead/schema': 1.11.20
|
||||||
packrup: 0.1.2
|
packrup: 0.1.2
|
||||||
|
|
||||||
|
'@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@unhead/schema': 1.11.20
|
||||||
|
'@unhead/shared': 1.11.20
|
||||||
|
hookable: 5.5.3
|
||||||
|
unhead: 1.11.20
|
||||||
|
vue: 3.4.21(typescript@5.9.3)
|
||||||
|
|
||||||
'@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3))':
|
'@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@unhead/schema': 1.11.20
|
'@unhead/schema': 1.11.20
|
||||||
@@ -12567,6 +12694,16 @@ snapshots:
|
|||||||
|
|
||||||
pify@4.0.1: {}
|
pify@4.0.1: {}
|
||||||
|
|
||||||
|
pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 6.6.4
|
||||||
|
vue: 3.4.21(typescript@5.9.3)
|
||||||
|
vue-demi: 0.14.10(vue@3.4.21(typescript@5.9.3))
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
|
||||||
pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)):
|
pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
@@ -14033,6 +14170,10 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
vue-demi@0.14.10(vue@3.4.21(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.4.21(typescript@5.9.3)
|
||||||
|
|
||||||
vue-demi@0.14.10(vue@3.5.28(typescript@5.9.3)):
|
vue-demi@0.14.10(vue@3.5.28(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.28(typescript@5.9.3)
|
vue: 3.5.28(typescript@5.9.3)
|
||||||
|
|||||||
Reference in New Issue
Block a user