Übersicht der Kontakte
Dieser Commit ist enthalten in:
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
239
v2_adminpanel/leads/templates/leads/all_contacts.html
Normale Datei
239
v2_adminpanel/leads/templates/leads/all_contacts.html
Normale Datei
@@ -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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren