Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-10 10:32:52 +00:00
committet von Server Deploy
Ursprung 7d67557be4
Commit ef153789cc
20 geänderte Dateien mit 13613 neuen und 333 gelöschten Zeilen

Datei anzeigen

@ -1,7 +1,7 @@
/**
* TASKMATE - Contacts Manager
* ===========================
* Kontaktverwaltung mit Kartenansicht
* Kontaktverwaltung mit Tabellenansicht
*/
import api from './api.js';
@ -12,11 +12,14 @@ class ContactsManager {
constructor() {
this.contacts = [];
this.filteredContacts = [];
this.selectedContacts = new Set();
this.allTags = new Set();
this.searchQuery = '';
this.filterTag = '';
this.sortBy = 'created_at';
this.sortOrder = 'desc';
this.currentPage = 1;
this.itemsPerPage = 25;
this.initialized = false;
}
@ -30,16 +33,30 @@ class ContactsManager {
// DOM Elements
this.contactsView = $('#view-contacts');
this.contactsGrid = $('#contacts-grid');
this.contactsTable = $('#contacts-table');
this.contactsTbody = $('#contacts-tbody');
this.contactsEmpty = $('#contacts-empty');
this.tagFilter = $('#contacts-tag-filter');
this.sortSelect = $('#contacts-sort');
this.selectAllCheckbox = $('#select-all-contacts');
this.bulkActions = $('#bulk-actions');
this.selectedCountSpan = $('#selected-count');
this.contactsCountSpan = $('#contacts-count');
this.newContactBtn = $('#btn-new-contact');
this.exportBtn = $('#btn-export-contacts');
this.bulkDeleteBtn = $('#btn-bulk-delete');
this.deselectAllBtn = $('#btn-deselect-all');
// Pagination
this.pagination = $('#contacts-pagination');
this.currentPageSpan = $('#current-page');
this.totalPagesSpan = $('#total-pages');
this.prevPageBtn = $('#btn-prev-page');
this.nextPageBtn = $('#btn-next-page');
console.log('[Contacts] DOM Elements check:');
console.log(' contactsView:', this.contactsView);
console.log(' newContactBtn:', this.newContactBtn);
console.log(' contactsGrid:', this.contactsGrid);
console.log(' contactsTable:', this.contactsTable);
// Modal Elements
this.contactModal = $('#contact-modal');
@ -87,19 +104,46 @@ class ContactsManager {
if (this.tagFilter) {
this.tagFilter.addEventListener('change', (e) => {
this.filterTag = e.target.value;
this.currentPage = 1;
this.filterContacts();
});
}
// Sort
if (this.sortSelect) {
this.sortSelect.addEventListener('change', (e) => {
const [sortBy, sortOrder] = e.target.value.split('-');
this.sortBy = sortBy;
this.sortOrder = sortOrder;
// Table sorting
$$('.sortable').forEach(th => {
th.addEventListener('click', (e) => {
const sortField = th.dataset.sort;
if (this.sortBy === sortField) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortBy = sortField;
this.sortOrder = 'asc';
}
// Update UI
$$('.sortable').forEach(el => el.classList.remove('sort-asc', 'sort-desc'));
th.classList.add(this.sortOrder === 'asc' ? 'sort-asc' : 'sort-desc');
this.sortContacts();
this.renderContacts();
});
});
// Select all checkbox
if (this.selectAllCheckbox) {
this.selectAllCheckbox.addEventListener('change', (e) => {
const checked = e.target.checked;
const visibleContacts = this.getPaginatedContacts();
if (checked) {
visibleContacts.forEach(contact => this.selectedContacts.add(contact.id));
} else {
visibleContacts.forEach(contact => this.selectedContacts.delete(contact.id));
}
this.updateBulkActions();
this.renderContacts();
});
}
// New Contact Button
@ -114,6 +158,46 @@ class ContactsManager {
console.warn('[Contacts] newContactBtn not found!');
}
// Export Button
if (this.exportBtn) {
this.exportBtn.addEventListener('click', () => this.exportContacts());
}
// Bulk Delete Button
if (this.bulkDeleteBtn) {
this.bulkDeleteBtn.addEventListener('click', () => this.bulkDelete());
}
// Deselect All Button
if (this.deselectAllBtn) {
this.deselectAllBtn.addEventListener('click', () => {
this.selectedContacts.clear();
this.selectAllCheckbox.checked = false;
this.updateBulkActions();
this.renderContacts();
});
}
// Pagination buttons
if (this.prevPageBtn) {
this.prevPageBtn.addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.renderContacts();
}
});
}
if (this.nextPageBtn) {
this.nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(this.filteredContacts.length / this.itemsPerPage);
if (this.currentPage < totalPages) {
this.currentPage++;
this.renderContacts();
}
});
}
// Modal Form
if (this.contactForm) {
this.contactForm.addEventListener('submit', (e) => {
@ -161,6 +245,7 @@ class ContactsManager {
socket.on('contact:deleted', (data) => {
console.log('[Contacts] Socket: contact deleted', data);
this.contacts = this.contacts.filter(c => c.id !== data.contactId);
this.selectedContacts.delete(data.contactId);
this.updateTagsList();
this.filterContacts();
});
@ -216,6 +301,7 @@ class ContactsManager {
});
this.sortContacts();
this.updateContactsCount();
this.renderContacts();
}
@ -243,56 +329,77 @@ class ContactsManager {
});
}
getPaginatedContacts() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
return this.filteredContacts.slice(start, end);
}
renderContacts() {
if (!this.contactsGrid) return;
if (!this.contactsTbody) return;
if (this.filteredContacts.length === 0) {
this.contactsGrid.classList.add('hidden');
this.contactsTable.parentElement.classList.add('hidden');
this.contactsEmpty.classList.remove('hidden');
return;
}
this.contactsGrid.classList.remove('hidden');
this.contactsTable.parentElement.classList.remove('hidden');
this.contactsEmpty.classList.add('hidden');
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join('');
this.contactsGrid.innerHTML = html;
const paginatedContacts = this.getPaginatedContacts();
const html = paginatedContacts.map(contact => this.createContactRow(contact)).join('');
this.contactsTbody.innerHTML = html;
// Bind card events
this.bindCardEvents();
// Update pagination
this.updatePagination();
// Bind row events
this.bindRowEvents();
}
createContactCard(contact) {
createContactRow(contact) {
const displayName = this.getContactDisplayName(contact);
const initials = this.getContactInitials(contact);
const tags = contact.tags || [];
const isSelected = this.selectedContacts.has(contact.id);
return `
<div class="contact-card" data-contact-id="${contact.id}">
<div class="contact-card-header">
<div class="contact-avatar">
${initials}
<tr data-contact-id="${contact.id}" ${isSelected ? 'class="selected"' : ''}>
<td class="checkbox-cell">
<input type="checkbox" class="table-checkbox contact-checkbox" data-contact-id="${contact.id}" ${isSelected ? 'checked' : ''}>
</td>
<td>
<div class="name-cell">
<div class="contact-avatar-small">${initials}</div>
<a href="#" class="contact-name-link" data-contact-id="${contact.id}">${displayName}</a>
</div>
<div class="contact-actions">
<button class="btn-icon btn-edit-contact" title="Bearbeiten">
<i class="fas fa-edit"></i>
</td>
<td>${contact.company || '-'}</td>
<td class="hide-mobile">${contact.position || '-'}</td>
<td>${contact.email ? `<a href="mailto:${contact.email}">${contact.email}</a>` : '-'}</td>
<td class="hide-mobile">${contact.phone || '-'}</td>
<td>
<div class="tags-cell">
${tags.length > 0 ? tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('') : '-'}
</div>
</td>
<td class="actions-cell">
<div class="table-actions">
<button class="btn-table-action btn-edit-contact" data-contact-id="${contact.id}" title="Bearbeiten">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<button class="btn-table-action btn-delete-contact-inline" data-contact-id="${contact.id}" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14M10 11v6M14 11v6"/>
</svg>
</button>
</div>
</div>
<div class="contact-card-body">
<h3 class="contact-name">${displayName}</h3>
${contact.company ? `<div class="contact-company">${contact.company}</div>` : ''}
${contact.position ? `<div class="contact-position">${contact.position}</div>` : ''}
${contact.email ? `<div class="contact-email"><i class="fas fa-envelope"></i> ${contact.email}</div>` : ''}
${contact.phone ? `<div class="contact-phone"><i class="fas fa-phone"></i> ${contact.phone}</div>` : ''}
${contact.mobile ? `<div class="contact-mobile"><i class="fas fa-mobile"></i> ${contact.mobile}</div>` : ''}
</div>
${tags.length > 0 ? `
<div class="contact-tags">
${tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('')}
</div>
` : ''}
</div>
</td>
</tr>
`;
}
@ -325,26 +432,97 @@ class ContactsManager {
return initials || '?';
}
bindCardEvents() {
bindRowEvents() {
// Contact checkboxes
$$('.contact-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const contactId = parseInt(e.target.dataset.contactId);
if (e.target.checked) {
this.selectedContacts.add(contactId);
} else {
this.selectedContacts.delete(contactId);
}
this.updateBulkActions();
this.updateRowSelection(contactId, e.target.checked);
});
});
// Edit buttons
$$('.btn-edit-contact').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const card = btn.closest('.contact-card');
const contactId = parseInt(card.dataset.contactId);
const contactId = parseInt(btn.dataset.contactId);
this.editContact(contactId);
});
});
// Card click
$$('.contact-card').forEach(card => {
card.addEventListener('click', () => {
const contactId = parseInt(card.dataset.contactId);
// Delete inline buttons
$$('.btn-delete-contact-inline').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const contactId = parseInt(btn.dataset.contactId);
this.deleteContactInline(contactId);
});
});
// Name links
$$('.contact-name-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const contactId = parseInt(link.dataset.contactId);
this.editContact(contactId);
});
});
}
updateRowSelection(contactId, selected) {
const row = $(`tr[data-contact-id="${contactId}"]`);
if (row) {
if (selected) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
}
}
updateBulkActions() {
const count = this.selectedContacts.size;
if (count > 0) {
this.bulkActions.classList.remove('hidden');
this.selectedCountSpan.textContent = count;
} else {
this.bulkActions.classList.add('hidden');
}
// Update select all checkbox state
const visibleContacts = this.getPaginatedContacts();
const allSelected = visibleContacts.length > 0 &&
visibleContacts.every(contact => this.selectedContacts.has(contact.id));
this.selectAllCheckbox.checked = allSelected;
}
updateContactsCount() {
const count = this.filteredContacts.length;
this.contactsCountSpan.textContent = `${count} ${count === 1 ? 'Kontakt' : 'Kontakte'}`;
}
updatePagination() {
const totalPages = Math.ceil(this.filteredContacts.length / this.itemsPerPage);
if (totalPages <= 1) {
this.pagination.classList.add('hidden');
return;
}
this.pagination.classList.remove('hidden');
this.currentPageSpan.textContent = this.currentPage;
this.totalPagesSpan.textContent = totalPages;
this.prevPageBtn.disabled = this.currentPage === 1;
this.nextPageBtn.disabled = this.currentPage === totalPages;
}
updateTagsList() {
// Collect all unique tags
this.allTags.clear();
@ -370,6 +548,120 @@ class ContactsManager {
}
}
async bulkDelete() {
const count = this.selectedContacts.size;
if (count === 0) return;
const contactNames = Array.from(this.selectedContacts).map(id => {
const contact = this.contacts.find(c => c.id === id);
return contact ? this.getContactDisplayName(contact) : '';
}).filter(Boolean);
const message = count === 1
? `Möchten Sie den Kontakt "${contactNames[0]}" wirklich löschen?`
: `Möchten Sie ${count} Kontakte wirklich löschen?`;
if (!confirm(message)) return;
try {
// Delete contacts one by one
for (const contactId of this.selectedContacts) {
await api.deleteContact(contactId);
}
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: `${count} ${count === 1 ? 'Kontakt' : 'Kontakte'} gelöscht`, type: 'success' }
}));
this.selectedContacts.clear();
await this.loadContacts();
} catch (error) {
console.error('[Contacts] Error during bulk delete:', error);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Fehler beim Löschen der Kontakte', type: 'error' }
}));
}
}
async deleteContactInline(contactId) {
const contact = this.contacts.find(c => c.id === contactId);
if (!contact) return;
const displayName = this.getContactDisplayName(contact);
if (!confirm(`Möchten Sie den Kontakt "${displayName}" wirklich löschen?`)) {
return;
}
try {
await api.deleteContact(contactId);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Kontakt gelöscht', type: 'success' }
}));
await this.loadContacts();
} catch (error) {
console.error('[Contacts] Error deleting contact:', error);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Fehler beim Löschen', type: 'error' }
}));
}
}
async exportContacts() {
try {
let contactsToExport = this.filteredContacts;
// Create CSV content
const headers = ['Vorname', 'Nachname', 'Firma', 'Position', 'E-Mail', 'Telefon', 'Mobil', 'Adresse', 'PLZ', 'Stadt', 'Land', 'Website', 'Tags', 'Notizen'];
const rows = [headers];
contactsToExport.forEach(contact => {
const row = [
contact.firstName || '',
contact.lastName || '',
contact.company || '',
contact.position || '',
contact.email || '',
contact.phone || '',
contact.mobile || '',
contact.address || '',
contact.postalCode || '',
contact.city || '',
contact.country || '',
contact.website || '',
(contact.tags || []).join(', '),
contact.notes || ''
];
rows.push(row);
});
// Convert to CSV
const csvContent = rows.map(row =>
row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',')
).join('\n');
// Create blob and download
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `kontakte_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: `${contactsToExport.length} Kontakte exportiert`, type: 'success' }
}));
} catch (error) {
console.error('[Contacts] Error exporting contacts:', error);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Fehler beim Exportieren', type: 'error' }
}));
}
}
showContactModal(contact = null) {
console.log('[Contacts] showContactModal called with:', contact);
console.log('[Contacts] contactModal element:', this.contactModal);