Files
TaskMate/frontend/js/contacts.js
hendrik_gebhardt@gmx.de 7d67557be4 Kontakt-Modul
2026-01-06 21:49:26 +00:00

543 Zeilen
16 KiB
JavaScript

/**
* TASKMATE - Contacts Manager
* ===========================
* Kontaktverwaltung mit Kartenansicht
*/
import api from './api.js';
import { $, $$, formatDate, debounce } from './utils.js';
import store from './store.js';
class ContactsManager {
constructor() {
this.contacts = [];
this.filteredContacts = [];
this.allTags = new Set();
this.searchQuery = '';
this.filterTag = '';
this.sortBy = 'created_at';
this.sortOrder = 'desc';
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.contactsGrid = $('#contacts-grid');
this.contactsEmpty = $('#contacts-empty');
this.tagFilter = $('#contacts-tag-filter');
this.sortSelect = $('#contacts-sort');
this.newContactBtn = $('#btn-new-contact');
console.log('[Contacts] DOM Elements check:');
console.log(' contactsView:', this.contactsView);
console.log(' newContactBtn:', this.newContactBtn);
console.log(' contactsGrid:', this.contactsGrid);
// 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.filterContacts();
});
}
// Sort
if (this.sortSelect) {
this.sortSelect.addEventListener('change', (e) => {
const [sortBy, sortOrder] = e.target.value.split('-');
this.sortBy = sortBy;
this.sortOrder = sortOrder;
this.sortContacts();
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!');
}
// 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.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.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;
}
});
}
renderContacts() {
if (!this.contactsGrid) return;
if (this.filteredContacts.length === 0) {
this.contactsGrid.classList.add('hidden');
this.contactsEmpty.classList.remove('hidden');
return;
}
this.contactsGrid.classList.remove('hidden');
this.contactsEmpty.classList.add('hidden');
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join('');
this.contactsGrid.innerHTML = html;
// Bind card events
this.bindCardEvents();
}
createContactCard(contact) {
const displayName = this.getContactDisplayName(contact);
const initials = this.getContactInitials(contact);
const tags = contact.tags || [];
return `
<div class="contact-card" data-contact-id="${contact.id}">
<div class="contact-card-header">
<div class="contact-avatar">
${initials}
</div>
<div class="contact-actions">
<button class="btn-icon btn-edit-contact" title="Bearbeiten">
<i class="fas fa-edit"></i>
</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>
`;
}
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 || '?';
}
bindCardEvents() {
// 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);
this.editContact(contactId);
});
});
// Card click
$$('.contact-card').forEach(card => {
card.addEventListener('click', () => {
const contactId = parseInt(card.dataset.contactId);
this.editContact(contactId);
});
});
}
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 = '<option value="">Alle Tags</option>';
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;
}
}
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();
}