/**
* 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 = `
`;
// 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 = `
`;
// 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 `
`;
}
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 = `
`;
// 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 => `
${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 => `
${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 = `
`;
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();
}