Files
minmal-file-cloud-email-pim…/frontend/src/views/ContactsView.vue
T
Stefan Hacker 82f3091f2e feat: Papierkorb + Bestaetigungsdialoge bei allen Loeschaktionen
Papierkorb:
- Dateien/Ordner werden beim Loeschen in den Papierkorb verschoben
  (Soft-Delete) statt sofort geloescht
- Papierkorb-Seite in der Sidebar mit Tabelle aller geloeschten Elemente
- Pro Element: Wiederherstellen (am Originalort) oder endgueltig loeschen
- "Papierkorb leeren" Button loescht alles unwiderruflich
- Backend: is_trashed, trashed_at, original_parent_id Felder im File-Model
- Getrashte Dateien erscheinen nicht in der normalen Dateiliste

Bestaetigungsdialoge (vorher fehlend):
- Kontakte: "Moechtest du XY wirklich loeschen?"
- Kalender Events: Bestaetigung vor dem Loeschen
- Kalender: Bestaetigung vor dem Loeschen (mit Hinweis auf Events)
- E-Mail Nachrichten: Bestaetigung mit Betreff-Vorschau
- Share-Link Dateien: Bestaetigung beim Loeschen aus geteiltem Ordner
- Admin SFTP-Backup-Ziele: Bestaetigung
- Admin Email-Konten: Bestaetigung

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:50:19 +02:00

226 lines
7.7 KiB
Vue

<template>
<div class="view-container">
<div class="view-header">
<h2>Kontakte</h2>
<div class="header-actions">
<Button icon="pi pi-book" label="Neues Adressbuch" size="small" outlined @click="showNewBook = true" />
<Button icon="pi pi-user-plus" label="Neuer Kontakt" size="small" @click="openNewContact" />
</div>
</div>
<div class="contacts-layout">
<aside class="books-sidebar">
<div v-for="book in addressBooks" :key="book.id"
class="book-item" :class="{ active: selectedBookId === book.id }"
@click="selectBook(book.id)">
<i class="pi pi-book"></i>
<span>{{ book.name }}</span>
<span class="count">{{ book.contact_count }}</span>
</div>
</aside>
<div class="contacts-main">
<div class="search-bar">
<InputText v-model="searchQuery" placeholder="Kontakte suchen..." fluid @input="loadContacts" />
</div>
<DataTable :value="contacts" :loading="loading" striped-rows @row-click="openEditContact">
<template #empty><p class="empty">Keine Kontakte</p></template>
<Column field="display_name" header="Name" sortable />
<Column field="email" header="E-Mail" sortable />
<Column field="phone" header="Telefon" />
<Column header="" style="width: 80px">
<template #body="{ data }">
<Button icon="pi pi-trash" text size="small" severity="danger" @click.stop="confirmDeleteContact(data)" />
</template>
</Column>
</DataTable>
</div>
</div>
<!-- New Address Book -->
<Dialog v-model:visible="showNewBook" header="Neues Adressbuch" modal :style="{ width: '400px' }">
<div class="field">
<label>Name</label>
<InputText v-model="newBookName" fluid autofocus @keyup.enter="createBook" />
</div>
<template #footer>
<Button label="Abbrechen" text @click="showNewBook = false" />
<Button label="Erstellen" @click="createBook" />
</template>
</Dialog>
<!-- Contact Form -->
<Dialog v-model:visible="showContactForm" :header="editingContact ? 'Kontakt bearbeiten' : 'Neuer Kontakt'" modal :style="{ width: '500px' }">
<div class="field">
<label>Name</label>
<InputText v-model="contactForm.display_name" fluid autofocus />
</div>
<div class="field">
<label>E-Mail</label>
<InputText v-model="contactForm.email" type="email" fluid />
</div>
<div class="field">
<label>Telefon</label>
<InputText v-model="contactForm.phone" fluid />
</div>
<div class="field">
<label>Organisation</label>
<InputText v-model="contactForm.organization" fluid />
</div>
<div class="field">
<label>Notizen</label>
<Textarea v-model="contactForm.notes" rows="3" fluid />
</div>
<template #footer>
<Button label="Abbrechen" text @click="showContactForm = false" />
<Button :label="editingContact ? 'Speichern' : 'Erstellen'" @click="saveContact" />
</template>
</Dialog>
<!-- Delete Confirm -->
<Dialog v-model:visible="showDeleteConfirm" header="Kontakt loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du <strong>{{ deleteTarget?.display_name }}</strong> wirklich loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="showDeleteConfirm = false" />
<Button label="Loeschen" severity="danger" @click="doDeleteContact" />
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useToast } from 'primevue/usetoast'
import apiClient from '../api/client'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
const toast = useToast()
const addressBooks = ref([])
const contacts = ref([])
const selectedBookId = ref(null)
const loading = ref(false)
const searchQuery = ref('')
const showNewBook = ref(false)
const newBookName = ref('')
const showContactForm = ref(false)
const editingContact = ref(null)
const contactForm = ref({ display_name: '', email: '', phone: '', organization: '', notes: '' })
async function loadBooks() {
const res = await apiClient.get('/addressbooks')
addressBooks.value = res.data
if (addressBooks.value.length && !selectedBookId.value) {
selectedBookId.value = addressBooks.value[0].id
await loadContacts()
}
if (!addressBooks.value.length) {
await apiClient.post('/addressbooks', { name: 'Kontakte' })
await loadBooks()
}
}
async function loadContacts() {
if (!selectedBookId.value) return
loading.value = true
try {
const params = searchQuery.value ? { search: searchQuery.value } : {}
const res = await apiClient.get(`/addressbooks/${selectedBookId.value}/contacts`, { params })
contacts.value = res.data
} finally {
loading.value = false
}
}
function selectBook(id) {
selectedBookId.value = id
loadContacts()
}
async function createBook() {
if (!newBookName.value.trim()) return
await apiClient.post('/addressbooks', { name: newBookName.value.trim() })
showNewBook.value = false
newBookName.value = ''
await loadBooks()
}
function openNewContact() {
editingContact.value = null
contactForm.value = { display_name: '', email: '', phone: '', organization: '', notes: '' }
showContactForm.value = true
}
function openEditContact(event) {
const c = event.data
editingContact.value = c
contactForm.value = {
display_name: c.display_name || '',
email: c.email || '',
phone: c.phone || '',
organization: '',
notes: '',
}
showContactForm.value = true
}
async function saveContact() {
if (!contactForm.value.display_name.trim()) return
try {
if (editingContact.value) {
await apiClient.put(`/contacts/${editingContact.value.id}`, contactForm.value)
} else {
await apiClient.post(`/addressbooks/${selectedBookId.value}/contacts`, contactForm.value)
}
showContactForm.value = false
await loadContacts()
await loadBooks()
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
}
}
const showDeleteConfirm = ref(false)
const deleteTarget = ref(null)
function confirmDeleteContact(contact) {
deleteTarget.value = contact
showDeleteConfirm.value = true
}
async function doDeleteContact() {
if (!deleteTarget.value) return
await apiClient.delete(`/contacts/${deleteTarget.value.id}`)
showDeleteConfirm.value = false
await loadContacts()
await loadBooks()
}
onMounted(loadBooks)
</script>
<style scoped>
.view-container { padding: 1.5rem; }
.view-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.view-header h2 { margin: 0; }
.header-actions { display: flex; gap: 0.5rem; }
.contacts-layout { display: flex; gap: 1rem; }
.books-sidebar { width: 220px; flex-shrink: 0; }
.book-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-radius: 6px; cursor: pointer; font-size: 0.875rem; }
.book-item:hover { background: var(--p-surface-100); }
.book-item.active { background: var(--p-primary-50); color: var(--p-primary-color); }
.book-item .count { margin-left: auto; color: var(--p-text-muted-color); font-size: 0.75rem; }
.contacts-main { flex: 1; }
.search-bar { margin-bottom: 1rem; }
.field { margin-bottom: 1rem; }
.field label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.875rem; }
.empty { text-align: center; color: var(--p-text-muted-color); padding: 2rem; }
</style>