Lead Management - Zwischenstand

Dieser Commit ist enthalten in:
2025-06-22 20:49:31 +02:00
Ursprung 8cb483a034
Commit 45e236ff1b
7 geänderte Dateien mit 311 neuen und 23 gelöschten Zeilen

Datei anzeigen

@@ -140,4 +140,15 @@ Public access: Port 80 via Nginx
- Don't abstract code that's only used once - Don't abstract code that's only used once
- Implement exactly what's requested, nothing more - Implement exactly what's requested, nothing more
## Recent Updates
### June 22, 2025 - 20:26
- Added Lead Management to main navigation (above Ressourcen Pool)
- Created Lead Management dashboard with:
- Overview statistics (institutions, contacts, user attribution)
- Recent activity feed showing who added/edited what
- Quick actions (add institution, view all, export)
- Shared information view between users rac00n and w@rh@mm3r
- Route: `/leads/management` accessible via navbar "Lead Management"
## Last Updated: June 22, 2025 ## Last Updated: June 22, 2025

Datei anzeigen

@@ -133,13 +133,16 @@ Das Admin Panel bietet alle benötigten Überwachungsfunktionen:
## Features Overview ## Features Overview
### Lead Management System ### Lead Management System
- Accessible via "Leads" button on Customers & Licenses page - **UPDATE 22.06.2025**: Jetzt direkt über Navbar "Lead Management" erreichbar
- Manage potential customers and contacts - Lead Management Dashboard unter `/leads/management`
- Gemeinsame Kontaktdatenbank zwischen rac00n und w@rh@mm3r
- Features: - Features:
- Dashboard mit Statistiken und Aktivitätsfeed
- Institution management - Institution management
- Contact persons with multiple phones/emails - Contact persons with multiple phones/emails
- Versioned notes system - Versioned notes system
- Full audit trail - Full audit trail
- Benutzer-Attribution (wer hat was hinzugefügt)
### Resource Pool Management ### Resource Pool Management
- Domain allocation system - Domain allocation system

Datei anzeigen

@@ -137,6 +137,14 @@ Key feature: Monthly partitioned `license_heartbeats` table.
### Status ### Status
**Vollständig implementiert** als Teil des Admin Panels unter `/leads/` **Vollständig implementiert** als Teil des Admin Panels unter `/leads/`
### Update June 22, 2025 - 20:26
- **Neuer Navbar-Eintrag**: "Lead Management" über "Ressourcen Pool"
- **Lead Management Dashboard** unter `/leads/management` mit:
- Übersicht Statistiken (Institutionen, Kontakte, Benutzer-Attribution)
- Aktivitätsfeed zeigt wer was hinzugefügt/bearbeitet hat
- Schnellaktionen (Institution hinzufügen, alle anzeigen, exportieren)
- Geteilte Informationsansicht zwischen rac00n und w@rh@mm3r
### Architecture ### Architecture
- **Modular Architecture**: Clean separation of concerns - **Modular Architecture**: Clean separation of concerns
- **Service Layer Pattern**: Business logic in `leads/services.py` - **Service Layer Pattern**: Business logic in `leads/services.py`

Datei anzeigen

@@ -9,22 +9,144 @@ from db import get_db_connection
from uuid import UUID from uuid import UUID
import traceback import traceback
# Initialize service # Service will be initialized per request
lead_repository = LeadRepository(get_db_connection) lead_repository = None
lead_service = None
def get_lead_service():
"""Get or create lead service instance"""
global lead_repository, lead_service
if lead_service is None:
lead_repository = LeadRepository(get_db_connection) # Pass the function, not call it
lead_service = LeadService(lead_repository) lead_service = LeadService(lead_repository)
return lead_service
# HTML Routes # HTML Routes
@leads_bp.route('/management')
@login_required
def lead_management():
"""Lead Management Dashboard"""
try:
# Get current user
current_user = flask_session.get('username', 'System')
other_user = 'w@rh@mm3r' if current_user == 'rac00n' else 'rac00n'
# Initialize defaults
total_institutions = 0
total_contacts = 0
my_entries = 0
other_entries = 0
activities = []
with get_db_connection() as conn:
cur = conn.cursor()
# Get statistics
cur.execute("SELECT COUNT(*) FROM lead_institutions")
result = cur.fetchone()
if result:
total_institutions = result[0]
cur.execute("SELECT COUNT(*) FROM lead_contacts")
result = cur.fetchone()
if result:
total_contacts = result[0]
# Count entries by current user
cur.execute("""
SELECT COUNT(*) FROM audit_log
WHERE username = %s
AND entity_type IN ('lead_institution', 'lead_contact')
AND action = 'CREATE'
""", (current_user,))
result = cur.fetchone()
if result:
my_entries = result[0]
# Count entries by other user
cur.execute("""
SELECT COUNT(*) FROM audit_log
WHERE username = %s
AND entity_type IN ('lead_institution', 'lead_contact')
AND action = 'CREATE'
""", (other_user,))
result = cur.fetchone()
if result:
other_entries = result[0]
# Get recent activities
cur.execute("""
SELECT timestamp, username, action, entity_type, additional_info
FROM audit_log
WHERE entity_type IN ('lead_institution', 'lead_contact')
ORDER BY timestamp DESC
LIMIT 10
""")
rows = cur.fetchall()
if rows:
for row in rows:
activities.append({
'timestamp': row[0],
'username': row[1],
'action': row[2],
'entity_type': row[3].replace('lead_', ''),
'additional_info': row[4]
})
cur.close()
return render_template('leads/lead_management.html',
total_institutions=total_institutions,
total_contacts=total_contacts,
my_entries=my_entries,
other_entries=other_entries,
other_user=other_user,
current_user=current_user,
recent_activities=activities)
except Exception as e:
import traceback
print(f"Error in lead_management: {str(e)}")
print(traceback.format_exc())
flash(f'Fehler beim Laden des Dashboards: {str(e)}', 'error')
current_user = flask_session.get('username', 'System')
return render_template('leads/lead_management.html',
total_institutions=0,
total_contacts=0,
my_entries=0,
other_entries=0,
other_user='',
current_user=current_user,
recent_activities=[])
@leads_bp.route('/') @leads_bp.route('/')
@login_required @login_required
def institutions(): def institutions():
"""List all institutions""" """List all institutions"""
try: try:
institutions = lead_service.list_institutions() institutions = get_lead_service().list_institutions()
return render_template('leads/institutions.html', institutions=institutions) return render_template('leads/institutions.html', institutions=institutions)
except Exception as e: except Exception as e:
flash(f'Fehler beim Laden der Institutionen: {str(e)}', 'error') flash(f'Fehler beim Laden der Institutionen: {str(e)}', 'error')
return render_template('leads/institutions.html', institutions=[]) return render_template('leads/institutions.html', institutions=[])
@leads_bp.route('/institution/add', methods=['POST'])
@login_required
def add_institution():
"""Add new institution from form"""
try:
name = request.form.get('name')
if not name:
flash('Name ist erforderlich', 'error')
return redirect(url_for('leads.lead_management'))
# Add institution
get_lead_service().create_institution(name, flask_session.get('username', 'System'))
flash(f'Institution "{name}" wurde erfolgreich hinzugefügt', 'success')
except Exception as e:
flash(f'Fehler beim Hinzufügen der Institution: {str(e)}', 'error')
return redirect(url_for('leads.lead_management'))
@leads_bp.route('/institution/<uuid:institution_id>') @leads_bp.route('/institution/<uuid:institution_id>')
@login_required @login_required
def institution_detail(institution_id): def institution_detail(institution_id):
@@ -35,7 +157,7 @@ def institution_detail(institution_id):
flash('Institution nicht gefunden', 'error') flash('Institution nicht gefunden', 'error')
return redirect(url_for('leads.institutions')) return redirect(url_for('leads.institutions'))
contacts = lead_service.list_contacts_by_institution(institution_id) contacts = get_lead_service().list_contacts_by_institution(institution_id)
return render_template('leads/institution_detail.html', return render_template('leads/institution_detail.html',
institution=institution, institution=institution,
contacts=contacts) contacts=contacts)
@@ -48,7 +170,7 @@ def institution_detail(institution_id):
def contact_detail(contact_id): def contact_detail(contact_id):
"""Show contact details with notes""" """Show contact details with notes"""
try: try:
contact = lead_service.get_contact_details(contact_id) contact = get_lead_service().get_contact_details(contact_id)
return render_template('leads/contact_detail.html', contact=contact) return render_template('leads/contact_detail.html', contact=contact)
except Exception as e: except Exception as e:
flash(f'Fehler beim Laden des Kontakts: {str(e)}', 'error') flash(f'Fehler beim Laden des Kontakts: {str(e)}', 'error')
@@ -59,7 +181,7 @@ def contact_detail(contact_id):
def all_contacts(): def all_contacts():
"""Show all contacts across all institutions""" """Show all contacts across all institutions"""
try: try:
contacts = lead_service.list_all_contacts() contacts = get_lead_service().list_all_contacts()
return render_template('leads/all_contacts.html', contacts=contacts) return render_template('leads/all_contacts.html', contacts=contacts)
except Exception as e: except Exception as e:
flash(f'Fehler beim Laden der Kontakte: {str(e)}', 'error') flash(f'Fehler beim Laden der Kontakte: {str(e)}', 'error')
@@ -72,7 +194,7 @@ def create_institution():
"""Create new institution""" """Create new institution"""
try: try:
data = request.get_json() data = request.get_json()
institution = lead_service.create_institution( institution = get_lead_service().create_institution(
data['name'], data['name'],
flask_session.get('username') flask_session.get('username')
) )
@@ -88,7 +210,7 @@ def update_institution(institution_id):
"""Update institution""" """Update institution"""
try: try:
data = request.get_json() data = request.get_json()
institution = lead_service.update_institution( institution = get_lead_service().update_institution(
institution_id, institution_id,
data['name'], data['name'],
flask_session.get('username') flask_session.get('username')
@@ -105,7 +227,7 @@ def create_contact():
"""Create new contact""" """Create new contact"""
try: try:
data = request.get_json() data = request.get_json()
contact = lead_service.create_contact(data, flask_session.get('username')) contact = get_lead_service().create_contact(data, flask_session.get('username'))
return jsonify({'success': True, 'contact': contact}) return jsonify({'success': True, 'contact': contact})
except ValueError as e: except ValueError as e:
return jsonify({'success': False, 'error': str(e)}), 400 return jsonify({'success': False, 'error': str(e)}), 400
@@ -118,7 +240,7 @@ def update_contact(contact_id):
"""Update contact""" """Update contact"""
try: try:
data = request.get_json() data = request.get_json()
contact = lead_service.update_contact( contact = get_lead_service().update_contact(
contact_id, contact_id,
data, data,
flask_session.get('username') flask_session.get('username')
@@ -135,7 +257,7 @@ def add_phone(contact_id):
"""Add phone to contact""" """Add phone to contact"""
try: try:
data = request.get_json() data = request.get_json()
detail = lead_service.add_phone( detail = get_lead_service().add_phone(
contact_id, contact_id,
data['phone_number'], data['phone_number'],
data.get('phone_type'), data.get('phone_type'),
@@ -153,7 +275,7 @@ def add_email(contact_id):
"""Add email to contact""" """Add email to contact"""
try: try:
data = request.get_json() data = request.get_json()
detail = lead_service.add_email( detail = get_lead_service().add_email(
contact_id, contact_id,
data['email'], data['email'],
data.get('email_type'), data.get('email_type'),
@@ -171,7 +293,7 @@ def update_detail(detail_id):
"""Update contact detail (phone/email)""" """Update contact detail (phone/email)"""
try: try:
data = request.get_json() data = request.get_json()
detail = lead_service.update_contact_detail( detail = get_lead_service().update_contact_detail(
detail_id, detail_id,
data['detail_value'], data['detail_value'],
data.get('detail_label'), data.get('detail_label'),
@@ -188,7 +310,7 @@ def update_detail(detail_id):
def delete_detail(detail_id): def delete_detail(detail_id):
"""Delete contact detail""" """Delete contact detail"""
try: try:
success = lead_service.delete_contact_detail( success = get_lead_service().delete_contact_detail(
detail_id, detail_id,
flask_session.get('username') flask_session.get('username')
) )
@@ -202,7 +324,7 @@ def add_note(contact_id):
"""Add note to contact""" """Add note to contact"""
try: try:
data = request.get_json() data = request.get_json()
note = lead_service.add_note( note = get_lead_service().add_note(
contact_id, contact_id,
data['note_text'], data['note_text'],
flask_session.get('username') flask_session.get('username')
@@ -219,7 +341,7 @@ def update_note(note_id):
"""Update note""" """Update note"""
try: try:
data = request.get_json() data = request.get_json()
note = lead_service.update_note( note = get_lead_service().update_note(
note_id, note_id,
data['note_text'], data['note_text'],
flask_session.get('username') flask_session.get('username')
@@ -235,7 +357,7 @@ def update_note(note_id):
def delete_note(note_id): def delete_note(note_id):
"""Delete note""" """Delete note"""
try: try:
success = lead_service.delete_note( success = get_lead_service().delete_note(
note_id, note_id,
flask_session.get('username') flask_session.get('username')
) )

Datei anzeigen

@@ -0,0 +1,141 @@
{% extends "base.html" %}
{% block title %}Lead Management{% 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">
<a href="{{ url_for('leads.institutions') }}" class="btn btn-primary">
<i class="bi bi-building"></i> Institutionen anzeigen
</a>
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addInstitutionModal">
<i class="bi bi-plus"></i> Institution 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>
<!-- Recent Activity -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Letzte Aktivitäten</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Zeitpunkt</th>
<th>Benutzer</th>
<th>Aktion</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{% for activity in recent_activities %}
<tr>
<td>{{ activity.timestamp.strftime('%d.%m.%Y %H:%M') }}</td>
<td>
<span class="badge bg-{{ 'primary' if activity.username == session.username else 'secondary' }}">
{{ activity.username }}
</span>
</td>
<td>
{% if activity.action == 'CREATE' %}
<span class="text-success"> Erstellt</span>
{% elif activity.action == 'UPDATE' %}
<span class="text-info">✏️ Bearbeitet</span>
{% elif activity.action == 'DELETE' %}
<span class="text-danger">🗑️ Gelöscht</span>
{% endif %}
</td>
<td>
{{ activity.entity_type|title }}
{% if activity.additional_info %}
- {{ activity.additional_info }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not recent_activities %}
<p class="text-muted text-center py-3">Noch keine Aktivitäten vorhanden.</p>
{% endif %}
</div>
</div>
</div>
<!-- Search -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Suche</h5>
</div>
<div class="card-body">
<form method="get" action="{{ url_for('leads.institutions') }}">
<div class="input-group">
<input type="text" class="form-control" name="search" placeholder="Nach Institution oder Kontakt suchen...">
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i> Suchen
</button>
</div>
</form>
</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>
{% endblock %}

Datei anzeigen

@@ -401,6 +401,12 @@
</li> </li>
</ul> </ul>
</li> </li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and request.endpoint.startswith('leads.') %}active{% endif %}" href="{{ url_for('leads.lead_management') }}">
<i class="bi bi-people"></i>
<span>Lead Management</span>
</a>
</li>
<li class="nav-item {% if request.endpoint in ['resources.resources', 'resources.add_resources'] %}has-active-child{% endif %}"> <li class="nav-item {% if request.endpoint in ['resources.resources', 'resources.add_resources'] %}has-active-child{% endif %}">
<a class="nav-link has-submenu {% if request.endpoint == 'resources.resources' %}active{% endif %}" href="{{ url_for('resources.resources') }}"> <a class="nav-link has-submenu {% if request.endpoint == 'resources.resources' %}active{% endif %}" href="{{ url_for('resources.resources') }}">
<i class="bi bi-box-seam"></i> <i class="bi bi-box-seam"></i>

Datei anzeigen

@@ -9,9 +9,6 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Kunden & Lizenzen</h2> <h2 class="mb-0">Kunden & Lizenzen</h2>
<div> <div>
<a href="{{ url_for('leads.institutions') }}" class="btn btn-primary">
<i class="bi bi-people"></i> Leads
</a>
<!-- Export Buttons ohne Dropdown --> <!-- Export Buttons ohne Dropdown -->
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{{ url_for('export.export_customers', format='excel') }}" class="btn btn-success btn-sm"> <a href="{{ url_for('export.export_customers', format='excel') }}" class="btn btn-success btn-sm">