Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92fe089619 | |||
| 7dcef0bc28 | |||
| 2ba091f738 | |||
| 5757c1172b | |||
| e7d5798857 | |||
| 29a414f385 | |||
| 69764e42c6 | |||
| d69ade6268 | |||
| ceaf3ae3ea | |||
| 169cb83f69 | |||
| f831a7e67e | |||
| cb4ffcaeda | |||
| 9b1f3fb7e8 | |||
| f48d89c368 | |||
| ad40e71757 | |||
| 911ceffdc5 |
6
.env
6
.env
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
IMAGE_TAG=v1.8.2
|
||||
IMAGE_TAG=v1.8.10
|
||||
PROJECT_NAME=at-mintel
|
||||
PROJECT_COLOR=#82ed20
|
||||
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
|
||||
@@ -10,13 +10,13 @@ AUTH_COOKIE_NAME=mintel_gatekeeper_session
|
||||
|
||||
# Host Config (Local)
|
||||
TRAEFIK_HOST=at-mintel.localhost
|
||||
DIRECTUS_HOST=cms.localhost
|
||||
DIRECTUS_HOST=cms-legacy.localhost
|
||||
|
||||
# Next.js
|
||||
NEXT_PUBLIC_BASE_URL=http://at-mintel.localhost
|
||||
|
||||
# Directus
|
||||
DIRECTUS_URL=http://cms.localhost
|
||||
DIRECTUS_URL=http://cms-legacy.localhost
|
||||
DIRECTUS_KEY=F9IIfahEjPq6NZhKyRLw516D8GotuFj79EGK7pGfIWg=
|
||||
DIRECTUS_SECRET=OZfxMu8lBxzaEnFGRKreNBoJpRiRu58U+HsVg2yWk4o=
|
||||
CORS_ENABLED=true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
IMAGE_TAG=v1.8.2
|
||||
IMAGE_TAG=v1.8.10
|
||||
PROJECT_NAME=sample-website
|
||||
PROJECT_COLOR=#82ed20
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- 'v*'
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -26,16 +26,17 @@ jobs:
|
||||
REF: ${{ github.ref }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
echo "🔎 Debug: Event=$EVENT, Ref=$REF, RefName=$REF_NAME, RunId=$RUN_ID"
|
||||
|
||||
# Fetch recent runs for the repository
|
||||
RUNS=$(curl -s -H "Authorization: token $GITEA_TOKEN" "https://git.infra.mintel.me/api/v1/repos/$REPO/actions/runs?limit=30")
|
||||
|
||||
case "$REF" in
|
||||
refs/tags/v*)
|
||||
refs/tags/*)
|
||||
echo "🚀 Release detected ($REF_NAME). Cancelling non-tag runs..."
|
||||
|
||||
# Fetch all runs
|
||||
RUNS=$(curl -s -H "Authorization: token $GITEA_TOKEN" "https://git.infra.mintel.me/api/v1/repos/$REPO/actions/runs")
|
||||
|
||||
# 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
|
||||
ID=$(echo "$run" | jq -r '.id')
|
||||
@@ -43,7 +44,7 @@ jobs:
|
||||
TITLE=$(echo "$run" | jq -r '.display_title')
|
||||
|
||||
case "$RUN_REF" in
|
||||
refs/tags/v*)
|
||||
refs/tags/*)
|
||||
echo "⏭️ Skipping parallel release run $ID ($TITLE) on $RUN_REF"
|
||||
;;
|
||||
*)
|
||||
@@ -54,7 +55,17 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -130,7 +141,7 @@ jobs:
|
||||
release:
|
||||
name: 🚀 Release
|
||||
needs: [lint, test, build]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
@@ -160,12 +171,13 @@ jobs:
|
||||
build-images:
|
||||
name: 🐳 Build ${{ matrix.name }}
|
||||
needs: [lint, test, build]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- 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
|
||||
while read local_ref local_sha remote_ref remote_sha
|
||||
do
|
||||
if [[ "$remote_ref" == refs/tags/v* ]]; then
|
||||
if [[ "$remote_ref" == refs/tags/* ]]; then
|
||||
TAG=${remote_ref#refs/tags/}
|
||||
echo "🏷️ Tag detected: $TAG, ensuring versions are synced..."
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sample-website",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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:apply": "./scripts/cms-apply.sh local",
|
||||
"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:logs": "cd packages/cms-infra && npm run logs",
|
||||
"dev:infra": "docker-compose up -d directus directus-db",
|
||||
@@ -29,6 +29,7 @@
|
||||
"@commitlint/config-conventional": "^20.4.0",
|
||||
"@mintel/eslint-config": "workspace:*",
|
||||
"@mintel/husky-config": "workspace:*",
|
||||
"@next/eslint-plugin-next": "16.1.6",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^20.17.16",
|
||||
@@ -36,7 +37,6 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"@next/eslint-plugin-next": "16.1.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"happy-dom": "^20.4.0",
|
||||
@@ -50,12 +50,13 @@
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"globals": "^17.3.0",
|
||||
"import-in-the-middle": "^3.0.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"require-in-the-middle": "^8.0.1"
|
||||
},
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"next": "16.1.6",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "acquisition-manager",
|
||||
"description": "Custom High-Fidelity Acquisition Management for Directus",
|
||||
"icon": "account_balance_wallet",
|
||||
"version": "1.8.2",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -11,17 +11,18 @@
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Acquisition Manager"
|
||||
"host": "app",
|
||||
"name": "acquisition manager"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<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>
|
||||
<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-content>
|
||||
<v-text-overflow text="Neuen Lead anlegen" />
|
||||
@@ -15,7 +23,7 @@
|
||||
v-for="lead in leads"
|
||||
:key="lead.id"
|
||||
:active="selectedLeadId === lead.id"
|
||||
class="lead-item"
|
||||
class="nav-item"
|
||||
clickable
|
||||
@click="selectLead(lead.id)"
|
||||
>
|
||||
@@ -29,166 +37,136 @@
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<template #title-outer:after>
|
||||
<v-notice v-if="notice" :type="notice.type" @close="notice = null" dismissible>
|
||||
{{ notice.message }}
|
||||
</v-notice>
|
||||
<template #subtitle>
|
||||
<template v-if="selectedLead">
|
||||
<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() }}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<v-notice type="success" style="margin-bottom: 16px;">
|
||||
DEBUG: Module Version 1.1.0 - Native Build - {{ new Date().toISOString() }}
|
||||
</v-notice>
|
||||
<template #actions>
|
||||
<v-button
|
||||
v-if="selectedLead?.status === 'new'"
|
||||
secondary
|
||||
:loading="loadingAudit"
|
||||
@click="runAudit"
|
||||
>
|
||||
<v-icon name="settings_suggest" left />
|
||||
Audit starten
|
||||
</v-button>
|
||||
|
||||
<div v-if="!selectedLead" class="empty-state">
|
||||
<v-info title="Lead auswählen" icon="auto_awesome" center>
|
||||
Wähle einen Lead in der Navigation aus oder
|
||||
<v-button x-small @click="showAddLead = true">registriere einen neuen Lead</v-button>.
|
||||
</v-info>
|
||||
</div>
|
||||
<template v-if="selectedLead?.status === 'audit_ready'">
|
||||
<v-button secondary :loading="loadingEmail" @click="sendAuditEmail">
|
||||
<v-icon name="mail" left />
|
||||
Audit E-Mail
|
||||
</v-button>
|
||||
<v-button :loading="loadingPdf" @click="generatePdf">
|
||||
<v-icon name="picture_as_pdf" left />
|
||||
PDF Erstellen
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<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>
|
||||
<v-button v-if="selectedLead?.audit_pdf_path" secondary icon v-tooltip.bottom="'PDF öffnen'" @click="openPdf">
|
||||
<v-icon name="open_in_new" />
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
v-if="selectedLead?.audit_pdf_path"
|
||||
primary
|
||||
:loading="loadingEmail"
|
||||
@click="sendEstimateEmail"
|
||||
>
|
||||
<v-icon name="send" left />
|
||||
Angebot senden
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template v-if="selectedLead.status === 'audit_ready'">
|
||||
<v-button secondary :loading="loadingEmail" @click="sendAuditEmail">
|
||||
<v-icon name="mail" left />
|
||||
Audit E-Mail
|
||||
</v-button>
|
||||
<v-button :loading="loadingPdf" @click="generatePdf">
|
||||
<v-icon name="picture_as_pdf" left />
|
||||
PDF Erstellen
|
||||
</v-button>
|
||||
</template>
|
||||
<template #empty-state>
|
||||
Wähle einen Lead in der Navigation aus oder
|
||||
<v-button x-small @click="openCreateDrawer">registriere einen neuen Lead</v-button>.
|
||||
</template>
|
||||
|
||||
<v-button v-if="selectedLead.audit_pdf_path" secondary icon v-tooltip.bottom="'PDF öffnen'" @click="openPdf">
|
||||
<v-icon name="open_in_new" />
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
v-if="selectedLead.audit_pdf_path"
|
||||
primary
|
||||
:loading="loadingEmail"
|
||||
@click="sendEstimateEmail"
|
||||
>
|
||||
<v-icon name="send" left />
|
||||
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 v-if="selectedLead" 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>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<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 class="field full">
|
||||
<span class="label">Briefing / Fokus</span>
|
||||
<div class="value text-block">{{ selectedLead.briefing || 'Kein Briefing hinterlegt.' }}</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>
|
||||
|
||||
<!-- Drawer: New Lead -->
|
||||
<v-drawer
|
||||
v-model="showAddLead"
|
||||
v-model="drawerActive"
|
||||
title="Neuen Lead registrieren"
|
||||
icon="person_add"
|
||||
@cancel="showAddLead = false"
|
||||
@cancel="drawerActive = false"
|
||||
>
|
||||
<div class="drawer-content">
|
||||
<div class="form-section">
|
||||
<div class="field">
|
||||
<span class="label">Organisation / Firma (Zentral)</span>
|
||||
<v-select
|
||||
<MintelSelect
|
||||
v-model="newLead.company"
|
||||
:items="companyOptions"
|
||||
placeholder="Bestehende Firma auswählen..."
|
||||
allow-add
|
||||
@add="openQuickAdd('company')"
|
||||
/>
|
||||
</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">
|
||||
<span class="label">Website URL</span>
|
||||
<v-input v-model="newLead.website_url" placeholder="https://..." />
|
||||
</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">
|
||||
<span class="label">Briefing / Fokus</span>
|
||||
<v-textarea v-model="newLead.briefing" placeholder="Besonderheiten für das Audit..." />
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">Kontaktperson (Optional)</span>
|
||||
<v-select
|
||||
<MintelSelect
|
||||
v-model="newLead.contact_person"
|
||||
:items="peopleOptions"
|
||||
placeholder="Person auswählen..."
|
||||
allow-add
|
||||
@add="openQuickAdd('person')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,12 +176,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</v-drawer>
|
||||
</private-view>
|
||||
</MintelManagerLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useApi } from '@directus/extensions-sdk';
|
||||
import { MintelManagerLayout, MintelSelect, MintelStatCard } from '@mintel/directus-extension-toolkit';
|
||||
|
||||
const api = useApi();
|
||||
const leads = ref<any[]>([]);
|
||||
@@ -211,16 +190,13 @@ const selectedLeadId = ref<string | null>(null);
|
||||
const loadingAudit = ref(false);
|
||||
const loadingPdf = ref(false);
|
||||
const loadingEmail = ref(false);
|
||||
const showAddLead = ref(false);
|
||||
const drawerActive = ref(false);
|
||||
const savingLead = ref(false);
|
||||
const notice = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
const newLead = ref({
|
||||
company_name: '', // Legacy
|
||||
company: null,
|
||||
website_url: '',
|
||||
contact_name: '', // Legacy
|
||||
contact_email: '', // Legacy
|
||||
contact_person: null,
|
||||
briefing: '',
|
||||
status: 'new'
|
||||
@@ -244,10 +220,11 @@ const peopleOptions = computed(() =>
|
||||
);
|
||||
|
||||
function getCompanyName(lead: any) {
|
||||
if (!lead) return '';
|
||||
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) {
|
||||
@@ -258,31 +235,32 @@ function getPersonName(id: string | any) {
|
||||
}
|
||||
|
||||
function goToPerson(id: string) {
|
||||
// Logic to navigate to people manager or open details
|
||||
notice.value = { type: 'info', message: `Navigiere zu Person: ${id}` };
|
||||
}
|
||||
|
||||
const selectedLead = computed(() => leads.value.find(l => l.id === selectedLeadId.value));
|
||||
|
||||
onMounted(fetchData);
|
||||
|
||||
async function fetchData() {
|
||||
const [leadsResp, peopleResp, companiesResp] = await Promise.all([
|
||||
api.get('/items/leads', {
|
||||
params: {
|
||||
sort: '-date_created',
|
||||
fields: '*.*'
|
||||
}
|
||||
}),
|
||||
api.get('/items/people', { params: { sort: 'last_name' } }),
|
||||
api.get('/items/companies', { params: { sort: 'name' } })
|
||||
]);
|
||||
leads.value = leadsResp.data.data;
|
||||
people.value = peopleResp.data.data;
|
||||
companies.value = companiesResp.data.data;
|
||||
|
||||
if (!selectedLeadId.value && leads.value.length > 0) {
|
||||
selectedLeadId.value = leads.value[0].id;
|
||||
try {
|
||||
const [leadsResp, peopleResp, companiesResp] = await Promise.all([
|
||||
api.get('/items/leads', {
|
||||
params: {
|
||||
sort: '-date_created',
|
||||
fields: '*.*'
|
||||
}
|
||||
}),
|
||||
api.get('/items/people', { params: { sort: 'last_name' } }),
|
||||
api.get('/items/companies', { params: { sort: 'name' } })
|
||||
]);
|
||||
leads.value = leadsResp.data.data;
|
||||
people.value = peopleResp.data.data;
|
||||
companies.value = companiesResp.data.data;
|
||||
|
||||
if (!selectedLeadId.value && leads.value.length > 0) {
|
||||
selectedLeadId.value = leads.value[0].id;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Fetch error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +272,17 @@ function selectLead(id: string) {
|
||||
selectedLeadId.value = id;
|
||||
}
|
||||
|
||||
function openCreateDrawer() {
|
||||
newLead.value = {
|
||||
company: null,
|
||||
website_url: '',
|
||||
contact_person: null,
|
||||
briefing: '',
|
||||
status: 'new'
|
||||
};
|
||||
drawerActive.value = true;
|
||||
}
|
||||
|
||||
async function runAudit() {
|
||||
if (!selectedLeadId.value) return;
|
||||
loadingAudit.value = true;
|
||||
@@ -356,8 +345,8 @@ function openPdf() {
|
||||
}
|
||||
|
||||
async function saveLead() {
|
||||
if (!newLead.value.company_name && !newLead.value.company) {
|
||||
notice.value = { type: 'danger', message: 'Firma oder Firmenname erforderlich.' };
|
||||
if (!newLead.value.company) {
|
||||
notice.value = { type: 'danger', message: 'Organisation erforderlich.' };
|
||||
return;
|
||||
}
|
||||
savingLead.value = true;
|
||||
@@ -368,19 +357,9 @@ async function saveLead() {
|
||||
};
|
||||
await api.post('/items/leads', payload);
|
||||
notice.value = { type: 'success', message: 'Lead erfolgreich registriert!' };
|
||||
showAddLead.value = false;
|
||||
drawerActive.value = false;
|
||||
await fetchLeads();
|
||||
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) {
|
||||
notice.value = { type: 'danger', message: `Fehler beim Speichern: ${e.message}` };
|
||||
} 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) {
|
||||
switch(status) {
|
||||
case 'new': return 'fiber_new';
|
||||
@@ -407,18 +390,13 @@ function getStatusColor(status: string) {
|
||||
default: return 'var(--theme--foreground-subdued)';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchData);
|
||||
</script>
|
||||
|
||||
<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: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; }
|
||||
|
||||
@@ -431,7 +409,7 @@ function getStatusColor(status: string) {
|
||||
|
||||
.ai-observations { display: flex; flex-direction: column; gap: 16px; }
|
||||
.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; }
|
||||
.page-title { font-weight: 600; }
|
||||
@@ -440,6 +418,4 @@ function getStatusColor(status: string) {
|
||||
.drawer-content { padding: 24px; display: flex; flex-direction: column; gap: 32px; }
|
||||
.form-section { display: flex; flex-direction: column; gap: 20px; }
|
||||
.drawer-actions { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
:deep(.v-list-item) { cursor: pointer !important; }
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "acquisition",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"directus:extension": {
|
||||
"type": "endpoint",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cli",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cloner",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"main": "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:
|
||||
infra-cms:
|
||||
image: directus/directus:11
|
||||
image: directus/directus:11.15.2
|
||||
ports:
|
||||
- "8059:8055"
|
||||
networks:
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
DB_CLIENT: "sqlite3"
|
||||
DB_FILENAME: "/directus/database/data.db"
|
||||
WEBSOCKETS_ENABLED: "true"
|
||||
PUBLIC_URL: "http://cms.localhost"
|
||||
EMAIL_TRANSPORT: "smtp"
|
||||
EMAIL_SMTP_HOST: "smtp.eu.mailgun.org"
|
||||
EMAIL_SMTP_PORT: "587"
|
||||
@@ -21,19 +22,29 @@ services:
|
||||
EMAIL_SMTP_PASSWORD: "4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6"
|
||||
EMAIL_SMTP_SECURE: "false"
|
||||
EMAIL_FROM: "postmaster@mg.mintel.me"
|
||||
LOG_LEVEL: "debug"
|
||||
SERVE_APP: "true"
|
||||
EXTENSIONS_AUTO_RELOAD: "true"
|
||||
volumes:
|
||||
- ./database:/directus/database
|
||||
- ./uploads:/directus/uploads
|
||||
- ./schema:/directus/schema
|
||||
- ./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:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.infra-cms.rule=Host(`cms.localhost`)"
|
||||
- "traefik.http.services.infra-cms.loadbalancer.server.port=8055"
|
||||
- "traefik.http.routers.at-mintel-infra-cms.rule=Host(`cms.localhost`)"
|
||||
- "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"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: mintel-infra-cms-internal
|
||||
name: at-mintel-cms-network
|
||||
infra:
|
||||
external: true
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,30 +1,28 @@
|
||||
{
|
||||
"name": "acquisition-manager",
|
||||
"description": "Custom High-Fidelity Acquisition Management for Directus",
|
||||
"icon": "account_balance_wallet",
|
||||
"version": "1.7.12",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
"directus-extension",
|
||||
"directus-extension-module"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Acquisition Manager"
|
||||
"host": "app",
|
||||
"name": "acquisition manager"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||
"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",
|
||||
"description": "Custom High-Fidelity Customer & Company Management for Directus",
|
||||
"icon": "supervisor_account",
|
||||
"version": "1.7.12",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
"directus-extension",
|
||||
"directus-extension-module"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Customer Manager"
|
||||
"host": "app",
|
||||
"name": "customer manager"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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,27 @@
|
||||
{
|
||||
"name": "feedback-commander",
|
||||
"description": "Custom High-Fidelity Feedback Management Extension for Directus",
|
||||
"icon": "view_kanban",
|
||||
"version": "1.7.12",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
"directus-extension",
|
||||
"directus-extension-module"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Feedback Commander"
|
||||
"host": "app",
|
||||
"name": "feedback commander"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,30 +1,28 @@
|
||||
{
|
||||
"name": "people-manager",
|
||||
"description": "Custom High-Fidelity People Management for Directus",
|
||||
"icon": "person",
|
||||
"version": "1.7.12",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
"directus-extension",
|
||||
"directus-extension-module"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "People Manager"
|
||||
"host": "app",
|
||||
"name": "people manager"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||
"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",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -37,7 +37,7 @@ collections:
|
||||
color: null
|
||||
display_template: '{{name}}'
|
||||
group: null
|
||||
hidden: true
|
||||
hidden: false
|
||||
icon: business
|
||||
item_duplication_fields: null
|
||||
note: null
|
||||
@@ -146,6 +146,30 @@ collections:
|
||||
versioning: false
|
||||
schema:
|
||||
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:
|
||||
- collection: client_users
|
||||
field: id
|
||||
@@ -1959,13 +1983,204 @@ fields:
|
||||
validation_message: null
|
||||
width: half
|
||||
schema:
|
||||
name: person
|
||||
table: visual_feedback_comments
|
||||
foreign_key_column: null
|
||||
- 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
|
||||
default_value: null
|
||||
max_length: 36
|
||||
numeric_precision: 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_unique: false
|
||||
is_indexed: false
|
||||
@@ -1989,6 +2204,28 @@ systemFields:
|
||||
schema:
|
||||
is_indexed: true
|
||||
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
|
||||
field: company
|
||||
related_collection: companies
|
||||
|
||||
@@ -1 +1 @@
|
||||
xmKX5
|
||||
--tVj
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "company-manager",
|
||||
"description": "Central Company Management for Directus",
|
||||
"icon": "business",
|
||||
"version": "1.8.2",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -11,17 +11,18 @@
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Company Manager"
|
||||
"host": "app",
|
||||
"name": "company manager"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||
"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",
|
||||
"description": "Custom High-Fidelity Customer & Company Management for Directus",
|
||||
"icon": "supervisor_account",
|
||||
"version": "1.8.2",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -11,17 +11,18 @@
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Customer Manager"
|
||||
"host": "app",
|
||||
"name": "customer manager"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +1,191 @@
|
||||
<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>
|
||||
<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-content>
|
||||
<v-text-overflow text="Neue Firma anlegen" />
|
||||
<v-text-overflow text="Neuen Kunden verlinken" />
|
||||
</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="company-item"
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:active="selectedItem?.id === item.id"
|
||||
class="nav-item"
|
||||
clickable
|
||||
@click="selectCompany(company)"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
<v-list-item-icon><v-icon name="business" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-text-overflow :text="company.name" />
|
||||
<v-text-overflow :text="item.company?.name" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<template #title-outer:after>
|
||||
<v-notice v-if="notice" :type="notice.type" @close="notice = null" dismissible>
|
||||
{{ notice.message }}
|
||||
</v-notice>
|
||||
<template #subtitle>
|
||||
<template v-if="selectedItem">
|
||||
{{ clientUsers.length }} Portal-Nutzer · {{ selectedItem.company?.domain }}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div v-if="!selectedCompany" class="empty-state">
|
||||
<v-info title="Firmen auswählen" icon="business" center>
|
||||
Wähle eine Firma in der Navigation aus oder
|
||||
<v-button x-small @click="openCreateCompany">erstelle eine neue Firma</v-button>.
|
||||
</v-info>
|
||||
</div>
|
||||
<template #actions>
|
||||
<v-button secondary rounded icon v-tooltip.bottom="'Kunden-Verlinkung bearbeiten'" @click="openEditDrawer">
|
||||
<v-icon name="edit" />
|
||||
</v-button>
|
||||
<v-button primary @click="openCreateClientUser">
|
||||
Portal-Nutzer hinzufügen
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="title">{{ selectedCompany.name }}</h1>
|
||||
<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>
|
||||
<template #empty-state>
|
||||
Wähle einen Kunden aus der Liste oder
|
||||
<v-button x-small @click="openCreateDrawer">verlinke eine neue Firma</v-button>.
|
||||
</template>
|
||||
|
||||
<v-table
|
||||
:headers="tableHeaders"
|
||||
:items="employees"
|
||||
:loading="loading"
|
||||
class="clickable-table"
|
||||
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"
|
||||
<!-- Main Content: Client Users Table -->
|
||||
<v-table
|
||||
:headers="tableHeaders"
|
||||
:items="clientUsers"
|
||||
:loading="loading"
|
||||
class="clickable-table"
|
||||
fixed-header
|
||||
@click:row="onRowClick"
|
||||
>
|
||||
<div v-if="drawerCompanyActive" class="drawer-content">
|
||||
<div class="form-section">
|
||||
<div class="field">
|
||||
<span class="label">Firmenname</span>
|
||||
<v-input v-model="companyForm.name" placeholder="z.B. KLZ Cables" autofocus />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<v-button primary block :loading="saving" @click="saveCompany">Speichern</v-button>
|
||||
<v-button primary block :loading="saving" @click="saveItem">Speichern</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</v-drawer>
|
||||
|
||||
<!-- Drawer: Employee Form -->
|
||||
<!-- Drawer: Client User Form -->
|
||||
<v-drawer
|
||||
v-model="drawerEmployeeActive"
|
||||
:title="isEditingEmployee ? 'Mitarbeiter bearbeiten' : 'Neuen Mitarbeiter anlegen'"
|
||||
v-model="drawerUserActive"
|
||||
:title="isEditingUser ? 'Portal-Nutzer bearbeiten' : 'Neuen Portal-Nutzer anlegen'"
|
||||
icon="person"
|
||||
@cancel="drawerEmployeeActive = false"
|
||||
@cancel="drawerUserActive = false"
|
||||
>
|
||||
<div v-if="drawerEmployeeActive" class="drawer-content">
|
||||
<div class="form-section">
|
||||
<div class="field">
|
||||
<span class="label">Vorname</span>
|
||||
<v-input v-model="employeeForm.first_name" placeholder="Vorname" autofocus />
|
||||
</div>
|
||||
<div v-if="drawerUserActive" class="drawer-content">
|
||||
<div class="form-section">
|
||||
<div class="field">
|
||||
<span class="label">Vorname</span>
|
||||
<v-input v-model="userForm.first_name" placeholder="Vorname" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Nachname</span>
|
||||
<v-input v-model="employeeForm.last_name" placeholder="Nachname" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">Nachname</span>
|
||||
<v-input v-model="userForm.last_name" placeholder="Nachname" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">E-Mail</span>
|
||||
<v-input v-model="employeeForm.email" placeholder="E-Mail Adresse" type="email" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">E-Mail</span>
|
||||
<v-input v-model="userForm.email" placeholder="E-Mail Adresse" type="email" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Zentrale Person (Verknüpfung)</span>
|
||||
<v-select
|
||||
v-model="employeeForm.person"
|
||||
:items="peopleOptions"
|
||||
placeholder="Person aus dem People Manager auswählen..."
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">Zentrale Person (Verknüpfung)</span>
|
||||
<v-select
|
||||
v-model="userForm.person"
|
||||
:items="peopleOptions"
|
||||
placeholder="Master-Person auswählen..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-divider v-if="isEditingEmployee" />
|
||||
<v-divider v-if="isEditingUser" />
|
||||
|
||||
<div v-if="isEditingEmployee" class="field">
|
||||
<span class="label">Temporäres Passwort</span>
|
||||
<v-input v-model="employeeForm.temporary_password" readonly class="password-input" />
|
||||
<p class="field-note">Wird beim Senden der Zugangsdaten automatisch generiert.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isEditingUser" class="field">
|
||||
<span class="label">Temporäres Passwort</span>
|
||||
<v-input v-model="userForm.temporary_password" readonly class="password-input" />
|
||||
<p class="field-note">Wird beim Senden der Zugangsdaten automatisch generiert.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-button
|
||||
v-tooltip.bottom="'Generiert PW, speichert es und sendet E-Mail'"
|
||||
secondary
|
||||
block
|
||||
:loading="invitingId === employeeForm.id"
|
||||
@click="inviteUser(employeeForm)"
|
||||
:loading="invitingId === userForm.id"
|
||||
@click="inviteUser(userForm)"
|
||||
>
|
||||
<v-icon name="send" left /> Zugangsdaten senden
|
||||
</v-button>
|
||||
@@ -163,38 +193,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</v-drawer>
|
||||
</private-view>
|
||||
</MintelManagerLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick, computed } from 'vue';
|
||||
import { useApi } from '@directus/extensions-sdk';
|
||||
import { MintelManagerLayout, MintelSelect } from '@mintel/directus-extension-toolkit';
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const companies = ref<any[]>([]);
|
||||
const selectedCompany = ref<any>(null);
|
||||
const employees = ref<any[]>([]);
|
||||
const items = ref<any[]>([]);
|
||||
const selectedItem = ref<any>(null);
|
||||
const clientUsers = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const invitingId = ref<string | null>(null);
|
||||
const notice = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
// Forms State
|
||||
const drawerCompanyActive = ref(false);
|
||||
const isEditingCompany = ref(false);
|
||||
const companyForm = ref({ id: '', name: '' });
|
||||
const companies = ref<any[]>([]);
|
||||
const people = ref<any[]>([]);
|
||||
|
||||
const drawerEmployeeActive = ref(false);
|
||||
const isEditingEmployee = ref(false);
|
||||
const employeeForm = ref({
|
||||
id: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
person: null,
|
||||
temporary_password: ''
|
||||
});
|
||||
const drawerActive = ref(false);
|
||||
const isEditing = ref(false);
|
||||
const form = ref({ id: null, company: null, contact_person: null, status: 'active', notes: '' });
|
||||
|
||||
const drawerUserActive = ref(false);
|
||||
const isEditingUser = ref(false);
|
||||
const userForm = ref({ id: '', first_name: '', last_name: '', email: '', person: null, temporary_password: '' });
|
||||
|
||||
const tableHeaders = [
|
||||
{ text: 'Name', value: 'name', sortable: true },
|
||||
@@ -202,180 +228,159 @@ const tableHeaders = [
|
||||
{ text: 'Zuletzt eingeladen', value: 'last_invited', sortable: true }
|
||||
];
|
||||
|
||||
const people = ref<any[]>([]);
|
||||
|
||||
const peopleOptions = computed(() =>
|
||||
people.value.map(p => ({
|
||||
text: `${p.first_name} ${p.last_name} (${p.email})`,
|
||||
value: p.id
|
||||
}))
|
||||
);
|
||||
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 })));
|
||||
|
||||
async function fetchData() {
|
||||
const [companiesResp, peopleResp] = await Promise.all([
|
||||
api.get('/items/companies', { params: { sort: 'name', fields: ['id', 'name'] } }),
|
||||
api.get('/items/people', { params: { sort: 'last_name' } })
|
||||
]);
|
||||
companies.value = companiesResp.data.data;
|
||||
people.value = peopleResp.data.data;
|
||||
loading.value = true;
|
||||
try {
|
||||
const [custResp, compResp, peopleResp] = await Promise.all([
|
||||
api.get('/items/customers', { params: { fields: ['*', 'company.*', 'contact_person.*'], sort: 'company.name' } }),
|
||||
api.get('/items/companies', { params: { sort: 'name' } }),
|
||||
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) {
|
||||
selectedCompany.value = company;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await api.get('/items/client_users', {
|
||||
params: {
|
||||
filter: { company: { _eq: company.id } },
|
||||
fields: ['*', 'person.*'],
|
||||
sort: 'first_name',
|
||||
},
|
||||
});
|
||||
employees.value = res.data.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
async function selectItem(item: any) {
|
||||
selectedItem.value = item;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await api.get('/items/client_users', {
|
||||
params: {
|
||||
filter: { company: { _eq: item.company.id } },
|
||||
fields: ['*', 'person.*'],
|
||||
sort: 'first_name',
|
||||
},
|
||||
});
|
||||
clientUsers.value = res.data.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Company Actions
|
||||
function openCreateCompany() {
|
||||
isEditingCompany.value = false;
|
||||
companyForm.value = { id: '', name: '' };
|
||||
drawerCompanyActive.value = true;
|
||||
function openCreateDrawer() {
|
||||
isEditing.value = false;
|
||||
form.value = { id: null, company: null, contact_person: null, status: 'active', notes: '' };
|
||||
drawerActive.value = true;
|
||||
}
|
||||
|
||||
async function openEditCompany() {
|
||||
if (!selectedCompany.value) return;
|
||||
companyForm.value = {
|
||||
id: selectedCompany.value.id,
|
||||
name: selectedCompany.value.name
|
||||
function openEditDrawer() {
|
||||
if (!selectedItem.value) return;
|
||||
isEditing.value = true;
|
||||
form.value = {
|
||||
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;
|
||||
await nextTick();
|
||||
drawerCompanyActive.value = true;
|
||||
drawerActive.value = true;
|
||||
}
|
||||
|
||||
async function saveCompany() {
|
||||
if (!companyForm.value.name) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isEditingCompany.value) {
|
||||
await api.patch(`/items/companies/${companyForm.value.id}`, { name: companyForm.value.name });
|
||||
notice.value = { type: 'success', message: 'Firma aktualisiert!' };
|
||||
} else {
|
||||
await api.post('/items/companies', { name: companyForm.value.name });
|
||||
notice.value = { type: 'success', message: 'Firma angelegt!' };
|
||||
}
|
||||
drawerCompanyActive.value = false;
|
||||
await fetchCompanies();
|
||||
if (selectedCompany.value?.id === companyForm.value.id) {
|
||||
selectedCompany.value.name = companyForm.value.name;
|
||||
}
|
||||
} catch (e: any) {
|
||||
notice.value = { type: 'danger', message: e.message };
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
async function saveItem() {
|
||||
if (!form.value.company) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await api.patch(`/items/customers/${form.value.id}`, form.value);
|
||||
notice.value = { type: 'success', message: 'Kunde aktualisiert!' };
|
||||
} else {
|
||||
await api.post('/items/customers', form.value);
|
||||
notice.value = { type: 'success', message: 'Neuer Kunde verlinkt!' };
|
||||
}
|
||||
drawerActive.value = false;
|
||||
await fetchData();
|
||||
if (form.value.id) {
|
||||
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 };
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Employee Actions
|
||||
function openCreateEmployee() {
|
||||
isEditingEmployee.value = false;
|
||||
employeeForm.value = { id: '', first_name: '', last_name: '', email: '', temporary_password: '' };
|
||||
drawerEmployeeActive.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;
|
||||
}
|
||||
// Client User Actions
|
||||
function openCreateClientUser() {
|
||||
isEditingUser.value = false;
|
||||
userForm.value = { id: '', first_name: '', last_name: '', email: '', person: null, temporary_password: '' };
|
||||
drawerUserActive.value = true;
|
||||
}
|
||||
|
||||
function onRowClick(event: any) {
|
||||
const item = event?.item || event;
|
||||
if (item && item.id) {
|
||||
openEditEmployee(item);
|
||||
}
|
||||
const item = event?.item || event;
|
||||
if (item && item.id) {
|
||||
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) {
|
||||
return new Date(dateStr).toLocaleString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
return new Date(dateStr).toLocaleString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
onMounted(fetchData);
|
||||
</script>
|
||||
|
||||
<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-name { font-weight: 600; }
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
.password-input :deep(textarea) {
|
||||
font-family: var(--family-monospace);
|
||||
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:hover) { background-color: var(--theme--background-subdued) !important; }
|
||||
:deep(.v-list-item) { cursor: pointer !important; }
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/directus-extension-toolkit",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"description": "Shared toolkit for Directus extensions in the Mintel ecosystem",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import globals from "globals";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...globals.es2021,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["**/dist/**", "**/node_modules/**", "**/.next/**", "**/build/**"],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/eslint-config",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "feedback-commander",
|
||||
"description": "Custom High-Fidelity Feedback Management Extension for Directus",
|
||||
"icon": "view_kanban",
|
||||
"version": "1.8.2",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -11,13 +11,13 @@
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Feedback Commander"
|
||||
"host": "app",
|
||||
"name": "feedback commander"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/gatekeeper",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global module, require */
|
||||
import path from "node:path";
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/husky-config",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_long_checks:
|
||||
@@ -65,11 +65,6 @@ jobs:
|
||||
PRJ_ID="${{ github.event.repository.name }}"
|
||||
|
||||
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"
|
||||
IMAGE_TAG="main-${SHORT_SHA}"
|
||||
ENV_FILE=".env.testing"
|
||||
@@ -81,9 +76,8 @@ jobs:
|
||||
IS_PROD="false"
|
||||
GOTIFY_TITLE="🧪 Testing-Deploy"
|
||||
GOTIFY_PRIORITY=4
|
||||
fi
|
||||
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"
|
||||
IMAGE_TAG="$TAG"
|
||||
ENV_FILE=".env.prod"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/infra",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/mail",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { Heading, Section, Text, Button, Link } from "@react-email/components";
|
||||
import { Heading, Section, Text, Button } from "@react-email/components";
|
||||
import { MintelLayout } from "../layouts/MintelLayout";
|
||||
|
||||
export interface SiteAuditTemplateProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-config",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-feedback",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -31,8 +31,32 @@ interface Feedback {
|
||||
comments: FeedbackComment[];
|
||||
}
|
||||
|
||||
export function FeedbackOverlay() {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
export function FeedbackOverlay({
|
||||
isActive: externalIsActive,
|
||||
onActiveChange
|
||||
}: {
|
||||
isActive?: boolean;
|
||||
onActiveChange?: (active: boolean) => void
|
||||
}) {
|
||||
// Transparent isolation: Disable overlays in Record Mode Studio
|
||||
const isExcluded = useMemo(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return (
|
||||
window.location.search.includes("embedded=true") ||
|
||||
window.name === "record-mode-iframe" ||
|
||||
(window.self !== window.top)
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (isExcluded) return null;
|
||||
|
||||
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>(
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./handlers";
|
||||
export * from "./components/FeedbackOverlay";
|
||||
export { FeedbackOverlay } from "./components/FeedbackOverlay";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-observability",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-utils",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/observability",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -13,7 +13,9 @@ const entryPoints = [
|
||||
|
||||
try {
|
||||
mkdirSync(resolve(__dirname, 'dist'), { recursive: true });
|
||||
} catch (e) { }
|
||||
} catch (_e) {
|
||||
// Ignore folder creation errors if it already exists
|
||||
}
|
||||
|
||||
console.log(`Building entry points...`);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/pdf",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
||||
@@ -525,13 +525,13 @@ export const FoldingMarks = () => (
|
||||
export const Footer = ({
|
||||
logo,
|
||||
companyData,
|
||||
bankData,
|
||||
_bankData,
|
||||
showDetails = true,
|
||||
showPageNumber = true,
|
||||
}: {
|
||||
logo?: string;
|
||||
companyData: any;
|
||||
bankData?: any;
|
||||
_bankData?: any;
|
||||
showDetails?: boolean;
|
||||
showPageNumber?: boolean;
|
||||
}) => (
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "@react-pdf/renderer";
|
||||
import {
|
||||
DocumentTitle,
|
||||
IndustrialListItem,
|
||||
IndustrialCard,
|
||||
Divider,
|
||||
COLORS,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "people-manager",
|
||||
"id": "people-manager",
|
||||
"description": "Custom High-Fidelity People Management for Directus",
|
||||
"icon": "person",
|
||||
"version": "1.8.2",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -11,19 +10,19 @@
|
||||
"directus-extension-module"
|
||||
],
|
||||
"directus:extension": {
|
||||
"id": "people-manager",
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "People Manager"
|
||||
"host": "app",
|
||||
"name": "people manager"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<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>
|
||||
<v-list nav>
|
||||
<v-list-item @click="openCreateDrawer" clickable>
|
||||
@@ -17,7 +25,7 @@
|
||||
v-for="person in people"
|
||||
:key="person.id"
|
||||
:active="selectedPerson?.id === person.id"
|
||||
class="person-item"
|
||||
class="nav-item"
|
||||
clickable
|
||||
@click="selectPerson(person)"
|
||||
>
|
||||
@@ -31,47 +39,42 @@
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<v-notice v-if="feedback" :type="feedback.type" @close="feedback = null" dismissible>
|
||||
{{ feedback.message }}
|
||||
</v-notice>
|
||||
<template #subtitle>
|
||||
<template v-if="selectedPerson">
|
||||
{{ getCompanyName(selectedPerson) }}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div v-if="!selectedPerson" class="empty-state">
|
||||
<v-info title="Person auswählen" icon="person" center>
|
||||
Wähle eine Person in der Navigation aus oder
|
||||
<v-button x-small @click="openCreateDrawer">erstelle eine neue Person</v-button>.
|
||||
</v-info>
|
||||
<template #actions>
|
||||
<v-button secondary rounded icon v-tooltip.bottom="'Person bearbeiten'" @click="openEditDrawer">
|
||||
<v-icon name="edit" />
|
||||
</v-button>
|
||||
<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 v-else>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="title">{{ selectedPerson.first_name }} {{ selectedPerson.last_name }}</h1>
|
||||
<p class="subtitle">{{ selectedPerson.email || 'Keine E-Mail angegeben' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<v-button secondary rounded icon v-tooltip="'Person bearbeiten'" @click="openEditDrawer">
|
||||
<v-icon name="edit" />
|
||||
</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 class="detail-item">
|
||||
<span class="label">Nachname</span>
|
||||
<p class="value">{{ selectedPerson.last_name }}</p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">E-Mail</span>
|
||||
<p class="value">{{ selectedPerson.email || '---' }}</p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Organisation</span>
|
||||
<p class="value">{{ getCompanyName(selectedPerson) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,24 +98,18 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<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 class="field">
|
||||
<span class="label">Zentrale Firma</span>
|
||||
<v-select
|
||||
<MintelSelect
|
||||
v-model="form.company"
|
||||
:items="companyOptions"
|
||||
placeholder="Bestehende Firma auswählen..."
|
||||
allow-add
|
||||
@add="openQuickAdd('company')"
|
||||
/>
|
||||
</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 class="drawer-actions">
|
||||
@@ -123,12 +120,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</v-drawer>
|
||||
</private-view>
|
||||
</MintelManagerLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ref, onMounted, computed, nextTick } from 'vue';
|
||||
import { useApi } from '@directus/extensions-sdk';
|
||||
import { MintelManagerLayout, MintelSelect } from '@mintel/directus-extension-toolkit';
|
||||
|
||||
const api = useApi();
|
||||
const people = ref([]);
|
||||
@@ -144,9 +142,7 @@ const form = ref({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
company: null,
|
||||
company_name: '',
|
||||
phone: ''
|
||||
company: null
|
||||
});
|
||||
|
||||
const companyOptions = computed(() =>
|
||||
@@ -159,9 +155,9 @@ const companyOptions = computed(() =>
|
||||
function getCompanyName(person: any) {
|
||||
if (!person) return '---';
|
||||
if (person.company) {
|
||||
return typeof person.company === 'object' ? person.company.name : (companies.value.find(c => c.id === person.company)?.name || person.company_name);
|
||||
return 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() {
|
||||
@@ -194,10 +190,8 @@ function openCreateDrawer() {
|
||||
id: null,
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
company: null,
|
||||
company_name: '',
|
||||
phone: ''
|
||||
email: '',
|
||||
company: null
|
||||
};
|
||||
drawerActive.value = true;
|
||||
}
|
||||
@@ -205,23 +199,12 @@ function openCreateDrawer() {
|
||||
function openEditDrawer() {
|
||||
isEditing.value = true;
|
||||
const person = selectedPerson.value;
|
||||
let companyId = null;
|
||||
let companyName = person.company_name || '';
|
||||
|
||||
if (person.company) {
|
||||
if (typeof person.company === 'object') {
|
||||
companyId = person.company.id;
|
||||
} else if (person.company.length === 36) { // Assume UUID
|
||||
companyId = person.company;
|
||||
} else {
|
||||
companyName = person.company;
|
||||
}
|
||||
}
|
||||
|
||||
form.value = {
|
||||
...person,
|
||||
company: companyId,
|
||||
company_name: companyName
|
||||
id: person.id,
|
||||
first_name: person.first_name,
|
||||
last_name: person.last_name,
|
||||
email: person.email,
|
||||
company: person.company?.id || person.company
|
||||
};
|
||||
drawerActive.value = true;
|
||||
}
|
||||
@@ -234,17 +217,20 @@ async function savePerson() {
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
let updatedItem;
|
||||
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!' };
|
||||
} 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!' };
|
||||
}
|
||||
drawerActive.value = false;
|
||||
await fetchData();
|
||||
if (isEditing.value) {
|
||||
selectedPerson.value = people.value.find(p => p.id === form.value.id);
|
||||
if (updatedItem) {
|
||||
selectedPerson.value = people.value.find(p => p.id === updatedItem.id) || updatedItem;
|
||||
}
|
||||
} catch (error) {
|
||||
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);
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 32px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/tsconfig",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.10",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "unified-dashboard",
|
||||
"description": "Unified Infrastructure Dashboard for Directus",
|
||||
"icon": "dashboard",
|
||||
"version": "1.8.2",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -11,13 +11,13 @@
|
||||
],
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "index.js",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "*",
|
||||
"name": "Unified Dashboard"
|
||||
"host": "app",
|
||||
"name": "unified dashboard"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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:
|
||||
globals:
|
||||
specifier: ^17.3.0
|
||||
version: 17.3.0
|
||||
import-in-the-middle:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
@@ -180,6 +183,9 @@ importers:
|
||||
'@directus/extensions-sdk':
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||
'@mintel/directus-extension-toolkit':
|
||||
specifier: workspace:*
|
||||
version: link:../directus-extension-toolkit
|
||||
vue:
|
||||
specifier: ^3.4.0
|
||||
version: 3.5.28(typescript@5.9.3)
|
||||
@@ -247,6 +253,9 @@ importers:
|
||||
'@directus/extensions-sdk':
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||
'@mintel/directus-extension-toolkit':
|
||||
specifier: workspace:*
|
||||
version: link:../directus-extension-toolkit
|
||||
vue:
|
||||
specifier: ^3.4.0
|
||||
version: 3.5.28(typescript@5.9.3)
|
||||
@@ -256,6 +265,9 @@ importers:
|
||||
'@directus/extensions-sdk':
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||
'@mintel/directus-extension-toolkit':
|
||||
specifier: workspace:*
|
||||
version: link:../directus-extension-toolkit
|
||||
vue:
|
||||
specifier: ^3.4.0
|
||||
version: 3.5.28(typescript@5.9.3)
|
||||
@@ -631,6 +643,9 @@ importers:
|
||||
'@directus/extensions-sdk':
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||
'@mintel/directus-extension-toolkit':
|
||||
specifier: workspace:*
|
||||
version: link:../directus-extension-toolkit
|
||||
vue:
|
||||
specifier: ^3.4.0
|
||||
version: 3.5.28(typescript@5.9.3)
|
||||
@@ -5109,6 +5124,10 @@ packages:
|
||||
resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@17.3.0:
|
||||
resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globalthis@1.0.4:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -13103,6 +13122,8 @@ snapshots:
|
||||
|
||||
globals@16.4.0: {}
|
||||
|
||||
globals@17.3.0: {}
|
||||
|
||||
globalthis@1.0.4:
|
||||
dependencies:
|
||||
define-properties: 1.2.1
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
EXTENSIONS_ROOT="$REPO_ROOT/packages"
|
||||
TARGET_DIR="$REPO_ROOT/packages/cms-infra/extensions"
|
||||
|
||||
# List of extensions to sync - including modules and endpoints
|
||||
EXTENSIONS=(
|
||||
# Strict local targets for bombproof isolation
|
||||
TARGET_DIRS=(
|
||||
"$REPO_ROOT/packages/cms-infra/extensions"
|
||||
"$REPO_ROOT/directus/extensions"
|
||||
)
|
||||
|
||||
# List of extension packages to sync
|
||||
EXTENSION_PACKAGES=(
|
||||
"acquisition"
|
||||
"acquisition-manager"
|
||||
"company-manager"
|
||||
@@ -17,60 +22,134 @@ EXTENSIONS=(
|
||||
"unified-dashboard"
|
||||
)
|
||||
|
||||
echo "🚀 Starting extension sync..."
|
||||
echo "🚀 Starting isolated extension sync..."
|
||||
|
||||
# Ensure target directory exists
|
||||
mkdir -p "$TARGET_DIR"
|
||||
# Ensure target directories exist
|
||||
for TARGET in "${TARGET_DIRS[@]}"; do
|
||||
mkdir -p "$TARGET"
|
||||
done
|
||||
|
||||
# Build the acquisition library first so extensions use the updated build
|
||||
echo "📦 Building acquisition-library..."
|
||||
(cd "$REPO_ROOT/packages/acquisition-library" && pnpm build)
|
||||
# Build the acquisition library if it exists
|
||||
if [ -d "$REPO_ROOT/packages/acquisition" ]; then
|
||||
echo "📦 Building acquisition..."
|
||||
(cd "$REPO_ROOT/packages/acquisition" && pnpm build)
|
||||
fi
|
||||
|
||||
for EXT in "${EXTENSIONS[@]}"; do
|
||||
EXT_PATH="$EXTENSIONS_ROOT/$EXT"
|
||||
for PKG in "${EXTENSION_PACKAGES[@]}"; do
|
||||
PKG_PATH="$EXTENSIONS_ROOT/$PKG"
|
||||
|
||||
if [ -d "$EXT_PATH" ]; then
|
||||
echo "📦 Building $EXT..."
|
||||
if [ -d "$PKG_PATH" ]; then
|
||||
echo "📦 Processing $PKG..."
|
||||
|
||||
# Build the extension
|
||||
# We use --if-present to avoid errors if build script is missing
|
||||
(cd "$EXT_PATH" && pnpm build)
|
||||
# 1. Build the extension
|
||||
(cd "$PKG_PATH" && pnpm build)
|
||||
|
||||
# Create target directory for this extension
|
||||
# Directus expects extensions to be in subdirectories matching their name
|
||||
mkdir -p "$TARGET_DIR/$EXT"
|
||||
|
||||
echo "🚚 Syncing $EXT to $TARGET_DIR/$EXT..."
|
||||
|
||||
# Clean target first to avoid ghost files
|
||||
rm -rf "${TARGET_DIR:?}/$EXT"/*
|
||||
|
||||
# Copy build artifacts and package metadata
|
||||
# Some extensions have index.js in root after build, some use dist/
|
||||
# We check for index.js and package.json
|
||||
if [ -f "$EXT_PATH/index.js" ]; then
|
||||
cp "$EXT_PATH/index.js" "$TARGET_DIR/$EXT/"
|
||||
fi
|
||||
|
||||
if [ -f "$EXT_PATH/package.json" ]; then
|
||||
cp "$EXT_PATH/package.json" "$TARGET_DIR/$EXT/"
|
||||
fi
|
||||
EXT_NAME="$PKG"
|
||||
echo "🚚 Syncing $EXT_NAME..."
|
||||
|
||||
if [ -d "$EXT_PATH/dist" ]; then
|
||||
cp -r "$EXT_PATH/dist" "$TARGET_DIR/$EXT/"
|
||||
fi
|
||||
|
||||
# Sync node_modules if they exist (sometimes needed if not everything is bundled)
|
||||
# Deactivated: Causes global scope pollution and login issues in Directus
|
||||
# if [ -d "$EXT_PATH/node_modules" ]; then
|
||||
# echo "📚 Syncing node_modules for $EXT..."
|
||||
# rsync -aL --delete "$EXT_PATH/node_modules/" "$TARGET_DIR/$EXT/node_modules/"
|
||||
# fi
|
||||
# 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"
|
||||
|
||||
echo "🚚 Syncing $EXT_NAME to $FINAL_TARGET..."
|
||||
|
||||
# Clean target first to avoid ghost files
|
||||
mkdir -p "$FINAL_TARGET"
|
||||
rm -rf "${FINAL_TARGET:?}"/*
|
||||
|
||||
# Copy build artifacts
|
||||
if [ -f "$PKG_PATH/dist/index.js" ]; then
|
||||
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 [ -f "$PKG_PATH/package.json" ]; then
|
||||
cp "$PKG_PATH/package.json" "$FINAL_TARGET/"
|
||||
# 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'] = {};
|
||||
|
||||
// Standard metadata for Directus 11.15.x (with core patch applied)
|
||||
pkg['directus:extension'].path = 'index.js';
|
||||
if (!pkg['directus:extension'].host) {
|
||||
pkg['directus:extension'].host = pkg['directus:extension'].type === 'endpoint' ? 'api' : 'app';
|
||||
}
|
||||
if (!pkg['directus:extension'].source) {
|
||||
pkg['directus:extension'].source = 'src/index.ts';
|
||||
}
|
||||
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
fi
|
||||
|
||||
echo "✅ $EXT synced."
|
||||
if [ -d "$PKG_PATH/dist" ]; then
|
||||
cp -r "$PKG_PATH/dist" "$FINAL_TARGET/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ $PKG synced."
|
||||
else
|
||||
echo "❌ Extension source not found: $EXT_PATH"
|
||||
echo "❌ Extension source not found: $PKG_PATH"
|
||||
fi
|
||||
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() {
|
||||
// 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) {
|
||||
return argTag;
|
||||
}
|
||||
@@ -16,11 +16,11 @@ function getVersionTag() {
|
||||
// 1. Check CI environment variables
|
||||
if (
|
||||
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;
|
||||
}
|
||||
if (process.env.TAG && process.env.TAG.startsWith("v")) {
|
||||
if (process.env.TAG && process.env.TAG.match(/^v?\d/)) {
|
||||
return process.env.TAG;
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ function getVersionTag() {
|
||||
const gitTag = execSync("git describe --tags --abbrev=0", {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
if (gitTag && gitTag.startsWith("v")) {
|
||||
if (gitTag && gitTag.match(/^v?\d/)) {
|
||||
return gitTag;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// 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