Files
at-mintel/packages/customer-manager/src/module.vue
2026-02-21 19:08:06 +01:00

539 lines
18 KiB
Vue

<template>
<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="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 Kunden verlinken" />
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item
v-for="item in items"
:key="item.id"
:active="selectedItem?.id === item.id"
class="nav-item"
clickable
@click="selectItem(item)"
>
<v-list-item-icon><v-icon name="business" /></v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="item.company?.name" />
</v-list-item-content>
</v-list-item>
</v-list>
</template>
<template #subtitle>
<template v-if="selectedItem">
{{ clientUsers.length }} Portal-Nutzer &middot; {{ selectedItem.company?.domain }}
</template>
</template>
<template #actions>
<v-button secondary rounded icon v-tooltip.bottom="'Kunden-Verlinkung bearbeiten'" @click="openEditDrawer">
<v-icon name="edit" />
</v-button>
<div @click="onDebugClick" style="display: inline-block; border: 2px solid lime;">
<v-button primary @click="openCreateClientUser">
Portal-Nutzer hinzufügen
</v-button>
</div>
<button style="background: red; color: white; padding: 4px 8px; border-radius: 4px; border: none; cursor: pointer; margin-left: 10px;" @click="onDebugClick">DEBUG</button>
<button style="background: blue; color: white; padding: 8px 16px; border-radius: 4px; border: none; cursor: pointer; margin-left: 10px;" @click="openCreateClientUser">NATIVE: Portal-Nutzer</button>
</template>
<template #empty-state>
Wähle einen Kunden aus der Liste oder
<v-button x-small @click="openCreateDrawer">verlinke eine neue Firma</v-button>.
<button id="debug-click-test" style="background: red; color: white; padding: 4px 8px; border-radius: 4px; border: none; cursor: pointer; margin-left: 10px;" @click="onDebugClick">DEBUG CLICK</button>
</template>
<!-- Main Content: Client Users Table -->
<v-table
:headers="tableHeaders"
:items="clientUsers"
: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>
<!-- 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="saveItem">Speichern</v-button>
</div>
</div>
</v-drawer>
<!-- Drawer: Client User Form -->
<v-drawer
v-model="drawerUserActive"
:title="isEditingUser ? 'Portal-Nutzer bearbeiten' : 'Neuen Portal-Nutzer anlegen'"
icon="person"
@cancel="drawerUserActive = false"
>
<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="userForm.last_name" placeholder="Nachname" />
</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="userForm.person"
:items="peopleOptions"
placeholder="Master-Person auswählen..."
/>
</div>
<v-divider v-if="isEditingUser" />
<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="saveClientUser">Daten speichern</v-button>
<template v-if="isEditingUser">
<v-divider />
<v-button
v-tooltip.bottom="'Generiert PW, speichert es und sendet E-Mail'"
secondary
block
:loading="invitingId === userForm.id"
@click="inviteUser(userForm)"
>
<v-icon name="send" left /> Zugangsdaten senden
</v-button>
</template>
</div>
</div>
</v-drawer>
<!-- Drawer: Quick Add Company -->
<v-drawer
v-model="quickAddCompanyActive"
title="Firma schnell anlegen"
icon="business"
@cancel="quickAddCompanyActive = false"
>
<div class="drawer-content">
<div class="form-section">
<div class="field">
<span class="label">Firmenname</span>
<v-input v-model="quickCompanyForm.name" placeholder="z.B. Schmidt GmbH" autofocus />
</div>
<div class="field">
<span class="label">Domain / Website</span>
<v-input v-model="quickCompanyForm.domain" placeholder="example.com" />
</div>
</div>
<div class="drawer-actions">
<v-button primary block :loading="savingQuick" @click="saveQuickCompany">Firma speichern</v-button>
</div>
</div>
</v-drawer>
<!-- Drawer: Quick Add Person -->
<v-drawer
v-model="quickAddPersonActive"
title="Person schnell anlegen"
icon="person"
@cancel="quickAddPersonActive = false"
>
<div class="drawer-content">
<div class="form-section">
<div class="field">
<span class="label">Vorname</span>
<v-input v-model="quickPersonForm.first_name" placeholder="Vorname" autofocus />
</div>
<div class="field">
<span class="label">Nachname</span>
<v-input v-model="quickPersonForm.last_name" placeholder="Nachname" />
</div>
<div class="field">
<span class="label">E-Mail</span>
<v-input v-model="quickPersonForm.email" placeholder="email@example.com" type="email" />
</div>
</div>
<div class="drawer-actions">
<v-button primary block :loading="savingQuick" @click="saveQuickPerson">Person speichern</v-button>
</div>
</div>
</v-drawer>
</MintelManagerLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed, watch } from 'vue';
import { useApi } from '@directus/extensions-sdk';
import { useRoute } from 'vue-router';
import { MintelManagerLayout, MintelSelect } from '@mintel/directus-extension-toolkit';
const api = useApi();
const route = useRoute();
function onDebugClick() {
console.log("=== [Customer Manager] DEBUG CLICK TRAPPED ===");
alert("Interactivity OK!");
}
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);
const companies = ref<any[]>([]);
const people = ref<any[]>([]);
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 quickAddCompanyActive = ref(false);
const quickAddPersonActive = ref(false);
const savingQuick = ref(false);
const quickCompanyForm = ref({ name: '', domain: '' });
const quickPersonForm = ref({ first_name: '', last_name: '', email: '' });
const tableHeaders = [
{ text: 'Name', value: 'name', sortable: true },
{ text: 'E-Mail', value: 'email', sortable: true },
{ text: 'Zuletzt eingeladen', value: 'last_invited', sortable: true }
];
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() {
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 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;
}
}
function openCreateDrawer() {
isEditing.value = false;
form.value = { id: null, company: null, contact_person: null, status: 'active', notes: '' };
drawerActive.value = true;
}
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
};
drawerActive.value = true;
}
async function saveItem() {
if (!form.value.company) {
notice.value = { type: 'danger', message: 'Bitte wählen Sie eine Firma aus.' };
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.response?.data?.errors?.[0]?.message || e.message || 'Speichern fehlgeschlagen'
};
} finally {
saving.value = false;
}
}
// 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) {
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') {
if (type === 'company') {
quickCompanyForm.value = { name: '', domain: '' };
quickAddCompanyActive.value = true;
} else {
quickPersonForm.value = { first_name: '', last_name: '', email: '' };
quickAddPersonActive.value = true;
}
}
async function saveQuickCompany() {
if (!quickCompanyForm.value.name) return;
savingQuick.value = true;
try {
const res = await api.post('/items/companies', quickCompanyForm.value);
await fetchData();
form.value.company = res.data.data.id;
quickAddCompanyActive.value = false;
notice.value = { type: 'success', message: 'Firma angelegt und ausgewählt.' };
} catch (e: any) {
notice.value = { type: 'danger', message: e.message };
} finally {
savingQuick.value = false;
}
}
async function saveQuickPerson() {
if (!quickPersonForm.value.first_name || !quickPersonForm.value.last_name) return;
savingQuick.value = true;
try {
const res = await api.post('/items/people', quickPersonForm.value);
await fetchData();
form.value.contact_person = res.data.data.id;
quickAddPersonActive.value = false;
notice.value = { type: 'success', message: 'Person angelegt und ausgewählt.' };
} catch (e: any) {
notice.value = { type: 'danger', message: e.message };
} finally {
savingQuick.value = false;
}
}
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'
});
}
function handleDeepLink() {
if (route.query.create === 'true') {
// Only open if not already open to avoid resetting form if user is already typing
if (!drawerActive.value) {
openCreateDrawer();
}
if (route.query.company) {
form.value.company = route.query.company as any;
}
if (route.query.contact_person) {
form.value.contact_person = route.query.contact_person as any;
}
}
}
watch(() => route.query.create, (newVal) => {
if (newVal === 'true') handleDeepLink();
});
onMounted(async () => {
await fetchData();
handleDeepLink();
});
</script>
<style scoped>
.user-cell { display: flex; align-items: center; gap: 12px; }
.user-name { font-weight: 600; }
.status-date { font-size: 12px; color: var(--theme--foreground-subdued); }
.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; }
.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;
color: var(--theme--primary) !important;
background: var(--theme--background-subdued) !important;
}
.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; }
</style>