feat(next-feedback): add onActiveChange prop for controlled activation
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

This commit is contained in:
2026-02-14 02:03:13 +01:00
parent f48d89c368
commit 9b1f3fb7e8
43 changed files with 121616 additions and 37174 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
],
"directus:extension": {
"type": "module",
"path": "dist/index.js",
"path": "index.js",
"source": "src/index.ts",
"host": "app",
"name": "company manager"

View File

@@ -1,14 +0,0 @@
import { defineModule } from '@directus/extensions-sdk';
import ModuleComponent from './module.vue';
export default defineModule({
id: 'company-manager',
name: 'Company Manager',
icon: 'business',
routes: [
{
path: '',
component: ModuleComponent,
},
],
});

View File

@@ -1,231 +0,0 @@
<template>
<private-view title="Company 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 Firma anlegen" />
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item
v-for="company in companies"
:key="company.id"
:active="selectedCompany?.id === company.id"
class="company-item"
clickable
@click="selectCompany(company)"
>
<v-list-item-icon>
<v-icon name="business" />
</v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="company.name" />
</v-list-item-content>
</v-list-item>
</v-list>
</template>
<div class="content-wrapper">
<v-notice v-if="feedback" :type="feedback.type" @close="feedback = null" dismissible>
{{ feedback.message }}
</v-notice>
<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>
<div v-else>
<header class="header">
<div class="header-left">
<h1 class="title">{{ selectedCompany.name }}</h1>
<p class="subtitle">{{ selectedCompany.industry || 'Keine Branche angegeben' }}</p>
</div>
<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="details-grid">
<div class="detail-item">
<span class="label">Website</span>
<p class="value">
<a v-if="selectedCompany.website" :href="selectedCompany.website" target="_blank">{{ selectedCompany.website }}</a>
<span v-else>---</span>
</p>
</div>
<div class="detail-item">
<span class="label">Adresse</span>
<p class="value">{{ selectedCompany.address || '---' }}</p>
</div>
</div>
</div>
</div>
<!-- Create/Edit Drawer -->
<v-drawer
v-model="drawerActive"
:title="isEditing ? 'Firma bearbeiten' : 'Neue Firma anlegen'"
icon="business"
@cancel="drawerActive = false"
>
<template #default>
<div class="drawer-content">
<div class="form-section">
<div class="field">
<span class="label">Firmenname</span>
<v-input v-model="form.name" placeholder="z.B. Schmidt GmbH" autofocus />
</div>
<div class="field">
<span class="label">Branche</span>
<v-input v-model="form.industry" placeholder="z.B. IT-Services" />
</div>
<div class="field">
<span class="label">Website</span>
<v-input v-model="form.website" placeholder="https://..." />
</div>
<div class="field">
<span class="label">Adresse</span>
<v-textarea v-model="form.address" placeholder="Straße, PLZ Ort" />
</div>
</div>
<div class="drawer-actions">
<v-button primary block :loading="saving" @click="saveCompany">
Firma speichern
</v-button>
</div>
</div>
</template>
</v-drawer>
</private-view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const api = useApi();
const companies = ref([]);
const selectedCompany = ref(null);
const feedback = ref(null);
const saving = ref(false);
const drawerActive = ref(false);
const isEditing = ref(false);
const form = ref({
id: null,
name: '',
industry: '',
website: '',
address: ''
});
async function fetchData() {
try {
const resp = await api.get('/items/companies', {
params: { sort: 'name' }
});
companies.value = resp.data.data;
} catch (error) {
console.error('Failed to fetch companies:', error);
}
}
function selectCompany(company: any) {
selectedCompany.value = company;
}
function openCreateDrawer() {
isEditing.value = false;
form.value = {
id: null,
name: '',
industry: '',
website: '',
address: ''
};
drawerActive.value = true;
}
function openEditDrawer() {
isEditing.value = true;
form.value = { ...selectedCompany.value };
drawerActive.value = true;
}
async function saveCompany() {
if (!form.value.name) {
feedback.value = { type: 'danger', message: 'Firmenname ist erforderlich.' };
return;
}
saving.value = true;
try {
if (isEditing.value) {
await api.patch(`/items/companies/${form.value.id}`, form.value);
feedback.value = { type: 'success', message: 'Firma aktualisiert!' };
} else {
await api.post('/items/companies', form.value);
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);
}
} catch (error) {
feedback.value = { type: 'danger', message: error.message };
} finally {
saving.value = false;
}
}
async function deleteCompany() {
if (!confirm('Soll diese Firma wirklich gelöscht werden?')) return;
try {
await api.delete(`/items/companies/${selectedCompany.value.id}`);
feedback.value = { type: 'success', message: 'Firma gelöscht.' };
selectedCompany.value = null;
await fetchData();
} catch (error) {
feedback.value = { type: 'danger', message: error.message };
}
}
onMounted(fetchData);
</script>
<style scoped>
.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>