Initial commit
Dieser Commit ist enthalten in:
6
v2_adminpanel/leads/__init__.py
Normale Datei
6
v2_adminpanel/leads/__init__.py
Normale Datei
@ -0,0 +1,6 @@
|
||||
# Lead Management Module
|
||||
from flask import Blueprint
|
||||
|
||||
leads_bp = Blueprint('leads', __name__, template_folder='templates')
|
||||
|
||||
from . import routes
|
||||
48
v2_adminpanel/leads/models.py
Normale Datei
48
v2_adminpanel/leads/models.py
Normale Datei
@ -0,0 +1,48 @@
|
||||
# Lead Management Data Models
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
@dataclass
|
||||
class Institution:
|
||||
id: UUID
|
||||
name: str
|
||||
metadata: Dict[str, Any]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
created_by: str
|
||||
contact_count: Optional[int] = 0
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
id: UUID
|
||||
institution_id: UUID
|
||||
first_name: str
|
||||
last_name: str
|
||||
position: Optional[str]
|
||||
extra_fields: Dict[str, Any]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
institution_name: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class ContactDetail:
|
||||
id: UUID
|
||||
contact_id: UUID
|
||||
detail_type: str # 'phone', 'email'
|
||||
detail_value: str
|
||||
detail_label: Optional[str] # 'Mobil', 'Geschäftlich', etc.
|
||||
is_primary: bool
|
||||
created_at: datetime
|
||||
|
||||
@dataclass
|
||||
class Note:
|
||||
id: UUID
|
||||
contact_id: UUID
|
||||
note_text: str
|
||||
version: int
|
||||
is_current: bool
|
||||
created_at: datetime
|
||||
created_by: str
|
||||
parent_note_id: Optional[UUID] = None
|
||||
359
v2_adminpanel/leads/repositories.py
Normale Datei
359
v2_adminpanel/leads/repositories.py
Normale Datei
@ -0,0 +1,359 @@
|
||||
# Database Repository for Lead Management
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from uuid import UUID
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
class LeadRepository:
|
||||
def __init__(self, get_db_connection):
|
||||
self.get_db_connection = get_db_connection
|
||||
|
||||
# Institution Methods
|
||||
def get_institutions_with_counts(self) -> List[Dict[str, Any]]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.metadata,
|
||||
i.created_at,
|
||||
i.updated_at,
|
||||
i.created_by,
|
||||
COUNT(c.id) as contact_count
|
||||
FROM lead_institutions i
|
||||
LEFT JOIN lead_contacts c ON c.institution_id = i.id
|
||||
GROUP BY i.id
|
||||
ORDER BY i.name
|
||||
"""
|
||||
|
||||
cur.execute(query)
|
||||
results = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
return results
|
||||
|
||||
def create_institution(self, name: str, created_by: str) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
INSERT INTO lead_institutions (name, created_by)
|
||||
VALUES (%s, %s)
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(query, (name, created_by))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def get_institution_by_id(self, institution_id: UUID) -> Optional[Dict[str, Any]]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
SELECT * FROM lead_institutions WHERE id = %s
|
||||
"""
|
||||
|
||||
cur.execute(query, (str(institution_id),))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def update_institution(self, institution_id: UUID, name: str) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
UPDATE lead_institutions
|
||||
SET name = %s, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(query, (name, str(institution_id)))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
# Contact Methods
|
||||
def get_contacts_by_institution(self, institution_id: UUID) -> List[Dict[str, Any]]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
c.*,
|
||||
i.name as institution_name
|
||||
FROM lead_contacts c
|
||||
JOIN lead_institutions i ON i.id = c.institution_id
|
||||
WHERE c.institution_id = %s
|
||||
ORDER BY c.last_name, c.first_name
|
||||
"""
|
||||
|
||||
cur.execute(query, (str(institution_id),))
|
||||
results = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
return results
|
||||
|
||||
def create_contact(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
INSERT INTO lead_contacts
|
||||
(institution_id, first_name, last_name, position, extra_fields)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(query, (
|
||||
str(data['institution_id']),
|
||||
data['first_name'],
|
||||
data['last_name'],
|
||||
data.get('position'),
|
||||
psycopg2.extras.Json(data.get('extra_fields', {}))
|
||||
))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def get_contact_with_details(self, contact_id: UUID) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Get contact base info
|
||||
query = """
|
||||
SELECT
|
||||
c.*,
|
||||
i.name as institution_name
|
||||
FROM lead_contacts c
|
||||
JOIN lead_institutions i ON i.id = c.institution_id
|
||||
WHERE c.id = %s
|
||||
"""
|
||||
|
||||
cur.execute(query, (str(contact_id),))
|
||||
contact = cur.fetchone()
|
||||
|
||||
if contact:
|
||||
# Get contact details (phones, emails)
|
||||
details_query = """
|
||||
SELECT * FROM lead_contact_details
|
||||
WHERE contact_id = %s
|
||||
ORDER BY detail_type, is_primary DESC, created_at
|
||||
"""
|
||||
cur.execute(details_query, (str(contact_id),))
|
||||
contact['details'] = cur.fetchall()
|
||||
|
||||
# Get notes
|
||||
notes_query = """
|
||||
SELECT * FROM lead_notes
|
||||
WHERE contact_id = %s AND is_current = true
|
||||
ORDER BY created_at DESC
|
||||
"""
|
||||
cur.execute(notes_query, (str(contact_id),))
|
||||
contact['notes'] = cur.fetchall()
|
||||
|
||||
cur.close()
|
||||
|
||||
return contact
|
||||
|
||||
def update_contact(self, contact_id: UUID, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
UPDATE lead_contacts
|
||||
SET first_name = %s, last_name = %s, position = %s,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(query, (
|
||||
data['first_name'],
|
||||
data['last_name'],
|
||||
data.get('position'),
|
||||
str(contact_id)
|
||||
))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
# Contact Details Methods
|
||||
def add_contact_detail(self, contact_id: UUID, detail_type: str,
|
||||
detail_value: str, detail_label: str = None) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
INSERT INTO lead_contact_details
|
||||
(contact_id, detail_type, detail_value, detail_label)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(query, (
|
||||
str(contact_id),
|
||||
detail_type,
|
||||
detail_value,
|
||||
detail_label
|
||||
))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def get_contact_detail_by_id(self, detail_id: UUID) -> Optional[Dict[str, Any]]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = "SELECT * FROM lead_contact_details WHERE id = %s"
|
||||
cur.execute(query, (str(detail_id),))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def update_contact_detail(self, detail_id: UUID, detail_value: str,
|
||||
detail_label: str = None) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
UPDATE lead_contact_details
|
||||
SET detail_value = %s, detail_label = %s, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(query, (detail_value, detail_label, str(detail_id)))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def delete_contact_detail(self, detail_id: UUID) -> bool:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
query = "DELETE FROM lead_contact_details WHERE id = %s"
|
||||
cur.execute(query, (str(detail_id),))
|
||||
|
||||
deleted = cur.rowcount > 0
|
||||
cur.close()
|
||||
|
||||
return deleted
|
||||
|
||||
# Notes Methods
|
||||
def create_note(self, contact_id: UUID, note_text: str, created_by: str) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query = """
|
||||
INSERT INTO lead_notes
|
||||
(contact_id, note_text, created_by)
|
||||
VALUES (%s, %s, %s)
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(query, (
|
||||
str(contact_id),
|
||||
note_text,
|
||||
created_by
|
||||
))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def update_note(self, note_id: UUID, note_text: str, updated_by: str) -> Dict[str, Any]:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# First, mark current version as not current
|
||||
update_old = """
|
||||
UPDATE lead_notes
|
||||
SET is_current = false
|
||||
WHERE id = %s
|
||||
"""
|
||||
cur.execute(update_old, (str(note_id),))
|
||||
|
||||
# Create new version
|
||||
create_new = """
|
||||
INSERT INTO lead_notes
|
||||
(contact_id, note_text, created_by, parent_note_id, version)
|
||||
SELECT contact_id, %s, %s, %s, version + 1
|
||||
FROM lead_notes
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
"""
|
||||
|
||||
cur.execute(create_new, (
|
||||
note_text,
|
||||
updated_by,
|
||||
str(note_id),
|
||||
str(note_id)
|
||||
))
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
return result
|
||||
|
||||
def delete_note(self, note_id: UUID) -> bool:
|
||||
with self.get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
# Soft delete by marking as not current
|
||||
query = """
|
||||
UPDATE lead_notes
|
||||
SET is_current = false
|
||||
WHERE id = %s
|
||||
"""
|
||||
cur.execute(query, (str(note_id),))
|
||||
|
||||
deleted = cur.rowcount > 0
|
||||
cur.close()
|
||||
|
||||
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
|
||||
397
v2_adminpanel/leads/routes.py
Normale Datei
397
v2_adminpanel/leads/routes.py
Normale Datei
@ -0,0 +1,397 @@
|
||||
# Routes for Lead Management
|
||||
from flask import render_template, request, jsonify, redirect, url_for, flash
|
||||
from auth.decorators import login_required
|
||||
from flask import session as flask_session
|
||||
from . import leads_bp
|
||||
from .services import LeadService
|
||||
from .repositories import LeadRepository
|
||||
from db import get_db_connection
|
||||
from uuid import UUID
|
||||
import traceback
|
||||
|
||||
# Service will be initialized per request
|
||||
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)
|
||||
return lead_service
|
||||
|
||||
# HTML Routes
|
||||
@leads_bp.route('/management')
|
||||
@login_required
|
||||
def lead_management():
|
||||
"""Lead Management Dashboard"""
|
||||
try:
|
||||
# Get institutions with contact counts
|
||||
institutions = get_lead_service().list_institutions()
|
||||
|
||||
# Get all contacts with institution names
|
||||
all_contacts = get_lead_service().list_all_contacts()
|
||||
|
||||
# Calculate totals
|
||||
total_institutions = len(institutions)
|
||||
total_contacts = len(all_contacts)
|
||||
|
||||
return render_template('leads/lead_management.html',
|
||||
total_institutions=total_institutions,
|
||||
total_contacts=total_contacts,
|
||||
institutions=institutions,
|
||||
all_contacts=all_contacts)
|
||||
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,
|
||||
institutions=[],
|
||||
all_contacts=[])
|
||||
|
||||
@leads_bp.route('/')
|
||||
@login_required
|
||||
def institutions():
|
||||
"""List all institutions"""
|
||||
try:
|
||||
institutions = get_lead_service().list_institutions()
|
||||
return render_template('leads/institutions.html', institutions=institutions)
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden der Institutionen: {str(e)}', 'error')
|
||||
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('/contact/add', methods=['POST'])
|
||||
@login_required
|
||||
def add_contact():
|
||||
"""Add new contact from form"""
|
||||
try:
|
||||
data = {
|
||||
'institution_id': request.form.get('institution_id'),
|
||||
'first_name': request.form.get('first_name'),
|
||||
'last_name': request.form.get('last_name'),
|
||||
'position': request.form.get('position')
|
||||
}
|
||||
|
||||
# Validate required fields
|
||||
if not data['institution_id'] or not data['first_name'] or not data['last_name']:
|
||||
flash('Institution, Vorname und Nachname sind erforderlich', 'error')
|
||||
return redirect(url_for('leads.lead_management'))
|
||||
|
||||
# Create contact
|
||||
contact = get_lead_service().create_contact(data, flask_session.get('username', 'System'))
|
||||
|
||||
# Add email if provided
|
||||
email = request.form.get('email')
|
||||
if email:
|
||||
get_lead_service().add_email(contact['id'], email, 'Primär', flask_session.get('username', 'System'))
|
||||
|
||||
# Add phone if provided
|
||||
phone = request.form.get('phone')
|
||||
if phone:
|
||||
get_lead_service().add_phone(contact['id'], phone, 'Primär', flask_session.get('username', 'System'))
|
||||
|
||||
flash(f'Kontakt "{data["first_name"]} {data["last_name"]}" wurde erfolgreich hinzugefügt', 'success')
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Hinzufügen des Kontakts: {str(e)}', 'error')
|
||||
|
||||
return redirect(url_for('leads.lead_management'))
|
||||
|
||||
@leads_bp.route('/institution/<uuid:institution_id>')
|
||||
@login_required
|
||||
def institution_detail(institution_id):
|
||||
"""Show institution with all contacts"""
|
||||
try:
|
||||
# Get institution through repository
|
||||
service = get_lead_service()
|
||||
institution = service.repo.get_institution_by_id(institution_id)
|
||||
if not institution:
|
||||
flash('Institution nicht gefunden', 'error')
|
||||
return redirect(url_for('leads.institutions'))
|
||||
|
||||
contacts = get_lead_service().list_contacts_by_institution(institution_id)
|
||||
return render_template('leads/institution_detail.html',
|
||||
institution=institution,
|
||||
contacts=contacts)
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden der Institution: {str(e)}', 'error')
|
||||
return redirect(url_for('leads.institutions'))
|
||||
|
||||
@leads_bp.route('/contact/<uuid:contact_id>')
|
||||
@login_required
|
||||
def contact_detail(contact_id):
|
||||
"""Show contact details with notes"""
|
||||
try:
|
||||
contact = get_lead_service().get_contact_details(contact_id)
|
||||
return render_template('leads/contact_detail.html', contact=contact)
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden des Kontakts: {str(e)}', 'error')
|
||||
return redirect(url_for('leads.institutions'))
|
||||
|
||||
@leads_bp.route('/contacts')
|
||||
@login_required
|
||||
def all_contacts():
|
||||
"""Show all contacts across all institutions"""
|
||||
try:
|
||||
contacts = get_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
|
||||
@leads_bp.route('/api/institutions', methods=['POST'])
|
||||
@login_required
|
||||
def create_institution():
|
||||
"""Create new institution"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
institution = get_lead_service().create_institution(
|
||||
data['name'],
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'institution': institution})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/institutions/<uuid:institution_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_institution(institution_id):
|
||||
"""Update institution"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
institution = get_lead_service().update_institution(
|
||||
institution_id,
|
||||
data['name'],
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'institution': institution})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/contacts', methods=['POST'])
|
||||
@login_required
|
||||
def create_contact():
|
||||
"""Create new contact"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
contact = get_lead_service().create_contact(data, flask_session.get('username'))
|
||||
return jsonify({'success': True, 'contact': contact})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/contacts/<uuid:contact_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_contact(contact_id):
|
||||
"""Update contact"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
contact = get_lead_service().update_contact(
|
||||
contact_id,
|
||||
data,
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'contact': contact})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/contacts/<uuid:contact_id>/phones', methods=['POST'])
|
||||
@login_required
|
||||
def add_phone(contact_id):
|
||||
"""Add phone to contact"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
detail = get_lead_service().add_phone(
|
||||
contact_id,
|
||||
data['phone_number'],
|
||||
data.get('phone_type'),
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'detail': detail})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/contacts/<uuid:contact_id>/emails', methods=['POST'])
|
||||
@login_required
|
||||
def add_email(contact_id):
|
||||
"""Add email to contact"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
detail = get_lead_service().add_email(
|
||||
contact_id,
|
||||
data['email'],
|
||||
data.get('email_type'),
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'detail': detail})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/details/<uuid:detail_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_detail(detail_id):
|
||||
"""Update contact detail (phone/email)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
detail = get_lead_service().update_contact_detail(
|
||||
detail_id,
|
||||
data['detail_value'],
|
||||
data.get('detail_label'),
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'detail': detail})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/details/<uuid:detail_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_detail(detail_id):
|
||||
"""Delete contact detail"""
|
||||
try:
|
||||
success = get_lead_service().delete_contact_detail(
|
||||
detail_id,
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': success})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/contacts/<uuid:contact_id>/notes', methods=['POST'])
|
||||
@login_required
|
||||
def add_note(contact_id):
|
||||
"""Add note to contact"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
note = get_lead_service().add_note(
|
||||
contact_id,
|
||||
data['note_text'],
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'note': note})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/notes/<uuid:note_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_note(note_id):
|
||||
"""Update note"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
note = get_lead_service().update_note(
|
||||
note_id,
|
||||
data['note_text'],
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': True, 'note': note})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@leads_bp.route('/api/notes/<uuid:note_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_note(note_id):
|
||||
"""Delete note"""
|
||||
try:
|
||||
success = get_lead_service().delete_note(
|
||||
note_id,
|
||||
flask_session.get('username')
|
||||
)
|
||||
return jsonify({'success': success})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# Export Routes
|
||||
@leads_bp.route('/export')
|
||||
@login_required
|
||||
def export_leads():
|
||||
"""Export leads data as Excel/CSV"""
|
||||
from utils.export import create_excel_export, create_csv_export
|
||||
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Query institutions with contact counts
|
||||
cur.execute("""
|
||||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.type,
|
||||
i.website,
|
||||
i.address,
|
||||
i.created_at,
|
||||
i.created_by,
|
||||
COUNT(DISTINCT c.id) as contact_count,
|
||||
COUNT(DISTINCT cd.id) as contact_detail_count,
|
||||
COUNT(DISTINCT n.id) as note_count
|
||||
FROM lead_institutions i
|
||||
LEFT JOIN lead_contacts c ON i.id = c.institution_id
|
||||
LEFT JOIN lead_contact_details cd ON c.id = cd.contact_id
|
||||
LEFT JOIN lead_notes n ON i.id = n.institution_id
|
||||
GROUP BY i.id, i.name, i.type, i.website, i.address, i.created_at, i.created_by
|
||||
ORDER BY i.name
|
||||
""")
|
||||
|
||||
# Prepare data for export
|
||||
data = []
|
||||
columns = ['ID', 'Institution', 'Typ', 'Website', 'Adresse',
|
||||
'Erstellt am', 'Erstellt von', 'Anzahl Kontakte',
|
||||
'Anzahl Kontaktdetails', 'Anzahl Notizen']
|
||||
|
||||
for row in cur.fetchall():
|
||||
data.append(list(row))
|
||||
|
||||
# Check format parameter
|
||||
format_type = request.args.get('format', 'excel').lower()
|
||||
|
||||
if format_type == 'csv':
|
||||
return create_csv_export(data, columns, 'leads')
|
||||
else:
|
||||
return create_excel_export(data, columns, 'leads')
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Export: {str(e)}', 'error')
|
||||
return redirect(url_for('leads.institutions'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
171
v2_adminpanel/leads/services.py
Normale Datei
171
v2_adminpanel/leads/services.py
Normale Datei
@ -0,0 +1,171 @@
|
||||
# Business Logic Service for Lead Management
|
||||
from typing import List, Dict, Any, Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from .repositories import LeadRepository
|
||||
|
||||
class LeadService:
|
||||
def __init__(self, repository: LeadRepository):
|
||||
self.repo = repository
|
||||
|
||||
# Institution Services
|
||||
def list_institutions(self) -> List[Dict[str, Any]]:
|
||||
"""Get all institutions with contact counts"""
|
||||
return self.repo.get_institutions_with_counts()
|
||||
|
||||
def create_institution(self, name: str, user: str) -> Dict[str, Any]:
|
||||
"""Create a new institution"""
|
||||
# Validation
|
||||
if not name or len(name.strip()) == 0:
|
||||
raise ValueError("Institution name cannot be empty")
|
||||
|
||||
# Create institution
|
||||
institution = self.repo.create_institution(name.strip(), user)
|
||||
|
||||
# Note: Audit logging removed as it requires different implementation
|
||||
# Can be added later with proper audit system integration
|
||||
|
||||
return institution
|
||||
|
||||
def update_institution(self, institution_id: UUID, name: str, user: str) -> Dict[str, Any]:
|
||||
"""Update institution name"""
|
||||
# Validation
|
||||
if not name or len(name.strip()) == 0:
|
||||
raise ValueError("Institution name cannot be empty")
|
||||
|
||||
# Get current institution
|
||||
current = self.repo.get_institution_by_id(institution_id)
|
||||
if not current:
|
||||
raise ValueError("Institution not found")
|
||||
|
||||
# Update
|
||||
institution = self.repo.update_institution(institution_id, name.strip())
|
||||
|
||||
return institution
|
||||
|
||||
# Contact Services
|
||||
def list_contacts_by_institution(self, institution_id: UUID) -> List[Dict[str, Any]]:
|
||||
"""Get all contacts for an institution"""
|
||||
return self.repo.get_contacts_by_institution(institution_id)
|
||||
|
||||
def create_contact(self, data: Dict[str, Any], user: str) -> Dict[str, Any]:
|
||||
"""Create a new contact"""
|
||||
# Validation
|
||||
if not data.get('first_name') or not data.get('last_name'):
|
||||
raise ValueError("First and last name are required")
|
||||
|
||||
if not data.get('institution_id'):
|
||||
raise ValueError("Institution ID is required")
|
||||
|
||||
# Create contact
|
||||
contact = self.repo.create_contact(data)
|
||||
|
||||
return contact
|
||||
|
||||
def get_contact_details(self, contact_id: UUID) -> Dict[str, Any]:
|
||||
"""Get full contact information including details and notes"""
|
||||
contact = self.repo.get_contact_with_details(contact_id)
|
||||
if not contact:
|
||||
raise ValueError("Contact not found")
|
||||
|
||||
# Group details by type
|
||||
contact['phones'] = [d for d in contact.get('details', []) if d['detail_type'] == 'phone']
|
||||
contact['emails'] = [d for d in contact.get('details', []) if d['detail_type'] == 'email']
|
||||
|
||||
return contact
|
||||
|
||||
def update_contact(self, contact_id: UUID, data: Dict[str, Any], user: str) -> Dict[str, Any]:
|
||||
"""Update contact information"""
|
||||
# Validation
|
||||
if not data.get('first_name') or not data.get('last_name'):
|
||||
raise ValueError("First and last name are required")
|
||||
|
||||
# Update contact
|
||||
contact = self.repo.update_contact(contact_id, data)
|
||||
|
||||
return contact
|
||||
|
||||
# Contact Details Services
|
||||
def add_phone(self, contact_id: UUID, phone_number: str,
|
||||
phone_type: str = None, user: str = None) -> Dict[str, Any]:
|
||||
"""Add phone number to contact"""
|
||||
if not phone_number:
|
||||
raise ValueError("Phone number is required")
|
||||
|
||||
detail = self.repo.add_contact_detail(
|
||||
contact_id, 'phone', phone_number, phone_type
|
||||
)
|
||||
|
||||
return detail
|
||||
|
||||
def add_email(self, contact_id: UUID, email: str,
|
||||
email_type: str = None, user: str = None) -> Dict[str, Any]:
|
||||
"""Add email to contact"""
|
||||
if not email:
|
||||
raise ValueError("Email is required")
|
||||
|
||||
# Basic email validation
|
||||
if '@' not in email:
|
||||
raise ValueError("Invalid email format")
|
||||
|
||||
detail = self.repo.add_contact_detail(
|
||||
contact_id, 'email', email, email_type
|
||||
)
|
||||
|
||||
return detail
|
||||
|
||||
def update_contact_detail(self, detail_id: UUID, detail_value: str,
|
||||
detail_label: str = None, user: str = None) -> Dict[str, Any]:
|
||||
"""Update a contact detail (phone/email)"""
|
||||
if not detail_value or len(detail_value.strip()) == 0:
|
||||
raise ValueError("Detail value cannot be empty")
|
||||
|
||||
# Get current detail to check type
|
||||
current_detail = self.repo.get_contact_detail_by_id(detail_id)
|
||||
if not current_detail:
|
||||
raise ValueError("Contact detail not found")
|
||||
|
||||
# Validation based on type
|
||||
if current_detail['detail_type'] == 'email' and '@' not in detail_value:
|
||||
raise ValueError("Invalid email format")
|
||||
|
||||
detail = self.repo.update_contact_detail(
|
||||
detail_id, detail_value.strip(), detail_label
|
||||
)
|
||||
|
||||
return detail
|
||||
|
||||
def delete_contact_detail(self, detail_id: UUID, user: str) -> bool:
|
||||
"""Delete a contact detail (phone/email)"""
|
||||
success = self.repo.delete_contact_detail(detail_id)
|
||||
|
||||
return success
|
||||
|
||||
# Note Services
|
||||
def add_note(self, contact_id: UUID, note_text: str, user: str) -> Dict[str, Any]:
|
||||
"""Add a note to contact"""
|
||||
if not note_text or len(note_text.strip()) == 0:
|
||||
raise ValueError("Note text cannot be empty")
|
||||
|
||||
note = self.repo.create_note(contact_id, note_text.strip(), user)
|
||||
|
||||
return note
|
||||
|
||||
def update_note(self, note_id: UUID, note_text: str, user: str) -> Dict[str, Any]:
|
||||
"""Update a note (creates new version)"""
|
||||
if not note_text or len(note_text.strip()) == 0:
|
||||
raise ValueError("Note text cannot be empty")
|
||||
|
||||
note = self.repo.update_note(note_id, note_text.strip(), user)
|
||||
|
||||
return note
|
||||
|
||||
def delete_note(self, note_id: UUID, user: str) -> bool:
|
||||
"""Delete a note (soft delete)"""
|
||||
success = self.repo.delete_note(note_id)
|
||||
|
||||
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 %}
|
||||
622
v2_adminpanel/leads/templates/leads/contact_detail.html
Normale Datei
622
v2_adminpanel/leads/templates/leads/contact_detail.html
Normale Datei
@ -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 %}
|
||||
159
v2_adminpanel/leads/templates/leads/institution_detail.html
Normale Datei
159
v2_adminpanel/leads/templates/leads/institution_detail.html
Normale Datei
@ -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 %}
|
||||
189
v2_adminpanel/leads/templates/leads/institutions.html
Normale Datei
189
v2_adminpanel/leads/templates/leads/institutions.html
Normale Datei
@ -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 %}
|
||||
367
v2_adminpanel/leads/templates/leads/lead_management.html
Normale Datei
367
v2_adminpanel/leads/templates/leads/lead_management.html
Normale Datei
@ -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 %}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren