chore: update lockfile and commit all pending release fixes
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 2m2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m14s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 2m2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m14s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
This commit is contained in:
2
.env
2
.env
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
IMAGE_TAG=v1.8.2
|
||||
IMAGE_TAG=1.8.4
|
||||
PROJECT_NAME=at-mintel
|
||||
PROJECT_COLOR=#82ed20
|
||||
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"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
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"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
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"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
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"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,131 +37,118 @@
|
||||
</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">
|
||||
<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 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">
|
||||
@@ -166,10 +161,12 @@
|
||||
</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>
|
||||
@@ -179,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[]>([]);
|
||||
@@ -192,7 +190,7 @@ 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);
|
||||
|
||||
@@ -222,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) {
|
||||
@@ -236,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,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;
|
||||
@@ -346,16 +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: null,
|
||||
website_url: '',
|
||||
contact_person: null,
|
||||
briefing: '',
|
||||
status: 'new'
|
||||
};
|
||||
} catch (e: any) {
|
||||
notice.value = { type: 'danger', message: `Fehler beim Speichern: ${e.message}` };
|
||||
} finally {
|
||||
@@ -363,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';
|
||||
@@ -382,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; }
|
||||
|
||||
@@ -406,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; }
|
||||
@@ -415,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>
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"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
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"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
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"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
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/directus-extension-toolkit": "workspace:*",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
xEDE-
|
||||
--tVj
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<private-view title="Company Manager">
|
||||
<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>
|
||||
@@ -17,7 +25,7 @@
|
||||
v-for="company in companies"
|
||||
:key="company.id"
|
||||
:active="selectedCompany?.id === company.id"
|
||||
class="company-item"
|
||||
class="nav-item"
|
||||
clickable
|
||||
@click="selectCompany(company)"
|
||||
>
|
||||
@@ -31,41 +39,30 @@
|
||||
</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="selectedCompany">
|
||||
{{ selectedCompany.domain || 'Keine Domain angegeben' }}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div v-if="!selectedCompany" class="empty-state">
|
||||
<v-info title="Firma auswählen" icon="business" center>
|
||||
Wähle eine Firma in der Navigation aus oder
|
||||
<v-button x-small @click="openCreateDrawer">erstelle eine neue Firma</v-button>.
|
||||
</v-info>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div v-else>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="title">{{ selectedCompany.name }}</h1>
|
||||
<p class="subtitle">{{ selectedCompany.domain || 'Keine Domain angegeben' }}</p>
|
||||
</div>
|
||||
<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 class="header-right">
|
||||
<v-button secondary rounded icon v-tooltip="'Firma bearbeiten'" @click="openEditDrawer">
|
||||
<v-icon name="edit" />
|
||||
</v-button>
|
||||
<v-button danger rounded icon v-tooltip="'Firma löschen'" @click="deleteCompany">
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<div class="detail-item full">
|
||||
<span class="label">Notizen / Adresse</span>
|
||||
<p class="value">{{ selectedCompany.notes || '---' }}</p>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -101,12 +98,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</v-drawer>
|
||||
</private-view>
|
||||
</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([]);
|
||||
@@ -144,14 +142,19 @@ function openCreateDrawer() {
|
||||
id: null,
|
||||
name: '',
|
||||
domain: '',
|
||||
notes: ''
|
||||
notes: ''
|
||||
};
|
||||
drawerActive.value = true;
|
||||
}
|
||||
|
||||
function openEditDrawer() {
|
||||
isEditing.value = true;
|
||||
form.value = { ...selectedCompany.value };
|
||||
form.value = {
|
||||
id: selectedCompany.value.id,
|
||||
name: selectedCompany.value.name,
|
||||
domain: selectedCompany.value.domain,
|
||||
notes: selectedCompany.value.notes
|
||||
};
|
||||
drawerActive.value = true;
|
||||
}
|
||||
|
||||
@@ -163,17 +166,20 @@ async function saveCompany() {
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
let updatedItem;
|
||||
if (isEditing.value) {
|
||||
await api.patch(`/items/companies/${form.value.id}`, form.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 {
|
||||
await api.post('/items/companies', form.value);
|
||||
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 (isEditing.value) {
|
||||
selectedCompany.value = companies.value.find(c => c.id === form.value.id);
|
||||
if (updatedItem) {
|
||||
selectedCompany.value = companies.value.find(c => c.id === updatedItem.id) || updatedItem;
|
||||
}
|
||||
} catch (error) {
|
||||
feedback.value = { type: 'danger', message: error.message };
|
||||
@@ -199,13 +205,7 @@ 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: flex; flex-direction: column; gap: 24px; margin-top: 32px; }
|
||||
.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; }
|
||||
|
||||
@@ -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,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,43 +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">{{ getCompanyName(selectedPerson) }}</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">Name</span>
|
||||
<p class="value">{{ selectedPerson.first_name }} {{ selectedPerson.last_name }}</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>
|
||||
|
||||
@@ -89,18 +96,20 @@
|
||||
<span class="label">Nachname</span>
|
||||
<v-input v-model="form.last_name" placeholder="Nachname" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">E-Mail</span>
|
||||
<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>
|
||||
|
||||
<div class="drawer-actions">
|
||||
@@ -111,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([]);
|
||||
@@ -131,8 +141,8 @@ const form = ref({
|
||||
id: null,
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: null,
|
||||
company_name: ''
|
||||
email: '',
|
||||
company: null
|
||||
});
|
||||
|
||||
const companyOptions = computed(() =>
|
||||
@@ -145,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() {
|
||||
@@ -180,8 +190,8 @@ function openCreateDrawer() {
|
||||
id: null,
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: null,
|
||||
company_name: ''
|
||||
email: '',
|
||||
company: null
|
||||
};
|
||||
drawerActive.value = true;
|
||||
}
|
||||
@@ -189,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;
|
||||
}
|
||||
@@ -218,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 };
|
||||
@@ -250,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 {
|
||||
|
||||
132
pnpm-lock.yaml
generated
132
pnpm-lock.yaml
generated
@@ -161,7 +161,7 @@ importers:
|
||||
devDependencies:
|
||||
'@directus/extensions-sdk':
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||
version: 11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)
|
||||
'@mintel/mail':
|
||||
specifier: workspace:*
|
||||
version: link:../mail
|
||||
@@ -180,6 +180,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 +250,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 +262,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)
|
||||
@@ -428,7 +437,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
|
||||
packages/next-config:
|
||||
dependencies:
|
||||
@@ -590,7 +599,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
|
||||
packages/pdf-library:
|
||||
dependencies:
|
||||
@@ -631,6 +640,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)
|
||||
@@ -8803,6 +8815,57 @@ snapshots:
|
||||
|
||||
'@directus/constants@11.0.3': {}
|
||||
|
||||
'@directus/extensions-sdk@11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@directus/composables': 10.1.12(vue@3.4.21(typescript@5.9.3))
|
||||
'@directus/constants': 11.0.3
|
||||
'@directus/extensions': 1.0.2(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(vue@3.4.21(typescript@5.9.3))
|
||||
'@directus/themes': 0.3.6(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))
|
||||
'@directus/types': 11.0.8(knex@3.1.0)(vue@3.4.21(typescript@5.9.3))
|
||||
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||
'@rollup/plugin-commonjs': 25.0.7(rollup@3.29.4)
|
||||
'@rollup/plugin-json': 6.1.0(rollup@3.29.4)
|
||||
'@rollup/plugin-node-resolve': 15.2.3(rollup@3.29.4)
|
||||
'@rollup/plugin-replace': 5.0.5(rollup@3.29.4)
|
||||
'@rollup/plugin-terser': 0.4.4(rollup@3.29.4)
|
||||
'@rollup/plugin-virtual': 3.0.2(rollup@3.29.4)
|
||||
'@vitejs/plugin-vue': 4.6.2(vite@4.5.2(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))(vue@3.4.21(typescript@5.9.3))
|
||||
chalk: 5.3.0
|
||||
commander: 10.0.1
|
||||
esbuild: 0.17.19
|
||||
execa: 7.2.0
|
||||
fs-extra: 11.2.0
|
||||
inquirer: 9.2.16
|
||||
ora: 6.3.1
|
||||
rollup: 3.29.4
|
||||
rollup-plugin-esbuild: 5.0.0(esbuild@0.17.19)(rollup@3.29.4)
|
||||
rollup-plugin-styles: 4.0.0(rollup@3.29.4)
|
||||
vite: 4.5.2(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
vue: 3.4.21(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- '@unhead/vue'
|
||||
- better-sqlite3
|
||||
- debug
|
||||
- knex
|
||||
- less
|
||||
- lightningcss
|
||||
- mysql
|
||||
- mysql2
|
||||
- pg
|
||||
- pg-native
|
||||
- pinia
|
||||
- pino
|
||||
- sass
|
||||
- sqlite3
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- tedious
|
||||
- terser
|
||||
- typescript
|
||||
- vue-router
|
||||
|
||||
'@directus/extensions-sdk@11.0.2(@types/node@22.19.10)(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(lightningcss@1.30.2)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@directus/composables': 10.1.12(vue@3.4.21(typescript@5.9.3))
|
||||
@@ -8854,6 +8917,32 @@ snapshots:
|
||||
- typescript
|
||||
- vue-router
|
||||
|
||||
'@directus/extensions@1.0.2(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(knex@3.1.0)(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(pino@10.3.1)(vue@3.4.21(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@directus/constants': 11.0.3
|
||||
'@directus/themes': 0.3.6(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))
|
||||
'@directus/types': 11.0.8(knex@3.1.0)(vue@3.4.21(typescript@5.9.3))
|
||||
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||
'@types/express': 4.17.21
|
||||
fs-extra: 11.2.0
|
||||
lodash-es: 4.17.21
|
||||
zod: 3.22.4
|
||||
optionalDependencies:
|
||||
knex: 3.1.0
|
||||
pino: 10.3.1
|
||||
vue: 3.4.21(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@unhead/vue'
|
||||
- better-sqlite3
|
||||
- mysql
|
||||
- mysql2
|
||||
- pg
|
||||
- pg-native
|
||||
- pinia
|
||||
- sqlite3
|
||||
- supports-color
|
||||
- tedious
|
||||
|
||||
'@directus/extensions@1.0.2(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(knex@3.1.0)(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(pino@10.3.1)(vue@3.4.21(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@directus/constants': 11.0.3
|
||||
@@ -8897,6 +8986,17 @@ snapshots:
|
||||
|
||||
'@directus/system-data@1.0.2': {}
|
||||
|
||||
'@directus/themes@0.3.6(@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||
'@sinclair/typebox': 0.32.15
|
||||
'@unhead/vue': 1.11.20(vue@3.4.21(typescript@5.9.3))
|
||||
decamelize: 6.0.0
|
||||
flat: 6.0.1
|
||||
lodash-es: 4.17.21
|
||||
pinia: 2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3))
|
||||
vue: 3.4.21(typescript@5.9.3)
|
||||
|
||||
'@directus/themes@0.3.6(@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3)))(pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))(vue@3.4.21(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@directus/utils': 11.0.7(vue@3.4.21(typescript@5.9.3))
|
||||
@@ -10988,6 +11088,14 @@ snapshots:
|
||||
'@unhead/schema': 1.11.20
|
||||
packrup: 0.1.2
|
||||
|
||||
'@unhead/vue@1.11.20(vue@3.4.21(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@unhead/schema': 1.11.20
|
||||
'@unhead/shared': 1.11.20
|
||||
hookable: 5.5.3
|
||||
unhead: 1.11.20
|
||||
vue: 3.4.21(typescript@5.9.3)
|
||||
|
||||
'@unhead/vue@1.11.20(vue@3.5.28(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@unhead/schema': 1.11.20
|
||||
@@ -14418,6 +14526,16 @@ snapshots:
|
||||
|
||||
pify@4.0.1: {}
|
||||
|
||||
pinia@2.3.1(typescript@5.9.3)(vue@3.4.21(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.4.21(typescript@5.9.3)
|
||||
vue-demi: 0.14.10(vue@3.4.21(typescript@5.9.3))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
pinia@2.3.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
@@ -15898,7 +16016,7 @@ snapshots:
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
|
||||
vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
|
||||
@@ -15936,7 +16054,7 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -16019,6 +16137,10 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vue-demi@0.14.10(vue@3.4.21(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.4.21(typescript@5.9.3)
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.28(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.5.28(typescript@5.9.3)
|
||||
|
||||
Reference in New Issue
Block a user