Übersicht der Kontakte

Dieser Commit ist enthalten in:
2025-06-19 17:59:50 +02:00
Ursprung 0e79e5ed57
Commit 9e5843afcf
5 geänderte Dateien mit 291 neuen und 2 gelöschten Zeilen

Datei anzeigen

@@ -295,4 +295,36 @@ class LeadRepository:
deleted = cur.rowcount > 0 deleted = cur.rowcount > 0
cur.close() cur.close()
return deleted return deleted
def get_all_contacts_with_institutions(self) -> List[Dict[str, Any]]:
"""Get all contacts with their institution information"""
with self.get_db_connection() as conn:
cur = conn.cursor(cursor_factory=RealDictCursor)
query = """
SELECT
c.id,
c.first_name,
c.last_name,
c.position,
c.created_at,
c.updated_at,
c.institution_id,
i.name as institution_name,
(SELECT COUNT(*) FROM lead_contact_details
WHERE contact_id = c.id AND detail_type = 'phone') as phone_count,
(SELECT COUNT(*) FROM lead_contact_details
WHERE contact_id = c.id AND detail_type = 'email') as email_count,
(SELECT COUNT(*) FROM lead_notes
WHERE contact_id = c.id AND is_current = true) as note_count
FROM lead_contacts c
JOIN lead_institutions i ON i.id = c.institution_id
ORDER BY c.last_name, c.first_name
"""
cur.execute(query)
results = cur.fetchall()
cur.close()
return results

Datei anzeigen

@@ -54,6 +54,17 @@ def contact_detail(contact_id):
flash(f'Fehler beim Laden des Kontakts: {str(e)}', 'error') flash(f'Fehler beim Laden des Kontakts: {str(e)}', 'error')
return redirect(url_for('leads.institutions')) return redirect(url_for('leads.institutions'))
@leads_bp.route('/contacts')
@login_required
def all_contacts():
"""Show all contacts across all institutions"""
try:
contacts = lead_service.list_all_contacts()
return render_template('leads/all_contacts.html', contacts=contacts)
except Exception as e:
flash(f'Fehler beim Laden der Kontakte: {str(e)}', 'error')
return render_template('leads/all_contacts.html', contacts=[])
# API Routes # API Routes
@leads_bp.route('/api/institutions', methods=['POST']) @leads_bp.route('/api/institutions', methods=['POST'])
@login_required @login_required

Datei anzeigen

@@ -143,4 +143,8 @@ class LeadService:
"""Delete a note (soft delete)""" """Delete a note (soft delete)"""
success = self.repo.delete_note(note_id) success = self.repo.delete_note(note_id)
return success return success
def list_all_contacts(self) -> List[Dict[str, Any]]:
"""Get all contacts across all institutions with summary info"""
return self.repo.get_all_contacts_with_institutions()

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

@@ -14,6 +14,9 @@
<button class="btn btn-primary" onclick="showCreateInstitutionModal()"> <button class="btn btn-primary" onclick="showCreateInstitutionModal()">
<i class="bi bi-plus-circle"></i> Neue Institution <i class="bi bi-plus-circle"></i> Neue Institution
</button> </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"> <a href="{{ url_for('customers.customers_licenses') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Zurück zu Kunden <i class="bi bi-arrow-left"></i> Zurück zu Kunden
</a> </a>