/** * TASKMATE - Contacts Manager * =========================== * Kontaktverwaltung mit Tabellenansicht */ import api from './api.js'; import { $, $$, formatDate, debounce } from './utils.js'; import store from './store.js'; 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; } async init() { console.log('[Contacts] init() called, initialized:', this.initialized); if (this.initialized) { await this.loadContacts(); return; } // DOM Elements this.contactsView = $('#view-contacts'); this.contactsTable = $('#contacts-table'); this.contactsTbody = $('#contacts-tbody'); this.contactsEmpty = $('#contacts-empty'); this.tagFilter = $('#contacts-tag-filter'); 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(' contactsTable:', this.contactsTable); // Modal Elements this.contactModal = $('#contact-modal'); this.modalTitle = $('#contact-modal-title'); this.contactForm = $('#contact-form'); console.log('[Contacts] Modal Elements check:'); console.log(' contactModal:', this.contactModal); console.log(' contactForm:', this.contactForm); this.contactIdInput = $('#contact-id'); this.firstNameInput = $('#contact-first-name'); this.lastNameInput = $('#contact-last-name'); this.companyInput = $('#contact-company'); this.positionInput = $('#contact-position'); this.emailInput = $('#contact-email'); this.phoneInput = $('#contact-phone'); this.mobileInput = $('#contact-mobile'); this.addressInput = $('#contact-address'); this.postalCodeInput = $('#contact-postal-code'); this.cityInput = $('#contact-city'); this.countryInput = $('#contact-country'); this.websiteInput = $('#contact-website'); this.notesInput = $('#contact-notes'); this.tagsInput = $('#contact-tags'); this.deleteContactBtn = $('#btn-delete-contact'); this.bindEvents(); this.initialized = true; console.log('[Contacts] Initialization complete'); await this.loadContacts(); // Store subscriptions store.subscribe('contacts', this.handleContactsUpdate.bind(this)); // Window events window.addEventListener('app:refresh', () => this.loadContacts()); window.addEventListener('modal:close', () => this.loadContacts()); } bindEvents() { console.log('[Contacts] bindEvents() called'); // Tag Filter if (this.tagFilter) { this.tagFilter.addEventListener('change', (e) => { this.filterTag = e.target.value; this.currentPage = 1; this.filterContacts(); }); } // 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 console.log('[Contacts] newContactBtn element:', this.newContactBtn); if (this.newContactBtn) { console.log('[Contacts] Binding click event to newContactBtn'); this.newContactBtn.addEventListener('click', () => { console.log('[Contacts] New contact button clicked!'); this.showContactModal(); }); } else { 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) => { e.preventDefault(); this.saveContact(); }); } // Delete Button if (this.deleteContactBtn) { this.deleteContactBtn.addEventListener('click', () => this.deleteContact()); } // Modal Close const modalCloseBtn = this.contactModal?.querySelector('.modal-close'); if (modalCloseBtn) { modalCloseBtn.addEventListener('click', () => this.hideContactModal()); } const modalCancelBtn = this.contactModal?.querySelector('.modal-cancel'); if (modalCancelBtn) { modalCancelBtn.addEventListener('click', () => this.hideContactModal()); } // Socket Events const socket = window.socket; if (socket) { socket.on('contact:created', (data) => { console.log('[Contacts] Socket: contact created', data); this.contacts.unshift(data.contact); this.updateTagsList(); this.filterContacts(); }); socket.on('contact:updated', (data) => { console.log('[Contacts] Socket: contact updated', data); const index = this.contacts.findIndex(c => c.id === data.contact.id); if (index !== -1) { this.contacts[index] = data.contact; this.updateTagsList(); this.filterContacts(); } }); 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(); }); } } async loadContacts() { try { console.log('[Contacts] Loading contacts...'); const response = await api.getContacts(); this.contacts = response.data || response || []; this.updateTagsList(); this.filterContacts(); console.log(`[Contacts] Loaded ${this.contacts.length} contacts`); } catch (error) { console.error('[Contacts] Error loading contacts:', error); window.dispatchEvent(new CustomEvent('toast:show', { detail: { message: 'Fehler beim Laden der Kontakte', type: 'error' } })); } } filterContacts() { this.filteredContacts = this.contacts.filter(contact => { // Search filter if (this.searchQuery) { const query = this.searchQuery.toLowerCase(); const searchFields = [ contact.firstName, contact.lastName, contact.company, contact.email, contact.position, contact.phone, contact.mobile ].filter(Boolean).join(' ').toLowerCase(); if (!searchFields.includes(query)) { return false; } } // Tag filter if (this.filterTag && contact.tags) { if (!contact.tags.includes(this.filterTag)) { return false; } } return true; }); this.sortContacts(); this.updateContactsCount(); this.renderContacts(); } sortContacts() { this.filteredContacts.sort((a, b) => { let aVal = a[this.sortBy] || ''; let bVal = b[this.sortBy] || ''; // Handle name sorting if (this.sortBy === 'name') { aVal = `${a.lastName || ''} ${a.firstName || ''}`.trim(); bVal = `${b.lastName || ''} ${b.firstName || ''}`.trim(); } if (typeof aVal === 'string') { aVal = aVal.toLowerCase(); bVal = bVal.toLowerCase(); } if (this.sortOrder === 'asc') { return aVal < bVal ? -1 : aVal > bVal ? 1 : 0; } else { return aVal > bVal ? -1 : aVal < bVal ? 1 : 0; } }); } getPaginatedContacts() { const start = (this.currentPage - 1) * this.itemsPerPage; const end = start + this.itemsPerPage; return this.filteredContacts.slice(start, end); } renderContacts() { if (!this.contactsTbody) return; if (this.filteredContacts.length === 0) { this.contactsTable.parentElement.classList.add('hidden'); this.contactsEmpty.classList.remove('hidden'); return; } this.contactsTable.parentElement.classList.remove('hidden'); this.contactsEmpty.classList.add('hidden'); const paginatedContacts = this.getPaginatedContacts(); const html = paginatedContacts.map(contact => this.createContactRow(contact)).join(''); this.contactsTbody.innerHTML = html; // Update pagination this.updatePagination(); // Bind row events this.bindRowEvents(); } createContactRow(contact) { const displayName = this.getContactDisplayName(contact); const initials = this.getContactInitials(contact); const tags = contact.tags || []; const isSelected = this.selectedContacts.has(contact.id); return `
${initials}
${displayName}
${contact.company || '-'} ${contact.position || '-'} ${contact.email ? `${contact.email}` : '-'} ${contact.phone || '-'}
${tags.length > 0 ? tags.map(tag => `${tag}`).join('') : '-'}
`; } getContactDisplayName(contact) { const parts = []; if (contact.firstName) parts.push(contact.firstName); if (contact.lastName) parts.push(contact.lastName); if (parts.length > 0) { return parts.join(' '); } return contact.company || 'Unbenannt'; } getContactInitials(contact) { let initials = ''; if (contact.firstName) { initials += contact.firstName.charAt(0).toUpperCase(); } if (contact.lastName) { initials += contact.lastName.charAt(0).toUpperCase(); } if (!initials && contact.company) { initials = contact.company.charAt(0).toUpperCase(); } return initials || '?'; } 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 contactId = parseInt(btn.dataset.contactId); this.editContact(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(); this.contacts.forEach(contact => { if (contact.tags) { contact.tags.forEach(tag => this.allTags.add(tag)); } }); // Update tag filter dropdown if (this.tagFilter) { const currentValue = this.tagFilter.value; this.tagFilter.innerHTML = ''; Array.from(this.allTags).sort().forEach(tag => { const option = document.createElement('option'); option.value = tag; option.textContent = tag; this.tagFilter.appendChild(option); }); this.tagFilter.value = currentValue; } } 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); if (!this.contactModal) { console.error('[Contacts] Contact modal not found!'); return; } console.log('[Contacts] Resetting form and showing modal'); // Reset form this.contactForm.reset(); this.contactIdInput.value = ''; if (contact) { // Edit mode this.modalTitle.textContent = 'Kontakt bearbeiten'; this.deleteContactBtn.classList.remove('hidden'); // Fill form this.contactIdInput.value = contact.id; this.firstNameInput.value = contact.firstName || ''; this.lastNameInput.value = contact.lastName || ''; this.companyInput.value = contact.company || ''; this.positionInput.value = contact.position || ''; this.emailInput.value = contact.email || ''; this.phoneInput.value = contact.phone || ''; this.mobileInput.value = contact.mobile || ''; this.addressInput.value = contact.address || ''; this.postalCodeInput.value = contact.postalCode || ''; this.cityInput.value = contact.city || ''; this.countryInput.value = contact.country || ''; this.websiteInput.value = contact.website || ''; this.notesInput.value = contact.notes || ''; this.tagsInput.value = (contact.tags || []).join(', '); } else { // Create mode this.modalTitle.textContent = 'Neuer Kontakt'; this.deleteContactBtn.classList.add('hidden'); } // Show modal overlay const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('hidden'); overlay.classList.add('visible'); } this.contactModal.classList.remove('hidden'); this.contactModal.classList.add('visible'); } hideContactModal() { if (this.contactModal) { this.contactModal.classList.remove('visible'); this.contactModal.classList.add('hidden'); // Hide modal overlay const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('visible'); overlay.classList.add('hidden'); } window.dispatchEvent(new CustomEvent('modal:close')); } } async editContact(contactId) { const contact = this.contacts.find(c => c.id === contactId); if (contact) { this.showContactModal(contact); } } async saveContact() { const contactId = this.contactIdInput.value; const contactData = { firstName: this.firstNameInput.value.trim(), lastName: this.lastNameInput.value.trim(), company: this.companyInput.value.trim(), position: this.positionInput.value.trim(), email: this.emailInput.value.trim(), phone: this.phoneInput.value.trim(), mobile: this.mobileInput.value.trim(), address: this.addressInput.value.trim(), postalCode: this.postalCodeInput.value.trim(), city: this.cityInput.value.trim(), country: this.countryInput.value.trim(), website: this.websiteInput.value.trim(), notes: this.notesInput.value.trim(), tags: this.tagsInput.value.split(',').map(t => t.trim()).filter(Boolean) }; try { if (contactId) { // Update await api.updateContact(contactId, contactData); window.dispatchEvent(new CustomEvent('toast:show', { detail: { message: 'Kontakt aktualisiert', type: 'success' } })); } else { // Create await api.createContact(contactData); window.dispatchEvent(new CustomEvent('toast:show', { detail: { message: 'Kontakt erstellt', type: 'success' } })); } this.hideContactModal(); await this.loadContacts(); } catch (error) { console.error('[Contacts] Error saving contact:', error); const errorMsg = error.response?.data?.errors?.[0] || 'Fehler beim Speichern'; window.dispatchEvent(new CustomEvent('toast:show', { detail: { message: errorMsg, type: 'error' } })); } } async deleteContact() { const contactId = this.contactIdInput.value; if (!contactId) return; const contact = this.contacts.find(c => c.id === parseInt(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' } })); this.hideContactModal(); 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' } })); } } handleContactsUpdate(contacts) { this.contacts = contacts; this.updateTagsList(); this.filterContacts(); } } // Singleton instance const contactsManager = new ContactsManager(); // Export instance for external access export { contactsManager }; // Export functions export async function initContacts() { await contactsManager.init(); } export function refreshContacts() { return contactsManager.loadContacts(); }