Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29a414f385 | |||
| 69764e42c6 | |||
| d69ade6268 | |||
| ceaf3ae3ea | |||
| 169cb83f69 | |||
| f831a7e67e | |||
| cb4ffcaeda | |||
| 9b1f3fb7e8 | |||
| f48d89c368 | |||
| ad40e71757 | |||
| 911ceffdc5 | |||
| 23358fc708 |
6
.env
6
.env
@@ -1,5 +1,5 @@
|
|||||||
# Project
|
# Project
|
||||||
IMAGE_TAG=v1.8.2
|
IMAGE_TAG=1.8.4
|
||||||
PROJECT_NAME=at-mintel
|
PROJECT_NAME=at-mintel
|
||||||
PROJECT_COLOR=#82ed20
|
PROJECT_COLOR=#82ed20
|
||||||
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
|
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
|
||||||
@@ -10,13 +10,13 @@ AUTH_COOKIE_NAME=mintel_gatekeeper_session
|
|||||||
|
|
||||||
# Host Config (Local)
|
# Host Config (Local)
|
||||||
TRAEFIK_HOST=at-mintel.localhost
|
TRAEFIK_HOST=at-mintel.localhost
|
||||||
DIRECTUS_HOST=cms.localhost
|
DIRECTUS_HOST=cms-legacy.localhost
|
||||||
|
|
||||||
# Next.js
|
# Next.js
|
||||||
NEXT_PUBLIC_BASE_URL=http://at-mintel.localhost
|
NEXT_PUBLIC_BASE_URL=http://at-mintel.localhost
|
||||||
|
|
||||||
# Directus
|
# Directus
|
||||||
DIRECTUS_URL=http://cms.localhost
|
DIRECTUS_URL=http://cms-legacy.localhost
|
||||||
DIRECTUS_KEY=F9IIfahEjPq6NZhKyRLw516D8GotuFj79EGK7pGfIWg=
|
DIRECTUS_KEY=F9IIfahEjPq6NZhKyRLw516D8GotuFj79EGK7pGfIWg=
|
||||||
DIRECTUS_SECRET=OZfxMu8lBxzaEnFGRKreNBoJpRiRu58U+HsVg2yWk4o=
|
DIRECTUS_SECRET=OZfxMu8lBxzaEnFGRKreNBoJpRiRu58U+HsVg2yWk4o=
|
||||||
CORS_ENABLED=true
|
CORS_ENABLED=true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Project
|
# Project
|
||||||
IMAGE_TAG=v1.8.2
|
IMAGE_TAG=1.8.4
|
||||||
PROJECT_NAME=sample-website
|
PROJECT_NAME=sample-website
|
||||||
PROJECT_COLOR=#82ed20
|
PROJECT_COLOR=#82ed20
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- '*'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -26,15 +26,16 @@ jobs:
|
|||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
REF_NAME: ${{ github.ref_name }}
|
REF_NAME: ${{ github.ref_name }}
|
||||||
EVENT: ${{ github.event_name }}
|
EVENT: ${{ github.event_name }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
run: |
|
run: |
|
||||||
echo "🔎 Debug: Event=$EVENT, Ref=$REF, RefName=$REF_NAME, RunId=$RUN_ID"
|
echo "🔎 Debug: Event=$EVENT, Ref=$REF, RefName=$REF_NAME, RunId=$RUN_ID"
|
||||||
|
|
||||||
case "$REF" in
|
# Fetch recent runs for the repository
|
||||||
refs/tags/v*)
|
RUNS=$(curl -s -H "Authorization: token $GITEA_TOKEN" "https://git.infra.mintel.me/api/v1/repos/$REPO/actions/runs?limit=30")
|
||||||
echo "🚀 Release detected ($REF_NAME). Cancelling non-tag runs..."
|
|
||||||
|
|
||||||
# Fetch all runs
|
case "$REF" in
|
||||||
RUNS=$(curl -s -H "Authorization: token $GITEA_TOKEN" "https://git.infra.mintel.me/api/v1/repos/$REPO/actions/runs")
|
refs/tags/*)
|
||||||
|
echo "🚀 Release detected ($REF_NAME). Cancelling non-tag runs..."
|
||||||
|
|
||||||
# Identify runs to cancel: in_progress/queued, NOT this run, and NOT a tag run
|
# Identify runs to cancel: in_progress/queued, NOT this run, and NOT a tag run
|
||||||
echo "$RUNS" | jq -c '.workflow_runs[] | select(.status == "in_progress" or .status == "queued") | select(.id | tostring != "'$RUN_ID'")' | while read -r run; do
|
echo "$RUNS" | jq -c '.workflow_runs[] | select(.status == "in_progress" or .status == "queued") | select(.id | tostring != "'$RUN_ID'")' | while read -r run; do
|
||||||
@@ -43,7 +44,7 @@ jobs:
|
|||||||
TITLE=$(echo "$run" | jq -r '.display_title')
|
TITLE=$(echo "$run" | jq -r '.display_title')
|
||||||
|
|
||||||
case "$RUN_REF" in
|
case "$RUN_REF" in
|
||||||
refs/tags/v*)
|
refs/tags/*)
|
||||||
echo "⏭️ Skipping parallel release run $ID ($TITLE) on $RUN_REF"
|
echo "⏭️ Skipping parallel release run $ID ($TITLE) on $RUN_REF"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -54,7 +55,17 @@ jobs:
|
|||||||
done
|
done
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "ℹ️ Regular push. No prioritization needed."
|
echo "ℹ️ Regular push. Checking for parallel release tag for SHA $SHA..."
|
||||||
|
|
||||||
|
# Check if there's a tag run for the SAME commit
|
||||||
|
TAG_RUN_ID=$(echo "$RUNS" | jq -r '.workflow_runs[] | select(.ref | startswith("refs/tags/")) | select(.head_sha == "'$SHA'") | .id' | head -n 1)
|
||||||
|
|
||||||
|
if [[ -n "$TAG_RUN_ID" && "$TAG_RUN_ID" != "null" ]]; then
|
||||||
|
echo "🚀 Found parallel tag run $TAG_RUN_ID for commit $SHA. Cancelling this branch run ($RUN_ID)..."
|
||||||
|
curl -X POST -s -H "Authorization: token $GITEA_TOKEN" "https://git.infra.mintel.me/api/v1/repos/$REPO/actions/runs/$RUN_ID/cancel"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "✅ No parallel tag run found. Proceeding."
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -130,7 +141,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: 🚀 Release
|
name: 🚀 Release
|
||||||
needs: [lint, test, build]
|
needs: [lint, test, build]
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -160,12 +171,13 @@ jobs:
|
|||||||
build-images:
|
build-images:
|
||||||
name: 🐳 Build ${{ matrix.name }}
|
name: 🐳 Build ${{ matrix.name }}
|
||||||
needs: [lint, test, build]
|
needs: [lint, test, build]
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
max-parallel: 1
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- image: nextjs
|
- image: nextjs
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
|
# Validate Directus SDK imports before push
|
||||||
|
# This prevents runtime crashes caused by importing non-existent exports
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
if [ -f "$SCRIPT_DIR/scripts/validate-sdk-imports.sh" ]; then
|
||||||
|
"$SCRIPT_DIR/scripts/validate-sdk-imports.sh" || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if we are pushing a tag
|
# Check if we are pushing a tag
|
||||||
while read local_ref local_sha remote_ref remote_sha
|
while read local_ref local_sha remote_ref remote_sha
|
||||||
do
|
do
|
||||||
if [[ "$remote_ref" == refs/tags/v* ]]; then
|
if [[ "$remote_ref" == refs/tags/* ]]; then
|
||||||
TAG=${remote_ref#refs/tags/}
|
TAG=${remote_ref#refs/tags/}
|
||||||
echo "🏷️ Tag detected: $TAG, ensuring versions are synced..."
|
echo "🏷️ Tag detected: $TAG, ensuring versions are synced..."
|
||||||
|
|
||||||
|
|||||||
@@ -80,3 +80,4 @@ Client websites scaffolded via the CLI use a **tag-based deployment** strategy:
|
|||||||
- **Git Tag `v*.*.*`**: Deploys to the `production` environment.
|
- **Git Tag `v*.*.*`**: Deploys to the `production` environment.
|
||||||
|
|
||||||
See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation.
|
See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sample-website",
|
"name": "sample-website",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
1
directus/extensions/acquisition-manager/index.js
Normal file
1
directus/extensions/acquisition-manager/index.js
Normal file
File diff suppressed because one or more lines are too long
28
directus/extensions/acquisition-manager/package.json
Normal file
28
directus/extensions/acquisition-manager/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "acquisition-manager",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "acquisition manager"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
121545
directus/extensions/acquisition/index.js
Normal file
121545
directus/extensions/acquisition/index.js
Normal file
File diff suppressed because one or more lines are too long
27
directus/extensions/acquisition/package.json
Normal file
27
directus/extensions/acquisition/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "acquisition",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "endpoint",
|
||||||
|
"path": "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/pdf": "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"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
directus/extensions/company-manager/index.js
Normal file
1
directus/extensions/company-manager/index.js
Normal file
File diff suppressed because one or more lines are too long
28
directus/extensions/company-manager/package.json
Normal file
28
directus/extensions/company-manager/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "company-manager",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "company manager"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
directus/extensions/customer-manager/index.js
Normal file
1
directus/extensions/customer-manager/index.js
Normal file
File diff suppressed because one or more lines are too long
28
directus/extensions/customer-manager/package.json
Normal file
28
directus/extensions/customer-manager/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "customer-manager",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "customer manager"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
directus/extensions/feedback-commander/index.js
Normal file
1
directus/extensions/feedback-commander/index.js
Normal file
File diff suppressed because one or more lines are too long
27
directus/extensions/feedback-commander/package.json
Normal file
27
directus/extensions/feedback-commander/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "feedback-commander",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "feedback commander"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
directus/extensions/people-manager/index.js
Normal file
1
directus/extensions/people-manager/index.js
Normal file
File diff suppressed because one or more lines are too long
28
directus/extensions/people-manager/package.json
Normal file
28
directus/extensions/people-manager/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "people-manager",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "people manager"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
directus/extensions/unified-dashboard/index.js
Normal file
1
directus/extensions/unified-dashboard/index.js
Normal file
File diff suppressed because one or more lines are too long
27
directus/extensions/unified-dashboard/package.json
Normal file
27
directus/extensions/unified-dashboard/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "unified-dashboard",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "unified dashboard"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
version: 1
|
|
||||||
directus: 11.15.1
|
|
||||||
vendor: postgres
|
|
||||||
collections: []
|
|
||||||
fields: []
|
|
||||||
systemFields:
|
|
||||||
- collection: directus_activity
|
|
||||||
field: timestamp
|
|
||||||
schema:
|
|
||||||
is_indexed: true
|
|
||||||
- collection: directus_revisions
|
|
||||||
field: activity
|
|
||||||
schema:
|
|
||||||
is_indexed: true
|
|
||||||
- collection: directus_revisions
|
|
||||||
field: parent
|
|
||||||
schema:
|
|
||||||
is_indexed: true
|
|
||||||
relations: []
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
S9WsV
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"cms:schema:snapshot": "./scripts/cms-snapshot.sh",
|
"cms:schema:snapshot": "./scripts/cms-snapshot.sh",
|
||||||
"cms:schema:apply": "./scripts/cms-apply.sh local",
|
"cms:schema:apply": "./scripts/cms-apply.sh local",
|
||||||
"cms:schema:apply:infra": "./scripts/cms-apply.sh infra",
|
"cms:schema:apply:infra": "./scripts/cms-apply.sh infra",
|
||||||
"cms:up": "./scripts/sync-extensions.sh && cd packages/cms-infra && npm run up -- --force-recreate",
|
"cms:up": "cd packages/cms-infra && npm run up -- --force-recreate",
|
||||||
"cms:down": "cd packages/cms-infra && npm run down",
|
"cms:down": "cd packages/cms-infra && npm run down",
|
||||||
"cms:logs": "cd packages/cms-infra && npm run logs",
|
"cms:logs": "cd packages/cms-infra && npm run logs",
|
||||||
"dev:infra": "docker-compose up -d directus directus-db",
|
"dev:infra": "docker-compose up -d directus directus-db",
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"@commitlint/config-conventional": "^20.4.0",
|
"@commitlint/config-conventional": "^20.4.0",
|
||||||
"@mintel/eslint-config": "workspace:*",
|
"@mintel/eslint-config": "workspace:*",
|
||||||
"@mintel/husky-config": "workspace:*",
|
"@mintel/husky-config": "workspace:*",
|
||||||
|
"@next/eslint-plugin-next": "16.1.6",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/node": "^20.17.16",
|
"@types/node": "^20.17.16",
|
||||||
@@ -36,7 +37,6 @@
|
|||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"@next/eslint-plugin-next": "16.1.6",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"happy-dom": "^20.4.0",
|
"happy-dom": "^20.4.0",
|
||||||
@@ -50,12 +50,13 @@
|
|||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"globals": "^17.3.0",
|
||||||
"import-in-the-middle": "^3.0.0",
|
"import-in-the-middle": "^3.0.0",
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"require-in-the-middle": "^8.0.1"
|
"require-in-the-middle": "^8.0.1"
|
||||||
},
|
},
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "acquisition-manager",
|
"name": "acquisition-manager",
|
||||||
"description": "Custom High-Fidelity Acquisition Management for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "account_balance_wallet",
|
"icon": "extension",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
@@ -11,17 +11,18 @@
|
|||||||
],
|
],
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Acquisition Manager"
|
"name": "acquisition manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<private-view title="Acquisition Manager">
|
<MintelManagerLayout
|
||||||
|
title="Acquisition Manager"
|
||||||
|
:item-title="getCompanyName(selectedLead) || 'Lead wählen'"
|
||||||
|
:is-empty="!selectedLead"
|
||||||
|
empty-title="Lead auswählen"
|
||||||
|
empty-icon="auto_awesome"
|
||||||
|
:notice="notice"
|
||||||
|
@close-notice="notice = null"
|
||||||
|
>
|
||||||
<template #navigation>
|
<template #navigation>
|
||||||
<v-list nav>
|
<v-list nav>
|
||||||
<v-list-item @click="showAddLead = true" clickable>
|
<v-list-item @click="openCreateDrawer" clickable>
|
||||||
<v-list-item-icon><v-icon name="add" color="var(--theme--primary)" /></v-list-item-icon>
|
<v-list-item-icon><v-icon name="add" color="var(--theme--primary)" /></v-list-item-icon>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-text-overflow text="Neuen Lead anlegen" />
|
<v-text-overflow text="Neuen Lead anlegen" />
|
||||||
@@ -15,7 +23,7 @@
|
|||||||
v-for="lead in leads"
|
v-for="lead in leads"
|
||||||
:key="lead.id"
|
:key="lead.id"
|
||||||
:active="selectedLeadId === lead.id"
|
:active="selectedLeadId === lead.id"
|
||||||
class="lead-item"
|
class="nav-item"
|
||||||
clickable
|
clickable
|
||||||
@click="selectLead(lead.id)"
|
@click="selectLead(lead.id)"
|
||||||
>
|
>
|
||||||
@@ -29,166 +37,136 @@
|
|||||||
</v-list>
|
</v-list>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #title-outer:after>
|
<template #subtitle>
|
||||||
<v-notice v-if="notice" :type="notice.type" @close="notice = null" dismissible>
|
<template v-if="selectedLead">
|
||||||
{{ notice.message }}
|
<v-icon name="language" x-small />
|
||||||
</v-notice>
|
<a :href="selectedLead.website_url" target="_blank" class="url-link">
|
||||||
|
{{ selectedLead.website_url.replace(/^https?:\/\//, '') }}
|
||||||
|
</a>
|
||||||
|
· Status: {{ selectedLead.status.toUpperCase() }}
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<template #actions>
|
||||||
<v-notice type="success" style="margin-bottom: 16px;">
|
<v-button
|
||||||
DEBUG: Module Version 1.1.0 - Native Build - {{ new Date().toISOString() }}
|
v-if="selectedLead?.status === 'new'"
|
||||||
</v-notice>
|
secondary
|
||||||
|
:loading="loadingAudit"
|
||||||
|
@click="runAudit"
|
||||||
|
>
|
||||||
|
<v-icon name="settings_suggest" left />
|
||||||
|
Audit starten
|
||||||
|
</v-button>
|
||||||
|
|
||||||
<div v-if="!selectedLead" class="empty-state">
|
<template v-if="selectedLead?.status === 'audit_ready'">
|
||||||
<v-info title="Lead auswählen" icon="auto_awesome" center>
|
<v-button secondary :loading="loadingEmail" @click="sendAuditEmail">
|
||||||
Wähle einen Lead in der Navigation aus oder
|
<v-icon name="mail" left />
|
||||||
<v-button x-small @click="showAddLead = true">registriere einen neuen Lead</v-button>.
|
Audit E-Mail
|
||||||
</v-info>
|
</v-button>
|
||||||
</div>
|
<v-button :loading="loadingPdf" @click="generatePdf">
|
||||||
|
<v-icon name="picture_as_pdf" left />
|
||||||
|
PDF Erstellen
|
||||||
|
</v-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<v-button v-if="selectedLead?.audit_pdf_path" secondary icon v-tooltip.bottom="'PDF öffnen'" @click="openPdf">
|
||||||
<header class="header">
|
<v-icon name="open_in_new" />
|
||||||
<div class="header-left">
|
</v-button>
|
||||||
<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">
|
|
||||||
{{ selectedLead.website_url.replace(/^https?:\/\//, '') }}
|
|
||||||
</a>
|
|
||||||
· Status: {{ selectedLead.status.toUpperCase() }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<v-button
|
|
||||||
v-if="selectedLead.status === 'new'"
|
|
||||||
secondary
|
|
||||||
:loading="loadingAudit"
|
|
||||||
@click="runAudit"
|
|
||||||
>
|
|
||||||
<v-icon name="settings_suggest" left />
|
|
||||||
Audit starten
|
|
||||||
</v-button>
|
|
||||||
|
|
||||||
<template v-if="selectedLead.status === 'audit_ready'">
|
<v-button
|
||||||
<v-button secondary :loading="loadingEmail" @click="sendAuditEmail">
|
v-if="selectedLead?.audit_pdf_path"
|
||||||
<v-icon name="mail" left />
|
primary
|
||||||
Audit E-Mail
|
:loading="loadingEmail"
|
||||||
</v-button>
|
@click="sendEstimateEmail"
|
||||||
<v-button :loading="loadingPdf" @click="generatePdf">
|
>
|
||||||
<v-icon name="picture_as_pdf" left />
|
<v-icon name="send" left />
|
||||||
PDF Erstellen
|
Angebot senden
|
||||||
</v-button>
|
</v-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-button v-if="selectedLead.audit_pdf_path" secondary icon v-tooltip.bottom="'PDF öffnen'" @click="openPdf">
|
<template #empty-state>
|
||||||
<v-icon name="open_in_new" />
|
Wähle einen Lead in der Navigation aus oder
|
||||||
</v-button>
|
<v-button x-small @click="openCreateDrawer">registriere einen neuen Lead</v-button>.
|
||||||
|
</template>
|
||||||
|
|
||||||
<v-button
|
<div v-if="selectedLead" class="sections">
|
||||||
v-if="selectedLead.audit_pdf_path"
|
<div class="main-info">
|
||||||
primary
|
<div class="form-grid">
|
||||||
:loading="loadingEmail"
|
<div class="field">
|
||||||
@click="sendEstimateEmail"
|
<span class="label">Kontaktperson</span>
|
||||||
>
|
<div v-if="selectedLead.contact_person" class="value person-link" @click="goToPerson(selectedLead.contact_person)">
|
||||||
<v-icon name="send" left />
|
{{ getPersonName(selectedLead.contact_person) }}
|
||||||
Angebot senden
|
|
||||||
</v-button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="sections">
|
|
||||||
<div class="main-info">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="field">
|
|
||||||
<span class="label">Kontaktperson</span>
|
|
||||||
<div v-if="selectedLead.contact_person" class="value person-link" @click="goToPerson(selectedLead.contact_person)">
|
|
||||||
{{ getPersonName(selectedLead.contact_person) }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="value text-subdued">Keine Person verknüpft</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<span class="label">E-Mail (Legacy)</span>
|
|
||||||
<div class="value">{{ selectedLead.contact_email || '—' }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="field full">
|
|
||||||
<span class="label">Briefing / Fokus</span>
|
|
||||||
<div class="value text-block">{{ selectedLead.briefing || 'Kein Briefing hinterlegt.' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="value text-subdued">Keine Person verknüpft</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field full">
|
||||||
<v-divider />
|
<span class="label">Briefing / Fokus</span>
|
||||||
|
<div class="value text-block">{{ selectedLead.briefing || 'Kein Briefing hinterlegt.' }}</div>
|
||||||
<div v-if="selectedLead.ai_state" class="ai-observations">
|
|
||||||
<h3 class="section-title">AI Observations & Estimation</h3>
|
|
||||||
|
|
||||||
<div class="metrics">
|
|
||||||
<v-info label="Projekt-Modus" :value="selectedLead.ai_state.projectType || 'Unbekannt'" />
|
|
||||||
<v-info label="Seitenanzahl" :value="selectedLead.ai_state.sitemap?.length || '0'" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-table
|
|
||||||
v-if="selectedLead.ai_state.sitemap"
|
|
||||||
:headers="[ { text: 'Seite', value: 'title' }, { text: 'URL', value: 'url' } ]"
|
|
||||||
:items="selectedLead.ai_state.sitemap"
|
|
||||||
class="observation-table"
|
|
||||||
>
|
|
||||||
<template #[`item.title`]="{ item }">
|
|
||||||
<span class="page-title">{{ item.title }}</span>
|
|
||||||
</template>
|
|
||||||
<template #[`item.url`]="{ item }">
|
|
||||||
<span class="page-url">{{ item.url }}</span>
|
|
||||||
</template>
|
|
||||||
</v-table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<div v-if="selectedLead.ai_state" class="ai-observations">
|
||||||
|
<h3 class="section-title">AI Observations & Estimation</h3>
|
||||||
|
|
||||||
|
<div class="metrics">
|
||||||
|
<MintelStatCard label="Projekt-Modus" :value="selectedLead.ai_state.projectType || 'Unbekannt'" icon="category" />
|
||||||
|
<MintelStatCard label="Seitenanzahl" :value="selectedLead.ai_state.sitemap?.length || '0'" icon="description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-table
|
||||||
|
v-if="selectedLead.ai_state.sitemap"
|
||||||
|
:headers="[ { text: 'Seite', value: 'title' }, { text: 'URL', value: 'url' } ]"
|
||||||
|
:items="selectedLead.ai_state.sitemap"
|
||||||
|
class="observation-table"
|
||||||
|
>
|
||||||
|
<template #[`item.title`]="{ item }">
|
||||||
|
<span class="page-title">{{ item.title }}</span>
|
||||||
|
</template>
|
||||||
|
<template #[`item.url`]="{ item }">
|
||||||
|
<span class="page-url">{{ item.url }}</span>
|
||||||
|
</template>
|
||||||
|
</v-table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Drawer: New Lead -->
|
<!-- Drawer: New Lead -->
|
||||||
<v-drawer
|
<v-drawer
|
||||||
v-model="showAddLead"
|
v-model="drawerActive"
|
||||||
title="Neuen Lead registrieren"
|
title="Neuen Lead registrieren"
|
||||||
icon="person_add"
|
icon="person_add"
|
||||||
@cancel="showAddLead = false"
|
@cancel="drawerActive = false"
|
||||||
>
|
>
|
||||||
<div class="drawer-content">
|
<div class="drawer-content">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">Organisation / Firma (Zentral)</span>
|
<span class="label">Organisation / Firma (Zentral)</span>
|
||||||
<v-select
|
<MintelSelect
|
||||||
v-model="newLead.company"
|
v-model="newLead.company"
|
||||||
:items="companyOptions"
|
:items="companyOptions"
|
||||||
placeholder="Bestehende Firma auswählen..."
|
placeholder="Bestehende Firma auswählen..."
|
||||||
|
allow-add
|
||||||
|
@add="openQuickAdd('company')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<span class="label">Website URL</span>
|
<span class="label">Website URL</span>
|
||||||
<v-input v-model="newLead.website_url" placeholder="https://..." />
|
<v-input v-model="newLead.website_url" placeholder="https://..." />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<span class="label">Ansprechpartner</span>
|
|
||||||
<v-input v-model="newLead.contact_name" placeholder="Vorname Nachname" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<span class="label">E-Mail Adresse</span>
|
|
||||||
<v-input v-model="newLead.contact_email" placeholder="email@beispiel.de" type="email" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">Briefing / Fokus</span>
|
<span class="label">Briefing / Fokus</span>
|
||||||
<v-textarea v-model="newLead.briefing" placeholder="Besonderheiten für das Audit..." />
|
<v-textarea v-model="newLead.briefing" placeholder="Besonderheiten für das Audit..." />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">Kontaktperson (Optional)</span>
|
<span class="label">Kontaktperson (Optional)</span>
|
||||||
<v-select
|
<MintelSelect
|
||||||
v-model="newLead.contact_person"
|
v-model="newLead.contact_person"
|
||||||
:items="peopleOptions"
|
:items="peopleOptions"
|
||||||
placeholder="Person auswählen..."
|
placeholder="Person auswählen..."
|
||||||
|
allow-add
|
||||||
|
@add="openQuickAdd('person')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,12 +176,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-drawer>
|
</v-drawer>
|
||||||
</private-view>
|
</MintelManagerLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useApi } from '@directus/extensions-sdk';
|
import { useApi } from '@directus/extensions-sdk';
|
||||||
|
import { MintelManagerLayout, MintelSelect, MintelStatCard } from '@mintel/directus-extension-toolkit';
|
||||||
|
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const leads = ref<any[]>([]);
|
const leads = ref<any[]>([]);
|
||||||
@@ -211,16 +190,13 @@ const selectedLeadId = ref<string | null>(null);
|
|||||||
const loadingAudit = ref(false);
|
const loadingAudit = ref(false);
|
||||||
const loadingPdf = ref(false);
|
const loadingPdf = ref(false);
|
||||||
const loadingEmail = ref(false);
|
const loadingEmail = ref(false);
|
||||||
const showAddLead = ref(false);
|
const drawerActive = ref(false);
|
||||||
const savingLead = ref(false);
|
const savingLead = ref(false);
|
||||||
const notice = ref<{ type: string; message: string } | null>(null);
|
const notice = ref<{ type: string; message: string } | null>(null);
|
||||||
|
|
||||||
const newLead = ref({
|
const newLead = ref({
|
||||||
company_name: '', // Legacy
|
|
||||||
company: null,
|
company: null,
|
||||||
website_url: '',
|
website_url: '',
|
||||||
contact_name: '', // Legacy
|
|
||||||
contact_email: '', // Legacy
|
|
||||||
contact_person: null,
|
contact_person: null,
|
||||||
briefing: '',
|
briefing: '',
|
||||||
status: 'new'
|
status: 'new'
|
||||||
@@ -244,10 +220,11 @@ const peopleOptions = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
function getCompanyName(lead: any) {
|
function getCompanyName(lead: any) {
|
||||||
|
if (!lead) return '';
|
||||||
if (lead.company) {
|
if (lead.company) {
|
||||||
return typeof lead.company === 'object' ? lead.company.name : (companies.value.find(c => c.id === lead.company)?.name || lead.company_name);
|
return typeof lead.company === 'object' ? lead.company.name : (companies.value.find(c => c.id === lead.company)?.name || 'Unbekannte Firma');
|
||||||
}
|
}
|
||||||
return lead.company_name;
|
return 'Unbekannte Organisation';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPersonName(id: string | any) {
|
function getPersonName(id: string | any) {
|
||||||
@@ -258,31 +235,32 @@ function getPersonName(id: string | any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goToPerson(id: string) {
|
function goToPerson(id: string) {
|
||||||
// Logic to navigate to people manager or open details
|
|
||||||
notice.value = { type: 'info', message: `Navigiere zu Person: ${id}` };
|
notice.value = { type: 'info', message: `Navigiere zu Person: ${id}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedLead = computed(() => leads.value.find(l => l.id === selectedLeadId.value));
|
const selectedLead = computed(() => leads.value.find(l => l.id === selectedLeadId.value));
|
||||||
|
|
||||||
onMounted(fetchData);
|
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const [leadsResp, peopleResp, companiesResp] = await Promise.all([
|
try {
|
||||||
api.get('/items/leads', {
|
const [leadsResp, peopleResp, companiesResp] = await Promise.all([
|
||||||
params: {
|
api.get('/items/leads', {
|
||||||
sort: '-date_created',
|
params: {
|
||||||
fields: '*.*'
|
sort: '-date_created',
|
||||||
}
|
fields: '*.*'
|
||||||
}),
|
}
|
||||||
api.get('/items/people', { params: { sort: 'last_name' } }),
|
}),
|
||||||
api.get('/items/companies', { params: { sort: 'name' } })
|
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;
|
leads.value = leadsResp.data.data;
|
||||||
companies.value = companiesResp.data.data;
|
people.value = peopleResp.data.data;
|
||||||
|
companies.value = companiesResp.data.data;
|
||||||
|
|
||||||
if (!selectedLeadId.value && leads.value.length > 0) {
|
if (!selectedLeadId.value && leads.value.length > 0) {
|
||||||
selectedLeadId.value = leads.value[0].id;
|
selectedLeadId.value = leads.value[0].id;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Fetch error:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +272,17 @@ function selectLead(id: string) {
|
|||||||
selectedLeadId.value = id;
|
selectedLeadId.value = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCreateDrawer() {
|
||||||
|
newLead.value = {
|
||||||
|
company: null,
|
||||||
|
website_url: '',
|
||||||
|
contact_person: null,
|
||||||
|
briefing: '',
|
||||||
|
status: 'new'
|
||||||
|
};
|
||||||
|
drawerActive.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function runAudit() {
|
async function runAudit() {
|
||||||
if (!selectedLeadId.value) return;
|
if (!selectedLeadId.value) return;
|
||||||
loadingAudit.value = true;
|
loadingAudit.value = true;
|
||||||
@@ -356,8 +345,8 @@ function openPdf() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveLead() {
|
async function saveLead() {
|
||||||
if (!newLead.value.company_name && !newLead.value.company) {
|
if (!newLead.value.company) {
|
||||||
notice.value = { type: 'danger', message: 'Firma oder Firmenname erforderlich.' };
|
notice.value = { type: 'danger', message: 'Organisation erforderlich.' };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
savingLead.value = true;
|
savingLead.value = true;
|
||||||
@@ -368,19 +357,9 @@ async function saveLead() {
|
|||||||
};
|
};
|
||||||
await api.post('/items/leads', payload);
|
await api.post('/items/leads', payload);
|
||||||
notice.value = { type: 'success', message: 'Lead erfolgreich registriert!' };
|
notice.value = { type: 'success', message: 'Lead erfolgreich registriert!' };
|
||||||
showAddLead.value = false;
|
drawerActive.value = false;
|
||||||
await fetchLeads();
|
await fetchLeads();
|
||||||
selectedLeadId.value = payload.id;
|
selectedLeadId.value = payload.id;
|
||||||
newLead.value = {
|
|
||||||
company_name: '',
|
|
||||||
company: null,
|
|
||||||
website_url: '',
|
|
||||||
contact_name: '',
|
|
||||||
contact_email: '',
|
|
||||||
contact_person: null,
|
|
||||||
briefing: '',
|
|
||||||
status: 'new'
|
|
||||||
};
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
notice.value = { type: 'danger', message: `Fehler beim Speichern: ${e.message}` };
|
notice.value = { type: 'danger', message: `Fehler beim Speichern: ${e.message}` };
|
||||||
} finally {
|
} finally {
|
||||||
@@ -388,6 +367,10 @@ async function saveLead() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openQuickAdd(type: string) {
|
||||||
|
notice.value = { type: 'info', message: `${type === 'company' ? 'Firma' : 'Person'} im jeweiligen Manager anlegen.` };
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusIcon(status: string) {
|
function getStatusIcon(status: string) {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case 'new': return 'fiber_new';
|
case 'new': return 'fiber_new';
|
||||||
@@ -407,18 +390,13 @@ function getStatusColor(status: string) {
|
|||||||
default: return 'var(--theme--foreground-subdued)';
|
default: return 'var(--theme--foreground-subdued)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(fetchData);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.content-wrapper { padding: 32px; height: 100%; display: flex; flex-direction: column; overflow-y: auto; }
|
|
||||||
.lead-item { cursor: pointer; }
|
|
||||||
.header { margin-bottom: 24px; display: flex; justify-content: space-between; align-items: flex-end; }
|
|
||||||
.header-right { display: flex; gap: 12px; }
|
|
||||||
.title { font-size: 24px; font-weight: 800; margin-bottom: 4px; color: var(--theme--foreground); }
|
|
||||||
.subtitle { color: var(--theme--foreground-subdued); font-size: 14px; display: flex; align-items: center; gap: 8px; }
|
|
||||||
.url-link { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
|
.url-link { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
|
||||||
.url-link:hover { border-bottom-color: currentColor; }
|
.url-link:hover { border-bottom-color: currentColor; }
|
||||||
.empty-state { height: 100%; display: flex; align-items: center; justify-content: center; }
|
|
||||||
|
|
||||||
.sections { display: flex; flex-direction: column; gap: 32px; }
|
.sections { display: flex; flex-direction: column; gap: 32px; }
|
||||||
|
|
||||||
@@ -431,7 +409,7 @@ function getStatusColor(status: string) {
|
|||||||
|
|
||||||
.ai-observations { display: flex; flex-direction: column; gap: 16px; }
|
.ai-observations { display: flex; flex-direction: column; gap: 16px; }
|
||||||
.section-title { font-size: 16px; font-weight: 700; color: var(--theme--foreground); margin-bottom: 8px; }
|
.section-title { font-size: 16px; font-weight: 700; color: var(--theme--foreground); margin-bottom: 8px; }
|
||||||
.metrics { display: flex; gap: 32px; margin-bottom: 16px; }
|
.metrics { display: flex; gap: 24px; margin-bottom: 16px; }
|
||||||
|
|
||||||
.observation-table { border: 1px solid var(--theme--border); border-radius: 8px; overflow: hidden; }
|
.observation-table { border: 1px solid var(--theme--border); border-radius: 8px; overflow: hidden; }
|
||||||
.page-title { font-weight: 600; }
|
.page-title { font-weight: 600; }
|
||||||
@@ -440,6 +418,4 @@ function getStatusColor(status: string) {
|
|||||||
.drawer-content { padding: 24px; display: flex; flex-direction: column; gap: 32px; }
|
.drawer-content { padding: 24px; display: flex; flex-direction: column; gap: 32px; }
|
||||||
.form-section { display: flex; flex-direction: column; gap: 20px; }
|
.form-section { display: flex; flex-direction: column; gap: 20px; }
|
||||||
.drawer-actions { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
|
.drawer-actions { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
|
||||||
:deep(.v-list-item) { cursor: pointer !important; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "acquisition",
|
"name": "acquisition",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "endpoint",
|
"type": "endpoint",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/cli",
|
"name": "@mintel/cli",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/cloner",
|
"name": "@mintel/cloner",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
11
packages/cms-infra/Dockerfile
Normal file
11
packages/cms-infra/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM directus/directus:11
|
||||||
|
|
||||||
|
USER root
|
||||||
|
# Install dependencies in a way that avoids metadata conflicts in the root
|
||||||
|
RUN mkdir -p /directus/lib-dependencies && \
|
||||||
|
cd /directus/lib-dependencies && \
|
||||||
|
npm init -y && \
|
||||||
|
npm install vue @vueuse/core vue-router
|
||||||
|
# Ensure they are in the NODE_PATH
|
||||||
|
ENV NODE_PATH="/directus/lib-dependencies/node_modules:${NODE_PATH}"
|
||||||
|
USER node
|
||||||
BIN
packages/cms-infra/database/data.db
Executable file → Normal file
BIN
packages/cms-infra/database/data.db
Executable file → Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
infra-cms:
|
infra-cms:
|
||||||
image: directus/directus:11
|
image: directus/directus:11.15.2
|
||||||
ports:
|
ports:
|
||||||
- "8059:8055"
|
- "8059:8055"
|
||||||
networks:
|
networks:
|
||||||
@@ -14,6 +14,7 @@ services:
|
|||||||
DB_CLIENT: "sqlite3"
|
DB_CLIENT: "sqlite3"
|
||||||
DB_FILENAME: "/directus/database/data.db"
|
DB_FILENAME: "/directus/database/data.db"
|
||||||
WEBSOCKETS_ENABLED: "true"
|
WEBSOCKETS_ENABLED: "true"
|
||||||
|
PUBLIC_URL: "http://cms.localhost"
|
||||||
EMAIL_TRANSPORT: "smtp"
|
EMAIL_TRANSPORT: "smtp"
|
||||||
EMAIL_SMTP_HOST: "smtp.eu.mailgun.org"
|
EMAIL_SMTP_HOST: "smtp.eu.mailgun.org"
|
||||||
EMAIL_SMTP_PORT: "587"
|
EMAIL_SMTP_PORT: "587"
|
||||||
@@ -21,19 +22,29 @@ services:
|
|||||||
EMAIL_SMTP_PASSWORD: "4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6"
|
EMAIL_SMTP_PASSWORD: "4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6"
|
||||||
EMAIL_SMTP_SECURE: "false"
|
EMAIL_SMTP_SECURE: "false"
|
||||||
EMAIL_FROM: "postmaster@mg.mintel.me"
|
EMAIL_FROM: "postmaster@mg.mintel.me"
|
||||||
|
LOG_LEVEL: "debug"
|
||||||
|
SERVE_APP: "true"
|
||||||
|
EXTENSIONS_AUTO_RELOAD: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- ./database:/directus/database
|
- ./database:/directus/database
|
||||||
- ./uploads:/directus/uploads
|
- ./uploads:/directus/uploads
|
||||||
- ./schema:/directus/schema
|
- ./schema:/directus/schema
|
||||||
- ./extensions:/directus/extensions
|
- ./extensions:/directus/extensions
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8055/server/health" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.infra-cms.rule=Host(`cms.localhost`)"
|
- "traefik.http.routers.at-mintel-infra-cms.rule=Host(`cms.localhost`)"
|
||||||
- "traefik.http.services.infra-cms.loadbalancer.server.port=8055"
|
- "traefik.http.services.at-mintel-infra-cms.loadbalancer.server.port=8055"
|
||||||
|
- "traefik.http.services.at-mintel-infra-cms.loadbalancer.healthcheck.path=/server/health"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: mintel-infra-cms-internal
|
name: at-mintel-cms-network
|
||||||
infra:
|
infra:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,30 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "acquisition-manager",
|
"name": "acquisition-manager",
|
||||||
"description": "Custom High-Fidelity Acquisition Management for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "account_balance_wallet",
|
"icon": "extension",
|
||||||
"version": "1.7.12",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
"directus-extension",
|
"directus-extension",
|
||||||
"directus-extension-module"
|
"directus-extension-module"
|
||||||
],
|
],
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Acquisition Manager"
|
"name": "acquisition manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
121545
packages/cms-infra/extensions/acquisition/index.js
Normal file
121545
packages/cms-infra/extensions/acquisition/index.js
Normal file
File diff suppressed because one or more lines are too long
27
packages/cms-infra/extensions/acquisition/package.json
Normal file
27
packages/cms-infra/extensions/acquisition/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "acquisition",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "endpoint",
|
||||||
|
"path": "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/pdf": "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"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/cms-infra/extensions/company-manager/index.js
Normal file
1
packages/cms-infra/extensions/company-manager/index.js
Normal file
File diff suppressed because one or more lines are too long
28
packages/cms-infra/extensions/company-manager/package.json
Normal file
28
packages/cms-infra/extensions/company-manager/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "company-manager",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "company manager"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,30 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "customer-manager",
|
"name": "customer-manager",
|
||||||
"description": "Custom High-Fidelity Customer & Company Management for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "supervisor_account",
|
"icon": "extension",
|
||||||
"version": "1.7.12",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
"directus-extension",
|
"directus-extension",
|
||||||
"directus-extension-module"
|
"directus-extension-module"
|
||||||
],
|
],
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Customer Manager"
|
"name": "customer manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,26 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "feedback-commander",
|
"name": "feedback-commander",
|
||||||
"description": "Custom High-Fidelity Feedback Management Extension for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "view_kanban",
|
"icon": "extension",
|
||||||
"version": "1.7.12",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
"directus-extension",
|
"directus-extension",
|
||||||
"directus-extension-module"
|
"directus-extension-module"
|
||||||
],
|
],
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Feedback Commander"
|
"name": "feedback commander"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,30 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "people-manager",
|
"name": "people-manager",
|
||||||
"description": "Custom High-Fidelity People Management for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "person",
|
"icon": "extension",
|
||||||
"version": "1.7.12",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
"directus-extension",
|
"directus-extension",
|
||||||
"directus-extension-module"
|
"directus-extension-module"
|
||||||
],
|
],
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "People Manager"
|
"name": "people manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
packages/cms-infra/extensions/unified-dashboard/index.js
Normal file
1
packages/cms-infra/extensions/unified-dashboard/index.js
Normal file
File diff suppressed because one or more lines are too long
27
packages/cms-infra/extensions/unified-dashboard/package.json
Normal file
27
packages/cms-infra/extensions/unified-dashboard/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "unified-dashboard",
|
||||||
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-module"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "module",
|
||||||
|
"path": "index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"host": "app",
|
||||||
|
"name": "unified dashboard"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"vue": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/cms-infra",
|
"name": "@mintel/cms-infra",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ collections:
|
|||||||
color: null
|
color: null
|
||||||
display_template: '{{name}}'
|
display_template: '{{name}}'
|
||||||
group: null
|
group: null
|
||||||
hidden: true
|
hidden: false
|
||||||
icon: business
|
icon: business
|
||||||
item_duplication_fields: null
|
item_duplication_fields: null
|
||||||
note: null
|
note: null
|
||||||
@@ -146,6 +146,30 @@ collections:
|
|||||||
versioning: false
|
versioning: false
|
||||||
schema:
|
schema:
|
||||||
name: visual_feedback_comments
|
name: visual_feedback_comments
|
||||||
|
- collection: customers
|
||||||
|
meta:
|
||||||
|
accountability: all
|
||||||
|
archive_app_filter: true
|
||||||
|
archive_field: null
|
||||||
|
archive_value: null
|
||||||
|
collapse: open
|
||||||
|
collection: customers
|
||||||
|
color: null
|
||||||
|
display_template: '{{company.name}}'
|
||||||
|
group: null
|
||||||
|
hidden: false
|
||||||
|
icon: handshake
|
||||||
|
item_duplication_fields: null
|
||||||
|
note: null
|
||||||
|
preview_url: null
|
||||||
|
singleton: false
|
||||||
|
sort: null
|
||||||
|
sort_field: null
|
||||||
|
translations: null
|
||||||
|
unarchive_value: null
|
||||||
|
versioning: false
|
||||||
|
schema:
|
||||||
|
name: customers
|
||||||
fields:
|
fields:
|
||||||
- collection: client_users
|
- collection: client_users
|
||||||
field: id
|
field: id
|
||||||
@@ -1959,13 +1983,204 @@ fields:
|
|||||||
validation_message: null
|
validation_message: null
|
||||||
width: half
|
width: half
|
||||||
schema:
|
schema:
|
||||||
name: person
|
foreign_key_column: null
|
||||||
table: visual_feedback_comments
|
- collection: customers
|
||||||
|
field: id
|
||||||
|
type: uuid
|
||||||
|
meta:
|
||||||
|
collection: customers
|
||||||
|
conditions: null
|
||||||
|
display: null
|
||||||
|
display_options: null
|
||||||
|
field: id
|
||||||
|
group: null
|
||||||
|
hidden: true
|
||||||
|
interface: null
|
||||||
|
note: null
|
||||||
|
options: null
|
||||||
|
readonly: false
|
||||||
|
required: false
|
||||||
|
searchable: true
|
||||||
|
sort: 1
|
||||||
|
special:
|
||||||
|
- uuid
|
||||||
|
translations: null
|
||||||
|
validation: null
|
||||||
|
validation_message: null
|
||||||
|
width: full
|
||||||
|
schema:
|
||||||
|
name: id
|
||||||
|
table: customers
|
||||||
data_type: char
|
data_type: char
|
||||||
default_value: null
|
default_value: null
|
||||||
max_length: 36
|
max_length: 36
|
||||||
numeric_precision: null
|
numeric_precision: null
|
||||||
numeric_scale: null
|
numeric_scale: null
|
||||||
|
is_nullable: false
|
||||||
|
is_unique: true
|
||||||
|
is_indexed: false
|
||||||
|
is_primary_key: true
|
||||||
|
is_generated: false
|
||||||
|
generation_expression: null
|
||||||
|
has_auto_increment: false
|
||||||
|
foreign_key_table: null
|
||||||
|
foreign_key_column: null
|
||||||
|
- collection: customers
|
||||||
|
field: company
|
||||||
|
type: uuid
|
||||||
|
meta:
|
||||||
|
collection: customers
|
||||||
|
conditions: null
|
||||||
|
display: null
|
||||||
|
display_options: null
|
||||||
|
field: company
|
||||||
|
group: null
|
||||||
|
hidden: false
|
||||||
|
interface: select-dropdown-m2o
|
||||||
|
note: null
|
||||||
|
options: null
|
||||||
|
readonly: false
|
||||||
|
required: true
|
||||||
|
searchable: true
|
||||||
|
sort: 2
|
||||||
|
special: null
|
||||||
|
translations: null
|
||||||
|
validation: null
|
||||||
|
validation_message: null
|
||||||
|
width: half
|
||||||
|
schema:
|
||||||
|
name: company
|
||||||
|
table: customers
|
||||||
|
data_type: char
|
||||||
|
default_value: null
|
||||||
|
max_length: 36
|
||||||
|
numeric_precision: null
|
||||||
|
numeric_scale: null
|
||||||
|
is_nullable: true
|
||||||
|
is_unique: false
|
||||||
|
is_indexed: false
|
||||||
|
is_primary_key: false
|
||||||
|
is_generated: false
|
||||||
|
generation_expression: null
|
||||||
|
has_auto_increment: false
|
||||||
|
foreign_key_table: companies
|
||||||
|
foreign_key_column: id
|
||||||
|
- collection: customers
|
||||||
|
field: contact_person
|
||||||
|
type: uuid
|
||||||
|
meta:
|
||||||
|
collection: customers
|
||||||
|
conditions: null
|
||||||
|
display: null
|
||||||
|
display_options: null
|
||||||
|
field: contact_person
|
||||||
|
group: null
|
||||||
|
hidden: false
|
||||||
|
interface: select-dropdown-m2o
|
||||||
|
note: null
|
||||||
|
options: null
|
||||||
|
readonly: false
|
||||||
|
required: false
|
||||||
|
searchable: true
|
||||||
|
sort: 3
|
||||||
|
special: null
|
||||||
|
translations: null
|
||||||
|
validation: null
|
||||||
|
validation_message: null
|
||||||
|
width: half
|
||||||
|
schema:
|
||||||
|
name: contact_person
|
||||||
|
table: customers
|
||||||
|
data_type: char
|
||||||
|
default_value: null
|
||||||
|
max_length: 36
|
||||||
|
numeric_precision: null
|
||||||
|
numeric_scale: null
|
||||||
|
is_nullable: true
|
||||||
|
is_unique: false
|
||||||
|
is_indexed: false
|
||||||
|
is_primary_key: false
|
||||||
|
is_generated: false
|
||||||
|
generation_expression: null
|
||||||
|
has_auto_increment: false
|
||||||
|
foreign_key_table: people
|
||||||
|
foreign_key_column: id
|
||||||
|
- collection: customers
|
||||||
|
field: status
|
||||||
|
type: string
|
||||||
|
meta:
|
||||||
|
collection: customers
|
||||||
|
conditions: null
|
||||||
|
display: null
|
||||||
|
display_options: null
|
||||||
|
field: status
|
||||||
|
group: null
|
||||||
|
hidden: false
|
||||||
|
interface: select-dropdown
|
||||||
|
note: null
|
||||||
|
options:
|
||||||
|
choices:
|
||||||
|
- text: Active
|
||||||
|
value: active
|
||||||
|
- text: Inactive
|
||||||
|
value: inactive
|
||||||
|
readonly: false
|
||||||
|
required: false
|
||||||
|
searchable: true
|
||||||
|
sort: 4
|
||||||
|
special: null
|
||||||
|
translations: null
|
||||||
|
validation: null
|
||||||
|
validation_message: null
|
||||||
|
width: half
|
||||||
|
schema:
|
||||||
|
name: status
|
||||||
|
table: customers
|
||||||
|
data_type: varchar
|
||||||
|
default_value: active
|
||||||
|
max_length: 255
|
||||||
|
numeric_precision: null
|
||||||
|
numeric_scale: null
|
||||||
|
is_nullable: true
|
||||||
|
is_unique: false
|
||||||
|
is_indexed: false
|
||||||
|
is_primary_key: false
|
||||||
|
is_generated: false
|
||||||
|
generation_expression: null
|
||||||
|
has_auto_increment: false
|
||||||
|
foreign_key_table: null
|
||||||
|
foreign_key_column: null
|
||||||
|
- collection: customers
|
||||||
|
field: notes
|
||||||
|
type: text
|
||||||
|
meta:
|
||||||
|
collection: customers
|
||||||
|
conditions: null
|
||||||
|
display: null
|
||||||
|
display_options: null
|
||||||
|
field: notes
|
||||||
|
group: null
|
||||||
|
hidden: false
|
||||||
|
interface: input-multiline
|
||||||
|
note: null
|
||||||
|
options: null
|
||||||
|
readonly: false
|
||||||
|
required: false
|
||||||
|
searchable: true
|
||||||
|
sort: 5
|
||||||
|
special: null
|
||||||
|
translations: null
|
||||||
|
validation: null
|
||||||
|
validation_message: null
|
||||||
|
width: full
|
||||||
|
schema:
|
||||||
|
name: notes
|
||||||
|
table: customers
|
||||||
|
data_type: text
|
||||||
|
default_value: null
|
||||||
|
max_length: null
|
||||||
|
numeric_precision: null
|
||||||
|
numeric_scale: null
|
||||||
is_nullable: true
|
is_nullable: true
|
||||||
is_unique: false
|
is_unique: false
|
||||||
is_indexed: false
|
is_indexed: false
|
||||||
@@ -1989,6 +2204,28 @@ systemFields:
|
|||||||
schema:
|
schema:
|
||||||
is_indexed: true
|
is_indexed: true
|
||||||
relations:
|
relations:
|
||||||
|
- collection: customers
|
||||||
|
field: company
|
||||||
|
related_collection: companies
|
||||||
|
schema:
|
||||||
|
on_update: null
|
||||||
|
on_delete: SET NULL
|
||||||
|
constraint_name: customers_company_foreign
|
||||||
|
table: customers
|
||||||
|
column: company
|
||||||
|
foreign_key_table: companies
|
||||||
|
foreign_key_column: id
|
||||||
|
- collection: customers
|
||||||
|
field: contact_person
|
||||||
|
related_collection: people
|
||||||
|
schema:
|
||||||
|
on_update: null
|
||||||
|
on_delete: SET NULL
|
||||||
|
constraint_name: customers_contact_person_foreign
|
||||||
|
table: customers
|
||||||
|
column: contact_person
|
||||||
|
foreign_key_table: people
|
||||||
|
foreign_key_column: id
|
||||||
- collection: client_users
|
- collection: client_users
|
||||||
field: company
|
field: company
|
||||||
related_collection: companies
|
related_collection: companies
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
xmKX5
|
--tVj
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "company-manager",
|
"name": "company-manager",
|
||||||
"description": "Central Company Management for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "business",
|
"icon": "extension",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
@@ -11,17 +11,18 @@
|
|||||||
],
|
],
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Company Manager"
|
"name": "company manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
packages/company-manager/src/index.ts
Normal file
14
packages/company-manager/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineModule } from '@directus/extensions-sdk';
|
||||||
|
import ModuleComponent from './module.vue';
|
||||||
|
|
||||||
|
export default defineModule({
|
||||||
|
id: 'company-manager',
|
||||||
|
name: 'Company Manager',
|
||||||
|
icon: 'business',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ModuleComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
217
packages/company-manager/src/module.vue
Normal file
217
packages/company-manager/src/module.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<MintelManagerLayout
|
||||||
|
title="Company Manager"
|
||||||
|
:item-title="selectedCompany?.name || 'Firma wählen'"
|
||||||
|
:is-empty="!selectedCompany"
|
||||||
|
empty-title="Firma auswählen"
|
||||||
|
empty-icon="business"
|
||||||
|
:notice="feedback"
|
||||||
|
@close-notice="feedback = null"
|
||||||
|
>
|
||||||
|
<template #navigation>
|
||||||
|
<v-list nav>
|
||||||
|
<v-list-item @click="openCreateDrawer" clickable>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon name="add" color="var(--theme--primary)" />
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-text-overflow text="Neue Firma anlegen" />
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-for="company in companies"
|
||||||
|
:key="company.id"
|
||||||
|
:active="selectedCompany?.id === company.id"
|
||||||
|
class="nav-item"
|
||||||
|
clickable
|
||||||
|
@click="selectCompany(company)"
|
||||||
|
>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon name="business" />
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-text-overflow :text="company.name" />
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #subtitle>
|
||||||
|
<template v-if="selectedCompany">
|
||||||
|
{{ selectedCompany.domain || 'Keine Domain angegeben' }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<v-button secondary rounded icon v-tooltip.bottom="'Firma bearbeiten'" @click="openEditDrawer">
|
||||||
|
<v-icon name="edit" />
|
||||||
|
</v-button>
|
||||||
|
<v-button danger rounded icon v-tooltip.bottom="'Firma löschen'" @click="deleteCompany">
|
||||||
|
<v-icon name="delete" />
|
||||||
|
</v-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty-state>
|
||||||
|
Wähle eine Firma in der Navigation aus oder
|
||||||
|
<v-button x-small @click="openCreateDrawer">erstelle eine neue Firma</v-button>.
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="selectedCompany" class="details-grid">
|
||||||
|
<div class="detail-item full">
|
||||||
|
<span class="label">Notizen / Adresse</span>
|
||||||
|
<p class="value">{{ selectedCompany.notes || '---' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create/Edit Drawer -->
|
||||||
|
<v-drawer
|
||||||
|
v-model="drawerActive"
|
||||||
|
:title="isEditing ? 'Firma bearbeiten' : 'Neue Firma anlegen'"
|
||||||
|
icon="business"
|
||||||
|
@cancel="drawerActive = false"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div class="drawer-content">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="field">
|
||||||
|
<span class="label">Firmenname</span>
|
||||||
|
<v-input v-model="form.name" placeholder="z.B. Schmidt GmbH" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="label">Domain / Website</span>
|
||||||
|
<v-input v-model="form.domain" placeholder="example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="label">Notizen / Adresse</span>
|
||||||
|
<v-textarea v-model="form.notes" placeholder="z.B. Branche, Adresse, etc." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-actions">
|
||||||
|
<v-button primary block :loading="saving" @click="saveCompany">
|
||||||
|
Firma speichern
|
||||||
|
</v-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-drawer>
|
||||||
|
</MintelManagerLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useApi } from '@directus/extensions-sdk';
|
||||||
|
import { MintelManagerLayout } from '@mintel/directus-extension-toolkit';
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const companies = ref([]);
|
||||||
|
const selectedCompany = ref(null);
|
||||||
|
const feedback = ref(null);
|
||||||
|
const saving = ref(false);
|
||||||
|
const drawerActive = ref(false);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
domain: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const resp = await api.get('/items/companies', {
|
||||||
|
params: { sort: 'name' }
|
||||||
|
});
|
||||||
|
companies.value = resp.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch companies:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCompany(company: any) {
|
||||||
|
selectedCompany.value = company;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDrawer() {
|
||||||
|
isEditing.value = false;
|
||||||
|
form.value = {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
domain: '',
|
||||||
|
notes: ''
|
||||||
|
};
|
||||||
|
drawerActive.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDrawer() {
|
||||||
|
isEditing.value = true;
|
||||||
|
form.value = {
|
||||||
|
id: selectedCompany.value.id,
|
||||||
|
name: selectedCompany.value.name,
|
||||||
|
domain: selectedCompany.value.domain,
|
||||||
|
notes: selectedCompany.value.notes
|
||||||
|
};
|
||||||
|
drawerActive.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCompany() {
|
||||||
|
if (!form.value.name) {
|
||||||
|
feedback.value = { type: 'danger', message: 'Firmenname ist erforderlich.' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
let updatedItem;
|
||||||
|
if (isEditing.value) {
|
||||||
|
const res = await api.patch(`/items/companies/${form.value.id}`, form.value);
|
||||||
|
updatedItem = res.data.data;
|
||||||
|
feedback.value = { type: 'success', message: 'Firma aktualisiert!' };
|
||||||
|
} else {
|
||||||
|
const res = await api.post('/items/companies', form.value);
|
||||||
|
updatedItem = res.data.data;
|
||||||
|
feedback.value = { type: 'success', message: 'Firma angelegt!' };
|
||||||
|
}
|
||||||
|
drawerActive.value = false;
|
||||||
|
await fetchData();
|
||||||
|
if (updatedItem) {
|
||||||
|
selectedCompany.value = companies.value.find(c => c.id === updatedItem.id) || updatedItem;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
feedback.value = { type: 'danger', message: error.message };
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCompany() {
|
||||||
|
if (!confirm('Soll diese Firma wirklich gelöscht werden?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/items/companies/${selectedCompany.value.id}`);
|
||||||
|
feedback.value = { type: 'success', message: 'Firma gelöscht.' };
|
||||||
|
selectedCompany.value = null;
|
||||||
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
feedback.value = { type: 'danger', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchData);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.details-grid { display: flex; flex-direction: column; gap: 24px; }
|
||||||
|
.detail-item { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.detail-item.full { width: 100%; }
|
||||||
|
.label { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--theme--foreground-subdued); letter-spacing: 0.5px; }
|
||||||
|
.value { font-size: 16px; font-weight: 500; }
|
||||||
|
.drawer-content { padding: 24px; display: flex; flex-direction: column; gap: 32px; }
|
||||||
|
.form-section { display: flex; flex-direction: column; gap: 20px; }
|
||||||
|
.field { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.drawer-actions { margin-top: 24px; }
|
||||||
|
</style>
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "customer-manager",
|
"name": "customer-manager",
|
||||||
"description": "Custom High-Fidelity Customer & Company Management for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "supervisor_account",
|
"icon": "extension",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
@@ -11,17 +11,18 @@
|
|||||||
],
|
],
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Customer Manager"
|
"name": "customer manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,161 +1,191 @@
|
|||||||
<template>
|
<template>
|
||||||
<private-view title="Customer Manager">
|
<MintelManagerLayout
|
||||||
|
title="Customer Manager"
|
||||||
|
:item-title="selectedItem?.company?.name || 'Kunde wählen'"
|
||||||
|
:is-empty="!selectedItem"
|
||||||
|
empty-title="Kunde auswählen"
|
||||||
|
empty-icon="handshake"
|
||||||
|
:notice="notice"
|
||||||
|
@close-notice="notice = null"
|
||||||
|
>
|
||||||
<template #navigation>
|
<template #navigation>
|
||||||
<v-list nav>
|
<v-list nav>
|
||||||
<v-list-item @click="openCreateCompany" clickable>
|
<v-list-item @click="openCreateDrawer" clickable>
|
||||||
<v-list-item-icon><v-icon name="add" color="var(--theme--primary)" /></v-list-item-icon>
|
<v-list-item-icon><v-icon name="add" color="var(--theme--primary)" /></v-list-item-icon>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-text-overflow text="Neue Firma anlegen" />
|
<v-text-overflow text="Neuen Kunden verlinken" />
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="company in companies"
|
v-for="item in items"
|
||||||
:key="company.id"
|
:key="item.id"
|
||||||
:active="selectedCompany?.id === company.id"
|
:active="selectedItem?.id === item.id"
|
||||||
class="company-item"
|
class="nav-item"
|
||||||
clickable
|
clickable
|
||||||
@click="selectCompany(company)"
|
@click="selectItem(item)"
|
||||||
>
|
>
|
||||||
<v-list-item-icon><v-icon name="business" /></v-list-item-icon>
|
<v-list-item-icon><v-icon name="business" /></v-list-item-icon>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-text-overflow :text="company.name" />
|
<v-text-overflow :text="item.company?.name" />
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #title-outer:after>
|
<template #subtitle>
|
||||||
<v-notice v-if="notice" :type="notice.type" @close="notice = null" dismissible>
|
<template v-if="selectedItem">
|
||||||
{{ notice.message }}
|
{{ clientUsers.length }} Portal-Nutzer · {{ selectedItem.company?.domain }}
|
||||||
</v-notice>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<template #actions>
|
||||||
<div v-if="!selectedCompany" class="empty-state">
|
<v-button secondary rounded icon v-tooltip.bottom="'Kunden-Verlinkung bearbeiten'" @click="openEditDrawer">
|
||||||
<v-info title="Firmen auswählen" icon="business" center>
|
<v-icon name="edit" />
|
||||||
Wähle eine Firma in der Navigation aus oder
|
</v-button>
|
||||||
<v-button x-small @click="openCreateCompany">erstelle eine neue Firma</v-button>.
|
<v-button primary @click="openCreateClientUser">
|
||||||
</v-info>
|
Portal-Nutzer hinzufügen
|
||||||
</div>
|
</v-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template #empty-state>
|
||||||
<header class="header">
|
Wähle einen Kunden aus der Liste oder
|
||||||
<div class="header-left">
|
<v-button x-small @click="openCreateDrawer">verlinke eine neue Firma</v-button>.
|
||||||
<h1 class="title">{{ selectedCompany.name }}</h1>
|
</template>
|
||||||
<p class="subtitle">{{ employees.length }} Kunden-Mitarbeiter</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<v-button secondary rounded icon v-tooltip.bottom="'Firma bearbeiten'" @click="openEditCompany">
|
|
||||||
<v-icon name="edit" />
|
|
||||||
</v-button>
|
|
||||||
<v-button primary @click="openCreateEmployee">
|
|
||||||
Mitarbeiter hinzufügen
|
|
||||||
</v-button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<v-table
|
<!-- Main Content: Client Users Table -->
|
||||||
:headers="tableHeaders"
|
<v-table
|
||||||
:items="employees"
|
:headers="tableHeaders"
|
||||||
:loading="loading"
|
:items="clientUsers"
|
||||||
class="clickable-table"
|
:loading="loading"
|
||||||
fixed-header
|
class="clickable-table"
|
||||||
@click:row="onRowClick"
|
fixed-header
|
||||||
>
|
@click:row="onRowClick"
|
||||||
<template #[`item.name`]="{ item }">
|
|
||||||
<div class="user-cell">
|
|
||||||
<v-avatar :name="item.first_name" x-small />
|
|
||||||
<span class="user-name">{{ item.first_name }} {{ item.last_name }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #[`item.last_invited`]="{ item }">
|
|
||||||
<span v-if="item.last_invited" class="status-date">
|
|
||||||
{{ formatDate(item.last_invited) }}
|
|
||||||
</span>
|
|
||||||
<v-chip v-else x-small>Noch nie</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-table>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Drawer: Company Form -->
|
|
||||||
<v-drawer
|
|
||||||
v-model="drawerCompanyActive"
|
|
||||||
:title="isEditingCompany ? 'Firma bearbeiten' : 'Neue Firma anlegen'"
|
|
||||||
icon="business"
|
|
||||||
@cancel="drawerCompanyActive = false"
|
|
||||||
>
|
>
|
||||||
<div v-if="drawerCompanyActive" class="drawer-content">
|
<template #[`item.name`]="{ item }">
|
||||||
<div class="form-section">
|
<div class="user-cell">
|
||||||
<div class="field">
|
<v-avatar :name="item.first_name" x-small />
|
||||||
<span class="label">Firmenname</span>
|
<span class="user-name">{{ item.first_name }} {{ item.last_name }}</span>
|
||||||
<v-input v-model="companyForm.name" placeholder="z.B. KLZ Cables" autofocus />
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
|
||||||
|
<template #[`item.last_invited`]="{ item }">
|
||||||
|
<span v-if="item.last_invited" class="status-date">
|
||||||
|
{{ formatDate(item.last_invited) }}
|
||||||
|
</span>
|
||||||
|
<v-chip v-else x-small>Noch nie</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-table>
|
||||||
|
|
||||||
|
<!-- Drawer: Customer (Link) Form -->
|
||||||
|
<v-drawer
|
||||||
|
v-model="drawerActive"
|
||||||
|
:title="isEditing ? 'Kunden-Verlinkung bearbeiten' : 'Kunden verlinken'"
|
||||||
|
icon="handshake"
|
||||||
|
@cancel="drawerActive = false"
|
||||||
|
>
|
||||||
|
<div v-if="drawerActive" class="drawer-content">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="field">
|
||||||
|
<span class="label">Organisation / Firma</span>
|
||||||
|
<MintelSelect
|
||||||
|
v-model="form.company"
|
||||||
|
:items="companyOptions"
|
||||||
|
placeholder="Firma auswählen..."
|
||||||
|
allow-add
|
||||||
|
@add="openQuickAdd('company')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<span class="label">Haupt-Ansprechpartner (optional)</span>
|
||||||
|
<MintelSelect
|
||||||
|
v-model="form.contact_person"
|
||||||
|
:items="peopleOptions"
|
||||||
|
placeholder="Person auswählen..."
|
||||||
|
allow-add
|
||||||
|
@add="openQuickAdd('person')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<span class="label">Status</span>
|
||||||
|
<v-select
|
||||||
|
v-model="form.status"
|
||||||
|
:items="[
|
||||||
|
{ text: 'Aktiv', value: 'active' },
|
||||||
|
{ text: 'Inaktiv', value: 'inactive' }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<span class="label">Notizen</span>
|
||||||
|
<v-textarea v-model="form.notes" placeholder="Besonderheiten zu diesem Kunden..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="drawer-actions">
|
<div class="drawer-actions">
|
||||||
<v-button primary block :loading="saving" @click="saveCompany">Speichern</v-button>
|
<v-button primary block :loading="saving" @click="saveItem">Speichern</v-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-drawer>
|
</v-drawer>
|
||||||
|
|
||||||
<!-- Drawer: Employee Form -->
|
<!-- Drawer: Client User Form -->
|
||||||
<v-drawer
|
<v-drawer
|
||||||
v-model="drawerEmployeeActive"
|
v-model="drawerUserActive"
|
||||||
:title="isEditingEmployee ? 'Mitarbeiter bearbeiten' : 'Neuen Mitarbeiter anlegen'"
|
:title="isEditingUser ? 'Portal-Nutzer bearbeiten' : 'Neuen Portal-Nutzer anlegen'"
|
||||||
icon="person"
|
icon="person"
|
||||||
@cancel="drawerEmployeeActive = false"
|
@cancel="drawerUserActive = false"
|
||||||
>
|
>
|
||||||
<div v-if="drawerEmployeeActive" class="drawer-content">
|
<div v-if="drawerUserActive" class="drawer-content">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">Vorname</span>
|
<span class="label">Vorname</span>
|
||||||
<v-input v-model="employeeForm.first_name" placeholder="Vorname" autofocus />
|
<v-input v-model="userForm.first_name" placeholder="Vorname" autofocus />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">Nachname</span>
|
<span class="label">Nachname</span>
|
||||||
<v-input v-model="employeeForm.last_name" placeholder="Nachname" />
|
<v-input v-model="userForm.last_name" placeholder="Nachname" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">E-Mail</span>
|
<span class="label">E-Mail</span>
|
||||||
<v-input v-model="employeeForm.email" placeholder="E-Mail Adresse" type="email" />
|
<v-input v-model="userForm.email" placeholder="E-Mail Adresse" type="email" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">Zentrale Person (Verknüpfung)</span>
|
<span class="label">Zentrale Person (Verknüpfung)</span>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="employeeForm.person"
|
v-model="userForm.person"
|
||||||
:items="peopleOptions"
|
:items="peopleOptions"
|
||||||
placeholder="Person aus dem People Manager auswählen..."
|
placeholder="Master-Person auswählen..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-divider v-if="isEditingEmployee" />
|
<v-divider v-if="isEditingUser" />
|
||||||
|
|
||||||
<div v-if="isEditingEmployee" class="field">
|
<div v-if="isEditingUser" class="field">
|
||||||
<span class="label">Temporäres Passwort</span>
|
<span class="label">Temporäres Passwort</span>
|
||||||
<v-input v-model="employeeForm.temporary_password" readonly class="password-input" />
|
<v-input v-model="userForm.temporary_password" readonly class="password-input" />
|
||||||
<p class="field-note">Wird beim Senden der Zugangsdaten automatisch generiert.</p>
|
<p class="field-note">Wird beim Senden der Zugangsdaten automatisch generiert.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-actions">
|
<div class="drawer-actions">
|
||||||
<v-button primary block :loading="saving" @click="saveEmployee">Daten speichern</v-button>
|
<v-button primary block :loading="saving" @click="saveClientUser">Daten speichern</v-button>
|
||||||
|
|
||||||
<template v-if="isEditingEmployee">
|
<template v-if="isEditingUser">
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-button
|
<v-button
|
||||||
v-tooltip.bottom="'Generiert PW, speichert es und sendet E-Mail'"
|
v-tooltip.bottom="'Generiert PW, speichert es und sendet E-Mail'"
|
||||||
secondary
|
secondary
|
||||||
block
|
block
|
||||||
:loading="invitingId === employeeForm.id"
|
:loading="invitingId === userForm.id"
|
||||||
@click="inviteUser(employeeForm)"
|
@click="inviteUser(userForm)"
|
||||||
>
|
>
|
||||||
<v-icon name="send" left /> Zugangsdaten senden
|
<v-icon name="send" left /> Zugangsdaten senden
|
||||||
</v-button>
|
</v-button>
|
||||||
@@ -163,38 +193,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-drawer>
|
</v-drawer>
|
||||||
</private-view>
|
</MintelManagerLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, nextTick, computed } from 'vue';
|
import { ref, onMounted, nextTick, computed } from 'vue';
|
||||||
import { useApi } from '@directus/extensions-sdk';
|
import { useApi } from '@directus/extensions-sdk';
|
||||||
|
import { MintelManagerLayout, MintelSelect } from '@mintel/directus-extension-toolkit';
|
||||||
|
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
const companies = ref<any[]>([]);
|
const items = ref<any[]>([]);
|
||||||
const selectedCompany = ref<any>(null);
|
const selectedItem = ref<any>(null);
|
||||||
const employees = ref<any[]>([]);
|
const clientUsers = ref<any[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const invitingId = ref<string | null>(null);
|
const invitingId = ref<string | null>(null);
|
||||||
const notice = ref<{ type: string; message: string } | null>(null);
|
const notice = ref<{ type: string; message: string } | null>(null);
|
||||||
|
|
||||||
// Forms State
|
const companies = ref<any[]>([]);
|
||||||
const drawerCompanyActive = ref(false);
|
const people = ref<any[]>([]);
|
||||||
const isEditingCompany = ref(false);
|
|
||||||
const companyForm = ref({ id: '', name: '' });
|
|
||||||
|
|
||||||
const drawerEmployeeActive = ref(false);
|
const drawerActive = ref(false);
|
||||||
const isEditingEmployee = ref(false);
|
const isEditing = ref(false);
|
||||||
const employeeForm = ref({
|
const form = ref({ id: null, company: null, contact_person: null, status: 'active', notes: '' });
|
||||||
id: '',
|
|
||||||
first_name: '',
|
const drawerUserActive = ref(false);
|
||||||
last_name: '',
|
const isEditingUser = ref(false);
|
||||||
email: '',
|
const userForm = ref({ id: '', first_name: '', last_name: '', email: '', person: null, temporary_password: '' });
|
||||||
person: null,
|
|
||||||
temporary_password: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableHeaders = [
|
const tableHeaders = [
|
||||||
{ text: 'Name', value: 'name', sortable: true },
|
{ text: 'Name', value: 'name', sortable: true },
|
||||||
@@ -202,180 +228,159 @@ const tableHeaders = [
|
|||||||
{ text: 'Zuletzt eingeladen', value: 'last_invited', sortable: true }
|
{ text: 'Zuletzt eingeladen', value: 'last_invited', sortable: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
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} (${p.email})`, value: p.id })));
|
||||||
const peopleOptions = computed(() =>
|
|
||||||
people.value.map(p => ({
|
|
||||||
text: `${p.first_name} ${p.last_name} (${p.email})`,
|
|
||||||
value: p.id
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const [companiesResp, peopleResp] = await Promise.all([
|
loading.value = true;
|
||||||
api.get('/items/companies', { params: { sort: 'name', fields: ['id', 'name'] } }),
|
try {
|
||||||
api.get('/items/people', { params: { sort: 'last_name' } })
|
const [custResp, compResp, peopleResp] = await Promise.all([
|
||||||
]);
|
api.get('/items/customers', { params: { fields: ['*', 'company.*', 'contact_person.*'], sort: 'company.name' } }),
|
||||||
companies.value = companiesResp.data.data;
|
api.get('/items/companies', { params: { sort: 'name' } }),
|
||||||
people.value = peopleResp.data.data;
|
api.get('/items/people', { params: { sort: 'last_name' } })
|
||||||
|
]);
|
||||||
|
items.value = custResp.data.data;
|
||||||
|
companies.value = compResp.data.data;
|
||||||
|
people.value = peopleResp.data.data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectCompany(company: any) {
|
async function selectItem(item: any) {
|
||||||
selectedCompany.value = company;
|
selectedItem.value = item;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/items/client_users', {
|
const res = await api.get('/items/client_users', {
|
||||||
params: {
|
params: {
|
||||||
filter: { company: { _eq: company.id } },
|
filter: { company: { _eq: item.company.id } },
|
||||||
fields: ['*', 'person.*'],
|
fields: ['*', 'person.*'],
|
||||||
sort: 'first_name',
|
sort: 'first_name',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
employees.value = res.data.data;
|
clientUsers.value = res.data.data;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Company Actions
|
function openCreateDrawer() {
|
||||||
function openCreateCompany() {
|
isEditing.value = false;
|
||||||
isEditingCompany.value = false;
|
form.value = { id: null, company: null, contact_person: null, status: 'active', notes: '' };
|
||||||
companyForm.value = { id: '', name: '' };
|
drawerActive.value = true;
|
||||||
drawerCompanyActive.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEditCompany() {
|
function openEditDrawer() {
|
||||||
if (!selectedCompany.value) return;
|
if (!selectedItem.value) return;
|
||||||
companyForm.value = {
|
isEditing.value = true;
|
||||||
id: selectedCompany.value.id,
|
form.value = {
|
||||||
name: selectedCompany.value.name
|
id: selectedItem.value.id,
|
||||||
|
company: selectedItem.value.company?.id || selectedItem.value.company,
|
||||||
|
contact_person: selectedItem.value.contact_person?.id || selectedItem.value.contact_person,
|
||||||
|
status: selectedItem.value.status,
|
||||||
|
notes: selectedItem.value.notes
|
||||||
};
|
};
|
||||||
isEditingCompany.value = true;
|
drawerActive.value = true;
|
||||||
await nextTick();
|
|
||||||
drawerCompanyActive.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCompany() {
|
async function saveItem() {
|
||||||
if (!companyForm.value.name) return;
|
if (!form.value.company) return;
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
if (isEditingCompany.value) {
|
if (isEditing.value) {
|
||||||
await api.patch(`/items/companies/${companyForm.value.id}`, { name: companyForm.value.name });
|
await api.patch(`/items/customers/${form.value.id}`, form.value);
|
||||||
notice.value = { type: 'success', message: 'Firma aktualisiert!' };
|
notice.value = { type: 'success', message: 'Kunde aktualisiert!' };
|
||||||
} else {
|
} else {
|
||||||
await api.post('/items/companies', { name: companyForm.value.name });
|
await api.post('/items/customers', form.value);
|
||||||
notice.value = { type: 'success', message: 'Firma angelegt!' };
|
notice.value = { type: 'success', message: 'Neuer Kunde verlinkt!' };
|
||||||
}
|
}
|
||||||
drawerCompanyActive.value = false;
|
drawerActive.value = false;
|
||||||
await fetchCompanies();
|
await fetchData();
|
||||||
if (selectedCompany.value?.id === companyForm.value.id) {
|
if (form.value.id) {
|
||||||
selectedCompany.value.name = companyForm.value.name;
|
const updated = items.value.find(i => i.id === form.value.id);
|
||||||
}
|
if (updated) selectItem(updated);
|
||||||
} catch (e: any) {
|
}
|
||||||
notice.value = { type: 'danger', message: e.message };
|
} catch (e: any) {
|
||||||
} finally {
|
notice.value = { type: 'danger', message: e.message };
|
||||||
saving.value = false;
|
} finally {
|
||||||
}
|
saving.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Employee Actions
|
// Client User Actions
|
||||||
function openCreateEmployee() {
|
function openCreateClientUser() {
|
||||||
isEditingEmployee.value = false;
|
isEditingUser.value = false;
|
||||||
employeeForm.value = { id: '', first_name: '', last_name: '', email: '', temporary_password: '' };
|
userForm.value = { id: '', first_name: '', last_name: '', email: '', person: null, temporary_password: '' };
|
||||||
drawerEmployeeActive.value = true;
|
drawerUserActive.value = true;
|
||||||
}
|
|
||||||
|
|
||||||
async function openEditEmployee(item: any) {
|
|
||||||
employeeForm.value = {
|
|
||||||
id: item.id || '',
|
|
||||||
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;
|
|
||||||
await nextTick();
|
|
||||||
drawerEmployeeActive.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveEmployee() {
|
|
||||||
if (!employeeForm.value.email || !selectedCompany.value) return;
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
if (isEditingEmployee.value) {
|
|
||||||
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,
|
|
||||||
person: employeeForm.value.person
|
|
||||||
});
|
|
||||||
notice.value = { type: 'success', message: 'Mitarbeiter aktualisiert!' };
|
|
||||||
} else {
|
|
||||||
await api.post('/items/client_users', {
|
|
||||||
first_name: employeeForm.value.first_name,
|
|
||||||
last_name: employeeForm.value.last_name,
|
|
||||||
email: employeeForm.value.email,
|
|
||||||
company: selectedCompany.value.id,
|
|
||||||
person: employeeForm.value.person
|
|
||||||
});
|
|
||||||
notice.value = { type: 'success', message: 'Mitarbeiter angelegt!' };
|
|
||||||
}
|
|
||||||
drawerEmployeeActive.value = false;
|
|
||||||
await selectCompany(selectedCompany.value);
|
|
||||||
} catch (e: any) {
|
|
||||||
notice.value = { type: 'danger', message: e.message };
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function inviteUser(user: any) {
|
|
||||||
invitingId.value = user.id;
|
|
||||||
try {
|
|
||||||
await api.post(`/flows/trigger/33443f6b-cec7-4668-9607-f33ea674d501`, [user.id]);
|
|
||||||
notice.value = { type: 'success', message: `Zugangsdaten für ${user.first_name} versendet. 📧` };
|
|
||||||
await selectCompany(selectedCompany.value);
|
|
||||||
if (drawerEmployeeActive.value && employeeForm.value.id === user.id) {
|
|
||||||
const updated = employees.value.find(e => e.id === user.id);
|
|
||||||
if (updated) {
|
|
||||||
employeeForm.value.temporary_password = updated.temporary_password;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
notice.value = { type: 'danger', message: `Fehler: ${e.message}` };
|
|
||||||
} finally {
|
|
||||||
invitingId.value = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRowClick(event: any) {
|
function onRowClick(event: any) {
|
||||||
const item = event?.item || event;
|
const item = event?.item || event;
|
||||||
if (item && item.id) {
|
if (item && item.id) {
|
||||||
openEditEmployee(item);
|
userForm.value = {
|
||||||
}
|
id: item.id,
|
||||||
|
first_name: item.first_name,
|
||||||
|
last_name: item.last_name,
|
||||||
|
email: item.email,
|
||||||
|
person: item.person?.id || item.person,
|
||||||
|
temporary_password: item.temporary_password
|
||||||
|
};
|
||||||
|
isEditingUser.value = true;
|
||||||
|
drawerUserActive.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveClientUser() {
|
||||||
|
if (!userForm.value.email || !selectedItem.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
first_name: userForm.value.first_name,
|
||||||
|
last_name: userForm.value.last_name,
|
||||||
|
email: userForm.value.email,
|
||||||
|
person: userForm.value.person,
|
||||||
|
company: selectedItem.value.company.id
|
||||||
|
};
|
||||||
|
if (isEditingUser.value) {
|
||||||
|
await api.patch(`/items/client_users/${userForm.value.id}`, payload);
|
||||||
|
} else {
|
||||||
|
await api.post('/items/client_users', payload);
|
||||||
|
}
|
||||||
|
drawerUserActive.value = false;
|
||||||
|
await selectItem(selectedItem.value);
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inviteUser(user: any) {
|
||||||
|
invitingId.value = user.id;
|
||||||
|
try {
|
||||||
|
await api.post(`/flows/trigger/33443f6b-cec7-4668-9607-f33ea674d501`, [user.id]);
|
||||||
|
notice.value = { type: 'success', message: `Zugangsdaten versendet. 📧` };
|
||||||
|
await selectItem(selectedItem.value);
|
||||||
|
} finally {
|
||||||
|
invitingId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openQuickAdd(type: 'company' | 'person') {
|
||||||
|
// Quick add logic can involve opening another drawer or navigating
|
||||||
|
// For now, we'll just show a notice
|
||||||
|
notice.value = { type: 'info', message: `${type === 'company' ? 'Firma' : 'Person'} im jeweiligen Manager anlegen.` };
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateStr: string) {
|
function formatDate(dateStr: string) {
|
||||||
return new Date(dateStr).toLocaleString('de-DE', {
|
return new Date(dateStr).toLocaleString('de-DE', {
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||||
hour: '2-digit', minute: '2-digit'
|
hour: '2-digit', minute: '2-digit'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(fetchData);
|
||||||
fetchData();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.content-wrapper { padding: 32px; height: 100%; display: flex; flex-direction: column; }
|
|
||||||
.company-item { cursor: pointer; }
|
|
||||||
.header { margin-bottom: 24px; display: flex; justify-content: space-between; align-items: flex-end; }
|
|
||||||
.header-right { display: flex; gap: 12px; }
|
|
||||||
.title { font-size: 24px; font-weight: 800; margin-bottom: 4px; }
|
|
||||||
.subtitle { color: var(--theme--foreground-subdued); font-size: 14px; }
|
|
||||||
.empty-state { height: 100%; display: flex; align-items: center; justify-content: center; }
|
|
||||||
.user-cell { display: flex; align-items: center; gap: 12px; }
|
.user-cell { display: flex; align-items: center; gap: 12px; }
|
||||||
.user-name { font-weight: 600; }
|
.user-name { font-weight: 600; }
|
||||||
.status-date { font-size: 12px; color: var(--theme--foreground-subdued); }
|
.status-date { font-size: 12px; color: var(--theme--foreground-subdued); }
|
||||||
@@ -385,6 +390,7 @@ onMounted(() => {
|
|||||||
.label { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--theme--foreground-subdued); letter-spacing: 0.5px; }
|
.label { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--theme--foreground-subdued); letter-spacing: 0.5px; }
|
||||||
.field-note { font-size: 11px; color: var(--theme--foreground-subdued); margin-top: 4px; }
|
.field-note { font-size: 11px; color: var(--theme--foreground-subdued); margin-top: 4px; }
|
||||||
.drawer-actions { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
|
.drawer-actions { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
|
||||||
.password-input :deep(textarea) {
|
.password-input :deep(textarea) {
|
||||||
font-family: var(--family-monospace);
|
font-family: var(--family-monospace);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -394,5 +400,4 @@ onMounted(() => {
|
|||||||
|
|
||||||
.clickable-table :deep(tbody tr) { cursor: pointer; transition: background-color 0.2s ease; }
|
.clickable-table :deep(tbody tr) { cursor: pointer; transition: background-color 0.2s ease; }
|
||||||
.clickable-table :deep(tbody tr:hover) { background-color: var(--theme--background-subdued) !important; }
|
.clickable-table :deep(tbody tr:hover) { background-color: var(--theme--background-subdued) !important; }
|
||||||
:deep(.v-list-item) { cursor: pointer !important; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/directus-extension-toolkit",
|
"name": "@mintel/directus-extension-toolkit",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"description": "Shared toolkit for Directus extensions in the Mintel ecosystem",
|
"description": "Shared toolkit for Directus extensions in the Mintel ecosystem",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
...globals.es2021,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ignores: ["**/dist/**", "**/node_modules/**", "**/.next/**", "**/build/**"],
|
ignores: ["**/dist/**", "**/node_modules/**", "**/.next/**", "**/build/**"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/eslint-config",
|
"name": "@mintel/eslint-config",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "feedback-commander",
|
"name": "feedback-commander",
|
||||||
"description": "Custom High-Fidelity Feedback Management Extension for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "view_kanban",
|
"icon": "extension",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
],
|
],
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Feedback Commander"
|
"name": "feedback commander"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/gatekeeper",
|
"name": "@mintel/gatekeeper",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/husky-config",
|
"name": "@mintel/husky-config",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- '*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
skip_long_checks:
|
skip_long_checks:
|
||||||
@@ -65,11 +65,6 @@ jobs:
|
|||||||
PRJ_ID="${{ github.event.repository.name }}"
|
PRJ_ID="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then
|
if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then
|
||||||
if [[ "$COMMIT_MSG" =~ ^chore: ]]; then
|
|
||||||
TARGET="skip"
|
|
||||||
GOTIFY_TITLE="ℹ️ Skip Deploy (Chore)"
|
|
||||||
GOTIFY_PRIORITY=2
|
|
||||||
else
|
|
||||||
TARGET="testing"
|
TARGET="testing"
|
||||||
IMAGE_TAG="main-${SHORT_SHA}"
|
IMAGE_TAG="main-${SHORT_SHA}"
|
||||||
ENV_FILE=".env.testing"
|
ENV_FILE=".env.testing"
|
||||||
@@ -81,9 +76,8 @@ jobs:
|
|||||||
IS_PROD="false"
|
IS_PROD="false"
|
||||||
GOTIFY_TITLE="🧪 Testing-Deploy"
|
GOTIFY_TITLE="🧪 Testing-Deploy"
|
||||||
GOTIFY_PRIORITY=4
|
GOTIFY_PRIORITY=4
|
||||||
fi
|
|
||||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
if [[ "$TAG" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
TARGET="production"
|
TARGET="production"
|
||||||
IMAGE_TAG="$TAG"
|
IMAGE_TAG="$TAG"
|
||||||
ENV_FILE=".env.prod"
|
ENV_FILE=".env.prod"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/infra",
|
"name": "@mintel/infra",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/mail",
|
"name": "@mintel/mail",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-config",
|
"name": "@mintel/next-config",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-feedback",
|
"name": "@mintel/next-feedback",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -31,8 +31,20 @@ interface Feedback {
|
|||||||
comments: FeedbackComment[];
|
comments: FeedbackComment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeedbackOverlay() {
|
export function FeedbackOverlay({
|
||||||
const [isActive, setIsActive] = useState(false);
|
isActive: externalIsActive,
|
||||||
|
onActiveChange
|
||||||
|
}: {
|
||||||
|
isActive?: boolean;
|
||||||
|
onActiveChange?: (active: boolean) => void
|
||||||
|
}) {
|
||||||
|
const [internalIsActive, setInternalIsActive] = useState(false);
|
||||||
|
|
||||||
|
const isActive = externalIsActive !== undefined ? externalIsActive : internalIsActive;
|
||||||
|
const setIsActive = (val: boolean) => {
|
||||||
|
if (externalIsActive === undefined) setInternalIsActive(val);
|
||||||
|
onActiveChange?.(val);
|
||||||
|
};
|
||||||
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
|
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./handlers";
|
export * from "./handlers";
|
||||||
export * from "./components/FeedbackOverlay";
|
export { FeedbackOverlay } from "./components/FeedbackOverlay";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-observability",
|
"name": "@mintel/next-observability",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-utils",
|
"name": "@mintel/next-utils",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/observability",
|
"name": "@mintel/observability",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/pdf",
|
"name": "@mintel/pdf",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
@@ -525,13 +525,13 @@ export const FoldingMarks = () => (
|
|||||||
export const Footer = ({
|
export const Footer = ({
|
||||||
logo,
|
logo,
|
||||||
companyData,
|
companyData,
|
||||||
bankData,
|
_bankData,
|
||||||
showDetails = true,
|
showDetails = true,
|
||||||
showPageNumber = true,
|
showPageNumber = true,
|
||||||
}: {
|
}: {
|
||||||
logo?: string;
|
logo?: string;
|
||||||
companyData: any;
|
companyData: any;
|
||||||
bankData?: any;
|
_bankData?: any;
|
||||||
showDetails?: boolean;
|
showDetails?: boolean;
|
||||||
showPageNumber?: boolean;
|
showPageNumber?: boolean;
|
||||||
}) => (
|
}) => (
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from "@react-pdf/renderer";
|
} from "@react-pdf/renderer";
|
||||||
import {
|
import {
|
||||||
DocumentTitle,
|
DocumentTitle,
|
||||||
IndustrialListItem,
|
|
||||||
IndustrialCard,
|
IndustrialCard,
|
||||||
Divider,
|
Divider,
|
||||||
COLORS,
|
COLORS,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "people-manager",
|
"name": "people-manager",
|
||||||
"id": "people-manager",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"description": "Custom High-Fidelity People Management for Directus",
|
"icon": "extension",
|
||||||
"icon": "person",
|
"version": "1.8.4",
|
||||||
"version": "1.8.2",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
@@ -11,19 +10,19 @@
|
|||||||
"directus-extension-module"
|
"directus-extension-module"
|
||||||
],
|
],
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"id": "people-manager",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "People Manager"
|
"name": "people manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "11.0.2",
|
"@directus/extensions-sdk": "11.0.2",
|
||||||
|
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<private-view title="People Manager">
|
<MintelManagerLayout
|
||||||
|
title="People Manager"
|
||||||
|
:item-title="`${selectedPerson?.first_name} ${selectedPerson?.last_name}` || 'Person wählen'"
|
||||||
|
:is-empty="!selectedPerson"
|
||||||
|
empty-title="Person auswählen"
|
||||||
|
empty-icon="person"
|
||||||
|
:notice="feedback"
|
||||||
|
@close-notice="feedback = null"
|
||||||
|
>
|
||||||
<template #navigation>
|
<template #navigation>
|
||||||
<v-list nav>
|
<v-list nav>
|
||||||
<v-list-item @click="openCreateDrawer" clickable>
|
<v-list-item @click="openCreateDrawer" clickable>
|
||||||
@@ -17,7 +25,7 @@
|
|||||||
v-for="person in people"
|
v-for="person in people"
|
||||||
:key="person.id"
|
:key="person.id"
|
||||||
:active="selectedPerson?.id === person.id"
|
:active="selectedPerson?.id === person.id"
|
||||||
class="person-item"
|
class="nav-item"
|
||||||
clickable
|
clickable
|
||||||
@click="selectPerson(person)"
|
@click="selectPerson(person)"
|
||||||
>
|
>
|
||||||
@@ -31,47 +39,42 @@
|
|||||||
</v-list>
|
</v-list>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<template #subtitle>
|
||||||
<v-notice v-if="feedback" :type="feedback.type" @close="feedback = null" dismissible>
|
<template v-if="selectedPerson">
|
||||||
{{ feedback.message }}
|
{{ getCompanyName(selectedPerson) }}
|
||||||
</v-notice>
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-if="!selectedPerson" class="empty-state">
|
<template #actions>
|
||||||
<v-info title="Person auswählen" icon="person" center>
|
<v-button secondary rounded icon v-tooltip.bottom="'Person bearbeiten'" @click="openEditDrawer">
|
||||||
Wähle eine Person in der Navigation aus oder
|
<v-icon name="edit" />
|
||||||
<v-button x-small @click="openCreateDrawer">erstelle eine neue Person</v-button>.
|
</v-button>
|
||||||
</v-info>
|
<v-button danger rounded icon v-tooltip.bottom="'Person löschen'" @click="deletePerson">
|
||||||
|
<v-icon name="delete" />
|
||||||
|
</v-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty-state>
|
||||||
|
Wähle eine Person in der Navigation aus oder
|
||||||
|
<v-button x-small @click="openCreateDrawer">erstelle eine neue Person</v-button>.
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="selectedPerson" class="details-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">Vorname</span>
|
||||||
|
<p class="value">{{ selectedPerson.first_name }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
<div v-else>
|
<span class="label">Nachname</span>
|
||||||
<header class="header">
|
<p class="value">{{ selectedPerson.last_name }}</p>
|
||||||
<div class="header-left">
|
</div>
|
||||||
<h1 class="title">{{ selectedPerson.first_name }} {{ selectedPerson.last_name }}</h1>
|
<div class="detail-item">
|
||||||
<p class="subtitle">{{ selectedPerson.email || 'Keine E-Mail angegeben' }}</p>
|
<span class="label">E-Mail</span>
|
||||||
</div>
|
<p class="value">{{ selectedPerson.email || '---' }}</p>
|
||||||
|
</div>
|
||||||
<div class="header-right">
|
<div class="detail-item">
|
||||||
<v-button secondary rounded icon v-tooltip="'Person bearbeiten'" @click="openEditDrawer">
|
<span class="label">Organisation</span>
|
||||||
<v-icon name="edit" />
|
<p class="value">{{ getCompanyName(selectedPerson) }}</p>
|
||||||
</v-button>
|
|
||||||
<v-button danger rounded icon v-tooltip="'Person löschen'" @click="deletePerson">
|
|
||||||
<v-icon name="delete" />
|
|
||||||
</v-button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
|
|
||||||
<div class="details-grid">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="label">Organisation / Firma</span>
|
|
||||||
<p class="value">{{ getCompanyName(selectedPerson) }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="label">Telefon</span>
|
|
||||||
<p class="value">{{ selectedPerson.phone || '---' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,24 +98,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">E-Mail</span>
|
<span class="label">E-Mail</span>
|
||||||
<v-input v-model="form.email" placeholder="email@beispiel.de" type="email" />
|
<v-input v-model="form.email" placeholder="E-Mail Adresse" type="email" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">Zentrale Firma</span>
|
<span class="label">Zentrale Firma</span>
|
||||||
<v-select
|
<MintelSelect
|
||||||
v-model="form.company"
|
v-model="form.company"
|
||||||
:items="companyOptions"
|
:items="companyOptions"
|
||||||
placeholder="Bestehende Firma auswählen..."
|
placeholder="Bestehende Firma auswählen..."
|
||||||
|
allow-add
|
||||||
|
@add="openQuickAdd('company')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
|
||||||
<v-input v-model="form.phone" placeholder="+49 ..." />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-actions">
|
<div class="drawer-actions">
|
||||||
@@ -123,12 +120,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-drawer>
|
</v-drawer>
|
||||||
</private-view>
|
</MintelManagerLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed, nextTick } from 'vue';
|
||||||
import { useApi } from '@directus/extensions-sdk';
|
import { useApi } from '@directus/extensions-sdk';
|
||||||
|
import { MintelManagerLayout, MintelSelect } from '@mintel/directus-extension-toolkit';
|
||||||
|
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const people = ref([]);
|
const people = ref([]);
|
||||||
@@ -144,9 +142,7 @@ const form = ref({
|
|||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
email: '',
|
email: '',
|
||||||
company: null,
|
company: null
|
||||||
company_name: '',
|
|
||||||
phone: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const companyOptions = computed(() =>
|
const companyOptions = computed(() =>
|
||||||
@@ -159,9 +155,9 @@ const companyOptions = computed(() =>
|
|||||||
function getCompanyName(person: any) {
|
function getCompanyName(person: any) {
|
||||||
if (!person) return '---';
|
if (!person) return '---';
|
||||||
if (person.company) {
|
if (person.company) {
|
||||||
return typeof person.company === 'object' ? person.company.name : (companies.value.find(c => c.id === person.company)?.name || person.company_name);
|
return typeof person.company === 'object' ? person.company.name : (companies.value.find(c => c.id === person.company)?.name || 'Unbekannte Firma');
|
||||||
}
|
}
|
||||||
return person.company_name || '---';
|
return '---';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
@@ -195,9 +191,7 @@ function openCreateDrawer() {
|
|||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
email: '',
|
email: '',
|
||||||
company: null,
|
company: null
|
||||||
company_name: '',
|
|
||||||
phone: ''
|
|
||||||
};
|
};
|
||||||
drawerActive.value = true;
|
drawerActive.value = true;
|
||||||
}
|
}
|
||||||
@@ -205,23 +199,12 @@ function openCreateDrawer() {
|
|||||||
function openEditDrawer() {
|
function openEditDrawer() {
|
||||||
isEditing.value = true;
|
isEditing.value = true;
|
||||||
const person = 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 = {
|
form.value = {
|
||||||
...person,
|
id: person.id,
|
||||||
company: companyId,
|
first_name: person.first_name,
|
||||||
company_name: companyName
|
last_name: person.last_name,
|
||||||
|
email: person.email,
|
||||||
|
company: person.company?.id || person.company
|
||||||
};
|
};
|
||||||
drawerActive.value = true;
|
drawerActive.value = true;
|
||||||
}
|
}
|
||||||
@@ -234,17 +217,20 @@ async function savePerson() {
|
|||||||
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
|
let updatedItem;
|
||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
await api.patch(`/items/people/${form.value.id}`, form.value);
|
const res = await api.patch(`/items/people/${form.value.id}`, form.value);
|
||||||
|
updatedItem = res.data.data;
|
||||||
feedback.value = { type: 'success', message: 'Person aktualisiert!' };
|
feedback.value = { type: 'success', message: 'Person aktualisiert!' };
|
||||||
} else {
|
} else {
|
||||||
await api.post('/items/people', form.value);
|
const res = await api.post('/items/people', form.value);
|
||||||
|
updatedItem = res.data.data;
|
||||||
feedback.value = { type: 'success', message: 'Person angelegt!' };
|
feedback.value = { type: 'success', message: 'Person angelegt!' };
|
||||||
}
|
}
|
||||||
drawerActive.value = false;
|
drawerActive.value = false;
|
||||||
await fetchData();
|
await fetchData();
|
||||||
if (isEditing.value) {
|
if (updatedItem) {
|
||||||
selectedPerson.value = people.value.find(p => p.id === form.value.id);
|
selectedPerson.value = people.value.find(p => p.id === updatedItem.id) || updatedItem;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
feedback.value = { type: 'danger', message: error.message };
|
feedback.value = { type: 'danger', message: error.message };
|
||||||
@@ -266,50 +252,18 @@ async function deletePerson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openQuickAdd(type: string) {
|
||||||
|
feedback.value = { type: 'info', message: `Firma im Company Manager anlegen.` };
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(fetchData);
|
onMounted(fetchData);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.content-wrapper {
|
|
||||||
padding: 32px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: var(--theme--foreground-subdued);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-grid {
|
.details-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
margin-top: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-item {
|
.detail-item {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/tsconfig",
|
"name": "@mintel/tsconfig",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.infra.mintel.me"
|
"registry": "https://npm.infra.mintel.me"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "unified-dashboard",
|
"name": "unified-dashboard",
|
||||||
"description": "Unified Infrastructure Dashboard for Directus",
|
"description": "Custom High-Fidelity Management for Directus",
|
||||||
"icon": "dashboard",
|
"icon": "extension",
|
||||||
"version": "1.8.2",
|
"version": "1.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
],
|
],
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"path": "index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "*",
|
"host": "app",
|
||||||
"name": "Unified Dashboard"
|
"name": "unified dashboard"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
|
"build": "directus-extension build",
|
||||||
"dev": "directus-extension build -w"
|
"dev": "directus-extension build -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
14
packages/unified-dashboard/src/index.ts
Normal file
14
packages/unified-dashboard/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineModule } from '@directus/extensions-sdk';
|
||||||
|
import ModuleComponent from './module.vue';
|
||||||
|
|
||||||
|
export default defineModule({
|
||||||
|
id: 'unified-dashboard',
|
||||||
|
name: 'Overview',
|
||||||
|
icon: 'dashboard',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ModuleComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
141
packages/unified-dashboard/src/module.vue
Normal file
141
packages/unified-dashboard/src/module.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<private-view title="Overview">
|
||||||
|
<div class="dashboard">
|
||||||
|
<header class="dashboard-header">
|
||||||
|
<h1 class="title">Infrastructure Stack</h1>
|
||||||
|
<p class="subtitle">Zentrale Schnittstelle für Firmen, Personen und Leads.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card" @click="navigateTo('/company-manager')">
|
||||||
|
<div class="stat-icon"><v-icon name="business" large /></div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<span class="stat-label">Firmen</span>
|
||||||
|
<span class="stat-value">{{ stats.companies }}</span>
|
||||||
|
</div>
|
||||||
|
<v-icon name="chevron_right" class="arrow" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card" @click="navigateTo('/people-manager')">
|
||||||
|
<div class="stat-icon"><v-icon name="person" large /></div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<span class="stat-label">Personen</span>
|
||||||
|
<span class="stat-value">{{ stats.people }}</span>
|
||||||
|
</div>
|
||||||
|
<v-icon name="chevron_right" class="arrow" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card" @click="navigateTo('/acquisition-manager')">
|
||||||
|
<div class="stat-icon"><v-icon name="auto_awesome" large /></div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<span class="stat-label">Leads</span>
|
||||||
|
<span class="stat-value">{{ stats.leads }}</span>
|
||||||
|
</div>
|
||||||
|
<v-icon name="chevron_right" class="arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recent-activity">
|
||||||
|
<h2 class="section-title">Schnellzugriff</h2>
|
||||||
|
<div class="action-grid">
|
||||||
|
<v-button secondary block @click="navigateTo('/people-manager?create=true')">
|
||||||
|
<v-icon name="person_add" left />
|
||||||
|
Neue Person anlegen
|
||||||
|
</v-button>
|
||||||
|
<v-button secondary block @click="navigateTo('/acquisition-manager?create=true')">
|
||||||
|
<v-icon name="add_link" left />
|
||||||
|
Neuen Lead registrieren
|
||||||
|
</v-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</private-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useApi } from '@directus/extensions-sdk';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const stats = ref({
|
||||||
|
companies: 0,
|
||||||
|
people: 0,
|
||||||
|
leads: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
try {
|
||||||
|
const [comp, peop, lead] = await Promise.all([
|
||||||
|
api.get('/items/companies?aggregate[count]=*'),
|
||||||
|
api.get('/items/people?aggregate[count]=*'),
|
||||||
|
api.get('/items/leads?aggregate[count]=*')
|
||||||
|
]);
|
||||||
|
stats.value = {
|
||||||
|
companies: comp.data.data[0].count,
|
||||||
|
people: peop.data.data[0].count,
|
||||||
|
leads: lead.data.data[0].count
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(path: string) {
|
||||||
|
router.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchStats);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard { padding: 40px; }
|
||||||
|
.dashboard-header { margin-bottom: 48px; }
|
||||||
|
.title { font-size: 32px; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 8px; }
|
||||||
|
.subtitle { color: var(--theme--foreground-subdued); font-size: 16px; }
|
||||||
|
|
||||||
|
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-bottom: 48px; }
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--theme--background-normal);
|
||||||
|
border: 1px solid var(--theme--border);
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
border-color: var(--theme--primary);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: var(--theme--background-subdued);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--theme--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content { display: flex; flex-direction: column; }
|
||||||
|
.stat-label { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--theme--foreground-subdued); letter-spacing: 0.5px; }
|
||||||
|
.stat-value { font-size: 28px; font-weight: 800; color: var(--theme--foreground); }
|
||||||
|
|
||||||
|
.arrow { position: absolute; right: 24px; opacity: 0.2; }
|
||||||
|
.stat-card:hover .arrow { opacity: 1; color: var(--theme--primary); }
|
||||||
|
|
||||||
|
.recent-activity { max-width: 600px; }
|
||||||
|
.section-title { font-size: 18px; font-weight: 700; margin-bottom: 24px; }
|
||||||
|
.action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
|
</style>
|
||||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -12,6 +12,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
globals:
|
||||||
|
specifier: ^17.3.0
|
||||||
|
version: 17.3.0
|
||||||
import-in-the-middle:
|
import-in-the-middle:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
@@ -180,6 +183,9 @@ importers:
|
|||||||
'@directus/extensions-sdk':
|
'@directus/extensions-sdk':
|
||||||
specifier: 11.0.2
|
specifier: 11.0.2
|
||||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||||
|
'@mintel/directus-extension-toolkit':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../directus-extension-toolkit
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.4.0
|
specifier: ^3.4.0
|
||||||
version: 3.5.28(typescript@5.9.3)
|
version: 3.5.28(typescript@5.9.3)
|
||||||
@@ -247,6 +253,9 @@ importers:
|
|||||||
'@directus/extensions-sdk':
|
'@directus/extensions-sdk':
|
||||||
specifier: 11.0.2
|
specifier: 11.0.2
|
||||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||||
|
'@mintel/directus-extension-toolkit':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../directus-extension-toolkit
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.4.0
|
specifier: ^3.4.0
|
||||||
version: 3.5.28(typescript@5.9.3)
|
version: 3.5.28(typescript@5.9.3)
|
||||||
@@ -256,6 +265,9 @@ importers:
|
|||||||
'@directus/extensions-sdk':
|
'@directus/extensions-sdk':
|
||||||
specifier: 11.0.2
|
specifier: 11.0.2
|
||||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||||
|
'@mintel/directus-extension-toolkit':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../directus-extension-toolkit
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.4.0
|
specifier: ^3.4.0
|
||||||
version: 3.5.28(typescript@5.9.3)
|
version: 3.5.28(typescript@5.9.3)
|
||||||
@@ -631,6 +643,9 @@ importers:
|
|||||||
'@directus/extensions-sdk':
|
'@directus/extensions-sdk':
|
||||||
specifier: 11.0.2
|
specifier: 11.0.2
|
||||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||||
|
'@mintel/directus-extension-toolkit':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../directus-extension-toolkit
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.4.0
|
specifier: ^3.4.0
|
||||||
version: 3.5.28(typescript@5.9.3)
|
version: 3.5.28(typescript@5.9.3)
|
||||||
@@ -5109,6 +5124,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
|
resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
globals@17.3.0:
|
||||||
|
resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
globalthis@1.0.4:
|
globalthis@1.0.4:
|
||||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -13103,6 +13122,8 @@ snapshots:
|
|||||||
|
|
||||||
globals@16.4.0: {}
|
globals@16.4.0: {}
|
||||||
|
|
||||||
|
globals@17.3.0: {}
|
||||||
|
|
||||||
globalthis@1.0.4:
|
globalthis@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
|
|||||||
@@ -4,10 +4,15 @@
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
EXTENSIONS_ROOT="$REPO_ROOT/packages"
|
EXTENSIONS_ROOT="$REPO_ROOT/packages"
|
||||||
TARGET_DIR="$REPO_ROOT/packages/cms-infra/extensions"
|
|
||||||
|
|
||||||
# List of extensions to sync - including modules and endpoints
|
# Strict local targets for bombproof isolation
|
||||||
EXTENSIONS=(
|
TARGET_DIRS=(
|
||||||
|
"$REPO_ROOT/packages/cms-infra/extensions"
|
||||||
|
"$REPO_ROOT/directus/extensions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# List of extension packages to sync
|
||||||
|
EXTENSION_PACKAGES=(
|
||||||
"acquisition"
|
"acquisition"
|
||||||
"acquisition-manager"
|
"acquisition-manager"
|
||||||
"company-manager"
|
"company-manager"
|
||||||
@@ -17,60 +22,134 @@ EXTENSIONS=(
|
|||||||
"unified-dashboard"
|
"unified-dashboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "🚀 Starting extension sync..."
|
echo "🚀 Starting isolated extension sync..."
|
||||||
|
|
||||||
# Ensure target directory exists
|
# Ensure target directories exist
|
||||||
mkdir -p "$TARGET_DIR"
|
for TARGET in "${TARGET_DIRS[@]}"; do
|
||||||
|
mkdir -p "$TARGET"
|
||||||
|
done
|
||||||
|
|
||||||
# Build the acquisition library first so extensions use the updated build
|
# Build the acquisition library if it exists
|
||||||
echo "📦 Building acquisition-library..."
|
if [ -d "$REPO_ROOT/packages/acquisition" ]; then
|
||||||
(cd "$REPO_ROOT/packages/acquisition-library" && pnpm build)
|
echo "📦 Building acquisition..."
|
||||||
|
(cd "$REPO_ROOT/packages/acquisition" && pnpm build)
|
||||||
|
fi
|
||||||
|
|
||||||
for EXT in "${EXTENSIONS[@]}"; do
|
for PKG in "${EXTENSION_PACKAGES[@]}"; do
|
||||||
EXT_PATH="$EXTENSIONS_ROOT/$EXT"
|
PKG_PATH="$EXTENSIONS_ROOT/$PKG"
|
||||||
|
|
||||||
if [ -d "$EXT_PATH" ]; then
|
if [ -d "$PKG_PATH" ]; then
|
||||||
echo "📦 Building $EXT..."
|
echo "📦 Processing $PKG..."
|
||||||
|
|
||||||
# Build the extension
|
# 1. Build the extension
|
||||||
# We use --if-present to avoid errors if build script is missing
|
(cd "$PKG_PATH" && pnpm build)
|
||||||
(cd "$EXT_PATH" && pnpm build)
|
|
||||||
|
|
||||||
# Create target directory for this extension
|
EXT_NAME="$PKG"
|
||||||
# Directus expects extensions to be in subdirectories matching their name
|
echo "🚚 Syncing $EXT_NAME..."
|
||||||
mkdir -p "$TARGET_DIR/$EXT"
|
|
||||||
|
|
||||||
echo "🚚 Syncing $EXT to $TARGET_DIR/$EXT..."
|
# 3. Sync to each target directory
|
||||||
|
for TARGET_BASE in "${TARGET_DIRS[@]}"; do
|
||||||
|
# FLAT STRUCTURE: Directus 11.15.x local scanner is FLAT.
|
||||||
|
FINAL_TARGET="$TARGET_BASE/$EXT_NAME"
|
||||||
|
|
||||||
# Clean target first to avoid ghost files
|
echo "🚚 Syncing $EXT_NAME to $FINAL_TARGET..."
|
||||||
rm -rf "${TARGET_DIR:?}/$EXT"/*
|
|
||||||
|
|
||||||
# Copy build artifacts and package metadata
|
# Clean target first to avoid ghost files
|
||||||
# Some extensions have index.js in root after build, some use dist/
|
mkdir -p "$FINAL_TARGET"
|
||||||
# We check for index.js and package.json
|
rm -rf "${FINAL_TARGET:?}"/*
|
||||||
if [ -f "$EXT_PATH/index.js" ]; then
|
|
||||||
cp "$EXT_PATH/index.js" "$TARGET_DIR/$EXT/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$EXT_PATH/package.json" ]; then
|
# Copy build artifacts
|
||||||
cp "$EXT_PATH/package.json" "$TARGET_DIR/$EXT/"
|
if [ -f "$PKG_PATH/dist/index.js" ]; then
|
||||||
fi
|
cp "$PKG_PATH/dist/index.js" "$FINAL_TARGET/index.js"
|
||||||
|
elif [ -f "$PKG_PATH/index.js" ]; then
|
||||||
|
cp "$PKG_PATH/index.js" "$FINAL_TARGET/index.js"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -d "$EXT_PATH/dist" ]; then
|
if [ -f "$PKG_PATH/package.json" ]; then
|
||||||
cp -r "$EXT_PATH/dist" "$TARGET_DIR/$EXT/"
|
cp "$PKG_PATH/package.json" "$FINAL_TARGET/"
|
||||||
fi
|
# We force the registration path to index.js and ensure host/source are set
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const pkgPath = '$FINAL_TARGET/package.json';
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||||
|
if (!pkg['directus:extension']) pkg['directus:extension'] = {};
|
||||||
|
|
||||||
# Sync node_modules if they exist (sometimes needed if not everything is bundled)
|
// Standard metadata for Directus 11.15.x (with core patch applied)
|
||||||
# Deactivated: Causes global scope pollution and login issues in Directus
|
pkg['directus:extension'].path = 'index.js';
|
||||||
# if [ -d "$EXT_PATH/node_modules" ]; then
|
if (!pkg['directus:extension'].host) {
|
||||||
# echo "📚 Syncing node_modules for $EXT..."
|
pkg['directus:extension'].host = pkg['directus:extension'].type === 'endpoint' ? 'api' : 'app';
|
||||||
# rsync -aL --delete "$EXT_PATH/node_modules/" "$TARGET_DIR/$EXT/node_modules/"
|
}
|
||||||
# fi
|
if (!pkg['directus:extension'].source) {
|
||||||
|
pkg['directus:extension'].source = 'src/index.ts';
|
||||||
|
}
|
||||||
|
|
||||||
echo "✅ $EXT synced."
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$PKG_PATH/dist" ]; then
|
||||||
|
cp -r "$PKG_PATH/dist" "$FINAL_TARGET/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ $PKG synced."
|
||||||
else
|
else
|
||||||
echo "❌ Extension source not found: $EXT_PATH"
|
echo "❌ Extension source not found: $PKG_PATH"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "✨ Extension sync complete!"
|
# Cleanup: remove anything from extensions root that isn't in our whitelist
|
||||||
|
WHITELIST=("${EXTENSION_PACKAGES[@]}" "endpoints" "hooks" "layouts" "modules" "operations" "panels" "displays" "interfaces")
|
||||||
|
|
||||||
|
for TARGET_BASE in "${TARGET_DIRS[@]}"; do
|
||||||
|
echo "🧹 Cleaning up $TARGET_BASE..."
|
||||||
|
for ITEM in "$TARGET_BASE"/*; do
|
||||||
|
[ -e "$ITEM" ] || continue
|
||||||
|
BN=$(basename "$ITEM")
|
||||||
|
IS_ALLOWED=false
|
||||||
|
for W in "${WHITELIST[@]}"; do
|
||||||
|
if [[ "$BN" == "$W" ]]; then IS_ALLOWED=true; break; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$IS_ALLOWED" = false ]; then
|
||||||
|
echo " 🗑️ Removing extra/legacy item: $BN"
|
||||||
|
rm -rf "$ITEM"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "🔧 Applying core patch to Directus 11.15.2 bundler..."
|
||||||
|
docker exec cms-infra-infra-cms-1 node -e '
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = "/directus/node_modules/.pnpm/@directus+extensions@file+packages+extensions_deep-diff@1.0.2_express@4.21.2_graphql@16_244b87fbecd929c2d2240e7b3abc1fe4/node_modules/@directus/extensions/dist/node.js";
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
let content = fs.readFileSync(path, "utf8");
|
||||||
|
|
||||||
|
// Patch the filter: allow string entrypoints for modules
|
||||||
|
const filterPatch = "extension.host === \"app\" && (extension.entrypoint.app || extension.entrypoint)";
|
||||||
|
if (!content.includes(filterPatch)) {
|
||||||
|
content = content.replace(
|
||||||
|
/extension\.host === \"app\" && !!extension\.entrypoint\.app/g,
|
||||||
|
filterPatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch all imports: handle string entrypoints (replace all occurrences of .app where it might fail)
|
||||||
|
if (!content.includes("(extension.entrypoint.app || extension.entrypoint)")) {
|
||||||
|
content = content.replace(
|
||||||
|
/extension\.entrypoint\.app/g,
|
||||||
|
"(extension.entrypoint.app || extension.entrypoint)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
|
console.log("✅ Core patched successfully.");
|
||||||
|
} else {
|
||||||
|
console.error("⚠️ Could not find node.js to patch!");
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "🔄 Restarting Directus container..."
|
||||||
|
docker restart cms-infra-infra-cms-1 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "✨ Sync complete! Extensions are in packages/cms-infra/extensions and core is patched."
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { execSync } from "child_process";
|
|||||||
*/
|
*/
|
||||||
function getVersionTag() {
|
function getVersionTag() {
|
||||||
// 0. Check arguments (passed from husky hook or manual run)
|
// 0. Check arguments (passed from husky hook or manual run)
|
||||||
const argTag = process.argv.slice(2).find((arg) => arg.startsWith("v"));
|
const argTag = process.argv.slice(2).find((arg) => arg.match(/^v?\d/));
|
||||||
if (argTag) {
|
if (argTag) {
|
||||||
return argTag;
|
return argTag;
|
||||||
}
|
}
|
||||||
@@ -16,11 +16,11 @@ function getVersionTag() {
|
|||||||
// 1. Check CI environment variables
|
// 1. Check CI environment variables
|
||||||
if (
|
if (
|
||||||
process.env.GITHUB_REF_NAME &&
|
process.env.GITHUB_REF_NAME &&
|
||||||
process.env.GITHUB_REF_NAME.startsWith("v")
|
process.env.GITHUB_REF_NAME.match(/^v?\d/)
|
||||||
) {
|
) {
|
||||||
return process.env.GITHUB_REF_NAME;
|
return process.env.GITHUB_REF_NAME;
|
||||||
}
|
}
|
||||||
if (process.env.TAG && process.env.TAG.startsWith("v")) {
|
if (process.env.TAG && process.env.TAG.match(/^v?\d/)) {
|
||||||
return process.env.TAG;
|
return process.env.TAG;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ function getVersionTag() {
|
|||||||
const gitTag = execSync("git describe --tags --abbrev=0", {
|
const gitTag = execSync("git describe --tags --abbrev=0", {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
}).trim();
|
}).trim();
|
||||||
if (gitTag && gitTag.startsWith("v")) {
|
if (gitTag && gitTag.match(/^v?\d/)) {
|
||||||
return gitTag;
|
return gitTag;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
// Fallback or silence
|
// Fallback or silence
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
scripts/validate-extensions.sh
Normal file
67
scripts/validate-extensions.sh
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
TARGET_DIRS=(
|
||||||
|
"$REPO_ROOT/packages/cms-infra/extensions"
|
||||||
|
"$REPO_ROOT/directus/extensions"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "🛡️ Directus Extension Validator"
|
||||||
|
echo "================================="
|
||||||
|
|
||||||
|
for TARGET in "${TARGET_DIRS[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "📂 Checking: $TARGET"
|
||||||
|
|
||||||
|
if [ ! -d "$TARGET" ]; then
|
||||||
|
echo " ❌ Directory does not exist!"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
CATEGORIES=("endpoints" "hooks" "layouts" "modules" "operations" "panels" "displays" "interfaces")
|
||||||
|
FOUND_ANY=false
|
||||||
|
|
||||||
|
for CAT in "${CATEGORIES[@]}"; do
|
||||||
|
CAT_PATH="$TARGET/$CAT"
|
||||||
|
if [ -d "$CAT_PATH" ]; then
|
||||||
|
EXTS=$(ls "$CAT_PATH")
|
||||||
|
if [ -n "$EXTS" ]; then
|
||||||
|
FOUND_ANY=true
|
||||||
|
echo " 📦 $CAT:"
|
||||||
|
for EXT in $EXTS; do
|
||||||
|
EXT_PATH="$CAT_PATH/$EXT"
|
||||||
|
if [ -f "$EXT_PATH/package.json" ]; then
|
||||||
|
VERSION=$(node -e "console.log(require('$EXT_PATH/package.json').version)")
|
||||||
|
echo " ✅ $EXT (v$VERSION)"
|
||||||
|
else
|
||||||
|
echo " ⚠️ $EXT (MISSING package.json!)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$FOUND_ANY" = false ]; then
|
||||||
|
echo " 📭 No extensions found in standard category folders."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for legacy files
|
||||||
|
LEGACY=$(find "$TARGET" -maxdepth 1 -not -path "$TARGET" -not -name ".*" -type d)
|
||||||
|
for L in $LEGACY; do
|
||||||
|
BN=$(basename "$L")
|
||||||
|
IS_CAT=false
|
||||||
|
for CAT in "${CATEGORIES[@]}"; do
|
||||||
|
if [ "$BN" == "$CAT" ]; then IS_CAT=true; break; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$IS_CAT" = false ]; then
|
||||||
|
echo " 🚨 LEGACY/UNRESOLVED FOLDER FOUND: $BN (Will NOT be loaded by Directus)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✨ Validation complete."
|
||||||
100
scripts/validate-sdk-imports.sh
Executable file
100
scripts/validate-sdk-imports.sh
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# validate-sdk-imports.sh
|
||||||
|
# Validates that Directus extensions only use exports that exist in @directus/extensions-sdk.
|
||||||
|
# Prevents the "SyntaxError: doesn't provide an export named" runtime crash
|
||||||
|
# that silently breaks ALL extensions in the browser.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
# Valid exports from @directus/extensions-sdk in Directus 11.x
|
||||||
|
# If Directus is upgraded, update this list by running:
|
||||||
|
# curl -s http://cms.localhost/admin/assets/@directus_extensions-sdk.*.entry.js | grep -oE 'export\{[^}]+\}'
|
||||||
|
VALID_EXPORTS=(
|
||||||
|
"defineDisplay"
|
||||||
|
"defineEndpoint"
|
||||||
|
"defineHook"
|
||||||
|
"defineInterface"
|
||||||
|
"defineLayout"
|
||||||
|
"defineModule"
|
||||||
|
"defineOperationApi"
|
||||||
|
"defineOperationApp"
|
||||||
|
"definePanel"
|
||||||
|
"defineTheme"
|
||||||
|
"getFieldsFromTemplate"
|
||||||
|
"getRelationType"
|
||||||
|
"useApi"
|
||||||
|
"useCollection"
|
||||||
|
"useExtensions"
|
||||||
|
"useFilterFields"
|
||||||
|
"useItems"
|
||||||
|
"useLayout"
|
||||||
|
"useSdk"
|
||||||
|
"useStores"
|
||||||
|
"useSync"
|
||||||
|
)
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
echo "🔍 Validating @directus/extensions-sdk imports..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Search all .ts and .vue files in extension directories
|
||||||
|
SEARCH_DIRS=(
|
||||||
|
"$REPO_ROOT/packages/cms-infra/extensions"
|
||||||
|
"$REPO_ROOT/packages/unified-dashboard"
|
||||||
|
"$REPO_ROOT/packages/customer-manager"
|
||||||
|
"$REPO_ROOT/packages/company-manager"
|
||||||
|
"$REPO_ROOT/packages/people-manager"
|
||||||
|
"$REPO_ROOT/packages/acquisition-manager"
|
||||||
|
"$REPO_ROOT/packages/feedback-commander"
|
||||||
|
)
|
||||||
|
|
||||||
|
for DIR in "${SEARCH_DIRS[@]}"; do
|
||||||
|
[ -d "$DIR" ] || continue
|
||||||
|
|
||||||
|
# Find all imports from @directus/extensions-sdk
|
||||||
|
while IFS= read -r line; do
|
||||||
|
FILE=$(echo "$line" | cut -d: -f1)
|
||||||
|
LINENUM=$(echo "$line" | cut -d: -f2)
|
||||||
|
CONTENT=$(echo "$line" | cut -d: -f3-)
|
||||||
|
|
||||||
|
# Extract imported names from the import statement
|
||||||
|
IMPORTS=$(echo "$CONTENT" | grep -oE '\{[^}]+\}' | tr -d '{}' | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed 's/ as .*//')
|
||||||
|
|
||||||
|
for IMPORT in $IMPORTS; do
|
||||||
|
[ -z "$IMPORT" ] && continue
|
||||||
|
FOUND=false
|
||||||
|
for VALID in "${VALID_EXPORTS[@]}"; do
|
||||||
|
if [ "$IMPORT" = "$VALID" ]; then
|
||||||
|
FOUND=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$FOUND" = false ]; then
|
||||||
|
echo "❌ INVALID IMPORT: '$IMPORT' in $FILE:$LINENUM"
|
||||||
|
echo " '$IMPORT' is NOT exported by @directus/extensions-sdk in Directus 11.x"
|
||||||
|
echo " This WILL crash ALL extensions at runtime!"
|
||||||
|
echo ""
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done < <(grep -rn "from ['\"]@directus/extensions-sdk['\"]" "$DIR" --include="*.ts" --include="*.vue" 2>/dev/null || true)
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "💥 Found $ERRORS invalid import(s)!"
|
||||||
|
echo ""
|
||||||
|
echo "Valid exports from @directus/extensions-sdk:"
|
||||||
|
printf " %s\n" "${VALID_EXPORTS[@]}"
|
||||||
|
echo ""
|
||||||
|
echo "Common fixes:"
|
||||||
|
echo " useRouter → import { useRouter } from 'vue-router'"
|
||||||
|
echo " useRoute → import { useRoute } from 'vue-router'"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ All @directus/extensions-sdk imports are valid."
|
||||||
|
fi
|
||||||
46
scripts/verify-extensions-live.sh
Executable file
46
scripts/verify-extensions-live.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
HOST="http://cms.localhost"
|
||||||
|
EXTENSIONS=("customer-manager" "people-manager" "company-manager" "feedback-commander" "unified-dashboard")
|
||||||
|
|
||||||
|
echo "🔍 Verifying extensions at $HOST..."
|
||||||
|
|
||||||
|
# 1. Check Main Manifest
|
||||||
|
MANIFEST=$(curl -s "$HOST/extensions/sources/index.js")
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "❌ Error: Manifest returned empty response."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Manifest loaded (${#MANIFEST} bytes)."
|
||||||
|
|
||||||
|
# 2. Check for unexpected 404/500
|
||||||
|
if echo "$MANIFEST" | grep -q "<!DOCTYPE html>"; then
|
||||||
|
echo "❌ Error: Manifest returned HTML (likely 404 or error page) instead of JS."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Verify each extension is in the bundle
|
||||||
|
FAILURE=0
|
||||||
|
for EXT in "${EXTENSIONS[@]}"; do
|
||||||
|
# Directus bundles strings usually, or imports them.
|
||||||
|
# We look for the ID or the unique module name from src (e.g. "Customer Manager")
|
||||||
|
# Or simply the path matching.
|
||||||
|
|
||||||
|
if echo "$MANIFEST" | grep -q "$EXT"; then
|
||||||
|
echo "✅ Found '$EXT' in manifest."
|
||||||
|
else
|
||||||
|
echo "❌ MISSING '$EXT' in manifest!"
|
||||||
|
FAILURE=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $FAILURE -eq 1 ]; then
|
||||||
|
echo "🚨 VERIFICATION FAILED: One or more extensions are missing from the public bundle."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "🎉 ALL EXTENSIONS VERIFIED."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user