Compare commits

...

11 Commits

Author SHA1 Message Date
f8847a7a10 feat(next-feedback): refine selector filters for tailwind and dynamic classes
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 8s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m59s
Monorepo Pipeline / 🧪 Test (push) Successful in 4m5s
Monorepo Pipeline / 🧹 Lint (push) Successful in 5m8s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 20s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 18s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 20s
Monorepo Pipeline / 🚀 Release (push) Successful in 8m3s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 10m25s
2026-02-13 12:03:33 +01:00
117b23db1e feat(next-feedback): improve selector precision with @medv/finder and fix client/server boundary 2026-02-13 12:03:11 +01:00
d6f9a24823 chore: sync versions to 1.8.0 2026-02-12 22:05:20 +01:00
422e4fccba feat(cloner): add cloner-library and finalize pdf-library rename 2026-02-12 22:04:40 +01:00
57ec4d7544 chore: bump versions 2026-02-12 21:47:55 +01:00
a4d021c658 feat(pdf): rename acquisition-library to pdf-library and update package name to @mintel/pdf
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 13s
Monorepo Pipeline / 🏗️ Build (push) Failing after 11s
Monorepo Pipeline / 🧪 Test (push) Failing after 25s
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
2026-02-12 21:46:45 +01:00
269d19bbef fix(acquisition): finalize extension build and components
- Fixed IndustrialCard export in SharedUI.
- Successfully built all extensions including acquisition-library.
- Verified sitemap and briefing module updates.
2026-02-12 21:27:39 +01:00
30ff08c66d fix(acquisition): standardize bundling and externalize React/PDF dependencies
- Added JSX support and correctly externalized react/pdf dependencies in esbuild.
- Fixed acquisition-library exports by removing missing DINLayout reference.
- Standardized extension entry points across all modules.
2026-02-12 21:26:30 +01:00
81deaf447f fix(acquisition): standardize bundling and externalize React/PDF dependencies
- Added JSX support to esbuild configuration.
- Externalized react, react-dom, and @react-pdf/renderer to avoid redundant bundling.
- Updated acquisition-library exports for modular PDF generation.
2026-02-12 21:24:15 +01:00
a0ebc58d6d fix(directus): resolve extension visibility and registration failures
- Corrected module_bar settings to restore custom extension visibility in UI.
- Fixed 'fs' dynamic require in acquisition endpoint by externalizing Node.js built-ins.
- Standardized local environment branding to AT Mintel.
2026-02-12 21:20:28 +01:00
7498c24c9a fix(directus): resolve login failures and standardize project branding
- Fixed project isolation bypass (identity shadowing) by prefixing database service name.
- Standardized health check paths and protocols in docker-compose.yml.
- Resolved extension SyntaxError caused by duplicate banner injections in build scripts.
- Migrated extension build system to clean esbuild-based bundles (removing shims).
- Updated sync-directus.sh for project-prefixed service name.
- Synchronized latest production data and branding (AT Mintel).
2026-02-12 19:21:53 +01:00
92 changed files with 7149 additions and 1023 deletions

View File

@@ -0,0 +1,82 @@
---
description: How to manage and deploy Directus CMS infrastructure changes.
---
# Directus CMS Infrastructure Workflow
This workflow ensures "Industrial Grade" consistency and stability across local, testing, and production environments for the `at-mintel` Directus CMS.
## 1. Local Development Lifecycle
### Starting the CMS
To start the local Directus instance with extensions:
```bash
cd packages/cms-infra
npm run up
```
### Modifying Schema
1. **Directus UI**: Make your changes directly in the local Directus Admin UI (Collections, Fields, Relations).
2. **Take Snapshot**:
```bash
cd packages/cms-infra
npm run snapshot:local
```
This updates `packages/cms-infra/schema/snapshot.yaml`.
3. **Commit**: Commit the updated `snapshot.yaml`.
## 2. Deploying Schema Changes
### To Local Environment (Reconciliation)
If you pull changes from Git and need to apply them to your local database:
```bash
cd packages/cms-infra
npm run schema:apply:local
```
> [!IMPORTANT]
> This command automatically runs `scripts/cms-reconcile.sh` to prevent "Field already exists" errors by registering database columns in Directus metadata first.
### To Production (Infra)
To deploy the local snapshot to the production server:
```bash
cd packages/cms-infra
npm run schema:apply:infra
```
This script:
1. Syncs built extensions via rsync.
2. Injects the `snapshot.yaml` into the remote container.
3. Runs `directus schema apply`.
4. Restarts Directus to clear the schema cache.
## 3. Data Synchronization
### Pulling from Production
To update your local environment with production data and assets:
```bash
cd packages/cms-infra
npm run sync:pull
```
### Pushing to Production
> [!CAUTION]
> This will overwrite production data. Use with extreme care.
```bash
cd packages/cms-infra
npm run sync:push
```
## 4. Extension Management
When modifying extensions in `packages/*-manager`:
1. Extensions are automatically built and synced when running `npm run up`.
2. To sync manually without restarting the stack:
```bash
cd packages/cms-infra
npm run build:extensions
```
## 5. Troubleshooting "Field already exists"
If `schema:apply` fails with "Field already exists", run:
```bash
./scripts/cms-reconcile.sh
```
This script ensures the database state matches Directus's internal field registry (`directus_fields`).

36
.env Normal file
View File

@@ -0,0 +1,36 @@
# Project
IMAGE_TAG=v1.8.0
PROJECT_NAME=at-mintel
PROJECT_COLOR=#82ed20
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
# Authentication
GATEKEEPER_PASSWORD=mintel
AUTH_COOKIE_NAME=mintel_gatekeeper_session
# Host Config (Local)
TRAEFIK_HOST=at-mintel.localhost
DIRECTUS_HOST=cms.localhost
# Next.js
NEXT_PUBLIC_BASE_URL=http://at-mintel.localhost
# Directus
DIRECTUS_URL=http://cms.localhost
DIRECTUS_KEY=F9IIfahEjPq6NZhKyRLw516D8GotuFj79EGK7pGfIWg=
DIRECTUS_SECRET=OZfxMu8lBxzaEnFGRKreNBoJpRiRu58U+HsVg2yWk4o=
CORS_ENABLED=true
CORS_ORIGIN=true
LOG_LEVEL=debug
DIRECTUS_ADMIN_EMAIL=mmintel@mintel.me
DIRECTUS_ADMIN_PASSWORD=Tim300493.
DIRECTUS_DB_NAME=directus
DIRECTUS_DB_USER=directus
DIRECTUS_DB_PASSWORD=mintel-db-pass
# Sentry / Glitchtip
SENTRY_DSN=
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js

View File

@@ -1,5 +1,5 @@
# Project
IMAGE_TAG=v1.7.12
IMAGE_TAG=v1.8.0
PROJECT_NAME=sample-website
PROJECT_COLOR=#82ed20

View File

@@ -1,6 +1,6 @@
{
"name": "sample-website",
"version": "1.7.12",
"version": "1.8.0",
"private": true,
"type": "module",
"scripts": {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
{
"name": "acquisition-manager",
"description": "Custom High-Fidelity Acquisition Management for Directus",
"icon": "account_balance_wallet",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"files": [
"dist"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "Acquisition Manager"
},
"scripts": {
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "acquisition",
"version": "1.8.0",
"type": "module",
"directus:extension": {
"type": "endpoint",
"path": "dist/index.js",
"source": "src/index.ts",
"host": "^11.0.0"
},
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs --watch"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"@mintel/acquisition": "workspace:*",
"@mintel/mail": "workspace:*",
"esbuild": "^0.25.0",
"typescript": "^5.6.3"
},
"dependencies": {
"jquery": "^3.7.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
{
"name": "customer-manager",
"description": "Custom High-Fidelity Customer & Company Management for Directus",
"icon": "supervisor_account",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"files": [
"dist"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "Customer Manager"
},
"scripts": {
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
{
"name": "feedback-commander",
"description": "Custom High-Fidelity Feedback Management Extension for Directus",
"icon": "view_kanban",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"files": [
"dist"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "Feedback Commander"
},
"scripts": {
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
{
"name": "people-manager",
"description": "Custom High-Fidelity People Management for Directus",
"icon": "person",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"files": [
"dist"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "People Manager"
},
"scripts": {
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

View File

@@ -0,0 +1 @@
S9WsV

View File

@@ -24,6 +24,12 @@ services:
directus:
image: registry.infra.mintel.me/mintel/directus:${IMAGE_TAG:-latest}
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8055/server/health" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
restart: always
networks:
- infra
@@ -35,7 +41,7 @@ services:
ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL:-admin@mintel.me}
ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD:-mintel-admin}
DB_CLIENT: 'pg'
DB_HOST: 'directus-db'
DB_HOST: 'at-mintel-directus-db'
DB_PORT: '5432'
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
DB_USER: ${DIRECTUS_DB_USER:-directus}
@@ -53,7 +59,7 @@ services:
- "traefik.http.routers.sample-website-directus.rule=Host(`${DIRECTUS_HOST:-cms.sample-website.localhost}`)"
- "traefik.http.services.sample-website-directus.loadbalancer.server.port=8055"
directus-db:
at-mintel-directus-db:
image: postgres:15-alpine
restart: always
networks:

View File

@@ -52,7 +52,7 @@
"pino-pretty": "^13.1.3",
"require-in-the-middle": "^8.0.1"
},
"version": "1.7.12",
"version": "1.8.0",
"pnpm": {
"overrides": {
"next": "16.1.6",

View File

@@ -1,44 +0,0 @@
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, 'dist/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);
});

View File

@@ -1,25 +0,0 @@
{
"name": "@mintel/acquisition",
"version": "1.7.12",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"scripts": {
"build": "node build.js",
"dev": "node build.js --watch"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"esbuild": "^0.25.0",
"typescript": "^5.6.3"
},
"dependencies": {
"@mintel/mail": "workspace:*",
"axios": "^1.7.9",
"crawlee": "^3.12.2",
"cheerio": "^1.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"@react-pdf/renderer": "^4.3.0"
}
}

View File

@@ -1,401 +0,0 @@
"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>
);

View File

@@ -1,69 +0,0 @@
"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>
)}
</>
);

View File

@@ -1,56 +0,0 @@
"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>
</>
);

View File

@@ -1,6 +0,0 @@
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";

View File

@@ -1,22 +0,0 @@
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);
}

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"name": "acquisition-manager",
"description": "Custom High-Fidelity Acquisition Management for Directus",
"icon": "account_balance_wallet",
"version": "1.7.12",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",

View File

@@ -23,7 +23,7 @@
<v-icon :name="getStatusIcon(lead.status)" :color="getStatusColor(lead.status)" />
</v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="lead.company_name" />
<v-text-overflow :text="getCompanyName(lead)" />
</v-list-item-content>
</v-list-item>
</v-list>
@@ -50,7 +50,7 @@
<template v-else>
<header class="header">
<div class="header-left">
<h1 class="title">{{ selectedLead.company_name }}</h1>
<h1 class="title">{{ getCompanyName(selectedLead) }}</h1>
<p class="subtitle">
<v-icon name="language" x-small />
<a :href="selectedLead.website_url" target="_blank" class="url-link">
@@ -156,7 +156,15 @@
<div class="drawer-content">
<div class="form-section">
<div class="field">
<span class="label">Firma</span>
<span class="label">Organisation / Firma (Zentral)</span>
<v-select
v-model="newLead.company"
:items="companyOptions"
placeholder="Bestehende Firma auswählen..."
/>
</div>
<div class="field">
<span class="label">Organisation / Firma (Legacy / Neu)</span>
<v-input v-model="newLead.company_name" placeholder="z.B. Schmidt GmbH" autofocus />
</div>
<div class="field">
@@ -208,17 +216,26 @@ const savingLead = ref(false);
const notice = ref<{ type: string; message: string } | null>(null);
const newLead = ref({
company_name: '',
company_name: '', // Legacy
company: null,
website_url: '',
contact_name: '',
contact_email: '',
contact_name: '', // Legacy
contact_email: '', // Legacy
contact_person: null,
briefing: '',
status: 'new'
});
const companies = ref<any[]>([]);
const people = ref<any[]>([]);
const companyOptions = computed(() =>
companies.value.map(c => ({
text: c.name,
value: c.id
}))
);
const peopleOptions = computed(() =>
people.value.map(p => ({
text: `${p.first_name} ${p.last_name}`,
@@ -226,7 +243,16 @@ const peopleOptions = computed(() =>
}))
);
function getPersonName(id: string) {
function getCompanyName(lead: any) {
if (lead.company) {
return typeof lead.company === 'object' ? lead.company.name : (companies.value.find(c => c.id === lead.company)?.name || lead.company_name);
}
return lead.company_name;
}
function getPersonName(id: string | any) {
if (!id) return '';
if (typeof id === 'object') return `${id.first_name} ${id.last_name}`;
const person = people.value.find(p => p.id === id);
return person ? `${person.first_name} ${person.last_name}` : id;
}
@@ -238,20 +264,32 @@ function goToPerson(id: string) {
const selectedLead = computed(() => leads.value.find(l => l.id === selectedLeadId.value));
onMounted(fetchLeads);
onMounted(fetchData);
async function fetchLeads() {
const [leadsResp, peopleResp] = await Promise.all([
api.get('/items/leads', { params: { sort: '-date_created' } }),
api.get('/items/people', { params: { sort: 'last_name' } })
async function fetchData() {
const [leadsResp, peopleResp, companiesResp] = await Promise.all([
api.get('/items/leads', {
params: {
sort: '-date_created',
fields: '*.*'
}
}),
api.get('/items/people', { params: { sort: 'last_name' } }),
api.get('/items/companies', { params: { sort: 'name' } })
]);
leads.value = leadsResp.data.data;
people.value = peopleResp.data.data;
companies.value = companiesResp.data.data;
if (!selectedLeadId.value && leads.value.length > 0) {
selectedLeadId.value = leads.value[0].id;
}
}
async function fetchLeads() {
await fetchData();
}
function selectLead(id: string) {
selectedLeadId.value = id;
}
@@ -318,7 +356,10 @@ function openPdf() {
}
async function saveLead() {
if (!newLead.value.company_name) return;
if (!newLead.value.company_name && !newLead.value.company) {
notice.value = { type: 'danger', message: 'Firma oder Firmenname erforderlich.' };
return;
}
savingLead.value = true;
try {
const payload = {
@@ -332,6 +373,7 @@ async function saveLead() {
selectedLeadId.value = payload.id;
newLead.value = {
company_name: '',
company: null,
website_url: '',
contact_name: '',
contact_email: '',

View File

@@ -21,25 +21,24 @@ build({
platform: 'node',
target: 'node18',
outfile: outfile,
format: 'esm',
// Bundle everything, including Directus SDK, to avoid resolution issues in Docker
external: [],
jsx: 'automatic',
loader: {
'.tsx': 'tsx',
'.ts': 'ts',
'.js': 'js',
},
external: ["@react-pdf/renderer", "react", "react-dom", "jsdom", "jsdom/*", "jquery", "jquery/*", "canvas", "fs", "path", "os", "http", "https", "zlib", "stream", "util", "url", "net", "tls", "crypto"],
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.onResolve({ filter: /^canvas/ }, args => ({ path: args.path, namespace: 'mock-canvas' }));
build.onLoad({ filter: /.*/, namespace: 'mock-canvas' }, () => ({ contents: 'export default {};', loader: 'js' }));
}
}, {
name: 'mock-jsdom',
setup(build) {
return;
build.onResolve({ filter: /^jsdom/ }, args => ({ path: args.path, namespace: 'mock-jsdom' }));
build.onLoad({ filter: /.*/, namespace: 'mock-jsdom' }, () => ({ contents: 'export default {};', loader: 'js' }));
}
}]
}).then(() => {

View File

@@ -1,6 +1,6 @@
{
"name": "acquisition",
"version": "1.7.12",
"version": "1.8.0",
"type": "module",
"directus:extension": {
"type": "endpoint",
@@ -9,12 +9,12 @@
"host": "^11.0.0"
},
"scripts": {
"build": "node build.js",
"dev": "node build.js --watch"
"build": "node build.mjs",
"dev": "node build.mjs --watch"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"@mintel/acquisition": "workspace:*",
"@mintel/pdf": "workspace:*",
"@mintel/mail": "workspace:*",
"esbuild": "^0.25.0",
"typescript": "^5.6.3"

View File

@@ -1,6 +1,5 @@
import "./shim";
import { defineEndpoint } from "@directus/extensions-sdk";
import { AcquisitionService, PdfEngine } from "../../acquisition-library/src/index";
import { AcquisitionService, PdfEngine } from "@mintel/pdf/server";
import { render, SiteAuditTemplate, ProjectEstimateTemplate } from "@mintel/mail";
import { createElement } from "react";
import * as path from "path";
@@ -40,22 +39,25 @@ export default defineEndpoint((router, { services, env }) => {
router.post("/audit-email/:id", async (req: any, res: any) => {
const { id } = req.params;
const { ItemsService, MailService } = services;
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
const peopleService = new ItemsService("people", { schema: req.schema, accountability: req.accountability });
const companiesService = new ItemsService("companies", { schema: req.schema, accountability: req.accountability });
const mailService = new MailService({ schema: req.schema, accountability: req.accountability });
try {
const lead = await leadsService.readOne(id);
const lead = await leadsService.readOne(id, { fields: ["*", "company.*", "contact_person.*"] });
if (!lead || !lead.ai_state) return res.status(400).send({ error: "Lead or Audit not ready" });
let recipientEmail = lead.contact_email;
let companyName = lead.company_name;
let companyName = lead.company?.name || lead.company_name;
if (lead.contact_person) {
const person = await peopleService.readOne(lead.contact_person);
if (person && person.email) {
recipientEmail = person.email;
companyName = person.company || lead.company_name;
recipientEmail = lead.contact_person.email || recipientEmail;
if (lead.contact_person.company) {
const personCompany = await companiesService.readOne(lead.contact_person.company);
companyName = personCompany?.name || companyName;
}
}
@@ -120,20 +122,22 @@ export default defineEndpoint((router, { services, env }) => {
const { id } = req.params;
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
const peopleService = new ItemsService("people", { schema: req.schema, accountability: req.accountability });
const companiesService = new ItemsService("companies", { schema: req.schema, accountability: req.accountability });
const mailService = new MailService({ schema: req.schema, accountability: req.accountability });
try {
const lead = await leadsService.readOne(id);
const lead = await leadsService.readOne(id, { fields: ["*", "company.*", "contact_person.*"] });
if (!lead || !lead.audit_pdf_path) return res.status(400).send({ error: "PDF not generated" });
let recipientEmail = lead.contact_email;
let companyName = lead.company_name;
let companyName = lead.company?.name || lead.company_name;
if (lead.contact_person) {
const person = await peopleService.readOne(lead.contact_person);
if (person && person.email) {
recipientEmail = person.email;
companyName = person.company || lead.company_name;
recipientEmail = lead.contact_person.email || recipientEmail;
if (lead.contact_person.company) {
const personCompany = await companiesService.readOne(lead.contact_person.company);
companyName = personCompany?.name || companyName;
}
}

View File

@@ -1,22 +0,0 @@
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);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/cli",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,41 @@
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 entryPoints = [
resolve(__dirname, 'src/index.ts')
];
try {
mkdirSync(resolve(__dirname, 'dist'), { recursive: true });
} catch (e) { }
console.log(`Building entry point...`);
build({
entryPoints: entryPoints,
bundle: true,
platform: 'node',
target: 'node18',
outdir: resolve(__dirname, 'dist'),
format: 'esm',
loader: {
'.ts': 'ts',
'.js': 'js',
},
external: ["playwright", "crawlee", "axios", "cheerio", "fs", "path", "os", "http", "https", "url", "stream", "util", "child_process"],
}).then(() => {
console.log("Build succeeded!");
}).catch((e) => {
if (e.errors) {
console.error("Build failed with errors:");
e.errors.forEach(err => console.error(` ${err.text} at ${err.location?.file}:${err.location?.line}`));
} else {
console.error("Build failed:", e);
}
process.exit(1);
});

View File

@@ -0,0 +1,30 @@
{
"name": "@mintel/cloner",
"version": "1.8.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs --watch"
},
"devDependencies": {
"esbuild": "^0.25.0",
"typescript": "^5.6.3",
"@types/node": "^22.0.0"
},
"dependencies": {
"playwright": "^1.40.0",
"crawlee": "^3.7.0",
"axios": "^1.6.0",
"cheerio": "^1.0.0-rc.12"
}
}

View File

@@ -0,0 +1,93 @@
import axios from "axios";
import fs from "node:fs";
import path from "node:path";
export interface AssetMap {
[originalUrl: string]: string;
}
export class AssetManager {
private userAgent: string;
constructor(userAgent: string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") {
this.userAgent = userAgent;
}
public sanitizePath(rawPath: string): string {
return rawPath
.split("/")
.map((p) => p.replace(/[^a-z0-9._-]/gi, "_"))
.join("/");
}
public async downloadFile(url: string, assetsDir: string): Promise<string | null> {
if (url.startsWith("//")) url = `https:${url}`;
if (!url.startsWith("http")) return null;
try {
const u = new URL(url);
const relPath = this.sanitizePath(u.hostname + u.pathname);
const dest = path.join(assetsDir, relPath);
if (fs.existsSync(dest)) return `./assets/${relPath}`;
const res = await axios.get(url, {
responseType: "arraybuffer",
headers: { "User-Agent": this.userAgent },
timeout: 15000,
validateStatus: () => true,
});
if (res.status !== 200) return null;
if (!fs.existsSync(path.dirname(dest)))
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, Buffer.from(res.data));
return `./assets/${relPath}`;
} catch {
return null;
}
}
public async processCssRecursively(
cssContent: string,
cssUrl: string,
assetsDir: string,
urlMap: AssetMap,
depth = 0,
): Promise<string> {
if (depth > 5) return cssContent;
const urlRegex = /(?:url\(["']?|@import\s+["'])([^"'\)]+)["']?\)?/gi;
let match;
let newContent = cssContent;
while ((match = urlRegex.exec(cssContent)) !== null) {
const originalUrl = match[1];
if (originalUrl.startsWith("data:") || originalUrl.startsWith("blob:"))
continue;
try {
const absUrl = new URL(originalUrl, cssUrl).href;
const local = await this.downloadFile(absUrl, assetsDir);
if (local) {
const u = new URL(cssUrl);
const cssPath = u.hostname + u.pathname;
const assetPath = new URL(absUrl).hostname + new URL(absUrl).pathname;
const rel = path.relative(
path.dirname(this.sanitizePath(cssPath)),
this.sanitizePath(assetPath),
);
newContent = newContent.split(originalUrl).join(rel);
urlMap[absUrl] = local;
}
} catch {
// Ignore
}
}
return newContent;
}
}

View File

@@ -0,0 +1,184 @@
import { chromium, Browser, BrowserContext, Page } from "playwright";
import fs from "node:fs";
import path from "node:path";
import axios from "axios";
import { AssetManager, AssetMap } from "./AssetManager.js";
export interface PageClonerOptions {
outputDir: string;
userAgent?: string;
}
export class PageCloner {
private options: PageClonerOptions;
private assetManager: AssetManager;
private userAgent: string;
constructor(options: PageClonerOptions) {
this.options = options;
this.userAgent = options.userAgent || "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36";
this.assetManager = new AssetManager(this.userAgent);
}
public async clone(targetUrl: string): Promise<string> {
const urlObj = new URL(targetUrl);
const domainSlug = urlObj.hostname.replace("www.", "");
const domainDir = path.resolve(this.options.outputDir, domainSlug);
const assetsDir = path.join(domainDir, "assets");
if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir, { recursive: true });
let pageSlug = urlObj.pathname.split("/").filter(Boolean).join("-");
if (!pageSlug) pageSlug = "index";
const htmlFilename = `${pageSlug}.html`;
console.log(`🚀 INDUSTRIAL CLONE: ${targetUrl}`);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
userAgent: this.userAgent,
viewport: { width: 1920, height: 1080 },
});
const page = await context.newPage();
const urlMap: AssetMap = {};
const foundAssets = new Set<string>();
page.on("response", (response) => {
if (response.status() === 200) {
const url = response.url();
if (url.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)/i)) {
foundAssets.add(url);
}
}
});
try {
await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 90000 });
// Scroll Wave
await page.evaluate(async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 400;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
window.scrollTo(0, 0);
resolve(true);
}
}, 100);
});
});
const fullHeight = await page.evaluate(() => document.body.scrollHeight);
await page.setViewportSize({ width: 1920, height: fullHeight + 1000 });
await page.waitForTimeout(3000);
// Sanitization
await page.evaluate(() => {
const assetPattern = /\.(jpg|jpeg|png|gif|svg|webp|mp4|webm|woff2?|ttf|otf)/i;
document.querySelectorAll("*").forEach((el) => {
if (["META", "LINK", "HEAD", "SCRIPT", "STYLE", "SVG", "PATH"].includes(el.tagName)) return;
const htmlEl = el as HTMLElement;
const style = window.getComputedStyle(htmlEl);
if (style.opacity === "0" || style.visibility === "hidden") {
htmlEl.style.setProperty("opacity", "1", "important");
htmlEl.style.setProperty("visibility", "visible", "important");
}
for (const attr of Array.from(el.attributes)) {
const name = attr.name.toLowerCase();
const val = attr.value;
if (assetPattern.test(val) || name.includes("src") || name.includes("image")) {
if (el.tagName === "IMG") {
const img = el as HTMLImageElement;
if (name.includes("srcset")) img.srcset = val;
else if (!img.src || img.src.includes("data:")) img.src = val;
}
if (el.tagName === "SOURCE") (el as HTMLSourceElement).srcset = val;
if (el.tagName === "VIDEO" || el.tagName === "AUDIO") (el as HTMLMediaElement).src = val;
if (val.match(/^(https?:\/\/|\/\/|\/)/) && !name.includes("href")) {
const bg = htmlEl.style.backgroundImage;
if (!bg || bg === "none") htmlEl.style.backgroundImage = `url('${val}')`;
}
}
}
});
if (document.body) {
document.body.style.setProperty("opacity", "1", "important");
document.body.style.setProperty("visibility", "visible", "important");
}
});
await page.waitForLoadState("networkidle");
await page.waitForTimeout(1000);
let content = await page.content();
const regexPatterns = [
/(?:src|href|url|data-[a-z-]+|srcset)=["']([^"'<>\s]+?\.(?:css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)(?:\?[^"']*)?)["']/gi,
/url\(["']?([^"'\)]+)["']?\)/gi,
];
for (const pattern of regexPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
try { foundAssets.add(new URL(match[1], targetUrl).href); } catch { }
}
}
for (const url of foundAssets) {
const local = await this.assetManager.downloadFile(url, assetsDir);
if (local) {
urlMap[url] = local;
const clean = url.split("?")[0];
urlMap[clean] = local;
if (clean.endsWith(".css")) {
try {
const { data } = await axios.get(url, { headers: { "User-Agent": this.userAgent } });
const processedCss = await this.assetManager.processCssRecursively(data, url, assetsDir, urlMap);
const relPath = this.assetManager.sanitizePath(new URL(url).hostname + new URL(url).pathname);
fs.writeFileSync(path.join(assetsDir, relPath), processedCss);
} catch { }
}
}
}
let finalContent = content;
const sortedUrls = Object.keys(urlMap).sort((a, b) => b.length - a.length);
if (sortedUrls.length > 0) {
const escaped = sortedUrls.map((u) => u.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
const masterRegex = new RegExp(`(${escaped.join("|")})`, "g");
finalContent = finalContent.replace(masterRegex, (match) => urlMap[match] || match);
}
const commonDirs = ["/wp-content/", "/wp-includes/", "/assets/", "/static/", "/images/"];
for (const dir of commonDirs) {
const localDir = `./assets/${urlObj.hostname}${dir}`;
finalContent = finalContent.split(`"${dir}`).join(`"${localDir}`).split(`'${dir}`).join(`'${localDir}`).split(`(${dir}`).join(`(${localDir}`);
}
const domainPattern = new RegExp(`https?://(www\\.)?${urlObj.hostname.replace(/\./g, "\\.")}[^"']*`, "gi");
finalContent = finalContent.replace(domainPattern, () => "./");
finalContent = finalContent.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, (match, scriptContent) => {
const lower = scriptContent.toLowerCase();
return (lower.includes("google-analytics") || lower.includes("gtag") || lower.includes("fbq") || lower.includes("lazy") || lower.includes("tracker")) ? "" : match;
});
const headEnd = finalContent.indexOf("</head>");
if (headEnd > -1) {
const stabilityCss = `\n<style>* { transition: none !important; animation: none !important; scroll-behavior: auto !important; } [data-aos], .reveal, .lazypath, .lazy-load, [data-src] { opacity: 1 !important; visibility: visible !important; transform: none !important; clip-path: none !important; } img, video, iframe { max-width: 100%; display: block; } a { pointer-events: none; cursor: default; } </style>`;
finalContent = finalContent.slice(0, headEnd) + stabilityCss + finalContent.slice(headEnd);
}
const finalPath = path.join(domainDir, htmlFilename);
fs.writeFileSync(finalPath, finalContent);
return finalPath;
} finally {
await browser.close();
}
}
}

View File

@@ -0,0 +1,123 @@
import { PlaywrightCrawler, RequestQueue } from 'crawlee';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { execSync } from 'node:child_process';
export interface WebsiteClonerOptions {
baseOutputDir: string;
maxRequestsPerCrawl?: number;
maxConcurrency?: number;
}
export class WebsiteCloner {
private options: WebsiteClonerOptions;
constructor(options: WebsiteClonerOptions) {
this.options = {
maxRequestsPerCrawl: 100,
maxConcurrency: 3,
...options
};
}
public async clone(targetUrl: string, outputDirName?: string): Promise<string> {
const urlObj = new URL(targetUrl);
const domain = urlObj.hostname;
const finalOutputDirName = outputDirName || domain.replace(/\./g, '-');
const baseOutputDir = path.resolve(this.options.baseOutputDir, finalOutputDirName);
if (fs.existsSync(baseOutputDir)) {
fs.rmSync(baseOutputDir, { recursive: true, force: true });
}
fs.mkdirSync(baseOutputDir, { recursive: true });
console.log(`🚀 Starting perfect recursive clone of ${targetUrl}...`);
console.log(`📂 Output: ${baseOutputDir}`);
const requestQueue = await RequestQueue.open();
await requestQueue.addRequest({ url: targetUrl });
const crawler = new PlaywrightCrawler({
requestQueue,
maxRequestsPerCrawl: this.options.maxRequestsPerCrawl,
maxConcurrency: this.options.maxConcurrency,
async requestHandler({ request, enqueueLinks, log }) {
const url = request.url;
log.info(`Capturing ${url}...`);
const u = new URL(url);
let relPath = u.pathname;
if (relPath === '/' || relPath === '') relPath = '/index.html';
if (!relPath.endsWith('.html') && !path.extname(relPath)) relPath += '/index.html';
if (relPath.startsWith('/')) relPath = relPath.substring(1);
const fullPath = path.join(baseOutputDir, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
try {
// Note: This assumes single-file-cli is available in the environment
execSync(`npx single-file-cli "${url}" "${fullPath}" --browser-headless=true --browser-wait-until=networkidle0`, {
stdio: 'inherit'
});
} catch (e) {
log.error(`Failed to capture ${url} with SingleFile`);
}
await enqueueLinks({
strategy: 'same-domain',
transformRequestFunction: (req) => {
if (/\.(download|pdf|zip|gz|exe|png|jpg|jpeg|gif|svg|css|js)$/i.test(req.url)) return false;
return req;
}
});
},
});
await crawler.run();
console.log('🔗 Rewriting internal links for offline navigation...');
const allFiles = this.getFiles(baseOutputDir).filter(f => f.endsWith('.html'));
for (const file of allFiles) {
let content = fs.readFileSync(file, 'utf8');
const fileRelToRoot = path.relative(baseOutputDir, file);
content = content.replace(/href="([^"]+)"/g, (match, href) => {
if (href.startsWith(targetUrl) || href.startsWith('/') || (!href.includes('://') && !href.startsWith('data:'))) {
try {
const linkUrl = new URL(href, targetUrl);
if (linkUrl.hostname === domain) {
let linkPath = linkUrl.pathname;
if (linkPath === '/' || linkPath === '') linkPath = '/index.html';
if (!linkPath.endsWith('.html') && !path.extname(linkPath)) linkPath += '/index.html';
if (linkPath.startsWith('/')) linkPath = linkPath.substring(1);
const relativeLink = path.relative(path.dirname(fileRelToRoot), linkPath);
return `href="${relativeLink}"`;
}
} catch (e) { }
}
return match;
});
fs.writeFileSync(file, content);
}
console.log(`\n✅ Done! Perfect clone complete in: ${baseOutputDir}`);
return baseOutputDir;
}
private getFiles(dir: string, fileList: string[] = []) {
const files = fs.readdirSync(dir);
for (const file of files) {
const name = path.join(dir, file);
if (fs.statSync(name).isDirectory()) {
this.getFiles(name, fileList);
} else {
fileList.push(name);
}
}
return fileList;
}
}

View File

@@ -0,0 +1,3 @@
export * from "./AssetManager.js";
export * from "./PageCloner.js";
export * from "./WebsiteCloner.js";

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"emitDeclarationOnly": true,
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"noEmit": false
},
"include": [
"src/**/*"
]
}

Binary file not shown.

View File

@@ -1,12 +1,17 @@
{
"name": "@mintel/cms-infra",
"version": "1.7.12",
"version": "1.8.0",
"private": true,
"type": "module",
"scripts": {
"up": "npm run build:extensions && docker compose up -d",
"down": "docker compose down",
"logs": "docker compose logs -f",
"build:extensions": "../../scripts/sync-extensions.sh"
"build:extensions": "../../scripts/sync-extensions.sh",
"schema:apply:local": "../../scripts/cms-apply.sh local",
"schema:apply:infra": "../../scripts/cms-apply.sh infra",
"snapshot:local": "../../scripts/cms-snapshot.sh",
"sync:push": "../../scripts/sync-directus.sh push infra",
"sync:pull": "../../scripts/sync-directus.sh pull infra"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "customer-manager",
"description": "Custom High-Fidelity Customer & Company Management for Directus",
"icon": "supervisor_account",
"version": "1.7.12",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",

View File

@@ -127,6 +127,15 @@
<v-input v-model="employeeForm.email" placeholder="E-Mail Adresse" type="email" />
</div>
<div class="field">
<span class="label">Zentrale Person (Verknüpfung)</span>
<v-select
v-model="employeeForm.person"
:items="peopleOptions"
placeholder="Person aus dem People Manager auswählen..."
/>
</div>
<v-divider v-if="isEditingEmployee" />
<div v-if="isEditingEmployee" class="field">
@@ -158,7 +167,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue';
import { ref, onMounted, nextTick, computed } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const api = useApi();
@@ -183,6 +192,7 @@ const employeeForm = ref({
first_name: '',
last_name: '',
email: '',
person: null,
temporary_password: ''
});
@@ -192,14 +202,22 @@ const tableHeaders = [
{ text: 'Zuletzt eingeladen', value: 'last_invited', sortable: true }
];
async function fetchCompanies() {
const res = await api.get('/items/companies', {
params: {
fields: ['id', 'name'],
sort: 'name',
},
});
companies.value = res.data.data;
const people = ref<any[]>([]);
const peopleOptions = computed(() =>
people.value.map(p => ({
text: `${p.first_name} ${p.last_name} (${p.email})`,
value: p.id
}))
);
async function fetchData() {
const [companiesResp, peopleResp] = await Promise.all([
api.get('/items/companies', { params: { sort: 'name', fields: ['id', 'name'] } }),
api.get('/items/people', { params: { sort: 'last_name' } })
]);
companies.value = companiesResp.data.data;
people.value = peopleResp.data.data;
}
async function selectCompany(company: any) {
@@ -209,7 +227,7 @@ async function selectCompany(company: any) {
const res = await api.get('/items/client_users', {
params: {
filter: { company: { _eq: company.id } },
fields: ['*'],
fields: ['*', 'person.*'],
sort: 'first_name',
},
});
@@ -273,6 +291,7 @@ async function openEditEmployee(item: any) {
first_name: item.first_name || '',
last_name: item.last_name || '',
email: item.email || '',
person: item.person?.id || item.person || null,
temporary_password: item.temporary_password || ''
};
isEditingEmployee.value = true;
@@ -288,7 +307,8 @@ async function saveEmployee() {
await api.patch(`/items/client_users/${employeeForm.value.id}`, {
first_name: employeeForm.value.first_name,
last_name: employeeForm.value.last_name,
email: employeeForm.value.email
email: employeeForm.value.email,
person: employeeForm.value.person
});
notice.value = { type: 'success', message: 'Mitarbeiter aktualisiert!' };
} else {
@@ -296,7 +316,8 @@ async function saveEmployee() {
first_name: employeeForm.value.first_name,
last_name: employeeForm.value.last_name,
email: employeeForm.value.email,
company: selectedCompany.value.id
company: selectedCompany.value.id,
person: employeeForm.value.person
});
notice.value = { type: 'success', message: 'Mitarbeiter angelegt!' };
}
@@ -343,7 +364,7 @@ function formatDate(dateStr: string) {
}
onMounted(() => {
fetchCompanies();
fetchData();
});
</script>

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/eslint-config",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -2,7 +2,7 @@
"name": "feedback-commander",
"description": "Custom High-Fidelity Feedback Management Extension for Directus",
"icon": "view_kanban",
"version": "1.7.12",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",

View File

@@ -78,7 +78,7 @@
<div class="card-text">{{ item.text }}</div>
<footer class="card-footer">
<div class="meta-tags">
<v-chip x-small outline>{{ item.project }}</v-chip>
<v-chip x-small outline>{{ item.company?.name || item.project }}</v-chip>
<v-icon :name="item.type === 'bug' ? 'bug_report' : 'lightbulb'" :color="item.type === 'bug' ? '#E91E63' : '#FFC107'" small />
</div>
<v-icon v-if="selectedItem?.id === item.id" name="chevron_right" small />
@@ -142,7 +142,8 @@
<TransitionGroup name="thread-list">
<div v-for="reply in comments" :key="reply.id" class="reply-bubble">
<header class="reply-header">
<span class="reply-user">{{ reply.user_name }}</span>
<span class="reply-user">{{ reply.user_name || 'System' }}</span>
<span v-if="reply.person" class="reply-person">({{ reply.person.first_name }} {{ reply.person.last_name }})</span>
<span class="reply-date">{{ formatDate(reply.date_created || reply.id) }}</span>
</header>
<div class="reply-text">{{ reply.text }}</div>
@@ -168,8 +169,12 @@
<v-card-title>Context</v-card-title>
<v-card-text class="meta-list">
<div class="meta-item">
<label><v-icon name="public" x-small /> Website</label>
<strong>{{ selectedItem.project }}</strong>
<label><v-icon name="business" x-small /> Organisation / Firma</label>
<strong>{{ selectedItem.company?.name || selectedItem.project }}</strong>
</div>
<div v-if="selectedItem.person" class="meta-item">
<label><v-icon name="person" x-small /> Zentrale Person</label>
<strong>{{ selectedItem.person.first_name }} {{ selectedItem.person.last_name }}</strong>
</div>
<div class="meta-item">
<label><v-icon name="link" x-small /> Source Path</label>
@@ -238,13 +243,14 @@ const statusOptions = [
];
const projects = computed(() => {
const projSet = new Set(items.value.map(i => i.project).filter(Boolean));
const projSet = new Set(items.value.map(i => i.company?.name || i.project).filter(Boolean));
return Array.from(projSet).sort();
});
const filteredItems = computed(() => {
return items.value.filter(item => {
const matchProject = currentProject.value === 'all' || item.project === currentProject.value;
const projectName = item.company?.name || item.project;
const matchProject = currentProject.value === 'all' || projectName === currentProject.value;
const status = item.status || 'open';
const matchStatus = currentStatusFilter.value === 'all' || status === currentStatusFilter.value;
return matchProject && matchStatus;
@@ -258,7 +264,8 @@ async function fetchData() {
const response = await api.get('/items/visual_feedback', {
params: {
sort: '-date_created,-id',
limit: 300
limit: 300,
fields: ['*', 'company.*', 'person.*']
}
});
items.value = response.data.data;
@@ -278,7 +285,8 @@ async function selectItem(item) {
const response = await api.get('/items/visual_feedback_comments', {
params: {
filter: { feedback_id: { _eq: item.id } },
sort: '-date_created,-id'
sort: '-date_created,-id',
fields: ['*', 'person.*']
}
});
comments.value = response.data.data;

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/gatekeeper",
"version": "1.7.12",
"version": "1.8.0",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/husky-config",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/infra",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/mail",
"version": "1.7.12",
"version": "1.8.0",
"private": false,
"publishConfig": {
"access": "public",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-config",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-feedback",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"
@@ -27,6 +27,7 @@
},
"dependencies": {
"@directus/sdk": "^21.0.0",
"@medv/finder": "^4.0.2",
"clsx": "^2.1.1",
"framer-motion": "^11.5.4",
"html2canvas": "^1.4.1",

View File

@@ -6,6 +6,7 @@ import { MessageSquare, X, Check, Plus, List, Send, User } from "lucide-react";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import html2canvas from "html2canvas";
import { finder } from "@medv/finder";
function cn(...inputs: any[]) {
return twMerge(clsx(inputs));
@@ -107,15 +108,16 @@ export function FeedbackOverlay() {
}, []);
const getSelector = (el: HTMLElement): string => {
if (el.id) return `#${el.id}`;
const path = [];
let curr: HTMLElement | null = el;
while (curr && curr.parentElement) {
const index = Array.from(curr.parentElement.children).indexOf(curr) + 1;
path.unshift(`${curr.tagName.toLowerCase()}:nth-child(${index})`);
curr = curr.parentElement;
}
return path.join(" > ");
return finder(el, {
root: document.body,
className: (name) =>
!name.startsWith('record-mode-') &&
!name.startsWith('feedback-') &&
!name.includes('[') &&
!name.includes('/') &&
!name.match(/^[a-z]-[0-9]/),
idName: (name) => !name.startsWith('__next') && !name.includes(':'),
});
};
useEffect(() => {

View File

@@ -6,7 +6,4 @@ export default defineConfig({
dts: true,
clean: true,
sourcemap: true,
banner: {
js: "'use client';",
},
});

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-observability",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-utils",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/observability",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,57 @@
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 entryPoints = [
resolve(__dirname, 'src/index.ts'),
resolve(__dirname, 'src/server.ts')
];
try {
mkdirSync(resolve(__dirname, 'dist'), { recursive: true });
} catch (e) { }
console.log(`Building entry points...`);
build({
entryPoints: entryPoints,
bundle: true,
platform: 'node',
target: 'node18',
outdir: resolve(__dirname, 'dist'),
format: 'esm',
jsx: 'automatic',
loader: {
'.tsx': 'tsx',
'.ts': 'ts',
'.js': 'js',
},
external: ["@react-pdf/renderer", "react", "react-dom", "jsdom", "jsdom/*", "jquery", "jquery/*", "canvas", "fs", "path", "os", "http", "https", "zlib", "stream", "util", "url", "net", "tls", "crypto"],
plugins: [{
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' }));
}
}, {
name: 'mock-jsdom',
setup(build) {
build.onResolve({ filter: /^jsdom/ }, args => ({ path: args.path, namespace: 'mock-jsdom' }));
build.onLoad({ filter: /.*/, namespace: 'mock-jsdom' }, () => ({ contents: 'export default {};', loader: 'js' }));
}
}]
}).then(() => {
console.log("Build succeeded!");
}).catch((e) => {
if (e.errors) {
console.error("Build failed with errors:");
e.errors.forEach(err => console.error(` ${err.text} at ${err.location?.file}:${err.location?.line}`));
} else {
console.error("Build failed:", e);
}
process.exit(1);
});

View File

@@ -0,0 +1,38 @@
{
"name": "@mintel/pdf",
"version": "1.8.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js",
"default": "./dist/server.js"
}
},
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs --watch"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"esbuild": "^0.25.0",
"typescript": "^5.6.3"
},
"dependencies": {
"@crawlee/cheerio": "^3.16.0",
"@mintel/mail": "workspace:*",
"@react-pdf/renderer": "^4.3.0",
"axios": "^1.7.9",
"cheerio": "^1.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4"
}
}

View File

@@ -0,0 +1,241 @@
"use client";
import * as React from "react";
import {
Page as PDFPage,
Text as PDFText,
View as PDFView,
StyleSheet as PDFStyleSheet,
} from "@react-pdf/renderer";
import {
pdfStyles,
Header,
Footer,
FoldingMarks,
DocumentTitle,
} from "./pdf/SharedUI.js";
import { SimpleLayout } from "./pdf/SimpleLayout.js";
const localStyles = PDFStyleSheet.create({
sectionContainer: {
marginTop: 0,
},
agbSection: {
marginBottom: 20,
},
labelRow: {
flexDirection: "row",
alignItems: "baseline",
marginBottom: 6,
},
monoNumber: {
fontSize: 7,
fontWeight: "bold",
color: "#94a3b8",
letterSpacing: 2,
width: 25,
},
sectionTitle: {
fontSize: 9,
fontWeight: "bold",
color: "#000000",
textTransform: "uppercase",
letterSpacing: 0.5,
},
officialText: {
fontSize: 8,
lineHeight: 1.5,
color: "#334155",
textAlign: "justify",
paddingLeft: 25,
},
});
const AGBSection = ({
index,
title,
children,
}: {
index: string;
title: string;
children: React.ReactNode;
}) => (
<PDFView style={localStyles.agbSection} wrap={false}>
<PDFView style={localStyles.labelRow}>
<PDFText style={localStyles.monoNumber}>{index}</PDFText>
<PDFText style={localStyles.sectionTitle}>{title}</PDFText>
</PDFView>
<PDFText style={localStyles.officialText}>{children}</PDFText>
</PDFView>
);
interface AgbsPDFProps {
headerIcon?: string;
footerLogo?: string;
mode?: "estimation" | "full";
}
export const AgbsPDF = ({
headerIcon,
footerLogo,
mode = "full",
}: AgbsPDFProps) => {
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065",
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65",
};
const content = (
<>
<DocumentTitle
title="Allgemeine Geschäftsbedingungen"
subLines={[`Stand: ${date}`]}
/>
<PDFView style={localStyles.sectionContainer}>
<AGBSection index="01" title="Geltungsbereich">
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge
zwischen Marc Mintel (nachfolgend Auftragnehmer) und dem jeweiligen
Kunden (nachfolgend Auftraggeber). Abweichende oder ergänzende
Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch
wenn ihrer Geltung nicht ausdrücklich widersprochen wird.
</AGBSection>
<AGBSection index="02" title="Vertragsgegenstand">
Der Auftragnehmer erbringt Dienstleistungen im Bereich:
Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen,
Schnittstellen und Automatisierungen sowie Hosting, Betrieb und
Wartung, sofern ausdrücklich vereinbard. Der Auftragnehmer schuldet
ausschließlich die vereinbarte technische Leistung, nicht jedoch einen
wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten,
Suchmaschinen-Rankings oder rechtliche Ergebnisse.
</AGBSection>
<AGBSection index="03" title="Mitwirkungspflichten des Auftraggebers">
Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung
erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen
rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen
insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback,
Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum,
DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen
aller Termine ohne Schadensersatzanspruch.
</AGBSection>
<AGBSection index="04" title="Ausführungs- und Bearbeitungszeiten">
Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine
garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie
ausdrücklich schriftlich als verbindlich vereinbart wurden.
</AGBSection>
<AGBSection index="05" title="Abnahme">
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv
nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine
wesentlichen Mängel angezeigt werden. Optische Abweichungen,
Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel
dar.
</AGBSection>
<AGBSection index="06" title="Haftung">
Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder
grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für
entgangenen Gewinn, Umsatzausfälle, Datenverlust,
Betriebsunterbrechungen, mittelbare oder Folgeschäden ist
ausgeschlossen, soweit gesetzlich zulässig.
</AGBSection>
<AGBSection index="07" title="Verfügbarkeit & Betrieb">
Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine
permanente Verfügbarkeit. Wartungsarbeiten, Updates,
Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen
Einschränkungen führen und begründen keine Haftungsansprüche.
</AGBSection>
<AGBSection index="07a" title="Betriebs- und Pflegeleistung">
Die Betriebs- und Pflegeleistung umfasst ausschließlich die
Sicherstellung des technischen Betriebs, Wartung, Updates,
Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender
Datensätze ohne Strukturänderung. Nicht Bestandteil sind die
Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle
Tätigkeiten, strategische Planung oder der Aufbau neuer
Features/Datenmodelle. Leistungen darüber hinaus gelten als
Neuentwicklung.
</AGBSection>
<AGBSection index="08" title="Drittanbieter & externe Systeme">
Der Auftragnehmer übernimmt keine Verantwortung für Leistungen,
Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder
Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der
jeweils aktuellen externen Schnittstellen gewährleistet werden.
</AGBSection>
<AGBSection index="09" title="Inhalte & Rechtliches">
Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche
Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten.
Der Auftragnehmer übernimmt keine rechtliche Prüfung.
</AGBSection>
<AGBSection index="10" title="Vergütung & Zahlungsverzug">
Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen
fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt,
Leistungen auszusetzen, Systeme offline zu nehmen oder laufende
Arbeiten zu stoppen.
</AGBSection>
<AGBSection index="11" title="Kündigung laufender Leistungen">
Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist
von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes
vereinbart ist.
</AGBSection>
<AGBSection index="12" title="Schlussbestimmungen">
Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist
der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein,
bleibt die Wirksamkeit der übrigen Regelungen unberührt.
</AGBSection>
</PDFView>
</>
);
if (mode === "full") {
return (
<SimpleLayout
companyData={companyData}
bankData={bankData}
footerLogo={footerLogo}
icon={headerIcon}
pageNumber="10"
showPageNumber={false}
>
{content}
</SimpleLayout>
);
}
return (
<PDFPage size="A4" style={pdfStyles.page}>
<FoldingMarks />
<Header icon={headerIcon} showAddress={false} />
{content}
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
showPageNumber={false}
/>
</PDFPage>
);
};

View File

@@ -0,0 +1,79 @@
"use client";
import * as React from "react";
import { Document as PDFDocument } from "@react-pdf/renderer";
import { EstimationPDF } from "./EstimationPDF.js";
import { AgbsPDF } from "./AgbsPDF.js";
import { SimpleLayout } from "./pdf/SimpleLayout.js";
import { ClosingModule } from "./pdf/modules/CommonModules.js";
interface CombinedProps {
estimationProps: any;
showAgbs?: boolean;
techDetails?: any[];
principles?: any[];
maintenanceDetails?: any[];
standardsDetails?: any[];
}
export const CombinedQuotePDF = ({
estimationProps,
showAgbs = true,
techDetails,
principles,
maintenanceDetails,
standardsDetails,
mode = "full",
}: CombinedProps & { mode?: "estimation" | "full" }) => {
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065",
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65",
};
const layoutProps = {
date,
icon: estimationProps.headerIcon,
footerLogo: estimationProps.footerLogo,
companyData,
bankData,
};
return (
<PDFDocument
title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}
>
<EstimationPDF
{...estimationProps}
mode={mode}
techDetails={techDetails}
principles={principles}
maintenanceDetails={maintenanceDetails}
standardsDetails={standardsDetails}
/>
{showAgbs && (
<AgbsPDF
mode={mode}
headerIcon={estimationProps.headerIcon}
footerLogo={estimationProps.footerLogo}
/>
)}
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>
);
};

View File

@@ -18,6 +18,8 @@ import { calculatePositions } from "../logic/pricing/calculator.js";
interface PDFProps {
state: any;
totalPrice: number;
monthlyPrice?: number;
totalPagesCount?: number;
pricing: any;
headerIcon?: string;
footerLogo?: string;

View File

@@ -0,0 +1,55 @@
'use client';
import * as React from 'react';
import { Page as PDFPage } from '@react-pdf/renderer';
import { FoldingMarks, Header, Footer, pdfStyles } from './SharedUI';
interface DINLayoutProps {
children: React.ReactNode;
sender?: string;
recipient?: {
title: string;
subtitle?: string;
address?: string;
phone?: string;
email?: string;
taxId?: string;
};
icon?: string;
footerLogo?: string;
companyData: any;
bankData: any;
showAddress?: boolean;
showFooterDetails?: boolean;
}
export const DINLayout = ({
children,
sender,
recipient,
icon,
footerLogo,
companyData,
bankData,
showAddress = true,
showFooterDetails = true
}: DINLayoutProps) => {
return (
<PDFPage size="A4" style={pdfStyles.page}>
<FoldingMarks />
<Header
sender={sender}
recipient={recipient}
icon={icon}
showAddress={showAddress}
/>
{children}
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={showFooterDetails}
/>
</PDFPage>
);
};

View File

@@ -0,0 +1,728 @@
"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
};
// Mintel Industrial Glyphs (strictly 1px stroke, 12x12px grid)
export const IndustrialGlyph = ({
type,
color = COLORS.TEXT_LIGHT,
size = 12,
}: {
type: string;
color?: string;
size?: number;
}) => {
const stroke = 1;
const scale = size / 12;
switch (type) {
case "base": // Skeletal cube base
return (
<PDFView style={{ width: size, height: size, position: "relative" }}>
<PDFView
style={{
position: "absolute",
top: 2 * scale,
left: 2 * scale,
width: 8 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
position: "absolute",
top: 0,
left: 0,
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
backgroundColor: "white",
}}
/>
</PDFView>
);
case "pages": // Layered rectangles
return (
<PDFView style={{ width: size, height: size, position: "relative" }}>
<PDFView
style={{
position: "absolute",
top: 3 * scale,
left: 3 * scale,
width: 6 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
position: "absolute",
top: 0,
left: 0,
width: 6 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
backgroundColor: "white",
}}
/>
</PDFView>
);
case "modules": // Four small squares grid
return (
<PDFView
style={{
width: size,
height: size,
flexDirection: "row",
flexWrap: "wrap",
gap: 2 * scale,
}}
>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
</PDFView>
);
case "logic": // Diamond with center point
return (
<PDFView
style={{
width: size,
height: size,
alignItems: "center",
justifyContent: "center",
}}
>
<PDFView
style={{
width: 8 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
transform: "rotate(45deg)",
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 2 * scale,
backgroundColor: color,
position: "absolute",
}}
/>
</PDFView>
);
case "interface": // Three horizontal lines of varying length
return (
<PDFView
style={{
width: size,
height: size,
justifyContent: "center",
gap: 2 * scale,
}}
>
<PDFView
style={{
width: 10 * scale,
height: stroke,
backgroundColor: color,
}}
/>
<PDFView
style={{ width: 6 * scale, height: stroke, backgroundColor: color }}
/>
<PDFView
style={{
width: 10 * scale,
height: stroke,
backgroundColor: color,
}}
/>
</PDFView>
);
case "management": // Framed grid
return (
<PDFView
style={{
width: size,
height: size,
borderWidth: stroke,
borderColor: color,
padding: 1 * scale,
}}
>
<PDFView
style={{
width: "100%",
height: 2 * scale,
backgroundColor: color,
marginBottom: 1 * scale,
}}
/>
<PDFView
style={{
width: "100%",
height: 2 * scale,
backgroundColor: color,
marginBottom: 1 * scale,
}}
/>
<PDFView
style={{ width: "100%", height: 2 * scale, backgroundColor: color }}
/>
</PDFView>
);
case "reveal": // Ascending bars
return (
<PDFView
style={{
width: size,
height: size,
flexDirection: "row",
alignItems: "flex-end",
gap: 1 * scale,
}}
>
<PDFView
style={{
width: 2 * scale,
height: 4 * scale,
backgroundColor: color,
opacity: 0.4,
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 7 * scale,
backgroundColor: color,
opacity: 0.7,
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 10 * scale,
backgroundColor: color,
}}
/>
</PDFView>
);
case "maintenance": // Circle with vertical notch
return (
<PDFView
style={{
width: size,
height: size,
borderRadius: 6 * scale,
borderWidth: stroke,
borderColor: color,
alignItems: "center",
}}
>
<PDFView
style={{
width: stroke,
height: 4 * scale,
backgroundColor: color,
marginTop: 1 * scale,
}}
/>
</PDFView>
);
default:
return (
<PDFView
style={{
width: size,
height: size,
borderWidth: stroke,
borderColor: COLORS.BLUEPRINT,
borderStyle: "dashed",
}}
/>
);
}
};
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 FoldingMarks = () => (
<>
<PDFView style={[pdfStyles.foldingMark, { top: 297.6 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 420.9, width: 15 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 595.3 }]} fixed />
</>
);
export const Footer = ({
logo,
companyData,
bankData,
showDetails = true,
showPageNumber = true,
}: {
logo?: string;
companyData: any;
bankData?: 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>
</>
)}
{!showDetails && (
<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>
);
export const IndustrialCard = ({
title,
children,
style = {},
}: {
title: string;
children: React.ReactNode;
style?: any;
}) => (
<PDFView style={[pdfStyles.blueprintBox, { marginBottom: 12 }, style]}>
<PDFText
style={{
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
color: COLORS.TEXT_LIGHT,
letterSpacing: 1,
marginBottom: 6,
textTransform: "uppercase",
}}
>
{title}
</PDFText>
{children}
</PDFView>
);

View File

@@ -33,6 +33,7 @@ interface SimpleLayoutProps {
icon?: string;
footerLogo?: string;
companyData: any;
bankData?: any;
showPageNumber?: boolean;
}
@@ -42,6 +43,7 @@ export const SimpleLayout = ({
icon,
footerLogo,
companyData,
bankData,
showPageNumber = true
}: SimpleLayoutProps) => {
return (
@@ -56,6 +58,7 @@ export const SimpleLayout = ({
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
showPageNumber={showPageNumber}
/>

View File

@@ -0,0 +1,218 @@
"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import {
DocumentTitle,
IndustrialListItem,
IndustrialCard,
Divider,
COLORS,
FONT_SIZES,
} from "../SharedUI";
const styles = StyleSheet.create({
industrialTextLead: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.4,
marginBottom: 16,
},
industrialText: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
marginBottom: 12,
},
industrialGrid2: { flexDirection: "row" },
industrialCol: { width: "46%" },
});
export const AboutModule = () => (
<>
<DocumentTitle
title="Expertise & Profil"
subLines={["Entwicklung & Technischer Partner für den Mittelstand"]}
isHero={true}
/>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={{ marginTop: 24 }}>
<PDFText style={styles.industrialTextLead}>
Begleitung mittelständischer Unternehmen und Agenturen bei der
Realisierung anspruchsvoller Web-Projekte. Als Senior Software Developer
mit over 15 Jahren Erfahrung wird das gesamte technische Spektrum
abgedeckt von der Architektur bis zum fertigen Produkt.
</PDFText>
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
]}
>
Erfahrung & Substanz
</PDFText>
<PDFText style={styles.industrialText}>
Der Werdegang umfasst alle Ebenen der Webentwicklung: von der
Teamleitung in Kreativagenturen bis zur Softwareentwicklung für
internationale Konzerne.
</PDFText>
<PDFText style={styles.industrialText}>
Die Kenntnis komplexer Enterprise-Systeme wird mit der Agilität
kombiniert, die im Mittelstand gefordert ist. Dieses Wissen
ermöglicht den Bau von Lösungen, die technologisch auf Augenhöhe mit
Konzern-Standards sind, jedoch ohne unnötigen bürokratischen
Overhead auskommen.
</PDFText>
</PDFView>
<PDFView style={styles.industrialCol}>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
]}
>
Fokus Einzelentwicklung
</PDFText>
<PDFText style={styles.industrialText}>
Die Umsetzung erfolgt bewusst als spezialisierter Einzelentwickler.
Dies garantiert maximale Geschwindigkeit, direkte Kommunikationswege
und volle technologische Verantwortung.
</PDFText>
<PDFText style={styles.industrialText}>
Als direkter technischer Sparringspartner bleibt die Codebasis von
der ersten bis zur letzten Zeile transparent und wartbar. Diese
Unmittelbarkeit stellt sicher, dass Ergebnisse sowohl technisch
sauber als auch wirtschaftlich sinnvoll realisiert werden.
</PDFText>
</PDFView>
</PDFView>
<PDFView
style={{
marginTop: 32,
paddingVertical: 16,
borderTopWidth: 1,
borderTopColor: COLORS.GRID,
}}
>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 4 },
]}
>
Infrastruktur & Souveränität
</PDFText>
<PDFText style={styles.industrialText}>
Es wird keine instabile Prototyp-Software geliefert, sondern
produktionsreife Systeme, die technisch skalierbar bleiben. Die
Codebasis folgt modernen Standards bei wachsenden Ansprüchen oder
dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos
übernommen und weitergeführt werden.
</PDFText>
</PDFView>
</PDFView>
</>
);
export const CrossSellModule = ({ state }: any) => {
const isWebsite = state.projectType === "website";
const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
const subtitle = isWebsite
? "Automatisierung und Prozessoptimierung"
: "Technische Infrastruktur ohne Kompromisse";
return (
<>
<DocumentTitle title={title} subLines={[subtitle]} isHero={true} />
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 16 }]}>
{isWebsite ? (
<>
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFText style={styles.industrialTextLead}>
Über die klassische Webpräsenz hinaus werden maßgeschneiderte
Lösungen zur Automatisierung von Routine-Prozessen angeboten.
Dies ermöglicht eine signifikante Effizienzsteigerung im
Tagesgeschäft.
</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: "bold" }]}>
Keine Abos. Keine komplexen neuen Systeme. Gezielte
Zeitersparnis.
</PDFText>
<PDFView
style={{
marginTop: 24,
padding: 16,
backgroundColor: "#f8fafc",
borderLeftWidth: 2,
borderLeftColor: COLORS.GRID,
}}
>
<PDFText
style={[
styles.industrialText,
{
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 4,
},
]}
>
Individuelle Analyse
</PDFText>
<PDFText style={styles.industrialText}>
Spezifische Prozesse werden auf technisches
Automatisierungspotenzial untersucht. Das Ergebnis liefert
Klarheit über die wirtschaftliche Sinnhaftigkeit einer
Umsetzung.
</PDFText>
</PDFView>
</PDFView>
<PDFView style={styles.industrialCol}>
<IndustrialCard title="DOKUMENT-AUTOMATION">
<PDFText style={styles.industrialText}>
Erstellung von PDF-Angeboten, Berichten oder Protokollen in
Sekunden statt Stunden.
</PDFText>
</IndustrialCard>
<IndustrialCard title="EXCEL-LOGIK">
<PDFText style={styles.industrialText}>
Intelligente Tabellen und automatisierte Auswertungen
bestehender Datensätze.
</PDFText>
</IndustrialCard>
<IndustrialCard title="KI-ASSISTENZ">
<PDFText style={styles.industrialText}>
Effiziente Verarbeitung von analogen Dokumenten oder
handschriftlichen Notizen mittels KI.
</PDFText>
</IndustrialCard>
</PDFView>
</>
) : (
<PDFView style={{ width: "100%" }}>
<PDFText style={styles.industrialTextLead}>
Bereitstellung einer stabilen technischen Basis ohne
Abhängigkeiten von Baukasten-Systemen oder Agenturen.
</PDFText>
<PDFText style={styles.industrialText}>
Entwicklung performanter Frontends und skalierbarer Backends. Die
Auslieferung erfolgt als kontrollierbarer und nachhaltiger
Quellcode.
</PDFText>
</PDFView>
)}
</PDFView>
</>
);
};

View File

@@ -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";
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>
)}
</>
);

View File

@@ -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";
const styles = StyleSheet.create({
section: { marginBottom: 32 },
intro: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
marginBottom: 24,
textAlign: "justify",
},
sitemapTree: { marginTop: 8 },
rootNode: {
padding: 12,
backgroundColor: COLORS.GRID,
marginBottom: 20,
borderLeftWidth: 2,
borderLeftColor: COLORS.CHARCOAL,
},
rootTitle: {
fontSize: FONT_SIZES.HEADING,
fontWeight: "bold",
color: COLORS.CHARCOAL,
letterSpacing: 0.5,
},
categorySection: { marginBottom: 20 },
categoryHeader: {
flexDirection: "row",
alignItems: "center",
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: COLORS.BLUEPRINT,
marginBottom: 10,
},
categoryIcon: {
width: 8,
height: 8,
backgroundColor: COLORS.GRID,
borderInlineWidth: 1,
borderColor: COLORS.DIVIDER,
marginRight: 10,
},
categoryTitle: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.CHARCOAL,
textTransform: "uppercase",
letterSpacing: 1,
},
pagesGrid: { flexDirection: "row", flexWrap: "wrap" },
pageCard: {
width: "48%",
marginRight: "2%",
marginBottom: 12,
padding: 10,
borderWidth: 1,
borderColor: COLORS.GRID,
backgroundColor: "#fafafa",
},
pageTitle: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.TEXT_MAIN,
marginBottom: 4,
},
pageDesc: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_DIM,
lineHeight: 1.3,
},
});
export const SitemapModule = ({ state }: any) => (
<>
<DocumentTitle title="Informationsarchitektur" isHero={true} />
<PDFView style={styles.section}>
<PDFText style={styles.intro}>
Die folgende Struktur definiert die logische Hierarchie und
Benutzerführung. Sie dient als Bauplan für die technische Umsetzung und
stellt sicher, dass alle relevanten Geschäftsbereiche intuitiv
auffindbar sind.
</PDFText>
<PDFView style={styles.sitemapTree}>
<PDFView style={styles.rootNode}>
<PDFText style={styles.rootTitle}>Seitenstruktur</PDFText>
</PDFView>
{state.sitemap?.map((cat: any, i: number) => (
<PDFView key={i} style={styles.categorySection} wrap={false}>
<PDFView style={styles.categoryHeader}>
<PDFView style={styles.categoryIcon} />
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
</PDFView>
<PDFView style={styles.pagesGrid}>
{cat.pages.map((p: any, j: number) => (
<PDFView
key={j}
style={[
styles.pageCard,
j % 2 === 1 ? { marginRight: 0 } : {},
]}
>
<PDFText style={styles.pageTitle}>{p.title}</PDFText>
{p.desc && (
<PDFText style={styles.pageDesc}>{p.desc}</PDFText>
)}
</PDFView>
))}
</PDFView>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);

View File

@@ -0,0 +1,15 @@
export * from "./logic/pricing/types.js";
export * from "./logic/pricing/constants.js";
export * from "./logic/pricing/calculator.js";
export * from "./components/EstimationPDF.js";
export * from "./components/pdf/SimpleLayout.js";
export * from "./components/pdf/SharedUI.js";
export * from "./components/pdf/modules/FrontPageModule.js";
export * from "./components/pdf/modules/BriefingModule.js";
export * from "./components/pdf/modules/SitemapModule.js";
export * from "./components/pdf/modules/EstimationModule.js";
export * from "./components/pdf/modules/CommonModules.js";
export * from "./components/pdf/modules/BrandingModules.js";
export * from "./components/pdf/modules/TransparenzModule.js";
export * from "./components/AgbsPDF.js";
export * from "./components/CombinedQuotePDF.js";

View File

@@ -0,0 +1,3 @@
export * from "./index.js";
export * from "./services/AcquisitionService.js";
export * from "./services/PdfEngine.js";

View File

@@ -1,4 +1,4 @@
import { CheerioCrawler } from "crawlee";
import { CheerioCrawler } from "@crawlee/cheerio";
import axios from "axios";
import { FileCacheAdapter } from "../utils/cache/FileCacheAdapter.js";
import { initialState } from "../logic/pricing/constants.js";

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"declaration": true,
"declarationDir": "dist",
"emitDeclarationOnly": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"outDir": "dist"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"build.mjs"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"name": "people-manager",
"description": "Custom High-Fidelity People Management for Directus",
"icon": "person",
"version": "1.7.12",
"version": "1.8.0",
"type": "module",
"keywords": [
"directus",

View File

@@ -64,8 +64,8 @@
<div class="details-grid">
<div class="detail-item">
<span class="label">Organisation</span>
<p class="value">{{ selectedPerson.company || '---' }}</p>
<span class="label">Organisation / Firma</span>
<p class="value">{{ getCompanyName(selectedPerson) }}</p>
</div>
<div class="detail-item">
<span class="label">Telefon</span>
@@ -98,8 +98,16 @@
<v-input v-model="form.email" placeholder="email@beispiel.de" type="email" />
</div>
<div class="field">
<span class="label">Organisation / Firma</span>
<v-input v-model="form.company" placeholder="z.B. Mintel" />
<span class="label">Zentrale Firma</span>
<v-select
v-model="form.company"
:items="companyOptions"
placeholder="Bestehende Firma auswählen..."
/>
</div>
<div class="field">
<span class="label">Firma (Legacy / Neu)</span>
<v-input v-model="form.company_name" placeholder="z.B. Mintel" />
</div>
<div class="field">
<span class="label">Telefon</span>
@@ -119,11 +127,12 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const api = useApi();
const people = ref([]);
const companies = ref([]);
const selectedPerson = ref(null);
const feedback = ref(null);
const saving = ref(false);
@@ -135,18 +144,43 @@ const form = ref({
first_name: '',
last_name: '',
email: '',
company: '',
company: null,
company_name: '',
phone: ''
});
async function fetchPeople() {
const companyOptions = computed(() =>
companies.value.map(c => ({
text: c.name,
value: c.id
}))
);
function getCompanyName(person: any) {
if (!person) return '---';
if (person.company) {
return typeof person.company === 'object' ? person.company.name : (companies.value.find(c => c.id === person.company)?.name || person.company_name);
}
return person.company_name || '---';
}
async function fetchData() {
try {
const response = await api.get('/items/people', {
params: { sort: 'last_name' }
});
people.ref = response.data.data;
const [peopleResp, companiesResp] = await Promise.all([
api.get('/items/people', {
params: {
sort: 'last_name',
fields: '*.*'
}
}),
api.get('/items/companies', {
params: { sort: 'name' }
})
]);
people.value = peopleResp.data.data;
companies.value = companiesResp.data.data;
} catch (error) {
console.error('Failed to fetch people:', error);
console.error('Failed to fetch data:', error);
}
}
@@ -156,13 +190,39 @@ function selectPerson(person) {
function openCreateDrawer() {
isEditing.value = false;
form.value = { id: null, first_name: '', last_name: '', email: '', company: '', phone: '' };
form.value = {
id: null,
first_name: '',
last_name: '',
email: '',
company: null,
company_name: '',
phone: ''
};
drawerActive.value = true;
}
function openEditDrawer() {
isEditing.value = true;
form.value = { ...selectedPerson.value };
const person = selectedPerson.value;
let companyId = null;
let companyName = person.company_name || '';
if (person.company) {
if (typeof person.company === 'object') {
companyId = person.company.id;
} else if (person.company.length === 36) { // Assume UUID
companyId = person.company;
} else {
companyName = person.company;
}
}
form.value = {
...person,
company: companyId,
company_name: companyName
};
drawerActive.value = true;
}
@@ -182,9 +242,9 @@ async function savePerson() {
feedback.value = { type: 'success', message: 'Person angelegt!' };
}
drawerActive.value = false;
await fetchPeople();
await fetchData();
if (isEditing.value) {
selectedPerson.value = form.value;
selectedPerson.value = people.value.find(p => p.id === form.value.id);
}
} catch (error) {
feedback.value = { type: 'danger', message: error.message };
@@ -200,13 +260,13 @@ async function deletePerson() {
await api.delete(`/items/people/${selectedPerson.value.id}`);
feedback.value = { type: 'success', message: 'Person gelöscht.' };
selectedPerson.value = null;
await fetchPeople();
await fetchData();
} catch (error) {
feedback.value = { type: 'danger', message: error.message };
}
}
onMounted(fetchPeople);
onMounted(fetchData);
</script>
<style scoped>

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/tsconfig",
"version": "1.7.12",
"version": "1.8.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

371
pnpm-lock.yaml generated
View File

@@ -158,50 +158,16 @@ importers:
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)
'@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
'@react-pdf/renderer':
specifier: ^4.3.0
version: 4.3.2(react@19.2.4)
axios:
specifier: ^1.7.9
version: 1.13.5
cheerio:
specifier: ^1.0.0
version: 1.2.0
crawlee:
specifier: ^3.12.2
version: 3.16.0(@types/node@22.19.10)
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.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/mail':
specifier: workspace:*
version: link:../mail
'@mintel/pdf':
specifier: workspace:*
version: link:../pdf-library
esbuild:
specifier: ^0.25.0
version: 0.25.12
@@ -249,8 +215,42 @@ importers:
specifier: ^5.0.0
version: 5.9.3
packages/cloner-library:
dependencies:
axios:
specifier: ^1.6.0
version: 1.13.5
cheerio:
specifier: ^1.0.0-rc.12
version: 1.2.0
crawlee:
specifier: ^3.7.0
version: 3.16.0(@types/node@22.19.10)(playwright@1.58.2)
playwright:
specifier: ^1.40.0
version: 1.58.2
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.10
esbuild:
specifier: ^0.25.0
version: 0.25.12
typescript:
specifier: ^5.6.3
version: 5.9.3
packages/cms-infra: {}
packages/company-manager:
devDependencies:
'@directus/extensions-sdk':
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)
vue:
specifier: ^3.4.0
version: 3.5.28(typescript@5.9.3)
packages/customer-manager:
devDependencies:
'@directus/extensions-sdk':
@@ -260,6 +260,21 @@ importers:
specifier: ^3.4.0
version: 3.5.28(typescript@5.9.3)
packages/directus-extension-toolkit:
devDependencies:
'@directus/extensions-sdk':
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)
typescript:
specifier: ^5.0.0
version: 5.9.3
vite:
specifier: ^5.0.0
version: 5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
vue:
specifier: ^3.4.0
version: 3.5.28(typescript@5.9.3)
packages/eslint-config:
dependencies:
'@eslint/eslintrc':
@@ -410,7 +425,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.0.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
packages/next-config:
dependencies:
@@ -429,6 +444,9 @@ importers:
'@directus/sdk':
specifier: ^21.0.0
version: 21.1.0
'@medv/finder':
specifier: ^4.0.2
version: 4.0.2
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -569,7 +587,41 @@ importers:
version: 5.9.3
vitest:
specifier: ^2.0.0
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
packages/pdf-library:
dependencies:
'@crawlee/cheerio':
specifier: ^3.16.0
version: 3.16.0
'@mintel/mail':
specifier: workspace:*
version: link:../mail
'@react-pdf/renderer':
specifier: ^4.3.0
version: 4.3.2(react@19.2.4)
axios:
specifier: ^1.7.9
version: 1.13.5
cheerio:
specifier: ^1.0.0
version: 1.2.0
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.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)
esbuild:
specifier: ^0.25.0
version: 0.25.12
typescript:
specifier: ^5.6.3
version: 5.9.3
packages/people-manager:
devDependencies:
@@ -582,6 +634,15 @@ importers:
packages/tsconfig: {}
packages/unified-dashboard:
devDependencies:
'@directus/extensions-sdk':
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)
vue:
specifier: ^3.4.0
version: 3.5.28(typescript@5.9.3)
packages:
'@acemir/cssom@0.9.31':
@@ -2059,6 +2120,9 @@ packages:
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
'@medv/finder@4.0.2':
resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -4928,6 +4992,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -6364,6 +6433,16 @@ packages:
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
playwright-core@1.58.2:
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
engines: {node: '>=18'}
hasBin: true
playwright@1.58.2:
resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
engines: {node: '>=18'}
hasBin: true
po-parser@2.1.1:
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
@@ -8440,14 +8519,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@crawlee/browser-pool@3.16.0':
'@crawlee/browser-pool@3.16.0(playwright@1.58.2)':
dependencies:
'@apify/log': 2.5.32
'@apify/timeout': 0.3.2
'@crawlee/core': 3.16.0
'@crawlee/types': 3.16.0
fingerprint-generator: 2.1.80
fingerprint-injector: 2.1.80
fingerprint-injector: 2.1.80(playwright@1.58.2)
lodash.merge: 4.6.2
nanoid: 3.3.11
ow: 0.28.2
@@ -8456,19 +8535,23 @@ snapshots:
quick-lru: 5.1.1
tiny-typed-emitter: 2.1.0
tslib: 2.8.1
optionalDependencies:
playwright: 1.58.2
transitivePeerDependencies:
- supports-color
'@crawlee/browser@3.16.0':
'@crawlee/browser@3.16.0(playwright@1.58.2)':
dependencies:
'@apify/timeout': 0.3.2
'@crawlee/basic': 3.16.0
'@crawlee/browser-pool': 3.16.0
'@crawlee/browser-pool': 3.16.0(playwright@1.58.2)
'@crawlee/types': 3.16.0
'@crawlee/utils': 3.16.0
ow: 0.28.2
tslib: 2.8.1
type-fest: 4.41.0
optionalDependencies:
playwright: 1.58.2
transitivePeerDependencies:
- supports-color
@@ -8585,13 +8668,13 @@ snapshots:
proper-lockfile: 4.1.2
tslib: 2.8.1
'@crawlee/playwright@3.16.0':
'@crawlee/playwright@3.16.0(playwright@1.58.2)':
dependencies:
'@apify/datastructures': 2.0.3
'@apify/log': 2.5.32
'@apify/timeout': 0.3.2
'@crawlee/browser': 3.16.0
'@crawlee/browser-pool': 3.16.0
'@crawlee/browser': 3.16.0(playwright@1.58.2)
'@crawlee/browser-pool': 3.16.0(playwright@1.58.2)
'@crawlee/core': 3.16.0
'@crawlee/types': 3.16.0
'@crawlee/utils': 3.16.0
@@ -8603,16 +8686,18 @@ snapshots:
ow: 0.28.2
string-comparison: 1.3.0
tslib: 2.8.1
optionalDependencies:
playwright: 1.58.2
transitivePeerDependencies:
- puppeteer
- supports-color
'@crawlee/puppeteer@3.16.0':
'@crawlee/puppeteer@3.16.0(playwright@1.58.2)':
dependencies:
'@apify/datastructures': 2.0.3
'@apify/log': 2.5.32
'@crawlee/browser': 3.16.0
'@crawlee/browser-pool': 3.16.0
'@crawlee/browser': 3.16.0(playwright@1.58.2)
'@crawlee/browser-pool': 3.16.0(playwright@1.58.2)
'@crawlee/types': 3.16.0
'@crawlee/utils': 3.16.0
cheerio: 1.0.0-rc.12
@@ -8708,57 +8793,6 @@ snapshots:
'@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)':
dependencies:
'@directus/composables': 10.1.12(vue@3.4.21(typescript@5.9.3))
@@ -8810,32 +8844,6 @@ snapshots:
- typescript
- 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))':
dependencies:
'@directus/constants': 11.0.3
@@ -8879,17 +8887,6 @@ snapshots:
'@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))':
dependencies:
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
@@ -9567,6 +9564,8 @@ snapshots:
globby: 11.1.0
read-yaml-file: 1.1.0
'@medv/finder@4.0.2': {}
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.8.1
@@ -10816,7 +10815,6 @@ snapshots:
'@types/node@22.19.10':
dependencies:
undici-types: 6.21.0
optional: true
'@types/parse-json@4.0.2': {}
@@ -10980,14 +10978,6 @@ snapshots:
'@unhead/schema': 1.11.20
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))':
dependencies:
'@unhead/schema': 1.11.20
@@ -11104,13 +11094,13 @@ snapshots:
optionalDependencies:
vite: 5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
vite: 5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
@@ -11947,22 +11937,24 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
crawlee@3.16.0(@types/node@22.19.10):
crawlee@3.16.0(@types/node@22.19.10)(playwright@1.58.2):
dependencies:
'@crawlee/basic': 3.16.0
'@crawlee/browser': 3.16.0
'@crawlee/browser-pool': 3.16.0
'@crawlee/browser': 3.16.0(playwright@1.58.2)
'@crawlee/browser-pool': 3.16.0(playwright@1.58.2)
'@crawlee/cheerio': 3.16.0
'@crawlee/cli': 3.16.0(@types/node@22.19.10)
'@crawlee/core': 3.16.0
'@crawlee/http': 3.16.0
'@crawlee/jsdom': 3.16.0
'@crawlee/linkedom': 3.16.0
'@crawlee/playwright': 3.16.0
'@crawlee/puppeteer': 3.16.0
'@crawlee/playwright': 3.16.0(playwright@1.58.2)
'@crawlee/puppeteer': 3.16.0(playwright@1.58.2)
'@crawlee/utils': 3.16.0
import-local: 3.2.0
tslib: 2.8.1
optionalDependencies:
playwright: 1.58.2
transitivePeerDependencies:
- '@types/node'
- bufferutil
@@ -12881,10 +12873,12 @@ snapshots:
header-generator: 2.1.80
tslib: 2.8.1
fingerprint-injector@2.1.80:
fingerprint-injector@2.1.80(playwright@1.58.2):
dependencies:
fingerprint-generator: 2.1.80
tslib: 2.8.1
optionalDependencies:
playwright: 1.58.2
fix-dts-default-cjs-exports@1.0.1:
dependencies:
@@ -12981,6 +12975,9 @@ snapshots:
fs.realpath@1.0.0: {}
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@@ -14405,16 +14402,6 @@ snapshots:
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)):
dependencies:
'@vue/devtools-api': 6.6.4
@@ -14473,6 +14460,14 @@ snapshots:
mlly: 1.8.0
pathe: 2.0.3
playwright-core@1.58.2: {}
playwright@1.58.2:
dependencies:
playwright-core: 1.58.2
optionalDependencies:
fsevents: 2.3.2
po-parser@2.1.1: {}
possible-typed-array-names@1.1.0: {}
@@ -15827,16 +15822,15 @@ snapshots:
- supports-color
- terser
vite-node@3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
vite-node@3.2.4(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
vite: 5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
@@ -15845,8 +15839,6 @@ snapshots:
- sugarss
- supports-color
- terser
- tsx
- yaml
vite@4.5.2(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
dependencies:
@@ -15890,25 +15882,7 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.2
vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
esbuild: 0.27.3
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.57.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.10
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
sass: 1.97.3
terser: 5.46.0
tsx: 4.21.0
yaml: 2.8.2
vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
@@ -15946,11 +15920,11 @@ snapshots:
- supports-color
- terser
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -15968,8 +15942,8 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
vite-node: 3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
vite: 5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
vite-node: 3.2.4(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
@@ -15978,7 +15952,6 @@ snapshots:
happy-dom: 20.5.3
jsdom: 27.4.0
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
@@ -15988,8 +15961,6 @@ snapshots:
- sugarss
- supports-color
- terser
- tsx
- yaml
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
@@ -16032,10 +16003,6 @@ snapshots:
- tsx
- 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)):
dependencies:
vue: 3.5.28(typescript@5.9.3)

View File

@@ -16,7 +16,10 @@ fi
case $ENV in
local)
PROJECT="infra-cms"
CMD_PREFIX="docker-compose -f packages/cms-infra/docker-compose.yml"
# Derive monorepo root
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT_DIR="$( dirname "$SCRIPT_DIR" )"
CMD_PREFIX="docker compose -f $ROOT_DIR/packages/cms-infra/docker-compose.yml"
LOCAL_CONTAINER=$($CMD_PREFIX ps -q $PROJECT)
if [ -z "$LOCAL_CONTAINER" ]; then
@@ -24,6 +27,9 @@ case $ENV in
exit 1
fi
echo "🧹 Reconciling database metadata..."
./scripts/cms-reconcile.sh
echo "🚀 Applying schema to LOCAL $PROJECT..."
docker exec "$LOCAL_CONTAINER" npx directus schema apply -y /directus/schema/snapshot.yaml
;;

56
scripts/cms-reconcile.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# Configuration
# Derive monorepo root
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT_DIR="$( dirname "$SCRIPT_DIR" )"
DB_PATH="$ROOT_DIR/packages/cms-infra/database/data.db"
if [ ! -f "$DB_PATH" ]; then
echo "❌ Database not found at $DB_PATH"
exit 1
fi
reconcile_table() {
local TABLE=$1
echo "🔍 Reconciling table: $TABLE"
# 1. Get all columns from SQLite
COLUMNS=$(sqlite3 "$DB_PATH" "PRAGMA table_info($TABLE);" | cut -d'|' -f2)
for COL in $COLUMNS; do
# Skip system columns if needed, but usually it's safer to just check if they exist in Directus
# 2. Check if field exists in directus_fields
EXISTS=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM directus_fields WHERE collection = '$TABLE' AND field = '$COL';")
if [ "$EXISTS" -eq 0 ]; then
echo " Registering missing field: $TABLE.$COL"
# Determine a basic interface based on column name or type (very simplified)
INTERFACE="input"
case $COL in
*id) INTERFACE="numeric" ;;
*text) INTERFACE="input-multiline" ;;
company|person|user_created|user_updated|feedback_id) INTERFACE="select-dropdown-m2o" ;;
date_created|date_updated) INTERFACE="datetime" ;;
screenshot|logo) INTERFACE="file" ;;
status|type) INTERFACE="select-dropdown" ;;
esac
sqlite3 "$DB_PATH" "INSERT INTO directus_fields (collection, field, interface) VALUES ('$TABLE', '$COL', '$INTERFACE');"
else
echo "✅ Field already registered: $TABLE.$COL"
fi
done
}
# Run for known problematic tables
reconcile_table "visual_feedback"
reconcile_table "visual_feedback_comments"
reconcile_table "people"
reconcile_table "leads"
reconcile_table "client_users"
reconcile_table "companies"
echo "✨ SQL Reconciliation complete!"

View File

@@ -37,7 +37,7 @@ esac
# Detect local containers
echo "🔍 Detecting local database..."
LOCAL_DB_CONTAINER=$(docker compose ps -q directus-db)
LOCAL_DB_CONTAINER=$(docker compose ps -q at-mintel-directus-db)
if [ -z "$LOCAL_DB_CONTAINER" ]; then
echo "❌ Local directus-db container not found. Is it running? (npm run dev)"
exit 1

View File

@@ -4,7 +4,7 @@
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
EXTENSIONS_ROOT="$REPO_ROOT/packages"
TARGET_DIR="$REPO_ROOT/packages/cms-infra/extensions"
TARGET_DIR="$REPO_ROOT/directus/extensions"
# List of extensions to sync - including modules and endpoints
EXTENSIONS=(
@@ -20,6 +20,10 @@ echo "🚀 Starting extension sync..."
# Ensure target directory exists
mkdir -p "$TARGET_DIR"
# Build the acquisition library first so extensions use the updated build
echo "📦 Building acquisition-library..."
(cd "$REPO_ROOT/packages/acquisition-library" && pnpm build)
for EXT in "${EXTENSIONS[@]}"; do
EXT_PATH="$EXTENSIONS_ROOT/$EXT"
@@ -55,10 +59,11 @@ for EXT in "${EXTENSIONS[@]}"; do
fi
# Sync node_modules if they exist (sometimes needed if not everything is bundled)
if [ -d "$EXT_PATH/node_modules" ]; then
echo "📚 Syncing node_modules for $EXT..."
rsync -a --delete "$EXT_PATH/node_modules/" "$TARGET_DIR/$EXT/node_modules/"
fi
# Deactivated: Causes global scope pollution and login issues in Directus
# if [ -d "$EXT_PATH/node_modules" ]; then
# echo "📚 Syncing node_modules for $EXT..."
# rsync -aL --delete "$EXT_PATH/node_modules/" "$TARGET_DIR/$EXT/node_modules/"
# fi
echo "$EXT synced."
else