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