diff --git a/app.py b/app.py index 245bd1f..c842394 100644 --- a/app.py +++ b/app.py @@ -5,28 +5,87 @@ import logging import numpy as np from datetime import datetime, timedelta from dotenv import load_dotenv -import requests -from collections import defaultdict -import ipaddress -import csv import sqlite3 from functools import wraps +from contextlib import contextmanager +import time +import threading app = Flask(__name__, static_folder='static') app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev') app.config['ALLOWED_IP_RANGES'] = os.getenv('ALLOWED_IP_RANGES', '192.168.0.0/16,10.0.0.0/8').split(',') app.config['VERSION'] = '1.2.16' +app.config['DATABASE'] = 'data/customers.db' +app.config['DATABASE_TIMEOUT'] = 20 +app.config['DATABASE_POOL_SIZE'] = 5 + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Pfad zur Datenbank -DB_FILE = 'data/customers.db' +# Thread-lokaler Speicher für Datenbankverbindungen +thread_local = threading.local() -# Lade Umgebungsvariablen -load_dotenv() +def get_db_connection(): + """Erstellt eine neue Datenbankverbindung für den aktuellen Thread""" + if not hasattr(thread_local, "connection"): + thread_local.connection = sqlite3.connect(app.config['DATABASE'], timeout=app.config['DATABASE_TIMEOUT']) + thread_local.connection.row_factory = sqlite3.Row + return thread_local.connection -# Statisches Passwort aus der .env Datei -STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password') +@contextmanager +def get_db(): + """Context Manager für Datenbankverbindungen""" + conn = get_db_connection() + try: + yield conn + except Exception: + conn.rollback() + raise + finally: + conn.commit() + +def init_db(): + """Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle.""" + with get_db() as conn: + c = conn.cursor() + + try: + # Erstelle die Kunden-Tabelle + c.execute(''' + CREATE TABLE IF NOT EXISTS customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nummer TEXT, + name TEXT, + strasse TEXT, + plz TEXT, + ort TEXT, + telefon TEXT, + mobil TEXT, + email TEXT, + bemerkung TEXT, + fachrichtung TEXT, + tag TEXT, + handy TEXT, + tele_firma TEXT, + kontakt1 TEXT, + kontakt2 TEXT, + kontakt3 TEXT + ) + ''') + + # Optimierte Indizes für die häufigsten Suchanfragen + c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name_ort ON customers(name, ort)') + c.execute('CREATE INDEX IF NOT EXISTS idx_customers_fachrichtung ON customers(fachrichtung)') + c.execute('CREATE INDEX IF NOT EXISTS idx_customers_tag ON customers(tag)') + c.execute('CREATE INDEX IF NOT EXISTS idx_customers_plz ON customers(plz)') + + # Zusammengesetzter Index für die häufigste Suchkombination + c.execute('CREATE INDEX IF NOT EXISTS idx_customers_search ON customers(name, ort, fachrichtung, tag)') + + logger.info('Datenbank initialisiert') + except Exception as e: + logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}') + raise def isIPInSubnet(ip, subnet): """Überprüft, ob eine IP-Adresse in einem Subnetz liegt.""" @@ -49,132 +108,75 @@ def isIPInSubnet(ip, subnet): logger.error(f"Fehler bei der IP-Überprüfung: {str(e)}") return False -def get_db_connection(): - """Erstellt eine neue Datenbankverbindung mit Timeout""" - conn = sqlite3.connect(DB_FILE, timeout=20) - conn.row_factory = sqlite3.Row - return conn - -def init_db(): - """Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle.""" - conn = get_db_connection() - c = conn.cursor() - - try: - # Erstelle die Kunden-Tabelle - c.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - nummer TEXT, - name TEXT, - strasse TEXT, - plz TEXT, - ort TEXT, - telefon TEXT, - mobil TEXT, - email TEXT, - bemerkung TEXT, - fachrichtung TEXT, - tag TEXT, - handy TEXT, - tele_firma TEXT, - kontakt1 TEXT, - kontakt2 TEXT, - kontakt3 TEXT - ) - ''') - - # Erstelle Indizes für alle Suchfelder - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_nummer ON customers(nummer)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_strasse ON customers(strasse)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_plz ON customers(plz)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_ort ON customers(ort)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_telefon ON customers(telefon)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_mobil ON customers(mobil)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_fachrichtung ON customers(fachrichtung)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_tag ON customers(tag)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_handy ON customers(handy)') - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_tele_firma ON customers(tele_firma)') - - # Erstelle einen zusammengesetzten Index für die häufigste Suchkombination - c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name_ort ON customers(name, ort)') - - conn.commit() - logger.info('Datenbank initialisiert') - except Exception as e: - logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}') - raise - finally: - conn.close() - def import_csv(): """Importiert die CSV-Datei in die Datenbank""" - conn = None try: - conn = get_db_connection() - c = conn.cursor() - - # Lösche bestehende Daten - c.execute('DELETE FROM customers') - - # Importiere MEDISOFT-Daten - if os.path.exists('data/customers.csv'): - logger.info("Importiere MEDISOFT-Daten...") - df = pd.read_csv('data/customers.csv', encoding='iso-8859-1') - df.columns = df.columns.str.strip().str.replace('"', '') - df = df.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x) + with get_db() as conn: + c = conn.cursor() - for _, row in df.iterrows(): - c.execute(''' - INSERT INTO customers ( - name, nummer, strasse, plz, ort, telefon, mobil, email, - fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3 - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', ( + # Lösche bestehende Daten + c.execute('DELETE FROM customers') + + # Importiere MEDISOFT-Daten + if os.path.exists('data/customers.csv'): + logger.info("Importiere MEDISOFT-Daten...") + df = pd.read_csv('data/customers.csv', encoding='iso-8859-1') + df.columns = df.columns.str.strip().str.replace('"', '') + df = df.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x) + + # Filtere Datensätze mit Fachrichtung "intern" + df = df[df['Fachrichtung'].str.lower() != 'intern'] + + # Bereite die Daten für den Batch-Insert vor + data = [( row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'], row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'medisoft', row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3'] - )) - else: - logger.warning("MEDISOFT CSV-Datei nicht gefunden") - - # Importiere MEDICONSULT-Daten - if os.path.exists('data/customers_snk.csv'): - logger.info("Importiere MEDICONSULT-Daten...") - df_snk = pd.read_csv('data/customers_snk.csv', encoding='iso-8859-1') - df_snk.columns = df_snk.columns.str.strip().str.replace('"', '') - df_snk = df_snk.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x) - - for _, row in df_snk.iterrows(): - c.execute(''' + ) for _, row in df.iterrows()] + + # Führe Batch-Insert durch + c.executemany(''' INSERT INTO customers ( name, nummer, strasse, plz, ort, telefon, mobil, email, fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', ( + ''', data) + else: + logger.warning("MEDISOFT CSV-Datei nicht gefunden") + + # Importiere MEDICONSULT-Daten + if os.path.exists('data/customers_snk.csv'): + logger.info("Importiere MEDICONSULT-Daten...") + df_snk = pd.read_csv('data/customers_snk.csv', encoding='iso-8859-1') + df_snk.columns = df_snk.columns.str.strip().str.replace('"', '') + df_snk = df_snk.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x) + + # Filtere Datensätze mit Fachrichtung "intern" + df_snk = df_snk[df_snk['Fachrichtung'].str.lower() != 'intern'] + + # Bereite die Daten für den Batch-Insert vor + data = [( row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'], row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'mediconsult', row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3'] - )) - else: - logger.warning("MEDICONSULT CSV-Datei nicht gefunden") - - conn.commit() - logger.info("CSV-Daten erfolgreich in die Datenbank importiert") + ) for _, row in df_snk.iterrows()] + + # Führe Batch-Insert durch + c.executemany(''' + INSERT INTO customers ( + name, nummer, strasse, plz, ort, telefon, mobil, email, + fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3 + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', data) + else: + logger.warning("MEDICONSULT CSV-Datei nicht gefunden") + + logger.info("CSV-Daten erfolgreich in die Datenbank importiert") except Exception as e: logger.error(f"Fehler beim Importieren der CSV-Datei: {str(e)}") raise - finally: - if conn: - conn.close() - -def clean_dataframe(df): - """Konvertiert NaN-Werte in None für JSON-Kompatibilität""" - return df.replace({np.nan: None}) @app.route('/login', methods=['GET', 'POST']) def login(): @@ -233,115 +235,113 @@ def search(): fachrichtung = request.args.get('fachrichtung', '') tag = request.args.get('tag', 'medisoft') - conn = get_db_connection() - c = conn.cursor() - - # Baue die SQL-Abfrage - sql_query = ''' - SELECT - nummer, - name, - strasse, - plz, - ort, - telefon, - mobil, - email, - fachrichtung, - tag, - handy, - tele_firma, - kontakt1, - kontakt2, - kontakt3 - FROM customers - WHERE 1=1 - ''' - params = [] - - # Füge die Suchbedingungen hinzu - if search_query: - # Optimierte Suche mit FTS (Full Text Search) - sql_query += """ - AND ( - name LIKE ? OR - nummer LIKE ? OR - fachrichtung LIKE ? OR - ort LIKE ? OR - plz LIKE ? OR - strasse LIKE ? OR - telefon LIKE ? OR - mobil LIKE ? OR - email LIKE ? OR - bemerkung LIKE ? OR - tag LIKE ? OR - handy LIKE ? OR - tele_firma LIKE ? OR - kontakt1 LIKE ? OR - kontakt2 LIKE ? OR - kontakt3 LIKE ? - ) - """ - search_term = f"%{search_query}%" - params.extend([search_term] * 16) # 16 Felder für die allgemeine Suche - - if name: - sql_query += " AND name LIKE ?" - params.append(f"%{name}%") - - if ort: - sql_query += " AND ort LIKE ?" - params.append(f"%{ort}%") - - if nummer: - sql_query += " AND nummer LIKE ?" - params.append(f"%{nummer}%") - - if plz: - sql_query += " AND plz LIKE ?" - params.append(f"%{plz}%") - - if fachrichtung: - sql_query += " AND fachrichtung LIKE ?" - params.append(f"%{fachrichtung}%") - - # Filter nach Tag - if tag != 'all': - sql_query += " AND tag = ?" - params.append(tag) - - # Füge LIMIT hinzu und optimiere die Sortierung - sql_query += " ORDER BY name LIMIT 100" - - # Führe die Abfrage aus - cursor = conn.cursor() - cursor.execute(sql_query, params) - results = cursor.fetchall() - - formatted_results = [] - for row in results: - customer = { - 'nummer': row[0], - 'name': row[1], - 'strasse': row[2], - 'plz': row[3], - 'ort': row[4], - 'telefon': row[5], - 'mobil': row[6], - 'email': row[7], - 'fachrichtung': row[8], - 'tag': row[9], - 'handy': row[10], - 'tele_firma': row[11], - 'kontakt1': row[12], - 'kontakt2': row[13], - 'kontakt3': row[14] - } - formatted_results.append(customer) + with get_db() as conn: + c = conn.cursor() + + # Baue die SQL-Abfrage + sql_query = ''' + SELECT + nummer, + name, + strasse, + plz, + ort, + telefon, + mobil, + email, + fachrichtung, + tag, + handy, + tele_firma, + kontakt1, + kontakt2, + kontakt3 + FROM customers + WHERE 1=1 + ''' + params = [] + + # Füge die Suchbedingungen hinzu + if search_query: + # Optimierte Suche mit FTS (Full Text Search) + sql_query += """ + AND ( + name LIKE ? OR + nummer LIKE ? OR + fachrichtung LIKE ? OR + ort LIKE ? OR + plz LIKE ? OR + strasse LIKE ? OR + telefon LIKE ? OR + mobil LIKE ? OR + email LIKE ? OR + bemerkung LIKE ? OR + tag LIKE ? OR + handy LIKE ? OR + tele_firma LIKE ? OR + kontakt1 LIKE ? OR + kontakt2 LIKE ? OR + kontakt3 LIKE ? + ) + """ + search_term = f"%{search_query}%" + params.extend([search_term] * 16) # 16 Felder für die allgemeine Suche + + if name: + sql_query += " AND name LIKE ?" + params.append(f"%{name}%") + + if ort: + sql_query += " AND ort LIKE ?" + params.append(f"%{ort}%") + + if nummer: + sql_query += " AND nummer LIKE ?" + params.append(f"%{nummer}%") + + if plz: + sql_query += " AND plz LIKE ?" + params.append(f"%{plz}%") + + if fachrichtung: + sql_query += " AND fachrichtung LIKE ?" + params.append(f"%{fachrichtung}%") + + # Filter nach Tag + if tag != 'all': + sql_query += " AND tag = ?" + params.append(tag) + + # Füge LIMIT hinzu und optimiere die Sortierung + sql_query += " ORDER BY name LIMIT 100" + + # Führe die Abfrage aus + c.execute(sql_query, params) + results = c.fetchall() + + formatted_results = [] + for row in results: + customer = { + 'nummer': row[0], + 'name': row[1], + 'strasse': row[2], + 'plz': row[3], + 'ort': row[4], + 'telefon': row[5], + 'mobil': row[6], + 'email': row[7], + 'fachrichtung': row[8], + 'tag': row[9], + 'handy': row[10], + 'tele_firma': row[11], + 'kontakt1': row[12], + 'kontakt2': row[13], + 'kontakt3': row[14] + } + formatted_results.append(customer) + + return jsonify(formatted_results) - conn.close() - return jsonify(formatted_results) - except Exception as e: logger.error(f"Fehler bei der Suche: {str(e)}") return jsonify({'error': str(e)}), 500 @@ -350,24 +350,22 @@ def search(): def get_fachrichtungen(): try: search_term = request.args.get('q', '').lower() - conn = get_db_connection() - c = conn.cursor() - - # Hole alle eindeutigen Fachrichtungen, die mit dem Suchbegriff übereinstimmen - c.execute(''' - SELECT DISTINCT fachrichtung - FROM customers - WHERE fachrichtung IS NOT NULL - AND fachrichtung != '' - AND LOWER(fachrichtung) LIKE ? - ORDER BY fachrichtung - LIMIT 10 - ''', (f'%{search_term}%',)) - - fachrichtungen = [row[0] for row in c.fetchall()] - conn.close() - - return jsonify(fachrichtungen) + with get_db() as conn: + c = conn.cursor() + + # Hole alle eindeutigen Fachrichtungen, die mit dem Suchbegriff übereinstimmen + c.execute(''' + SELECT DISTINCT fachrichtung + FROM customers + WHERE fachrichtung IS NOT NULL + AND fachrichtung != '' + AND LOWER(fachrichtung) LIKE ? + ORDER BY fachrichtung + LIMIT 10 + ''', (f'%{search_term}%',)) + + fachrichtungen = [row[0] for row in c.fetchall()] + return jsonify(fachrichtungen) except Exception as e: logger.error(f"Fehler beim Abrufen der Fachrichtungen: {str(e)}") return jsonify([]) @@ -376,24 +374,22 @@ def get_fachrichtungen(): def get_orte(): try: search_term = request.args.get('q', '').lower() - conn = get_db_connection() - c = conn.cursor() - - # Hole alle eindeutigen Orte, die mit dem Suchbegriff übereinstimmen - c.execute(''' - SELECT DISTINCT ort - FROM customers - WHERE ort IS NOT NULL - AND ort != '' - AND LOWER(ort) LIKE ? - ORDER BY ort - LIMIT 10 - ''', (f'%{search_term}%',)) - - orte = [row[0] for row in c.fetchall()] - conn.close() - - return jsonify(orte) + with get_db() as conn: + c = conn.cursor() + + # Hole alle eindeutigen Orte, die mit dem Suchbegriff übereinstimmen + c.execute(''' + SELECT DISTINCT ort + FROM customers + WHERE ort IS NOT NULL + AND ort != '' + AND LOWER(ort) LIKE ? + ORDER BY ort + LIMIT 10 + ''', (f'%{search_term}%',)) + + orte = [row[0] for row in c.fetchall()] + return jsonify(orte) except Exception as e: logger.error(f"Fehler beim Abrufen der Orte: {str(e)}") return jsonify([]) @@ -406,10 +402,10 @@ def init_app(app): os.makedirs('data', exist_ok=True) # Lösche die alte Datenbank, falls sie existiert - if os.path.exists(DB_FILE): + if os.path.exists(app.config['DATABASE']): try: - os.remove(DB_FILE) - logger.info(f"Alte Datenbank {DB_FILE} wurde gelöscht") + os.remove(app.config['DATABASE']) + logger.info(f"Alte Datenbank {app.config['DATABASE']} wurde gelöscht") except Exception as e: logger.error(f"Fehler beim Löschen der alten Datenbank: {str(e)}") diff --git a/static/js/main.js b/static/js/main.js index 76ad62e..7cae238 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -2,6 +2,15 @@ let searchTimeout; let lastResults = []; let fachrichtungTimeout; let ortTimeout; +let currentPage = 1; +let totalPages = 1; +let currentResults = []; +let currentSearchQuery = ''; +let currentFilters = { + fachrichtung: '', + plz: '', + ort: '' +}; function createPhoneLink(phone) { if (!phone) return ''; @@ -220,14 +229,13 @@ function exportToVCF(customer) { const vcfData = [ 'BEGIN:VCARD', 'VERSION:3.0', - `FN:${customer.vorname || ''} ${customer.nachname || ''}`, - `N:${customer.nachname || ''};${customer.vorname || ''};;`, + `FN:${customer.name || ''}`, + `N:${customer.name || ''};;;`, `TEL;TYPE=CELL:${customer.telefon || ''}`, - `TEL;TYPE=HOME:${customer.telefon_2 || ''}`, + `TEL;TYPE=HOME:${customer.mobil || ''}`, `EMAIL:${customer.email || ''}`, - `ADR;TYPE=HOME:;;${customer.strasse || ''};${customer.plz || ''};${customer.ort || ''};${customer.land || ''}`, - `ORG:${customer.firma || ''}`, - `NOTE:${customer.notizen || ''}`, + `ADR;TYPE=HOME:;;${customer.strasse || ''};${customer.plz || ''};${customer.ort || ''};`, + `ORG:${customer.fachrichtung || ''}`, 'END:VCARD' ].join('\n'); @@ -235,7 +243,7 @@ function exportToVCF(customer) { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `kontakt_${customer.vorname || ''}_${customer.nachname || ''}_${new Date().toISOString().split('T')[0]}.vcf`; + a.download = `kontakt_${customer.name || ''}_${new Date().toISOString().split('T')[0]}.vcf`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); @@ -332,6 +340,7 @@ function clearInput(inputId) { } async function searchCustomers() { + let searchTimeout; const loading = document.getElementById('loading'); const results = document.getElementById('results'); const generalSearch = document.getElementById('q').value; @@ -341,6 +350,8 @@ async function searchCustomers() { const plzSearch = document.getElementById('plzInput').value; const fachrichtungSearch = document.getElementById('fachrichtungInput').value; const tagFilter = document.getElementById('tagFilter').value; + currentSearchQuery = generalSearch; + currentPage = 1; // Zeige Ladeanimation loading.style.display = 'block';