Files
at-mintel/packages/people-manager/src/module.vue
Marc Mintel 9b1f3fb7e8
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m1s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (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
feat(next-feedback): add onActiveChange prop for controlled activation
2026-02-14 02:03:13 +01:00

341 lines
7.6 KiB
Vue

<template>
<private-view title="People Manager">
<template #navigation>
<v-list nav>
<v-list-item @click="openCreateDrawer" clickable>
<v-list-item-icon>
<v-icon name="add" color="var(--theme--primary)" />
</v-list-item-icon>
<v-list-item-content>
<v-text-overflow text="Neue Person anlegen" />
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item
v-for="person in people"
:key="person.id"
:active="selectedPerson?.id === person.id"
class="person-item"
clickable
@click="selectPerson(person)"
>
<v-list-item-icon>
<v-icon name="person" />
</v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="`${person.first_name} ${person.last_name}`" />
</v-list-item-content>
</v-list-item>
</v-list>
</template>
<div class="content-wrapper">
<v-notice v-if="feedback" :type="feedback.type" @close="feedback = null" dismissible>
{{ feedback.message }}
</v-notice>
<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>
</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>
</div>
<!-- Create/Edit Drawer -->
<v-drawer
v-model="drawerActive"
:title="isEditing ? 'Person bearbeiten' : 'Neue Person anlegen'"
icon="person"
@cancel="drawerActive = false"
>
<template #default>
<div class="drawer-content">
<div class="form-section">
<div class="field">
<span class="label">Vorname</span>
<v-input v-model="form.first_name" placeholder="Vorname" autofocus />
</div>
<div class="field">
<span class="label">Nachname</span>
<v-input v-model="form.last_name" placeholder="Nachname" />
</div>
<div class="field">
<span class="label">Zentrale Firma</span>
<v-select
v-model="form.company"
:items="companyOptions"
placeholder="Bestehende Firma auswählen..."
/>
</div>
<div class="field">
<span class="label">Firma (Legacy / Neu)</span>
<v-input v-model="form.company_name" placeholder="z.B. Mintel" />
</div>
</div>
<div class="drawer-actions">
<v-button primary block :loading="saving" @click="savePerson">
Person speichern
</v-button>
</div>
</div>
</template>
</v-drawer>
</private-view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const api = useApi();
const people = ref([]);
const companies = ref([]);
const selectedPerson = ref(null);
const feedback = ref(null);
const saving = ref(false);
const drawerActive = ref(false);
const isEditing = ref(false);
const form = ref({
id: null,
first_name: '',
last_name: '',
company: null,
company_name: ''
});
const companyOptions = computed(() =>
companies.value.map(c => ({
text: c.name,
value: c.id
}))
);
function getCompanyName(person: any) {
if (!person) return '---';
if (person.company) {
return typeof person.company === 'object' ? person.company.name : (companies.value.find(c => c.id === person.company)?.name || person.company_name);
}
return person.company_name || '---';
}
async function fetchData() {
try {
const [peopleResp, companiesResp] = await Promise.all([
api.get('/items/people', {
params: {
sort: 'last_name',
fields: '*.*'
}
}),
api.get('/items/companies', {
params: { sort: 'name' }
})
]);
people.value = peopleResp.data.data;
companies.value = companiesResp.data.data;
} catch (error) {
console.error('Failed to fetch data:', error);
}
}
function selectPerson(person) {
selectedPerson.value = person;
}
function openCreateDrawer() {
isEditing.value = false;
form.value = {
id: null,
first_name: '',
last_name: '',
company: null,
company_name: ''
};
drawerActive.value = true;
}
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
};
drawerActive.value = true;
}
async function savePerson() {
if (!form.value.first_name || !form.value.last_name) {
feedback.value = { type: 'danger', message: 'Vor- und Nachname sind erforderlich.' };
return;
}
saving.value = true;
try {
if (isEditing.value) {
await api.patch(`/items/people/${form.value.id}`, form.value);
feedback.value = { type: 'success', message: 'Person aktualisiert!' };
} else {
await api.post('/items/people', form.value);
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);
}
} catch (error) {
feedback.value = { type: 'danger', message: error.message };
} finally {
saving.value = false;
}
}
async function deletePerson() {
if (!confirm('Soll diese Person wirklich gelöscht werden?')) return;
try {
await api.delete(`/items/people/${selectedPerson.value.id}`);
feedback.value = { type: 'success', message: 'Person gelöscht.' };
selectedPerson.value = null;
await fetchData();
} catch (error) {
feedback.value = { type: 'danger', message: error.message };
}
}
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 {
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;
}
.value {
font-size: 16px;
font-weight: 500;
}
.drawer-content {
padding: 24px;
display: flex;
flex-direction: column;
gap: 32px;
}
.form-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.field {
display: flex;
flex-direction: column;
gap: 8px;
}
.drawer-actions {
margin-top: 24px;
}
</style>