Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,239 @@
{% extends "base.html" %}
{% block title %}Alle Kontakte{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-md-8">
<h1 class="h2 mb-0">
<i class="bi bi-people"></i> Alle Kontakte
</h1>
<p class="text-muted mb-0">Übersicht aller Kontakte aus allen Institutionen</p>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('leads.institutions') }}" class="btn btn-secondary">
<i class="bi bi-building"></i> Zu Institutionen
</a>
</div>
</div>
<!-- Search and Filter Bar -->
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchInput"
placeholder="Nach Name, Institution oder Position suchen..."
onkeyup="filterContacts()">
</div>
</div>
<div class="col-md-3">
<select class="form-select" id="institutionFilter" onchange="filterContacts()">
<option value="">Alle Institutionen</option>
{% set institutions = contacts | map(attribute='institution_name') | unique | sort %}
{% for institution in institutions %}
<option value="{{ institution }}">{{ institution }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="sortContacts('name')">
<i class="bi bi-sort-alpha-down"></i> Name
</button>
<button type="button" class="btn btn-outline-secondary" onclick="sortContacts('institution')">
<i class="bi bi-building"></i> Institution
</button>
<button type="button" class="btn btn-outline-secondary" onclick="sortContacts('updated')">
<i class="bi bi-clock"></i> Aktualisiert
</button>
</div>
</div>
</div>
<!-- Contacts Table -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="contactsTable">
<thead>
<tr>
<th>Name</th>
<th>Institution</th>
<th>Position</th>
<th>Kontaktdaten</th>
<th>Notizen</th>
<th>Zuletzt aktualisiert</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr data-name="{{ contact.last_name }} {{ contact.first_name }}"
data-institution="{{ contact.institution_name }}"
data-updated="{{ contact.updated_at or contact.created_at }}">
<td>
<a href="{{ url_for('leads.contact_detail', contact_id=contact.id) }}"
class="text-decoration-none">
<strong>{{ contact.last_name }}, {{ contact.first_name }}</strong>
</a>
</td>
<td>
<a href="{{ url_for('leads.institution_detail', institution_id=contact.institution_id) }}"
class="text-decoration-none text-muted">
{{ contact.institution_name }}
</a>
</td>
<td>{{ contact.position or '-' }}</td>
<td>
{% if contact.phone_count > 0 %}
<span class="badge bg-info me-1" title="Telefonnummern">
<i class="bi bi-telephone"></i> {{ contact.phone_count }}
</span>
{% endif %}
{% if contact.email_count > 0 %}
<span class="badge bg-primary" title="E-Mail-Adressen">
<i class="bi bi-envelope"></i> {{ contact.email_count }}
</span>
{% endif %}
{% if contact.phone_count == 0 and contact.email_count == 0 %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if contact.note_count > 0 %}
<span class="badge bg-secondary">
<i class="bi bi-sticky"></i> {{ contact.note_count }}
</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{{ (contact.updated_at or contact.created_at).strftime('%d.%m.%Y %H:%M') }}
</td>
<td>
<a href="{{ url_for('leads.contact_detail', contact_id=contact.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Details
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not contacts %}
<div class="text-center py-4">
<p class="text-muted">Noch keine Kontakte vorhanden.</p>
<a href="{{ url_for('leads.institutions') }}" class="btn btn-primary">
<i class="bi bi-building"></i> Zu Institutionen
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Contact Count -->
{% if contacts %}
<div class="row mt-3">
<div class="col-12">
<p class="text-muted">
<span id="visibleCount">{{ contacts|length }}</span> von {{ contacts|length }} Kontakten angezeigt
</p>
</div>
</div>
{% endif %}
</div>
<script>
// Current sort order
let currentSort = { field: 'name', ascending: true };
// Filter contacts
function filterContacts() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const institutionFilter = document.getElementById('institutionFilter').value.toLowerCase();
const rows = document.querySelectorAll('#contactsTable tbody tr');
let visibleCount = 0;
rows.forEach(row => {
const text = row.textContent.toLowerCase();
const institution = row.getAttribute('data-institution').toLowerCase();
const matchesSearch = searchTerm === '' || text.includes(searchTerm);
const matchesInstitution = institutionFilter === '' || institution === institutionFilter;
if (matchesSearch && matchesInstitution) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
// Update visible count
const countElement = document.getElementById('visibleCount');
if (countElement) {
countElement.textContent = visibleCount;
}
}
// Sort contacts
function sortContacts(field) {
const tbody = document.querySelector('#contactsTable tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// Toggle sort order if same field
if (currentSort.field === field) {
currentSort.ascending = !currentSort.ascending;
} else {
currentSort.field = field;
currentSort.ascending = true;
}
// Sort rows
rows.sort((a, b) => {
let aValue, bValue;
switch(field) {
case 'name':
aValue = a.getAttribute('data-name');
bValue = b.getAttribute('data-name');
break;
case 'institution':
aValue = a.getAttribute('data-institution');
bValue = b.getAttribute('data-institution');
break;
case 'updated':
aValue = new Date(a.getAttribute('data-updated'));
bValue = new Date(b.getAttribute('data-updated'));
break;
}
if (field === 'updated') {
return currentSort.ascending ? aValue - bValue : bValue - aValue;
} else {
const comparison = aValue.localeCompare(bValue);
return currentSort.ascending ? comparison : -comparison;
}
});
// Re-append sorted rows
rows.forEach(row => tbody.appendChild(row));
// Update button states
document.querySelectorAll('.btn-group button').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Set initial sort
sortContacts('name');
});
</script>
{% endblock %}

Datei anzeigen

@ -0,0 +1,622 @@
{% extends "base.html" %}
{% block title %}{{ contact.first_name }} {{ contact.last_name }} - Kontakt-Details{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-md-8">
<h1 class="h2 mb-0">
<i class="bi bi-person"></i> {{ contact.first_name }} {{ contact.last_name }}
</h1>
<p class="mb-0">
<span class="text-muted">{{ contact.position or 'Keine Position' }}</span>
<span class="mx-2"></span>
<a href="{{ url_for('leads.institution_detail', institution_id=contact.institution_id) }}"
class="text-decoration-none">
{{ contact.institution_name }}
</a>
</p>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-outline-primary" onclick="editContact()">
<i class="bi bi-pencil"></i> Bearbeiten
</button>
<a href="{{ url_for('leads.institution_detail', institution_id=contact.institution_id) }}"
class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</a>
</div>
</div>
<div class="row">
<!-- Contact Details -->
<div class="col-md-6">
<!-- Phone Numbers -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-telephone"></i> Telefonnummern</h5>
<button class="btn btn-sm btn-primary" onclick="showAddPhoneModal()">
<i class="bi bi-plus"></i>
</button>
</div>
<div class="card-body">
{% if contact.phones %}
<ul class="list-group list-group-flush">
{% for phone in contact.phones %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>{{ phone.detail_value }}</strong>
{% if phone.detail_label %}
<span class="badge bg-secondary">{{ phone.detail_label }}</span>
{% endif %}
</div>
<div>
<button class="btn btn-sm btn-outline-primary"
onclick="editDetail('{{ phone.id }}', '{{ phone.detail_value }}', '{{ phone.detail_label or '' }}', 'phone')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger"
onclick="deleteDetail('{{ phone.id }}')">
<i class="bi bi-trash"></i>
</button>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted mb-0">Keine Telefonnummern hinterlegt.</p>
{% endif %}
</div>
</div>
<!-- Email Addresses -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-envelope"></i> E-Mail-Adressen</h5>
<button class="btn btn-sm btn-primary" onclick="showAddEmailModal()">
<i class="bi bi-plus"></i>
</button>
</div>
<div class="card-body">
{% if contact.emails %}
<ul class="list-group list-group-flush">
{% for email in contact.emails %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<a href="mailto:{{ email.detail_value }}">{{ email.detail_value }}</a>
{% if email.detail_label %}
<span class="badge bg-secondary">{{ email.detail_label }}</span>
{% endif %}
</div>
<div>
<button class="btn btn-sm btn-outline-primary"
onclick="editDetail('{{ email.id }}', '{{ email.detail_value }}', '{{ email.detail_label or '' }}', 'email')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger"
onclick="deleteDetail('{{ email.id }}')">
<i class="bi bi-trash"></i>
</button>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted mb-0">Keine E-Mail-Adressen hinterlegt.</p>
{% endif %}
</div>
</div>
</div>
<!-- Notes -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-journal-text"></i> Notizen</h5>
</div>
<div class="card-body">
<!-- New Note Form -->
<div class="mb-3">
<textarea class="form-control" id="newNoteText" rows="3"
placeholder="Neue Notiz hinzufügen..."></textarea>
<button class="btn btn-primary btn-sm mt-2" onclick="addNote()">
<i class="bi bi-plus"></i> Notiz speichern
</button>
</div>
<!-- Notes List -->
<div id="notesList">
{% for note in contact.notes %}
<div class="card mb-2" id="note-{{ note.id }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<small class="text-muted">
<i class="bi bi-clock"></i>
{{ note.created_at.strftime('%d.%m.%Y %H:%M') }}
{% if note.created_by %} • {{ note.created_by }}{% endif %}
{% if note.version > 1 %}
<span class="badge bg-info">v{{ note.version }}</span>
{% endif %}
</small>
<div>
<button class="btn btn-sm btn-link p-0 mx-1"
onclick="editNote('{{ note.id }}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-link text-danger p-0 mx-1"
onclick="deleteNote('{{ note.id }}')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="note-content" id="note-content-{{ note.id }}">
{{ note.note_text|nl2br|safe }}
</div>
<div class="note-edit d-none" id="note-edit-{{ note.id }}">
<textarea class="form-control mb-2" id="note-edit-text-{{ note.id }}">{{ note.note_text }}</textarea>
<button class="btn btn-sm btn-primary" onclick="saveNote('{{ note.id }}')">
Speichern
</button>
<button class="btn btn-sm btn-secondary" onclick="cancelEdit('{{ note.id }}')">
Abbrechen
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% if not contact.notes %}
<p class="text-muted">Noch keine Notizen vorhanden.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<!-- Edit Contact Modal -->
<div class="modal fade" id="editContactModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Kontakt bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editContactForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="editFirstName" class="form-label">Vorname</label>
<input type="text" class="form-control" id="editFirstName"
value="{{ contact.first_name }}" required>
</div>
<div class="col-md-6 mb-3">
<label for="editLastName" class="form-label">Nachname</label>
<input type="text" class="form-control" id="editLastName"
value="{{ contact.last_name }}" required>
</div>
</div>
<div class="mb-3">
<label for="editPosition" class="form-label">Position</label>
<input type="text" class="form-control" id="editPosition"
value="{{ contact.position or '' }}">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="updateContact()">Speichern</button>
</div>
</div>
</div>
</div>
<!-- Add Phone Modal -->
<div class="modal fade" id="addPhoneModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Telefonnummer hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addPhoneForm">
<div class="mb-3">
<label for="phoneNumber" class="form-label">Telefonnummer</label>
<input type="tel" class="form-control" id="phoneNumber" required>
</div>
<div class="mb-3">
<label for="phoneType" class="form-label">Typ</label>
<select class="form-select" id="phoneType">
<option value="">Bitte wählen...</option>
<option value="Mobil">Mobil</option>
<option value="Geschäftlich">Geschäftlich</option>
<option value="Privat">Privat</option>
<option value="Fax">Fax</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="savePhone()">Speichern</button>
</div>
</div>
</div>
</div>
<!-- Add Email Modal -->
<div class="modal fade" id="addEmailModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">E-Mail-Adresse hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addEmailForm">
<div class="mb-3">
<label for="emailAddress" class="form-label">E-Mail-Adresse</label>
<input type="email" class="form-control" id="emailAddress" required>
</div>
<div class="mb-3">
<label for="emailType" class="form-label">Typ</label>
<select class="form-select" id="emailType">
<option value="">Bitte wählen...</option>
<option value="Geschäftlich">Geschäftlich</option>
<option value="Privat">Privat</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="saveEmail()">Speichern</button>
</div>
</div>
</div>
</div>
<!-- Edit Detail Modal -->
<div class="modal fade" id="editDetailModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editDetailTitle">Detail bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editDetailForm">
<input type="hidden" id="editDetailId">
<input type="hidden" id="editDetailType">
<div class="mb-3">
<label for="editDetailValue" class="form-label" id="editDetailValueLabel">Wert</label>
<input type="text" class="form-control" id="editDetailValue" required>
</div>
<div class="mb-3">
<label for="editDetailLabel" class="form-label">Typ</label>
<select class="form-select" id="editDetailLabel">
<option value="">Bitte wählen...</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="updateDetail()">Speichern</button>
</div>
</div>
</div>
</div>
<script>
const contactId = '{{ contact.id }}';
// Contact functions
function editContact() {
new bootstrap.Modal(document.getElementById('editContactModal')).show();
}
async function updateContact() {
const firstName = document.getElementById('editFirstName').value.trim();
const lastName = document.getElementById('editLastName').value.trim();
const position = document.getElementById('editPosition').value.trim();
if (!firstName || !lastName) {
alert('Bitte geben Sie Vor- und Nachname ein.');
return;
}
try {
const response = await fetch(`/leads/api/contacts/${contactId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
position: position
})
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
// Phone functions
function showAddPhoneModal() {
document.getElementById('addPhoneForm').reset();
new bootstrap.Modal(document.getElementById('addPhoneModal')).show();
}
async function savePhone() {
const phoneNumber = document.getElementById('phoneNumber').value.trim();
const phoneType = document.getElementById('phoneType').value;
if (!phoneNumber) {
alert('Bitte geben Sie eine Telefonnummer ein.');
return;
}
try {
const response = await fetch(`/leads/api/contacts/${contactId}/phones`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone_number: phoneNumber,
phone_type: phoneType
})
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
// Email functions
function showAddEmailModal() {
document.getElementById('addEmailForm').reset();
new bootstrap.Modal(document.getElementById('addEmailModal')).show();
}
async function saveEmail() {
const email = document.getElementById('emailAddress').value.trim();
const emailType = document.getElementById('emailType').value;
if (!email) {
alert('Bitte geben Sie eine E-Mail-Adresse ein.');
return;
}
try {
const response = await fetch(`/leads/api/contacts/${contactId}/emails`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
email_type: emailType
})
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
// Edit detail
function editDetail(detailId, detailValue, detailLabel, detailType) {
document.getElementById('editDetailId').value = detailId;
document.getElementById('editDetailType').value = detailType;
document.getElementById('editDetailValue').value = detailValue;
// Set appropriate input type and options based on detail type
const valueInput = document.getElementById('editDetailValue');
const labelSelect = document.getElementById('editDetailLabel');
const valueLabel = document.getElementById('editDetailValueLabel');
labelSelect.innerHTML = '<option value="">Bitte wählen...</option>';
if (detailType === 'phone') {
valueInput.type = 'tel';
valueLabel.textContent = 'Telefonnummer';
document.getElementById('editDetailTitle').textContent = 'Telefonnummer bearbeiten';
// Add phone type options
['Mobil', 'Geschäftlich', 'Privat', 'Fax'].forEach(type => {
const option = document.createElement('option');
option.value = type;
option.textContent = type;
if (type === detailLabel) option.selected = true;
labelSelect.appendChild(option);
});
} else if (detailType === 'email') {
valueInput.type = 'email';
valueLabel.textContent = 'E-Mail-Adresse';
document.getElementById('editDetailTitle').textContent = 'E-Mail-Adresse bearbeiten';
// Add email type options
['Geschäftlich', 'Privat'].forEach(type => {
const option = document.createElement('option');
option.value = type;
option.textContent = type;
if (type === detailLabel) option.selected = true;
labelSelect.appendChild(option);
});
}
new bootstrap.Modal(document.getElementById('editDetailModal')).show();
}
// Update detail
async function updateDetail() {
const detailId = document.getElementById('editDetailId').value;
const detailValue = document.getElementById('editDetailValue').value.trim();
const detailLabel = document.getElementById('editDetailLabel').value;
if (!detailValue) {
alert('Bitte geben Sie einen Wert ein.');
return;
}
try {
const response = await fetch(`/leads/api/details/${detailId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
detail_value: detailValue,
detail_label: detailLabel
})
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
// Delete detail
async function deleteDetail(detailId) {
if (!confirm('Möchten Sie diesen Eintrag wirklich löschen?')) {
return;
}
try {
const response = await fetch(`/leads/api/details/${detailId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler beim Löschen');
}
} catch (error) {
alert('Fehler: ' + error.message);
}
}
// Note functions
async function addNote() {
const noteText = document.getElementById('newNoteText').value.trim();
if (!noteText) {
alert('Bitte geben Sie eine Notiz ein.');
return;
}
try {
const response = await fetch(`/leads/api/contacts/${contactId}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ note_text: noteText })
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
function editNote(noteId) {
// Text aus dem angezeigten Content holen
const contentDiv = document.getElementById(`note-content-${noteId}`);
const textarea = document.getElementById(`note-edit-text-${noteId}`);
// Der Text ist bereits im Textarea durch das Template
document.getElementById(`note-content-${noteId}`).classList.add('d-none');
document.getElementById(`note-edit-${noteId}`).classList.remove('d-none');
// Fokus auf Textarea setzen
textarea.focus();
}
function cancelEdit(noteId) {
document.getElementById(`note-content-${noteId}`).classList.remove('d-none');
document.getElementById(`note-edit-${noteId}`).classList.add('d-none');
}
async function saveNote(noteId) {
const noteText = document.getElementById(`note-edit-text-${noteId}`).value.trim();
if (!noteText) {
alert('Notiz darf nicht leer sein.');
return;
}
try {
const response = await fetch(`/leads/api/notes/${noteId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ note_text: noteText })
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
async function deleteNote(noteId) {
if (!confirm('Möchten Sie diese Notiz wirklich löschen?')) {
return;
}
try {
const response = await fetch(`/leads/api/notes/${noteId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler beim Löschen');
}
} catch (error) {
alert('Fehler: ' + error.message);
}
}
</script>
<style>
.note-content {
white-space: pre-wrap;
}
</style>
{% endblock %}

Datei anzeigen

@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block title %}{{ institution.name }} - Lead-Details{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-md-8">
<h1 class="h2 mb-0">
<i class="bi bi-building"></i> {{ institution.name }}
</h1>
<small class="text-muted">
Erstellt am {{ institution.created_at.strftime('%d.%m.%Y') }}
{% if institution.created_by %}von {{ institution.created_by }}{% endif %}
</small>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-primary" onclick="showCreateContactModal()">
<i class="bi bi-person-plus"></i> Neuer Kontakt
</button>
<a href="{{ url_for('leads.institutions') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</a>
</div>
</div>
<!-- Contacts Table -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-people"></i> Kontakte
<span class="badge bg-secondary">{{ contacts|length }}</span>
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Erstellt am</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr>
<td>
<a href="{{ url_for('leads.contact_detail', contact_id=contact.id) }}"
class="text-decoration-none">
<strong>{{ contact.first_name }} {{ contact.last_name }}</strong>
</a>
</td>
<td>{{ contact.position or '-' }}</td>
<td>{{ contact.created_at.strftime('%d.%m.%Y') }}</td>
<td>
<a href="{{ url_for('leads.contact_detail', contact_id=contact.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Details
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not contacts %}
<div class="text-center py-4">
<p class="text-muted">Noch keine Kontakte für diese Institution.</p>
<button class="btn btn-primary" onclick="showCreateContactModal()">
<i class="bi bi-person-plus"></i> Ersten Kontakt anlegen
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Create Contact Modal -->
<div class="modal fade" id="contactModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Neuer Kontakt</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="contactForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="firstName" class="form-label">Vorname</label>
<input type="text" class="form-control" id="firstName" required>
</div>
<div class="col-md-6 mb-3">
<label for="lastName" class="form-label">Nachname</label>
<input type="text" class="form-control" id="lastName" required>
</div>
</div>
<div class="mb-3">
<label for="position" class="form-label">Position</label>
<input type="text" class="form-control" id="position"
placeholder="z.B. Geschäftsführer, Vertriebsleiter">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="saveContact()">Speichern</button>
</div>
</div>
</div>
</div>
<script>
// Show create contact modal
function showCreateContactModal() {
document.getElementById('contactForm').reset();
new bootstrap.Modal(document.getElementById('contactModal')).show();
}
// Save contact
async function saveContact() {
const firstName = document.getElementById('firstName').value.trim();
const lastName = document.getElementById('lastName').value.trim();
const position = document.getElementById('position').value.trim();
if (!firstName || !lastName) {
alert('Bitte geben Sie Vor- und Nachname ein.');
return;
}
try {
const response = await fetch('/leads/api/contacts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
institution_id: '{{ institution.id }}',
first_name: firstName,
last_name: lastName,
position: position
})
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
</script>
{% endblock %}

Datei anzeigen

@ -0,0 +1,189 @@
{% extends "base.html" %}
{% block title %}Lead-Verwaltung - Institutionen{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-md-8">
<h1 class="h2 mb-0">
<i class="bi bi-building"></i> Lead-Institutionen
</h1>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-primary" onclick="showCreateInstitutionModal()">
<i class="bi bi-plus-circle"></i> Neue Institution
</button>
<a href="{{ url_for('leads.all_contacts') }}" class="btn btn-outline-primary">
<i class="bi bi-people"></i> Alle Kontakte
</a>
<a href="{{ url_for('customers.customers_licenses') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Zurück zu Kunden
</a>
</div>
</div>
<!-- Search Bar -->
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchInput"
placeholder="Institution suchen..." onkeyup="filterInstitutions()">
</div>
</div>
<div class="col-md-6 text-end">
<a href="{{ url_for('leads.export_leads', format='excel') }}" class="btn btn-outline-success">
<i class="bi bi-file-excel"></i> Excel Export
</a>
<a href="{{ url_for('leads.export_leads', format='csv') }}" class="btn btn-outline-info">
<i class="bi bi-file-text"></i> CSV Export
</a>
</div>
</div>
<!-- Institutions Table -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="institutionsTable">
<thead>
<tr>
<th>Institution</th>
<th>Anzahl Kontakte</th>
<th>Erstellt am</th>
<th>Erstellt von</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for institution in institutions %}
<tr>
<td>
<a href="{{ url_for('leads.institution_detail', institution_id=institution.id) }}"
class="text-decoration-none">
<strong>{{ institution.name }}</strong>
</a>
</td>
<td>
<span class="badge bg-secondary">{{ institution.contact_count }}</span>
</td>
<td>{{ institution.created_at.strftime('%d.%m.%Y') }}</td>
<td>{{ institution.created_by or '-' }}</td>
<td>
<a href="{{ url_for('leads.institution_detail', institution_id=institution.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Details
</a>
<button class="btn btn-sm btn-outline-secondary"
onclick="editInstitution('{{ institution.id }}', '{{ institution.name }}')">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not institutions %}
<div class="text-center py-4">
<p class="text-muted">Noch keine Institutionen vorhanden.</p>
<button class="btn btn-primary" onclick="showCreateInstitutionModal()">
<i class="bi bi-plus-circle"></i> Erste Institution anlegen
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Create/Edit Institution Modal -->
<div class="modal fade" id="institutionModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="institutionModalTitle">Neue Institution</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="institutionForm">
<input type="hidden" id="institutionId">
<div class="mb-3">
<label for="institutionName" class="form-label">Name der Institution</label>
<input type="text" class="form-control" id="institutionName" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="saveInstitution()">Speichern</button>
</div>
</div>
</div>
</div>
<script>
// Filter institutions
function filterInstitutions() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const rows = document.querySelectorAll('#institutionsTable tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
}
// Show create institution modal
function showCreateInstitutionModal() {
document.getElementById('institutionModalTitle').textContent = 'Neue Institution';
document.getElementById('institutionId').value = '';
document.getElementById('institutionName').value = '';
new bootstrap.Modal(document.getElementById('institutionModal')).show();
}
// Edit institution
function editInstitution(id, name) {
document.getElementById('institutionModalTitle').textContent = 'Institution bearbeiten';
document.getElementById('institutionId').value = id;
document.getElementById('institutionName').value = name;
new bootstrap.Modal(document.getElementById('institutionModal')).show();
}
// Save institution
async function saveInstitution() {
const id = document.getElementById('institutionId').value;
const name = document.getElementById('institutionName').value.trim();
if (!name) {
alert('Bitte geben Sie einen Namen ein.');
return;
}
try {
const url = id
? `/leads/api/institutions/${id}`
: '/leads/api/institutions';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: name })
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
}
</script>
{% endblock %}

Datei anzeigen

@ -0,0 +1,367 @@
{% extends "base.html" %}
{% block title %}Lead Management{% endblock %}
{% block extra_css %}
<style>
.nav-tabs .nav-link {
color: #495057;
border: 1px solid transparent;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
.nav-tabs .nav-link.active {
color: #495057;
background-color: #fff;
border-color: #dee2e6 #dee2e6 #fff;
}
.tab-content {
border: 1px solid #dee2e6;
border-top: none;
padding: 1.5rem;
background-color: #fff;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-5">
<div class="mb-4">
<h2>📊 Lead Management</h2>
</div>
<!-- Overview Stats -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<h3 class="mb-0">{{ total_institutions }}</h3>
<small class="text-muted">Institutionen</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<h3 class="mb-0">{{ total_contacts }}</h3>
<small class="text-muted">Kontakte</small>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Schnellaktionen</h5>
<div class="d-flex gap-2">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addInstitutionModal">
<i class="bi bi-building-add"></i> Institution hinzufügen
</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addContactModal">
<i class="bi bi-person-plus"></i> Kontakt hinzufügen
</button>
<a href="{{ url_for('leads.export_leads') }}" class="btn btn-outline-secondary">
<i class="bi bi-download"></i> Exportieren
</a>
</div>
</div>
</div>
<!-- Tabbed View -->
<div class="card">
<div class="card-body p-0">
<!-- Tabs -->
<ul class="nav nav-tabs" id="leadTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="institutions-tab" data-bs-toggle="tab" data-bs-target="#institutions" type="button" role="tab">
<i class="bi bi-building"></i> Institutionen
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contacts-tab" data-bs-toggle="tab" data-bs-target="#contacts" type="button" role="tab">
<i class="bi bi-people"></i> Kontakte
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content" id="leadTabContent">
<!-- Institutions Tab -->
<div class="tab-pane fade show active" id="institutions" role="tabpanel">
<!-- Filter for Institutions -->
<div class="row mb-3">
<div class="col-md-6">
<input type="text" class="form-control" id="institutionSearch" placeholder="Institution suchen...">
</div>
<div class="col-md-3">
<select class="form-select" id="institutionSort">
<option value="name">Alphabetisch</option>
<option value="date">Datum hinzugefügt</option>
<option value="contacts">Anzahl Kontakte</option>
</select>
</div>
<div class="col-md-3">
<button class="btn btn-outline-primary w-100" id="filterNoContacts">
<i class="bi bi-funnel"></i> Ohne Kontakte
</button>
</div>
</div>
<!-- Institutions List -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Anzahl Kontakte</th>
<th>Erstellt am</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for institution in institutions %}
<tr>
<td>
<a href="{{ url_for('leads.institution_detail', institution_id=institution.id) }}">
{{ institution.name }}
</a>
</td>
<td>{{ institution.contact_count }}</td>
<td>{{ institution.created_at.strftime('%d.%m.%Y') }}</td>
<td>
<a href="{{ url_for('leads.institution_detail', institution_id=institution.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Details
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not institutions %}
<p class="text-muted text-center py-3">Keine Institutionen vorhanden.</p>
{% endif %}
</div>
</div>
<!-- Contacts Tab -->
<div class="tab-pane fade" id="contacts" role="tabpanel">
<!-- Filter for Contacts -->
<div class="row mb-3">
<div class="col-md-4">
<input type="text" class="form-control" id="contactSearch" placeholder="Kontakt suchen...">
</div>
<div class="col-md-3">
<select class="form-select" id="institutionFilter">
<option value="">Alle Institutionen</option>
{% for institution in institutions %}
<option value="{{ institution.id }}">{{ institution.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button class="btn btn-outline-primary w-100" id="filterNoEmail">
<i class="bi bi-envelope-slash"></i> Ohne E-Mail
</button>
</div>
<div class="col-md-3">
<button class="btn btn-outline-primary w-100" id="filterNoPhone">
<i class="bi bi-telephone-x"></i> Ohne Telefon
</button>
</div>
</div>
<!-- Contacts List -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Institution</th>
<th>E-Mail</th>
<th>Telefon</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for contact in all_contacts %}
<tr>
<td>
<a href="{{ url_for('leads.contact_detail', contact_id=contact.id) }}">
{{ contact.first_name }} {{ contact.last_name }}
</a>
</td>
<td>{{ contact.position or '-' }}</td>
<td>{{ contact.institution_name }}</td>
<td>
{% if contact.emails %}
<small>{{ contact.emails[0] }}</small>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if contact.phones %}
<small>{{ contact.phones[0] }}</small>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('leads.contact_detail', contact_id=contact.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Details
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not all_contacts %}
<p class="text-muted text-center py-3">Keine Kontakte vorhanden.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Institution Modal -->
<div class="modal fade" id="addInstitutionModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="{{ url_for('leads.add_institution') }}">
<div class="modal-header">
<h5 class="modal-title">Neue Institution hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="name" class="form-label">Name der Institution</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Add Contact Modal -->
<div class="modal fade" id="addContactModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="{{ url_for('leads.add_contact') }}">
<div class="modal-header">
<h5 class="modal-title">Neuen Kontakt hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="institution_id" class="form-label">Institution</label>
<select class="form-select" id="institution_id" name="institution_id" required>
<option value="">Bitte wählen...</option>
{% for institution in institutions %}
<option value="{{ institution.id }}">{{ institution.name }}</option>
{% endfor %}
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="first_name" class="form-label">Vorname</label>
<input type="text" class="form-control" id="first_name" name="first_name" required>
</div>
<div class="col-md-6 mb-3">
<label for="last_name" class="form-label">Nachname</label>
<input type="text" class="form-control" id="last_name" name="last_name" required>
</div>
</div>
<div class="mb-3">
<label for="position" class="form-label">Position</label>
<input type="text" class="form-control" id="position" name="position">
</div>
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-3">
<label for="phone" class="form-label">Telefon</label>
<input type="tel" class="form-control" id="phone" name="phone">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Filter functionality
document.addEventListener('DOMContentLoaded', function() {
// Institution search
const institutionSearch = document.getElementById('institutionSearch');
if (institutionSearch) {
institutionSearch.addEventListener('input', function() {
filterTable('institutions', this.value.toLowerCase(), 0);
});
}
// Contact search
const contactSearch = document.getElementById('contactSearch');
if (contactSearch) {
contactSearch.addEventListener('input', function() {
filterTable('contacts', this.value.toLowerCase(), [0, 1, 2]);
});
}
// Filter table function
function filterTable(tabId, searchTerm, columnIndices) {
const table = document.querySelector(`#${tabId} table tbody`);
const rows = table.getElementsByTagName('tr');
Array.from(rows).forEach(row => {
let match = false;
const indices = Array.isArray(columnIndices) ? columnIndices : [columnIndices];
indices.forEach(index => {
const cell = row.getElementsByTagName('td')[index];
if (cell && cell.textContent.toLowerCase().includes(searchTerm)) {
match = true;
}
});
row.style.display = match || searchTerm === '' ? '' : 'none';
});
}
// Institution filter
const institutionFilter = document.getElementById('institutionFilter');
if (institutionFilter) {
institutionFilter.addEventListener('change', function() {
const selectedInstitution = this.value;
const table = document.querySelector('#contacts table tbody');
const rows = table.getElementsByTagName('tr');
Array.from(rows).forEach(row => {
const institutionCell = row.getElementsByTagName('td')[2];
if (selectedInstitution === '' || institutionCell.textContent === this.options[this.selectedIndex].text) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
}
});
</script>
{% endblock %}