829 lines
27 KiB
Vue
829 lines
27 KiB
Vue
<template>
|
|
<div class="contact-management">
|
|
<CrudDataTable
|
|
ref="tableRef"
|
|
title="Kontakte"
|
|
entity-name="Kontakt"
|
|
:columns="contactColumns"
|
|
data-source="/api/contacts"
|
|
storage-key="contactTableColumns"
|
|
:show-view-button="canView"
|
|
:show-edit-button="canEdit"
|
|
:show-delete-button="canDelete"
|
|
:show-export-button="canExport"
|
|
@view="viewContact"
|
|
@create="openNewContactDialog"
|
|
@edit="editContact"
|
|
@delete="confirmDelete"
|
|
@data-loaded="onDataLoaded"
|
|
>
|
|
<!-- Custom Filter Buttons -->
|
|
<template #filter-buttons="{ loadData }">
|
|
<div class="flex gap-2 mb-4">
|
|
<Button
|
|
label="Alle"
|
|
:outlined="typeFilter !== 'all'"
|
|
@click="filterByType('all', loadData)"
|
|
size="small"
|
|
/>
|
|
<Button
|
|
label="Debitoren"
|
|
:outlined="typeFilter !== 'debtor'"
|
|
@click="filterByType('debtor', loadData)"
|
|
size="small"
|
|
/>
|
|
<Button
|
|
label="Kreditoren"
|
|
:outlined="typeFilter !== 'creditor'"
|
|
@click="filterByType('creditor', loadData)"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Custom Column Templates -->
|
|
<template #body-companyName="{ data }">
|
|
<div class="font-semibold">{{ data.companyName }}</div>
|
|
</template>
|
|
|
|
<template #body-street="{ data }">
|
|
{{ data.street }}
|
|
</template>
|
|
|
|
<template #body-zipCode="{ data }">
|
|
{{ data.zipCode }}
|
|
</template>
|
|
|
|
<template #body-city="{ data }">
|
|
{{ data.city }}
|
|
</template>
|
|
|
|
<template #body-country="{ data }">
|
|
{{ data.country }}
|
|
</template>
|
|
|
|
<template #body-contactPersons="{ data }">
|
|
<div v-if="data.contactPersons?.length > 0">
|
|
<div v-for="person in data.contactPersons.slice(0, 2)" :key="person.id" class="mb-1">
|
|
<div class="font-medium">
|
|
{{ person.firstName }} {{ person.lastName }}
|
|
<Tag v-if="person.isPrimary" value="Primär" severity="info" class="ml-1" />
|
|
</div>
|
|
<div v-if="person.position" class="text-sm text-500">{{ person.position }}</div>
|
|
</div>
|
|
<div v-if="data.contactPersons.length > 2" class="text-sm text-500">
|
|
+{{ data.contactPersons.length - 2 }} weitere
|
|
</div>
|
|
</div>
|
|
<span v-else class="text-500">Keine Ansprechpartner</span>
|
|
</template>
|
|
|
|
<template #body-phone="{ data }">
|
|
<a v-if="data.phone" :href="'tel:' + data.phone" class="text-primary">{{ data.phone }}</a>
|
|
</template>
|
|
|
|
<template #body-fax="{ data }">
|
|
{{ data.fax }}
|
|
</template>
|
|
|
|
<template #body-email="{ data }">
|
|
<a v-if="data.email" :href="'mailto:' + data.email" class="text-primary">{{ data.email }}</a>
|
|
</template>
|
|
|
|
<template #body-website="{ data }">
|
|
<a v-if="data.website" :href="data.website" target="_blank" class="text-primary">
|
|
{{ data.website }}
|
|
</a>
|
|
</template>
|
|
|
|
<template #body-type="{ data }">
|
|
<div class="flex gap-1">
|
|
<Tag v-if="data.isDebtor" value="Debitor" severity="success" />
|
|
<Tag v-if="data.isCreditor" value="Kreditor" severity="warning" />
|
|
</div>
|
|
</template>
|
|
|
|
<template #body-status="{ data }">
|
|
<Tag :value="data.isActive ? 'Aktiv' : 'Inaktiv'" :severity="data.isActive ? 'success' : 'secondary'" />
|
|
</template>
|
|
|
|
<template #body-notes="{ data }">
|
|
<div v-if="data.notes" class="text-sm text-500 line-clamp-2">{{ data.notes }}</div>
|
|
</template>
|
|
|
|
<template #body-createdAt="{ data }">
|
|
{{ formatDate(data.createdAt) }}
|
|
</template>
|
|
|
|
<template #body-updatedAt="{ data }">
|
|
{{ formatDate(data.updatedAt) }}
|
|
</template>
|
|
</CrudDataTable>
|
|
|
|
<!-- Contact Dialog -->
|
|
<Dialog
|
|
v-model:visible="contactDialog"
|
|
:header="editingContact?.id ? 'Kontakt bearbeiten' : 'Neuer Kontakt'"
|
|
:modal="true"
|
|
:style="{ width: '800px' }"
|
|
:closable="!saving"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<!-- Company Information -->
|
|
<div class="font-semibold">Firmeninformationen</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="companyName">Firmenname *</label>
|
|
<InputText
|
|
id="companyName"
|
|
v-model="editingContact.companyName"
|
|
:class="{ 'p-invalid': submitted && !editingContact.companyName }"
|
|
:disabled="saving"
|
|
/>
|
|
<small v-if="submitted && !editingContact.companyName" class="p-error">Firmenname ist erforderlich</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="companyNumber">Kundennummer</label>
|
|
<InputText id="companyNumber" v-model="editingContact.companyNumber" :disabled="saving" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Address -->
|
|
<Divider />
|
|
<div class="font-semibold">Adresse</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label for="street">Straße</label>
|
|
<InputText id="street" v-model="editingContact.street" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="zipCode">PLZ</label>
|
|
<InputText id="zipCode" v-model="editingContact.zipCode" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="city">Ort</label>
|
|
<InputText id="city" v-model="editingContact.city" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label for="country">Land</label>
|
|
<InputText id="country" v-model="editingContact.country" :disabled="saving" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Information -->
|
|
<Divider />
|
|
<div class="font-semibold">Kontaktinformationen</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="phone">Telefon</label>
|
|
<InputText id="phone" v-model="editingContact.phone" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="fax">Fax</label>
|
|
<InputText id="fax" v-model="editingContact.fax" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="email">E-Mail</label>
|
|
<InputText id="email" v-model="editingContact.email" type="email" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="website">Website</label>
|
|
<InputText id="website" v-model="editingContact.website" :disabled="saving" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tax Information -->
|
|
<Divider />
|
|
<div class="font-semibold">Steuerinformationen</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="taxNumber">Steuernummer</label>
|
|
<InputText id="taxNumber" v-model="editingContact.taxNumber" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="vatNumber">USt-IdNr.</label>
|
|
<InputText id="vatNumber" v-model="editingContact.vatNumber" :disabled="saving" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Type and Status -->
|
|
<Divider />
|
|
<div class="font-semibold">Typ und Status</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex items-center gap-2">
|
|
<Checkbox inputId="isDebtor" v-model="editingContact.isDebtor" :binary="true" :disabled="saving" />
|
|
<label for="isDebtor">Debitor</label>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<Checkbox inputId="isCreditor" v-model="editingContact.isCreditor" :binary="true" :disabled="saving" />
|
|
<label for="isCreditor">Kreditor</label>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 col-span-2">
|
|
<Checkbox inputId="isActive" v-model="editingContact.isActive" :binary="true" :disabled="saving" />
|
|
<label for="isActive">Aktiv</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<Divider />
|
|
<div class="flex flex-col gap-2">
|
|
<label for="notes">Notizen</label>
|
|
<Textarea id="notes" v-model="editingContact.notes" rows="3" :disabled="saving" />
|
|
</div>
|
|
|
|
<!-- Contact Persons -->
|
|
<Divider />
|
|
<div class="flex justify-between items-center">
|
|
<div class="font-semibold">Ansprechpartner</div>
|
|
<Button
|
|
label="Hinzufügen"
|
|
icon="pi pi-plus"
|
|
size="small"
|
|
outlined
|
|
@click="addContactPerson"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div v-for="(person, index) in editingContact.contactPersons" :key="index" class="border p-3 rounded-md">
|
|
<div class="flex justify-between items-start mb-3">
|
|
<div class="font-medium">Ansprechpartner {{ index + 1 }}</div>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
size="small"
|
|
text
|
|
rounded
|
|
severity="danger"
|
|
@click="removeContactPerson(index)"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'salutation-' + index">Anrede</label>
|
|
<Select
|
|
:id="'salutation-' + index"
|
|
v-model="person.salutation"
|
|
:options="salutations"
|
|
placeholder="Auswählen"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'title-' + index">Titel</label>
|
|
<InputText :id="'title-' + index" v-model="person.title" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'firstName-' + index">Vorname</label>
|
|
<InputText :id="'firstName-' + index" v-model="person.firstName" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'lastName-' + index">Nachname</label>
|
|
<InputText :id="'lastName-' + index" v-model="person.lastName" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'position-' + index">Position</label>
|
|
<InputText :id="'position-' + index" v-model="person.position" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'department-' + index">Abteilung</label>
|
|
<InputText :id="'department-' + index" v-model="person.department" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'person-phone-' + index">Telefon</label>
|
|
<InputText :id="'person-phone-' + index" v-model="person.phone" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label :for="'mobile-' + index">Mobil</label>
|
|
<InputText :id="'mobile-' + index" v-model="person.mobile" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label :for="'person-email-' + index">E-Mail</label>
|
|
<InputText :id="'person-email-' + index" v-model="person.email" type="email" :disabled="saving" />
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 col-span-2">
|
|
<Checkbox
|
|
:inputId="'isPrimary-' + index"
|
|
v-model="person.isPrimary"
|
|
:binary="true"
|
|
@change="setPrimaryContact(index)"
|
|
:disabled="saving"
|
|
/>
|
|
<label :for="'isPrimary-' + index">Primärer Ansprechpartner</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" outlined @click="closeContactDialog" :disabled="saving" />
|
|
<Button label="Speichern" @click="saveContact" :loading="saving" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
<Dialog
|
|
v-model:visible="deleteDialog"
|
|
header="Kontakt löschen"
|
|
:modal="true"
|
|
:style="{ width: '450px' }"
|
|
:closable="!deleting"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<i class="pi pi-exclamation-triangle text-3xl text-red-500"></i>
|
|
<span>Möchten Sie diesen Kontakt wirklich löschen?</span>
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Abbrechen" outlined @click="deleteDialog = false" :disabled="deleting" />
|
|
<Button label="Löschen" severity="danger" @click="deleteContact" :loading="deleting" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- View Contact Dialog (Read-Only) -->
|
|
<Dialog
|
|
v-model:visible="viewDialog"
|
|
header="Kontakt anzeigen"
|
|
:modal="true"
|
|
:style="{ width: '800px' }"
|
|
>
|
|
<div v-if="viewingContact" class="flex flex-col gap-4">
|
|
<!-- Company Information -->
|
|
<div class="font-semibold">Firmeninformationen</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Firmenname</label>
|
|
<div>{{ viewingContact.companyName }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Kundennummer</label>
|
|
<div>{{ viewingContact.companyNumber || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Address -->
|
|
<Divider />
|
|
<div class="font-semibold">Adresse</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label class="font-medium text-sm text-500">Straße</label>
|
|
<div>{{ viewingContact.street || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">PLZ</label>
|
|
<div>{{ viewingContact.zipCode || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Ort</label>
|
|
<div>{{ viewingContact.city || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label class="font-medium text-sm text-500">Land</label>
|
|
<div>{{ viewingContact.country || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Information -->
|
|
<Divider />
|
|
<div class="font-semibold">Kontaktinformationen</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Telefon</label>
|
|
<div>
|
|
<a v-if="viewingContact.phone" :href="'tel:' + viewingContact.phone" class="text-primary">
|
|
{{ viewingContact.phone }}
|
|
</a>
|
|
<span v-else>-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Fax</label>
|
|
<div>{{ viewingContact.fax || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">E-Mail</label>
|
|
<div>
|
|
<a v-if="viewingContact.email" :href="'mailto:' + viewingContact.email" class="text-primary">
|
|
{{ viewingContact.email }}
|
|
</a>
|
|
<span v-else>-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Website</label>
|
|
<div>
|
|
<a v-if="viewingContact.website" :href="viewingContact.website" target="_blank" class="text-primary">
|
|
{{ viewingContact.website }}
|
|
</a>
|
|
<span v-else>-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tax Information -->
|
|
<Divider />
|
|
<div class="font-semibold">Steuerinformationen</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Steuernummer</label>
|
|
<div>{{ viewingContact.taxNumber || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">USt-IdNr.</label>
|
|
<div>{{ viewingContact.vatNumber || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Type and Status -->
|
|
<Divider />
|
|
<div class="font-semibold">Typ und Status</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Tag v-if="viewingContact.isDebtor" value="Debitor" severity="success" />
|
|
<Tag v-if="viewingContact.isCreditor" value="Kreditor" severity="warning" />
|
|
<Tag :value="viewingContact.isActive ? 'Aktiv' : 'Inaktiv'"
|
|
:severity="viewingContact.isActive ? 'success' : 'secondary'" />
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<Divider />
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Notizen</label>
|
|
<div class="whitespace-pre-wrap">{{ viewingContact.notes || '-' }}</div>
|
|
</div>
|
|
|
|
<!-- Contact Persons -->
|
|
<Divider />
|
|
<div class="font-semibold">Ansprechpartner</div>
|
|
<div v-if="viewingContact.contactPersons?.length > 0" class="flex flex-col gap-3">
|
|
<div v-for="(person, index) in viewingContact.contactPersons" :key="index"
|
|
class="border p-3 rounded-md">
|
|
<div class="flex justify-between items-start mb-3">
|
|
<div class="font-medium">
|
|
{{ person.salutation }} {{ person.title }} {{ person.firstName }} {{ person.lastName }}
|
|
<Tag v-if="person.isPrimary" value="Primär" severity="info" class="ml-2" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3 text-sm">
|
|
<div v-if="person.position">
|
|
<span class="text-500">Position:</span> {{ person.position }}
|
|
</div>
|
|
<div v-if="person.department">
|
|
<span class="text-500">Abteilung:</span> {{ person.department }}
|
|
</div>
|
|
<div v-if="person.phone">
|
|
<span class="text-500">Telefon:</span>
|
|
<a :href="'tel:' + person.phone" class="text-primary">{{ person.phone }}</a>
|
|
</div>
|
|
<div v-if="person.mobile">
|
|
<span class="text-500">Mobil:</span>
|
|
<a :href="'tel:' + person.mobile" class="text-primary">{{ person.mobile }}</a>
|
|
</div>
|
|
<div v-if="person.email" class="col-span-2">
|
|
<span class="text-500">E-Mail:</span>
|
|
<a :href="'mailto:' + person.email" class="text-primary">{{ person.email }}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-500">Keine Ansprechpartner</div>
|
|
|
|
<!-- Timestamps -->
|
|
<Divider />
|
|
<div class="grid grid-cols-2 gap-4 text-sm text-500">
|
|
<div>
|
|
<span class="font-medium">Erstellt am:</span> {{ formatDate(viewingContact.createdAt) }}
|
|
</div>
|
|
<div>
|
|
<span class="font-medium">Zuletzt geändert:</span> {{ formatDate(viewingContact.updatedAt) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Schließen" @click="viewDialog = false" />
|
|
<Button
|
|
v-if="canEdit"
|
|
label="Bearbeiten"
|
|
icon="pi pi-pencil"
|
|
@click="editFromView"
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useToast } from 'primevue/usetoast'
|
|
import { usePermissionStore } from '../stores/permissions'
|
|
import CrudDataTable from '../components/CrudDataTable.vue'
|
|
import Dialog from 'primevue/dialog'
|
|
import Button from 'primevue/button'
|
|
import InputText from 'primevue/inputtext'
|
|
import Textarea from 'primevue/textarea'
|
|
import Tag from 'primevue/tag'
|
|
import Checkbox from 'primevue/checkbox'
|
|
import Select from 'primevue/select'
|
|
import Divider from 'primevue/divider'
|
|
|
|
const toast = useToast()
|
|
const tableRef = ref(null)
|
|
const permissionStore = usePermissionStore()
|
|
|
|
// Permissions
|
|
const canView = computed(() => permissionStore.canView('contacts'))
|
|
const canCreate = computed(() => permissionStore.canCreate('contacts'))
|
|
const canEdit = computed(() => permissionStore.canEdit('contacts'))
|
|
const canDelete = computed(() => permissionStore.canDelete('contacts'))
|
|
const canExport = computed(() => permissionStore.canExport('contacts'))
|
|
|
|
// Column definitions
|
|
const contactColumns = [
|
|
{ key: 'companyName', label: 'Firma', field: 'companyName', sortable: true, style: 'min-width: 200px', default: true },
|
|
{ key: 'companyNumber', label: 'Kundennummer', field: 'companyNumber', default: false },
|
|
{ key: 'street', label: 'Straße', field: 'street', default: false },
|
|
{ key: 'zipCode', label: 'PLZ', field: 'zipCode', default: false },
|
|
{ key: 'city', label: 'Ort', field: 'city', sortable: true, default: true },
|
|
{ key: 'country', label: 'Land', field: 'country', default: false },
|
|
{ key: 'contactPersons', label: 'Ansprechpartner', style: 'min-width: 200px', default: true },
|
|
{ key: 'phone', label: 'Telefon', field: 'phone', default: true },
|
|
{ key: 'fax', label: 'Fax', field: 'fax', default: false },
|
|
{ key: 'email', label: 'E-Mail', field: 'email', default: true },
|
|
{ key: 'website', label: 'Website', field: 'website', default: true },
|
|
{ key: 'taxNumber', label: 'Steuernummer', field: 'taxNumber', default: false },
|
|
{ key: 'vatNumber', label: 'USt-IdNr.', field: 'vatNumber', default: false },
|
|
{
|
|
key: 'type',
|
|
label: 'Typ (Debitor/Kreditor)',
|
|
default: true,
|
|
exportFormatter: (data) => {
|
|
const types = []
|
|
if (data.isDebtor) types.push('Debitor')
|
|
if (data.isCreditor) types.push('Kreditor')
|
|
return types.join(', ')
|
|
}
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
default: true,
|
|
exportFormatter: (data) => data.isActive ? 'Aktiv' : 'Inaktiv'
|
|
},
|
|
{ key: 'notes', label: 'Notizen', field: 'notes', default: false },
|
|
{ key: 'createdAt', label: 'Erstellt am', field: 'createdAt', default: false },
|
|
{ key: 'updatedAt', label: 'Zuletzt geändert', field: 'updatedAt', default: false }
|
|
]
|
|
|
|
// State
|
|
const contactDialog = ref(false)
|
|
const viewDialog = ref(false)
|
|
const deleteDialog = ref(false)
|
|
const editingContact = ref(null)
|
|
const viewingContact = ref(null)
|
|
const submitted = ref(false)
|
|
const saving = ref(false)
|
|
const deleting = ref(false)
|
|
const typeFilter = ref('all')
|
|
|
|
const salutations = ref(['Herr', 'Frau', 'Divers'])
|
|
|
|
// Helper functions
|
|
const emptyContact = () => ({
|
|
companyName: '',
|
|
companyNumber: '',
|
|
street: '',
|
|
zipCode: '',
|
|
city: '',
|
|
country: 'Deutschland',
|
|
phone: '',
|
|
fax: '',
|
|
email: '',
|
|
website: '',
|
|
taxNumber: '',
|
|
vatNumber: '',
|
|
isDebtor: false,
|
|
isCreditor: false,
|
|
isActive: true,
|
|
notes: '',
|
|
contactPersons: []
|
|
})
|
|
|
|
const emptyContactPerson = () => ({
|
|
salutation: null,
|
|
title: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
position: '',
|
|
department: '',
|
|
phone: '',
|
|
mobile: '',
|
|
email: '',
|
|
isPrimary: false
|
|
})
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return ''
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
// Filter by type
|
|
const filterByType = (type, loadData) => {
|
|
typeFilter.value = type
|
|
const params = {}
|
|
|
|
if (type === 'debtor') {
|
|
params.isDebtor = 'true'
|
|
} else if (type === 'creditor') {
|
|
params.isCreditor = 'true'
|
|
}
|
|
|
|
loadData(params)
|
|
}
|
|
|
|
// CRUD Operations
|
|
const openNewContactDialog = () => {
|
|
editingContact.value = emptyContact()
|
|
submitted.value = false
|
|
contactDialog.value = true
|
|
}
|
|
|
|
const viewContact = (contact) => {
|
|
viewingContact.value = { ...contact }
|
|
viewDialog.value = true
|
|
}
|
|
|
|
const editContact = (contact) => {
|
|
editingContact.value = { ...contact }
|
|
submitted.value = false
|
|
contactDialog.value = true
|
|
}
|
|
|
|
const closeContactDialog = () => {
|
|
contactDialog.value = false
|
|
editingContact.value = null
|
|
submitted.value = false
|
|
}
|
|
|
|
const saveContact = async () => {
|
|
submitted.value = true
|
|
|
|
if (!editingContact.value.companyName) {
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
const isEdit = !!editingContact.value.id
|
|
const url = isEdit ? `/api/contacts/${editingContact.value.id}` : '/api/contacts'
|
|
const method = isEdit ? 'PUT' : 'POST'
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/ld+json',
|
|
'Accept': 'application/ld+json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(editingContact.value)
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
|
|
if (error.violations && error.violations.length > 0) {
|
|
const errorMessages = error.violations
|
|
.map(v => `${v.propertyPath}: ${v.message}`)
|
|
.join('\n')
|
|
throw new Error(errorMessages)
|
|
}
|
|
|
|
throw new Error(error['hydra:description'] || error.message || 'Fehler beim Speichern')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: `Kontakt wurde ${isEdit ? 'aktualisiert' : 'erstellt'}`,
|
|
life: 3000
|
|
})
|
|
|
|
closeContactDialog()
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error saving contact:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: error.message || 'Kontakt konnte nicht gespeichert werden',
|
|
life: 5000
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const confirmDelete = (contact) => {
|
|
editingContact.value = contact
|
|
deleteDialog.value = true
|
|
}
|
|
|
|
const deleteContact = async () => {
|
|
deleting.value = true
|
|
|
|
try {
|
|
const response = await fetch(`/api/contacts/${editingContact.value.id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Fehler beim Löschen des Kontakts')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Kontakt wurde gelöscht',
|
|
life: 3000
|
|
})
|
|
|
|
deleteDialog.value = false
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error deleting contact:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Kontakt konnte nicht gelöscht werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
|
|
// Contact Person Management
|
|
const addContactPerson = () => {
|
|
editingContact.value.contactPersons.push(emptyContactPerson())
|
|
}
|
|
|
|
const removeContactPerson = (index) => {
|
|
editingContact.value.contactPersons.splice(index, 1)
|
|
}
|
|
|
|
const setPrimaryContact = (index) => {
|
|
if (editingContact.value.contactPersons[index].isPrimary) {
|
|
editingContact.value.contactPersons.forEach((person, i) => {
|
|
if (i !== index) {
|
|
person.isPrimary = false
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const editFromView = () => {
|
|
editingContact.value = { ...viewingContact.value }
|
|
viewDialog.value = false
|
|
submitted.value = false
|
|
contactDialog.value = true
|
|
}
|
|
|
|
const onDataLoaded = (data) => {
|
|
// Optional: Do something when data is loaded
|
|
}
|
|
</script>
|