/** * TASKMATE - Kontaktmanagement * ============================= * Frontend-Modul für Kontakte * VERSION: 332-fixed-api-paths */ import { api } from './api.js'; import store from './store.js'; import { $, $$, createElement, formatDate, debounce } from './utils.js'; class ContactsManager { constructor() { this.container = null; this.currentView = 'list'; // list, detail, form this.currentContact = null; this.searchQuery = ''; this.activeFilter = 'all'; // all, person, company this.isInitialized = false; // Bind methods this.handleSearch = debounce(this.handleSearch.bind(this), 300); this.handleContactClick = this.handleContactClick.bind(this); this.handleAddContact = this.handleAddContact.bind(this); this.handleFilterChange = this.handleFilterChange.bind(this); } async init() { console.log('[Contacts] Initializing... VERSION: 332-fixed-api-paths'); this.container = $('#view-contacts'); if (!this.container) { console.error('[Contacts] Container not found'); return; } // Initial setup this.renderLayout(); // Only load contacts if user is authenticated and project is selected const currentProject = store.getCurrentProject(); if (currentProject && currentProject.id) { await this.loadContacts(); } else { console.warn('[Contacts] Waiting for project selection before loading contacts'); } // Subscribe to store updates this.subscribeToUpdates(); this.isInitialized = true; console.log('[Contacts] Initialized'); } renderLayout() { this.container.innerHTML = `

Kontakte

0 Kontakte
`; // Bind events this.bindEvents(); } bindEvents() { // Add person button const addPersonBtn = this.container.querySelector('#add-person-btn'); if (addPersonBtn) { addPersonBtn.replaceWith(addPersonBtn.cloneNode(true)); const newAddPersonBtn = this.container.querySelector('#add-person-btn'); newAddPersonBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.showContactForm(null, 'person'); }); } // Add company/organization button const addCompanyBtn = this.container.querySelector('#add-company-btn'); if (addCompanyBtn) { addCompanyBtn.replaceWith(addCompanyBtn.cloneNode(true)); const newAddCompanyBtn = this.container.querySelector('#add-company-btn'); newAddCompanyBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.showContactForm(null, 'company'); }); } // Filter button clicks const filterButtons = this.container.querySelectorAll('.filter-btn'); filterButtons.forEach(btn => { btn.addEventListener('click', (e) => { const filter = e.currentTarget.dataset.filter; this.handleFilterChange({ target: { value: filter } }); }); }); } async loadContacts() { try { const currentProject = store.getCurrentProject(); if (!currentProject || !currentProject.id) { console.warn('[Contacts] No current project available'); return; } const params = new URLSearchParams({ project_id: currentProject.id }); // Suche wird serverseitig gefiltert if (this.searchQuery) { params.append('search', this.searchQuery); } // NICHT mehr serverseitig filtern - alle Kontakte laden // Filter wird jetzt clientseitig in getFilteredContacts() angewendet const response = await api.request(`/contacts?${params}`); if (response.success) { this.allContacts = response.data; // Alle Kontakte speichern this.updateCounts(); this.renderContacts(); } } catch (error) { console.error('[Contacts] Error loading contacts:', error); this.showError('Fehler beim Laden der Kontakte'); } } getFilteredContacts() { if (!this.allContacts) return []; if (this.activeFilter === 'all') { return this.allContacts; } return this.allContacts.filter(c => c.type === this.activeFilter); } updateCounts() { const contacts = this.allContacts || []; // Gesamtzahl (zeigt gefilterte Anzahl) const filteredContacts = this.getFilteredContacts(); const totalCount = this.container.querySelector('.contact-count'); if (totalCount) { totalCount.textContent = `${filteredContacts.length} Kontakte`; } // Filter-Counts (zeigt immer alle Zahlen korrekt) const counts = { all: contacts.length, person: contacts.filter(c => c.type === 'person').length, company: contacts.filter(c => c.type === 'company').length }; Object.entries(counts).forEach(([filter, count]) => { const element = this.container.querySelector(`[data-filter-count="${filter}"]`); if (element) { element.textContent = count; } }); } renderContacts() { const content = this.container.querySelector('#contacts-content'); if (!content) return; const contacts = this.getFilteredContacts(); if (contacts.length === 0) { content.innerHTML = `

Keine Kontakte vorhanden

Erstellen Sie Ihren ersten Kontakt

`; return; } // Gruppiere Kontakte alphabetisch const grouped = this.groupContactsByLetter(contacts); content.innerHTML = `
${Object.entries(grouped).map(([letter, contacts]) => `

${letter}

${contacts.map(contact => this.renderContactCard(contact)).join('')}
`).join('')}
`; // Bind click events content.querySelectorAll('.contact-card').forEach(card => { card.addEventListener('click', () => { const contactId = card.dataset.contactId; this.handleContactClick(contactId); }); }); } groupContactsByLetter(contacts) { const grouped = {}; contacts.forEach(contact => { const firstLetter = (contact.display_name || '?')[0].toUpperCase(); if (!grouped[firstLetter]) { grouped[firstLetter] = []; } grouped[firstLetter].push(contact); }); // Sortiere die Gruppen return Object.keys(grouped) .sort() .reduce((result, key) => { result[key] = grouped[key]; return result; }, {}); } renderContactCard(contact) { const primaryEmail = contact.details?.find(d => d.type === 'email' && d.is_primary) || contact.details?.find(d => d.type === 'email'); const primaryPhone = contact.details?.find(d => d.type === 'phone' && d.is_primary) || contact.details?.find(d => d.type === 'phone'); // Icons für Avatar basierend auf Kontakttyp const avatarIcon = contact.type === 'person' ? ` ` : ` `; return `
${contact.avatar_url ? `${contact.display_name}` : avatarIcon }

${contact.display_name}

${contact.type === 'person' && contact.position ? `

${contact.position} ${contact.parent_company_name ? ` bei ${contact.parent_company_name}` : ''}

` : '' } ${contact.type === 'company' && contact.industry ? `

${contact.industry}

` : '' }
${primaryEmail ? ` ${primaryEmail.value} ` : '' } ${primaryPhone ? ` ${primaryPhone.value} ` : '' }
`; } getInitials(contact) { if (contact.type === 'person') { const first = contact.first_name?.[0] || ''; const last = contact.last_name?.[0] || ''; return (first + last).toUpperCase() || '?'; } else { return contact.company_name?.[0]?.toUpperCase() || '?'; } } getAvatarColor(contact) { // Generiere eine konsistente Farbe basierend auf dem Namen const name = contact.display_name || ''; let hash = 0; for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } const colors = [ '#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16' ]; return colors[Math.abs(hash) % colors.length]; } async handleContactClick(contactId) { try { const response = await api.request(`/contacts/${contactId}`); if (response.success) { this.showContactDetail(response.data); } } catch (error) { console.error('[Contacts] Error loading contact:', error); this.showError('Fehler beim Laden des Kontakts'); } } showContactDetail(contact) { console.log('[Contacts] showContactDetail called with:', contact); this.currentContact = contact; this.currentView = 'detail'; const content = this.container.querySelector('#contacts-content'); if (!content) return; const initials = this.getInitials(contact); content.innerHTML = `
${contact.avatar_url ? `${contact.display_name}` : `${initials}` }

${contact.display_name}

${contact.type === 'person' && contact.position ? `

${contact.position} ${contact.parent_company_name ? ` bei ${contact.parent_company_name}` : ''}

` : '' } ${contact.type === 'company' && contact.industry ? `

${contact.industry}

` : '' }

Kontaktdaten

${this.renderContactDetails(contact.details)}
${this.renderAdditionalInfo(contact)} ${contact.notes ? `

Notizen

${contact.notes}
` : ''}

Aktivitäten

${this.renderInteractions(contact.interactions)}
`; // Bind detail events this.bindDetailEvents(); } renderContactDetails(details) { if (!details || details.length === 0) { return '

Keine Kontaktdaten vorhanden

'; } const getIconSvg = (type) => { const icons = { email: '', phone: '', mobile: '', fax: '', address: '', social: '' }; return icons[type] || ''; }; return details.map(detail => `
${getIconSvg(detail.type)}
${detail.label} ${detail.value}
`).join(''); } renderAdditionalInfo(contact) { const sections = []; if (contact.type === 'person') { const fields = []; if (contact.salutation) fields.push(['Anrede', contact.salutation]); if (contact.department) fields.push(['Abteilung', contact.department]); if (fields.length > 0) { sections.push(`

Person

${fields.map(([label, value]) => `
${label}
${value}
`).join('')}
`); } } if (contact.type === 'company') { const fields = []; if (contact.company_type) fields.push(['Typ', contact.company_type]); if (contact.website) fields.push(['Website', `${contact.website}`]); if (fields.length > 0) { sections.push(`

Institution

${fields.map(([label, value]) => `
${label}
${value}
`).join('')}
`); } } return sections.join(''); } renderInteractions(interactions) { if (!interactions || interactions.length === 0) { return '

Noch keine Aktivitäten vorhanden

'; } const getIconSvg = (type) => { const icons = { call: '', email: '', meeting: '', note: '', task: '' }; return icons[type] || ''; }; const typeLabels = { call: 'Anruf', email: 'E-Mail', meeting: 'Treffen', note: 'Notiz', task: 'Aufgabe' }; return interactions.map(interaction => `
${getIconSvg(interaction.type)}
${typeLabels[interaction.type]} ${formatDate(interaction.interaction_date)}
${interaction.subject ? `

${interaction.subject}

` : ''} ${interaction.content ? `

${interaction.content}

` : ''} von ${interaction.created_by_name}
`).join(''); } bindDetailEvents() { // Back button const backBtn = this.container.querySelector('#back-to-list'); if (backBtn) { backBtn.addEventListener('click', () => { this.currentView = 'list'; this.renderContacts(); }); } // Edit button const editBtn = this.container.querySelector('#edit-contact'); if (editBtn) { editBtn.addEventListener('click', () => { this.showContactForm(this.currentContact); }); } // Delete button const deleteBtn = this.container.querySelector('#delete-contact'); if (deleteBtn) { deleteBtn.addEventListener('click', () => { this.handleDeleteContact(this.currentContact); }); } // Add interaction const addInteractionBtn = this.container.querySelector('#add-interaction'); if (addInteractionBtn) { addInteractionBtn.addEventListener('click', () => { this.showInteractionModal(); }); } } showContactForm(contact = null, defaultType = 'person') { const isEdit = !!contact; const contactType = contact?.type || defaultType; this.currentView = 'form'; try { // Erstelle Overlay const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.setAttribute('data-close-modal', ''); document.body.appendChild(overlay); // Erstelle Modal-Container const modal = document.createElement('div'); modal.className = 'modal modal-large'; // Titel basierend auf Typ und Edit-Modus const formTitle = isEdit ? 'Kontakt bearbeiten' : (contactType === 'company' ? 'Neue Institution' : 'Neuer Kontakt'); // Build HTML string first const modalContent = `

${formTitle}

${this.renderTypeSpecificFields(contactType, contact)}

Kontaktdaten

${contact?.details ? contact.details.map((d, i) => this.renderDetailField(d, i)).join('') : this.renderDetailField(null, 0) }
`; console.log('[Contacts] Setting modal innerHTML...'); modal.innerHTML = modalContent; console.log('[Contacts] Modal innerHTML set'); document.body.appendChild(modal); // Store overlay reference for cleanup modal._overlay = overlay; // Wait for DOM to be ready requestAnimationFrame(() => { console.log('[Contacts] Adding visible classes...'); overlay.classList.add('visible'); modal.classList.add('visible'); // Double wait to ensure rendering is complete requestAnimationFrame(() => { console.log('[Contacts] Binding form events...'); this.bindFormEvents(contact, modal); }); }); } catch (error) { console.error('[Contacts] Error showing contact form:', error); this.showError('Fehler beim Öffnen des Formulars'); } } renderTypeSpecificFields(type, contact = null) { if (type === 'person') { return `
`; } else { return `
`; } } getCompanyOptions(selectedId = null) { // Filter nur Firmen aus den geladenen Kontakten const companies = this.allContacts?.filter(c => c.type === 'company') || []; return companies.map(company => ` `).join(''); } renderDetailField(detail, index) { const types = { email: 'E-Mail', phone: 'Telefon', mobile: 'Mobil', fax: 'Fax', address: 'Adresse', social: 'Social Media' }; const labels = { email: ['Geschäftlich', 'Privat', 'Andere'], phone: ['Arbeit', 'Privat', 'Andere'], mobile: ['Arbeit', 'Privat', 'Andere'], fax: ['Arbeit', 'Privat'], address: ['Geschäft', 'Privat', 'Andere'], social: ['LinkedIn', 'Twitter', 'Facebook', 'Instagram', 'XING', 'Andere'] }; return `
`; } bindFormEvents(existingContact = null, modal = null) { console.log('[Contacts] bindFormEvents called'); // Try with modal context first const form = modal ? modal.querySelector('#contact-form') : document.querySelector('#contact-form'); if (!form) { console.error('[Contacts] Form not found!'); return; } console.log('[Contacts] Form found, binding events...'); // Double-check DOM is ready const buttons = form.querySelectorAll('button'); console.log('[Contacts] Buttons found:', buttons.length); // List all buttons for debugging buttons.forEach((btn, idx) => { console.log(`[Contacts] Button ${idx}:`, btn.id, btn.className, btn.textContent.trim()); }); // Type change const typeInputs = form.querySelectorAll('input[name="type"]'); typeInputs.forEach(input => { input.addEventListener('change', (e) => { const fieldsContainer = form.querySelector('#type-specific-fields'); if (fieldsContainer) { fieldsContainer.innerHTML = this.renderTypeSpecificFields(e.target.value); } }); }); // Detail type change form.addEventListener('change', (e) => { if (e.target.classList.contains('detail-type')) { const index = e.target.closest('.detail-field').dataset.index; const labelSelect = form.querySelector(`select[name="details[${index}][label]"]`); // Update label options based on type const labels = { email: ['Geschäftlich', 'Privat', 'Andere'], phone: ['Arbeit', 'Privat', 'Andere'], mobile: ['Arbeit', 'Privat', 'Andere'], fax: ['Arbeit', 'Privat'], address: ['Geschäft', 'Privat', 'Andere'], social: ['LinkedIn', 'Twitter', 'Facebook', 'Instagram', 'XING', 'Andere'] }; const options = (labels[e.target.value] || labels.email).map(label => `` ).join(''); labelSelect.innerHTML = options; } }); // Add detail const addDetailBtn = form.querySelector('#add-detail'); if (addDetailBtn) { addDetailBtn.addEventListener('click', () => { const detailsList = form.querySelector('#contact-details-list'); const currentCount = detailsList.querySelectorAll('.detail-field').length; const newField = document.createElement('div'); newField.innerHTML = this.renderDetailField(null, currentCount); detailsList.appendChild(newField.firstElementChild); }); } // Remove detail form.addEventListener('click', (e) => { if (e.target.closest('.btn-remove-detail')) { const field = e.target.closest('.detail-field'); field.remove(); // Reindex remaining fields form.querySelectorAll('.detail-field').forEach((field, index) => { field.dataset.index = index; field.querySelectorAll('[name^="details["]').forEach(input => { input.name = input.name.replace(/\[\d+\]/, `[${index}]`); }); }); } }); // Close form const closeModal = () => { if (modal && modal._overlay) { modal.classList.remove('visible'); modal._overlay.classList.remove('visible'); setTimeout(() => { modal.remove(); modal._overlay.remove(); }, 300); } if (existingContact) { this.showContactDetail(existingContact); } else { this.currentView = 'list'; this.renderContacts(); } }; // Modal close handlers if (modal) { modal.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', closeModal); }); // Overlay click to close if (modal._overlay) { modal._overlay.addEventListener('click', closeModal); } } // Use event delegation for buttons form.addEventListener('click', async (e) => { // Check if click was on submit button if (e.target.id === 'submit-form' || e.target.classList.contains('btn-primary')) { console.log('[Contacts] Submit button clicked via delegation'); e.preventDefault(); e.stopPropagation(); try { await this.handleFormSubmit(form, existingContact, modal); } catch (error) { console.error('[Contacts] Form submit error:', error); } } // Check if click was on cancel button if (e.target.id === 'cancel-form') { console.log('[Contacts] Cancel button clicked'); e.preventDefault(); closeModal(); } }); // Prevent form submit just in case form.addEventListener('submit', (e) => { console.log('[Contacts] Form submit prevented'); e.preventDefault(); return false; }); } async handleFormSubmit(form, existingContact = null, modal = null) { console.log('[Contacts] handleFormSubmit called'); const formData = new FormData(form); const currentProject = store.getCurrentProject(); if (!currentProject || !currentProject.id) { this.showError('Kein Projekt ausgewählt'); return; } const data = { type: formData.get('type'), project_id: currentProject.id, details: [] }; // Type-specific fields if (data.type === 'person') { data.salutation = formData.get('salutation') || null; data.first_name = formData.get('first_name') || null; data.last_name = formData.get('last_name') || null; data.position = formData.get('position') || null; data.department = formData.get('department') || null; data.birth_date = formData.get('birth_date') || null; data.parent_company_id = formData.get('parent_company_id') || null; // Validierung für Person if (!data.last_name) { this.showError('Nachname ist erforderlich'); return; } // Generate display_name data.display_name = `${data.first_name || ''} ${data.last_name || ''}`.trim(); } else { data.company_name = formData.get('company_name') || null; data.company_type = formData.get('company_type') || null; data.industry = formData.get('industry') || null; data.website = formData.get('website') || null; // Validierung für Firma if (!data.company_name) { this.showError('Firmenname ist erforderlich'); return; } // Set display_name for company data.display_name = data.company_name; } // Common fields data.notes = formData.get('notes') || null; // Details const detailFields = form.querySelectorAll('.detail-field'); detailFields.forEach((field, index) => { const type = form.querySelector(`select[name="details[${index}][type]"]`)?.value; const label = form.querySelector(`select[name="details[${index}][label]"]`)?.value; const value = form.querySelector(`input[name="details[${index}][value]"]`)?.value; const isPrimary = form.querySelector(`input[name="details[${index}][is_primary]"]`)?.checked; if (value) { data.details.push({ type, label, value, is_primary: isPrimary }); } }); try { const isEdit = !!existingContact; const url = isEdit ? `/contacts/${existingContact.id}` : '/contacts'; const method = isEdit ? 'PUT' : 'POST'; console.log('[Contacts] Sending request:', method, url, data); const response = await api.request(url, { method: method, body: data }); console.log('[Contacts] API Response:', response); if (response.success) { this.showSuccess(response.message || `Kontakt erfolgreich ${isEdit ? 'aktualisiert' : 'erstellt'}`); // Close modal with animation if (modal && modal._overlay) { modal.classList.remove('visible'); modal._overlay.classList.remove('visible'); setTimeout(() => { modal.remove(); modal._overlay.remove(); }, 300); } await this.loadContacts(); // Zur Detailansicht wechseln if (response.data && response.data.id) { console.log('[Contacts] Loading full contact details for ID:', response.data.id); // Load full contact details try { const detailResponse = await api.request(`/contacts/${response.data.id}`); if (detailResponse.success && detailResponse.data) { this.showContactDetail(detailResponse.data); } else { console.error('[Contacts] Could not load contact details'); await this.loadContacts(); } } catch (error) { console.error('[Contacts] Error loading contact details:', error); await this.loadContacts(); } } else { console.error('[Contacts] No data in response!'); await this.loadContacts(); } } } catch (error) { console.error('[Contacts] Error saving contact:', error); this.showError(error.message || 'Fehler beim Speichern des Kontakts'); } } async handleDeleteContact(contact) { if (!confirm(`Möchten Sie den Kontakt "${contact.display_name}" wirklich löschen?`)) { return; } try { const response = await api.request(`/contacts/${contact.id}`, { method: 'DELETE' }); if (response.success) { this.showSuccess('Kontakt erfolgreich gelöscht'); await this.loadContacts(); this.currentView = 'list'; this.renderContacts(); } } catch (error) { console.error('[Contacts] Error deleting contact:', error); this.showError('Fehler beim Löschen des Kontakts'); } } showInteractionModal() { const modal = document.createElement('div'); modal.className = 'modal modal-medium'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.classList.add('show'); // Handle form submit const form = modal.querySelector('#interaction-form'); form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); const data = { type: formData.get('type'), subject: formData.get('subject'), content: formData.get('content') }; try { const response = await api.request( `/contacts/${this.currentContact.id}/interactions`, { method: 'POST', body: data } ); if (response.success) { this.showSuccess('Aktivität hinzugefügt'); // Refresh contact detail const contactResponse = await api.request(`/contacts/${this.currentContact.id}`); if (contactResponse.success) { this.showContactDetail(contactResponse.data); } // Close modal modal.remove(); } } catch (error) { console.error('[Contacts] Error adding interaction:', error); this.showError('Fehler beim Hinzufügen der Aktivität'); } }); // Close modal events modal.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => modal.remove()); }); } handleAddContact() { this.showContactForm(); } handleFilterChange(e) { this.activeFilter = e.target.value; // Update active state for buttons this.container.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === this.activeFilter ); }); // Nur Anzeige aktualisieren, nicht neu laden this.updateCounts(); this.renderContacts(); } handleSearch(value) { this.searchQuery = value; this.loadContacts(); } filterData() { // Called by global search this.handleSearch(this.searchQuery); } subscribeToUpdates() { // Subscribe to relevant store updates window.addEventListener('contact:created', () => this.loadContacts()); window.addEventListener('contact:updated', () => this.loadContacts()); window.addEventListener('contact:deleted', () => this.loadContacts()); // Subscribe to project changes to reload contacts store.subscribe('projects', () => { if (this.isInitialized) { this.loadContacts(); } }); } showError(message) { window.dispatchEvent(new CustomEvent('toast:show', { detail: { message, type: 'error' } })); } showSuccess(message) { window.dispatchEvent(new CustomEvent('toast:show', { detail: { message, type: 'success' } })); } } // Export singleton export const contactsManager = new ContactsManager(); export function initContacts() { return contactsManager.init(); }