- Session-Timeout auf 60 Minuten erhöht (ACCESS_TOKEN_EXPIRY + SESSION_TIMEOUT) - AegisSight Light Theme: Gold-Akzent (#C8A851) statt Indigo - Navigation-Tabs in eigene Zeile unter Header verschoben (HTML-Struktur) - Filter-Bar durch kompaktes Popover mit Checkboxen ersetzt (Mehrfachauswahl) - Archiv-Funktion repariert (lädt jetzt per API statt leerem Store) - Filter-Bugs behoben: Reset-Button ID, Default-Werte, Ohne-Datum-Filter - Mehrspalten-Layout Feature entfernt - Online-Status vom Header an User-Avatar verschoben (grüner Punkt) - Lupen-Icon entfernt - CLAUDE.md: Docker-Deploy und CSS-Tricks Regeln aktualisiert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1397 Zeilen
49 KiB
JavaScript
1397 Zeilen
49 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="contacts-layout">
|
|
<!-- Hauptbereich ohne Sidebar -->
|
|
<main class="contacts-main full-width">
|
|
<div class="contacts-header">
|
|
<div class="header-left">
|
|
<h2>Kontakte</h2>
|
|
<span class="contact-count">0 Kontakte</span>
|
|
</div>
|
|
|
|
<div class="header-center">
|
|
<!-- Filter Buttons -->
|
|
<div class="filter-buttons">
|
|
<button class="filter-btn ${this.activeFilter === 'all' ? 'active' : ''}" data-filter="all">
|
|
Alle <span class="filter-badge" data-filter-count="all">0</span>
|
|
</button>
|
|
<button class="filter-btn ${this.activeFilter === 'person' ? 'active' : ''}" data-filter="person" title="Personen">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="12" cy="7" r="4"/>
|
|
</svg>
|
|
<span class="filter-badge" data-filter-count="person">0</span>
|
|
</button>
|
|
<button class="filter-btn ${this.activeFilter === 'company' ? 'active' : ''}" data-filter="company" title="Institutionen">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 21h18"/>
|
|
<path d="M5 21V7l8-4v18"/>
|
|
<path d="M19 21V11l-6-4"/>
|
|
<path d="M9 9v.01"/>
|
|
<path d="M9 12v.01"/>
|
|
<path d="M9 15v.01"/>
|
|
<path d="M9 18v.01"/>
|
|
</svg>
|
|
<span class="filter-badge" data-filter-count="company">0</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header-actions">
|
|
<button class="btn btn-primary" id="add-person-btn">
|
|
<span style="font-weight: bold; margin-right: 4px;">+</span>
|
|
Neuer Kontakt
|
|
</button>
|
|
<button class="btn btn-primary" id="add-company-btn">
|
|
<span style="font-weight: bold; margin-right: 4px;">+</span>
|
|
Neue Institution
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="contacts-content" id="contacts-content">
|
|
<!-- Wird dynamisch gefüllt -->
|
|
</div>
|
|
</main>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="empty-state">
|
|
<svg class="empty-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="9" cy="7" r="4"/>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
</svg>
|
|
<h3>Keine Kontakte vorhanden</h3>
|
|
<p>Erstellen Sie Ihren ersten Kontakt</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Gruppiere Kontakte alphabetisch
|
|
const grouped = this.groupContactsByLetter(contacts);
|
|
|
|
content.innerHTML = `
|
|
<div class="contacts-list">
|
|
${Object.entries(grouped).map(([letter, contacts]) => `
|
|
<div class="contact-group">
|
|
<h3 class="group-letter">${letter}</h3>
|
|
<div class="contact-cards">
|
|
${contacts.map(contact => this.renderContactCard(contact)).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
|
|
// 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'
|
|
? `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="12" cy="7" r="4"/>
|
|
</svg>`
|
|
: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 21h18"/>
|
|
<path d="M5 21V7l8-4v18"/>
|
|
<path d="M19 21V11l-6-4"/>
|
|
<path d="M9 9v.01"/>
|
|
<path d="M9 12v.01"/>
|
|
<path d="M9 15v.01"/>
|
|
<path d="M9 18v.01"/>
|
|
</svg>`;
|
|
|
|
return `
|
|
<div class="contact-card" data-contact-id="${contact.id}">
|
|
<div class="contact-avatar" style="background-color: ${this.getAvatarColor(contact)}">
|
|
${contact.avatar_url
|
|
? `<img src="${contact.avatar_url}" alt="${contact.display_name}">`
|
|
: avatarIcon
|
|
}
|
|
</div>
|
|
|
|
<div class="contact-info">
|
|
<h4 class="contact-name">${contact.display_name}</h4>
|
|
|
|
${contact.type === 'person' && contact.position ?
|
|
`<p class="contact-position">${contact.position}
|
|
${contact.parent_company_name ? ` bei ${contact.parent_company_name}` : ''}
|
|
</p>` : ''
|
|
}
|
|
|
|
${contact.type === 'company' && contact.industry ?
|
|
`<p class="contact-industry">${contact.industry}</p>` : ''
|
|
}
|
|
|
|
<div class="contact-details">
|
|
${primaryEmail ?
|
|
`<span class="detail-item">
|
|
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
|
<path d="M22 6l-10 7L2 6"/>
|
|
</svg>
|
|
${primaryEmail.value}
|
|
</span>` : ''
|
|
}
|
|
|
|
${primaryPhone ?
|
|
`<span class="detail-item">
|
|
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
|
</svg>
|
|
${primaryPhone.value}
|
|
</span>` : ''
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 = `
|
|
<div class="contact-detail">
|
|
<div class="detail-header">
|
|
<button class="btn-back" id="back-to-list">
|
|
<svg class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
</svg>
|
|
Zurück
|
|
</button>
|
|
|
|
<div class="detail-actions">
|
|
<button class="btn btn-secondary" id="edit-contact">
|
|
<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<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>
|
|
Bearbeiten
|
|
</button>
|
|
|
|
<button class="btn btn-danger" id="delete-contact">
|
|
<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/>
|
|
</svg>
|
|
Löschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-content">
|
|
<div class="detail-main">
|
|
<div class="contact-header-detail">
|
|
<div class="avatar-large" style="background-color: ${this.getAvatarColor(contact)}">
|
|
${contact.avatar_url
|
|
? `<img src="${contact.avatar_url}" alt="${contact.display_name}">`
|
|
: `<span>${initials}</span>`
|
|
}
|
|
</div>
|
|
|
|
<div class="contact-title">
|
|
<h1>${contact.display_name}</h1>
|
|
${contact.type === 'person' && contact.position ?
|
|
`<p class="subtitle">${contact.position}
|
|
${contact.parent_company_name ? ` bei ${contact.parent_company_name}` : ''}
|
|
</p>` : ''
|
|
}
|
|
${contact.type === 'company' && contact.industry ?
|
|
`<p class="subtitle">${contact.industry}</p>` : ''
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Kontaktdetails -->
|
|
<section class="detail-section">
|
|
<h3>Kontaktdaten</h3>
|
|
<div class="details-grid">
|
|
${this.renderContactDetails(contact.details)}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Weitere Informationen -->
|
|
${this.renderAdditionalInfo(contact)}
|
|
|
|
<!-- Notizen -->
|
|
${contact.notes ? `
|
|
<section class="detail-section">
|
|
<h3>Notizen</h3>
|
|
<div class="notes-content">${contact.notes}</div>
|
|
</section>
|
|
` : ''}
|
|
|
|
<!-- Interaktionen -->
|
|
<section class="detail-section">
|
|
<div class="section-header">
|
|
<h3>Aktivitäten</h3>
|
|
<button class="btn btn-sm" id="add-interaction">
|
|
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 5v14M5 12h14"/>
|
|
</svg>
|
|
Hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<div class="interactions-list">
|
|
${this.renderInteractions(contact.interactions)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<aside class="detail-sidebar">
|
|
<!-- Meta-Informationen -->
|
|
<div class="sidebar-section">
|
|
<h4>Informationen</h4>
|
|
<dl class="meta-list">
|
|
<dt>Erstellt am</dt>
|
|
<dd>${formatDate(contact.created_at)}</dd>
|
|
|
|
<dt>Erstellt von</dt>
|
|
<dd>${contact.created_by_name || 'System'}</dd>
|
|
|
|
${contact.updated_at !== contact.created_at ? `
|
|
<dt>Aktualisiert</dt>
|
|
<dd>${formatDate(contact.updated_at)}</dd>
|
|
` : ''}
|
|
</dl>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Bind detail events
|
|
this.bindDetailEvents();
|
|
}
|
|
|
|
renderContactDetails(details) {
|
|
if (!details || details.length === 0) {
|
|
return '<p class="text-muted">Keine Kontaktdaten vorhanden</p>';
|
|
}
|
|
|
|
const getIconSvg = (type) => {
|
|
const icons = {
|
|
email: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path d="M22 6l-10 7L2 6"/>',
|
|
phone: '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>',
|
|
mobile: '<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><path d="M12 18h.01"/>',
|
|
fax: '<path d="M6 9V2h12v7"/><rect x="6" y="9" width="12" height="12"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><circle cx="18" cy="12" r="1"/>',
|
|
address: '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>',
|
|
social: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'
|
|
};
|
|
return icons[type] || '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>';
|
|
};
|
|
|
|
return details.map(detail => `
|
|
<div class="detail-item">
|
|
<svg class="detail-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
${getIconSvg(detail.type)}
|
|
</svg>
|
|
<div class="detail-content">
|
|
<span class="detail-label">${detail.label}</span>
|
|
<span class="detail-value">${detail.value}</span>
|
|
</div>
|
|
</div>
|
|
`).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(`
|
|
<section class="detail-section">
|
|
<h3>Person</h3>
|
|
<dl class="info-list">
|
|
${fields.map(([label, value]) => `
|
|
<dt>${label}</dt>
|
|
<dd>${value}</dd>
|
|
`).join('')}
|
|
</dl>
|
|
</section>
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (contact.type === 'company') {
|
|
const fields = [];
|
|
|
|
if (contact.company_type) fields.push(['Typ', contact.company_type]);
|
|
if (contact.website) fields.push(['Website', `<a href="${contact.website}" target="_blank">${contact.website}</a>`]);
|
|
|
|
if (fields.length > 0) {
|
|
sections.push(`
|
|
<section class="detail-section">
|
|
<h3>Institution</h3>
|
|
<dl class="info-list">
|
|
${fields.map(([label, value]) => `
|
|
<dt>${label}</dt>
|
|
<dd>${value}</dd>
|
|
`).join('')}
|
|
</dl>
|
|
</section>
|
|
`);
|
|
}
|
|
}
|
|
|
|
return sections.join('');
|
|
}
|
|
|
|
renderInteractions(interactions) {
|
|
if (!interactions || interactions.length === 0) {
|
|
return '<p class="text-muted">Noch keine Aktivitäten vorhanden</p>';
|
|
}
|
|
|
|
const getIconSvg = (type) => {
|
|
const icons = {
|
|
call: '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>',
|
|
email: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path d="M22 6l-10 7L2 6"/>',
|
|
meeting: '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',
|
|
note: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/>',
|
|
task: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>'
|
|
};
|
|
return icons[type] || '<circle cx="12" cy="12" r="10"/>';
|
|
};
|
|
|
|
const typeLabels = {
|
|
call: 'Anruf',
|
|
email: 'E-Mail',
|
|
meeting: 'Treffen',
|
|
note: 'Notiz',
|
|
task: 'Aufgabe'
|
|
};
|
|
|
|
return interactions.map(interaction => `
|
|
<div class="interaction-item">
|
|
<div class="interaction-icon">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
${getIconSvg(interaction.type)}
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="interaction-content">
|
|
<div class="interaction-header">
|
|
<span class="interaction-type">${typeLabels[interaction.type]}</span>
|
|
<span class="interaction-date">${formatDate(interaction.interaction_date)}</span>
|
|
</div>
|
|
|
|
${interaction.subject ? `<h4>${interaction.subject}</h4>` : ''}
|
|
${interaction.content ? `<p>${interaction.content}</p>` : ''}
|
|
|
|
<span class="interaction-author">von ${interaction.created_by_name}</span>
|
|
</div>
|
|
</div>
|
|
`).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 = `
|
|
<div class="form-header">
|
|
<h2>${formTitle}</h2>
|
|
<button type="button" class="btn-close modal-close" data-close-modal>
|
|
<svg class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 6L6 18M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="contact-form" class="contact-form">
|
|
<!-- Kontakttyp -->
|
|
<div class="form-section">
|
|
<label class="form-label">Kontakttyp</label>
|
|
<div class="radio-group">
|
|
<label class="radio-item">
|
|
<input type="radio" name="type" value="person"
|
|
${contactType === 'person' ? 'checked' : ''}>
|
|
<span>Person</span>
|
|
</label>
|
|
|
|
<label class="radio-item">
|
|
<input type="radio" name="type" value="company"
|
|
${contactType === 'company' ? 'checked' : ''}>
|
|
<span>Institution</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dynamische Felder basierend auf Typ -->
|
|
<div id="type-specific-fields">
|
|
${this.renderTypeSpecificFields(contactType, contact)}
|
|
</div>
|
|
|
|
<!-- Kontaktdetails -->
|
|
<div class="form-section">
|
|
<h3>Kontaktdaten</h3>
|
|
<div id="contact-details-list" class="details-list">
|
|
${contact?.details ?
|
|
contact.details.map((d, i) => this.renderDetailField(d, i)).join('') :
|
|
this.renderDetailField(null, 0)
|
|
}
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-secondary btn-sm" id="add-detail">
|
|
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 5v14M5 12h14"/>
|
|
</svg>
|
|
Kontaktdaten hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Notizen -->
|
|
<div class="form-section">
|
|
<label class="form-label">Notizen</label>
|
|
<textarea name="notes" class="form-control" rows="4"
|
|
placeholder="Zusätzliche Informationen...">${contact?.notes || ''}</textarea>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="cancel-form">
|
|
Abbrechen
|
|
</button>
|
|
<button type="button" class="btn btn-primary" id="submit-form">
|
|
${isEdit ? 'Speichern' : 'Erstellen'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
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 `
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Anrede</label>
|
|
<select name="salutation" class="form-control">
|
|
<option value="">-- Keine --</option>
|
|
<option value="Herr" ${contact?.salutation === 'Herr' ? 'selected' : ''}>Herr</option>
|
|
<option value="Frau" ${contact?.salutation === 'Frau' ? 'selected' : ''}>Frau</option>
|
|
<option value="Dr." ${contact?.salutation === 'Dr.' ? 'selected' : ''}>Dr.</option>
|
|
<option value="Prof." ${contact?.salutation === 'Prof.' ? 'selected' : ''}>Prof.</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Vorname</label>
|
|
<input type="text" name="first_name" class="form-control"
|
|
value="${contact?.first_name || ''}" placeholder="Max">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Nachname <span class="required">*</span></label>
|
|
<input type="text" name="last_name" class="form-control"
|
|
value="${contact?.last_name || ''}" placeholder="Mustermann" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Position</label>
|
|
<input type="text" name="position" class="form-control"
|
|
value="${contact?.position || ''}" placeholder="Geschäftsführer">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Abteilung</label>
|
|
<input type="text" name="department" class="form-control"
|
|
value="${contact?.department || ''}" placeholder="Vertrieb">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Institution</label>
|
|
<select name="parent_company_id" class="form-control">
|
|
<option value="">-- Keine Institution --</option>
|
|
${this.getCompanyOptions(contact?.parent_company_id)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
`;
|
|
} else {
|
|
return `
|
|
<div class="form-row">
|
|
<div class="form-group full-width">
|
|
<label class="form-label">Name <span class="required">*</span></label>
|
|
<input type="text" name="company_name" class="form-control"
|
|
value="${contact?.company_name || ''}" placeholder="Musterinstitution GmbH" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Institutionstyp</label>
|
|
<input type="text" name="company_type" class="form-control"
|
|
value="${contact?.company_type || ''}" placeholder="GmbH, e.V., etc.">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Branche</label>
|
|
<input type="text" name="industry" class="form-control"
|
|
value="${contact?.industry || ''}" placeholder="IT">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Website</label>
|
|
<input type="url" name="website" class="form-control"
|
|
value="${contact?.website || ''}" placeholder="https://example.com">
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
getCompanyOptions(selectedId = null) {
|
|
// Filter nur Firmen aus den geladenen Kontakten
|
|
const companies = this.allContacts?.filter(c => c.type === 'company') || [];
|
|
|
|
return companies.map(company => `
|
|
<option value="${company.id}" ${company.id === selectedId ? 'selected' : ''}>
|
|
${company.display_name}
|
|
</option>
|
|
`).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 `
|
|
<div class="detail-field" data-index="${index}">
|
|
<div class="detail-controls">
|
|
<select name="details[${index}][type]" class="form-control detail-type">
|
|
${Object.entries(types).map(([value, label]) => `
|
|
<option value="${value}" ${detail?.type === value ? 'selected' : ''}>
|
|
${label}
|
|
</option>
|
|
`).join('')}
|
|
</select>
|
|
|
|
<select name="details[${index}][label]" class="form-control detail-label">
|
|
${(labels[detail?.type || 'email'] || labels.email).map(label => `
|
|
<option value="${label}" ${detail?.label === label ? 'selected' : ''}>
|
|
${label}
|
|
</option>
|
|
`).join('')}
|
|
</select>
|
|
|
|
<input type="text" name="details[${index}][value]" class="form-control detail-value"
|
|
value="${detail?.value || ''}" placeholder="Wert eingeben..." required>
|
|
|
|
<label class="checkbox-primary">
|
|
<input type="checkbox" name="details[${index}][is_primary]"
|
|
${detail?.is_primary ? 'checked' : ''}>
|
|
<span>Primär</span>
|
|
</label>
|
|
|
|
<button type="button" class="btn-icon btn-remove-detail" data-index="${index}">
|
|
<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 =>
|
|
`<option value="${label}">${label}</option>`
|
|
).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 = `
|
|
<div class="modal-header">
|
|
<h2>Neue Aktivität hinzufügen</h2>
|
|
<button class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
|
|
<form id="interaction-form" class="modal-body">
|
|
<div class="form-group">
|
|
<label class="form-label">Typ</label>
|
|
<select name="type" class="form-control" required>
|
|
<option value="call">Anruf</option>
|
|
<option value="email">E-Mail</option>
|
|
<option value="meeting">Treffen</option>
|
|
<option value="note">Notiz</option>
|
|
<option value="task">Aufgabe</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Betreff</label>
|
|
<input type="text" name="subject" class="form-control" placeholder="Kurze Zusammenfassung">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Beschreibung</label>
|
|
<textarea name="content" class="form-control" rows="4" placeholder="Details..."></textarea>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
|
<button type="submit" form="interaction-form" class="btn btn-primary">Hinzufügen</button>
|
|
</div>
|
|
`;
|
|
|
|
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();
|
|
} |