UI-Anpassungen
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
7d67557be4
Commit
ef153789cc
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* TASKMATE - Contacts Manager
|
||||
* ===========================
|
||||
* Kontaktverwaltung mit Kartenansicht
|
||||
* Kontaktverwaltung mit Tabellenansicht
|
||||
*/
|
||||
|
||||
import api from './api.js';
|
||||
@ -12,11 +12,14 @@ 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;
|
||||
}
|
||||
|
||||
@ -30,16 +33,30 @@ class ContactsManager {
|
||||
|
||||
// DOM Elements
|
||||
this.contactsView = $('#view-contacts');
|
||||
this.contactsGrid = $('#contacts-grid');
|
||||
this.contactsTable = $('#contacts-table');
|
||||
this.contactsTbody = $('#contacts-tbody');
|
||||
this.contactsEmpty = $('#contacts-empty');
|
||||
this.tagFilter = $('#contacts-tag-filter');
|
||||
this.sortSelect = $('#contacts-sort');
|
||||
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(' contactsGrid:', this.contactsGrid);
|
||||
console.log(' contactsTable:', this.contactsTable);
|
||||
|
||||
// Modal Elements
|
||||
this.contactModal = $('#contact-modal');
|
||||
@ -87,19 +104,46 @@ class ContactsManager {
|
||||
if (this.tagFilter) {
|
||||
this.tagFilter.addEventListener('change', (e) => {
|
||||
this.filterTag = e.target.value;
|
||||
this.currentPage = 1;
|
||||
this.filterContacts();
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (this.sortSelect) {
|
||||
this.sortSelect.addEventListener('change', (e) => {
|
||||
const [sortBy, sortOrder] = e.target.value.split('-');
|
||||
this.sortBy = sortBy;
|
||||
this.sortOrder = sortOrder;
|
||||
// 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
|
||||
@ -114,6 +158,46 @@ class ContactsManager {
|
||||
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) => {
|
||||
@ -161,6 +245,7 @@ class ContactsManager {
|
||||
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();
|
||||
});
|
||||
@ -216,6 +301,7 @@ class ContactsManager {
|
||||
});
|
||||
|
||||
this.sortContacts();
|
||||
this.updateContactsCount();
|
||||
this.renderContacts();
|
||||
}
|
||||
|
||||
@ -243,56 +329,77 @@ class ContactsManager {
|
||||
});
|
||||
}
|
||||
|
||||
getPaginatedContacts() {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = start + this.itemsPerPage;
|
||||
return this.filteredContacts.slice(start, end);
|
||||
}
|
||||
|
||||
renderContacts() {
|
||||
if (!this.contactsGrid) return;
|
||||
if (!this.contactsTbody) return;
|
||||
|
||||
if (this.filteredContacts.length === 0) {
|
||||
this.contactsGrid.classList.add('hidden');
|
||||
this.contactsTable.parentElement.classList.add('hidden');
|
||||
this.contactsEmpty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.contactsGrid.classList.remove('hidden');
|
||||
this.contactsTable.parentElement.classList.remove('hidden');
|
||||
this.contactsEmpty.classList.add('hidden');
|
||||
|
||||
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join('');
|
||||
this.contactsGrid.innerHTML = html;
|
||||
const paginatedContacts = this.getPaginatedContacts();
|
||||
const html = paginatedContacts.map(contact => this.createContactRow(contact)).join('');
|
||||
this.contactsTbody.innerHTML = html;
|
||||
|
||||
// Bind card events
|
||||
this.bindCardEvents();
|
||||
// Update pagination
|
||||
this.updatePagination();
|
||||
|
||||
// Bind row events
|
||||
this.bindRowEvents();
|
||||
}
|
||||
|
||||
createContactCard(contact) {
|
||||
createContactRow(contact) {
|
||||
const displayName = this.getContactDisplayName(contact);
|
||||
const initials = this.getContactInitials(contact);
|
||||
const tags = contact.tags || [];
|
||||
const isSelected = this.selectedContacts.has(contact.id);
|
||||
|
||||
return `
|
||||
<div class="contact-card" data-contact-id="${contact.id}">
|
||||
<div class="contact-card-header">
|
||||
<div class="contact-avatar">
|
||||
${initials}
|
||||
<tr data-contact-id="${contact.id}" ${isSelected ? 'class="selected"' : ''}>
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" class="table-checkbox contact-checkbox" data-contact-id="${contact.id}" ${isSelected ? 'checked' : ''}>
|
||||
</td>
|
||||
<td>
|
||||
<div class="name-cell">
|
||||
<div class="contact-avatar-small">${initials}</div>
|
||||
<a href="#" class="contact-name-link" data-contact-id="${contact.id}">${displayName}</a>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<button class="btn-icon btn-edit-contact" title="Bearbeiten">
|
||||
<i class="fas fa-edit"></i>
|
||||
</td>
|
||||
<td>${contact.company || '-'}</td>
|
||||
<td class="hide-mobile">${contact.position || '-'}</td>
|
||||
<td>${contact.email ? `<a href="mailto:${contact.email}">${contact.email}</a>` : '-'}</td>
|
||||
<td class="hide-mobile">${contact.phone || '-'}</td>
|
||||
<td>
|
||||
<div class="tags-cell">
|
||||
${tags.length > 0 ? tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('') : '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<div class="table-actions">
|
||||
<button class="btn-table-action btn-edit-contact" data-contact-id="${contact.id}" title="Bearbeiten">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-table-action btn-delete-contact-inline" data-contact-id="${contact.id}" title="Löschen">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14M10 11v6M14 11v6"/>
|
||||
</svg>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -325,26 +432,97 @@ class ContactsManager {
|
||||
return initials || '?';
|
||||
}
|
||||
|
||||
bindCardEvents() {
|
||||
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 card = btn.closest('.contact-card');
|
||||
const contactId = parseInt(card.dataset.contactId);
|
||||
const contactId = parseInt(btn.dataset.contactId);
|
||||
this.editContact(contactId);
|
||||
});
|
||||
});
|
||||
|
||||
// Card click
|
||||
$$('.contact-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const contactId = parseInt(card.dataset.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();
|
||||
@ -370,6 +548,120 @@ class ContactsManager {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren