All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 6s
Monorepo Pipeline / 🧪 Test (push) Successful in 56s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m22s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m51s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Summary of changes: - Corrected Directus extensions to use 'vue-router' for 'useRouter' instead of '@directus/extensions-sdk' (Fixed runtime crash). - Standardized extension folder structure and moved built extensions to the root 'directus/extensions' directory. - Updated 'scripts/sync-extensions.sh' and 'scripts/validate-extensions.sh' for better extension management. - Added 'scripts/validate-sdk-imports.sh' as a safeguard against future invalid SDK imports. - Integrated import validation into the '.husky/pre-push' hook. - Standardized Docker restart policies and network configurations in 'cms-infra/docker-compose.yml'. - Updated tracked 'data.db' with the correct 'module_bar' settings to ensure extension visibility. - Cleaned up legacy files and consolidated extension package source code. This commit captures the full state of the repository after resolving the 'missing extensions' issue.
357 lines
8.1 KiB
Vue
357 lines
8.1 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">{{ selectedPerson.email || 'Keine E-Mail angegeben' }}</p>
|
|
</div>
|
|
|
|
<div class="header-right">
|
|
<v-button secondary rounded icon v-tooltip="'Person bearbeiten'" @click="openEditDrawer">
|
|
<v-icon name="edit" />
|
|
</v-button>
|
|
<v-button danger rounded icon v-tooltip="'Person löschen'" @click="deletePerson">
|
|
<v-icon name="delete" />
|
|
</v-button>
|
|
</div>
|
|
</header>
|
|
|
|
<v-divider />
|
|
|
|
<div class="details-grid">
|
|
<div class="detail-item">
|
|
<span class="label">Organisation / Firma</span>
|
|
<p class="value">{{ getCompanyName(selectedPerson) }}</p>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">Telefon</span>
|
|
<p class="value">{{ selectedPerson.phone || '---' }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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">E-Mail</span>
|
|
<v-input v-model="form.email" placeholder="email@beispiel.de" type="email" />
|
|
</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 class="field">
|
|
<span class="label">Telefon</span>
|
|
<v-input v-model="form.phone" placeholder="+49 ..." />
|
|
</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: '',
|
|
email: '',
|
|
company: null,
|
|
company_name: '',
|
|
phone: ''
|
|
});
|
|
|
|
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: '',
|
|
email: '',
|
|
company: null,
|
|
company_name: '',
|
|
phone: ''
|
|
};
|
|
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>
|