7 Commits

5 changed files with 408 additions and 311 deletions

View File

@@ -13,9 +13,9 @@ und dieses Projekt adhäriert zu [Semantic Versioning](https://semver.org/lang/d
## [1.2.16] - 2024-03-21 ## [1.2.16] - 2024-03-21
### Geändert ### Geändert
- Verbesserte Darstellung der Suchergebnisse mit rechtsbündigen Aktionen - Verbesserte Suchfunktion: Highlighting für allgemeine Suche in allen Feldern
- Optimierte CSS-Styles für bessere Lesbarkeit und Layout - Optimierte Reset-Buttons in den Suchfeldern
- JavaScript-Code in separate Datei ausgelagert für bessere Wartbarkeit - Verbesserte CSS-Styles für die Suchfeld-Icons
## [1.2.15] - 2024-03-20 ## [1.2.15] - 2024-03-20
### Hinzugefügt ### Hinzugefügt

492
app.py
View File

@@ -5,28 +5,87 @@ import logging
import numpy as np import numpy as np
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
import requests
from collections import defaultdict
import ipaddress
import csv
import sqlite3 import sqlite3
from functools import wraps from functools import wraps
from contextlib import contextmanager
import time
import threading
app = Flask(__name__, static_folder='static') app = Flask(__name__, static_folder='static')
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev') 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['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['VERSION'] = '1.2.17'
app.config['DATABASE'] = 'data/customers.db'
app.config['DATABASE_TIMEOUT'] = 20
app.config['DATABASE_POOL_SIZE'] = 5
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Pfad zur Datenbank # Thread-lokaler Speicher für Datenbankverbindungen
DB_FILE = 'data/customers.db' thread_local = threading.local()
# Lade Umgebungsvariablen def get_db_connection():
load_dotenv() """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 @contextmanager
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password') 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): def isIPInSubnet(ip, subnet):
"""Überprüft, ob eine IP-Adresse in einem Subnetz liegt.""" """Ü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)}") logger.error(f"Fehler bei der IP-Überprüfung: {str(e)}")
return False 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(): def import_csv():
"""Importiert die CSV-Datei in die Datenbank""" """Importiert die CSV-Datei in die Datenbank"""
conn = None
try: try:
conn = get_db_connection() with get_db() as conn:
c = conn.cursor() c = conn.cursor()
# Lösche bestehende Daten # Lösche bestehende Daten
c.execute('DELETE FROM customers') c.execute('DELETE FROM customers')
# Importiere MEDISOFT-Daten # Importiere MEDISOFT-Daten
if os.path.exists('data/customers.csv'): if os.path.exists('data/customers.csv'):
logger.info("Importiere MEDISOFT-Daten...") logger.info("Importiere MEDISOFT-Daten...")
df = pd.read_csv('data/customers.csv', encoding='iso-8859-1') df = pd.read_csv('data/customers.csv', encoding='iso-8859-1')
df.columns = df.columns.str.strip().str.replace('"', '') df.columns = df.columns.str.strip().str.replace('"', '')
df = df.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x) df = df.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x)
for _, row in df.iterrows(): # Filtere Datensätze mit Fachrichtung "intern"
c.execute(''' df = df[df['Fachrichtung'].str.lower() != 'intern']
INSERT INTO customers (
name, nummer, strasse, plz, ort, telefon, mobil, email, # Bereite die Daten für den Batch-Insert vor
fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3 data = [(
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'], row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'medisoft', row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'medisoft',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3'] row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
)) ) for _, row in df.iterrows()]
else:
logger.warning("MEDISOFT CSV-Datei nicht gefunden")
# Importiere MEDICONSULT-Daten # Führe Batch-Insert durch
if os.path.exists('data/customers_snk.csv'): c.executemany('''
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('''
INSERT INTO customers ( INSERT INTO customers (
name, nummer, strasse, plz, ort, telefon, mobil, email, name, nummer, strasse, plz, ort, telefon, mobil, email,
fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3 fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 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['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'mediconsult', row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'mediconsult',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3'] row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
)) ) for _, row in df_snk.iterrows()]
else:
logger.warning("MEDICONSULT CSV-Datei nicht gefunden")
conn.commit() # Führe Batch-Insert durch
logger.info("CSV-Daten erfolgreich in die Datenbank importiert") 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: except Exception as e:
logger.error(f"Fehler beim Importieren der CSV-Datei: {str(e)}") logger.error(f"Fehler beim Importieren der CSV-Datei: {str(e)}")
raise 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']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
@@ -191,7 +193,7 @@ def login():
if request.method == 'POST': if request.method == 'POST':
password = request.form.get('password') password = request.form.get('password')
if password == STATIC_PASSWORD: if password == os.environ.get('LOGIN_PASSWORD'):
session['logged_in'] = True session['logged_in'] = True
logger.info("Erfolgreicher Login") logger.info("Erfolgreicher Login")
return redirect(url_for('index')) return redirect(url_for('index'))
@@ -233,114 +235,112 @@ def search():
fachrichtung = request.args.get('fachrichtung', '') fachrichtung = request.args.get('fachrichtung', '')
tag = request.args.get('tag', 'medisoft') tag = request.args.get('tag', 'medisoft')
conn = get_db_connection() with get_db() as conn:
c = conn.cursor() c = conn.cursor()
# Baue die SQL-Abfrage # Baue die SQL-Abfrage
sql_query = ''' sql_query = '''
SELECT SELECT
nummer, nummer,
name, name,
strasse, strasse,
plz, plz,
ort, ort,
telefon, telefon,
mobil, mobil,
email, email,
fachrichtung, fachrichtung,
tag, tag,
handy, handy,
tele_firma, tele_firma,
kontakt1, kontakt1,
kontakt2, kontakt2,
kontakt3 kontakt3
FROM customers FROM customers
WHERE 1=1 WHERE 1=1
''' '''
params = [] params = []
# Füge die Suchbedingungen hinzu # Füge die Suchbedingungen hinzu
if search_query: if search_query:
# Optimierte Suche mit FTS (Full Text Search) # Optimierte Suche mit FTS (Full Text Search)
sql_query += """ sql_query += """
AND ( AND (
name LIKE ? OR name LIKE ? OR
nummer LIKE ? OR nummer LIKE ? OR
fachrichtung LIKE ? OR fachrichtung LIKE ? OR
ort LIKE ? OR ort LIKE ? OR
plz LIKE ? OR plz LIKE ? OR
strasse LIKE ? OR strasse LIKE ? OR
telefon LIKE ? OR telefon LIKE ? OR
mobil LIKE ? OR mobil LIKE ? OR
email LIKE ? OR email LIKE ? OR
bemerkung LIKE ? OR bemerkung LIKE ? OR
tag LIKE ? OR tag LIKE ? OR
handy LIKE ? OR handy LIKE ? OR
tele_firma LIKE ? OR tele_firma LIKE ? OR
kontakt1 LIKE ? OR kontakt1 LIKE ? OR
kontakt2 LIKE ? OR kontakt2 LIKE ? OR
kontakt3 LIKE ? kontakt3 LIKE ?
) )
""" """
search_term = f"%{search_query}%" search_term = f"%{search_query}%"
params.extend([search_term] * 16) # 16 Felder für die allgemeine Suche params.extend([search_term] * 16) # 16 Felder für die allgemeine Suche
if name: if name:
sql_query += " AND name LIKE ?" sql_query += " AND name LIKE ?"
params.append(f"%{name}%") params.append(f"%{name}%")
if ort: if ort:
sql_query += " AND ort LIKE ?" sql_query += " AND ort LIKE ?"
params.append(f"%{ort}%") params.append(f"%{ort}%")
if nummer: if nummer:
sql_query += " AND nummer LIKE ?" sql_query += " AND nummer LIKE ?"
params.append(f"%{nummer}%") params.append(f"%{nummer}%")
if plz: if plz:
sql_query += " AND plz LIKE ?" sql_query += " AND plz LIKE ?"
params.append(f"%{plz}%") params.append(f"%{plz}%")
if fachrichtung: if fachrichtung:
sql_query += " AND fachrichtung LIKE ?" sql_query += " AND fachrichtung LIKE ?"
params.append(f"%{fachrichtung}%") params.append(f"%{fachrichtung}%")
# Filter nach Tag # Filter nach Tag
if tag != 'all': if tag != 'all':
sql_query += " AND tag = ?" sql_query += " AND tag = ?"
params.append(tag) params.append(tag)
# Füge LIMIT hinzu und optimiere die Sortierung # Füge LIMIT hinzu und optimiere die Sortierung
sql_query += " ORDER BY name LIMIT 100" sql_query += " ORDER BY name LIMIT 100"
# Führe die Abfrage aus # Führe die Abfrage aus
cursor = conn.cursor() c.execute(sql_query, params)
cursor.execute(sql_query, params) results = c.fetchall()
results = cursor.fetchall()
formatted_results = [] formatted_results = []
for row in results: for row in results:
customer = { customer = {
'nummer': row[0], 'nummer': row[0],
'name': row[1], 'name': row[1],
'strasse': row[2], 'strasse': row[2],
'plz': row[3], 'plz': row[3],
'ort': row[4], 'ort': row[4],
'telefon': row[5], 'telefon': row[5],
'mobil': row[6], 'mobil': row[6],
'email': row[7], 'email': row[7],
'fachrichtung': row[8], 'fachrichtung': row[8],
'tag': row[9], 'tag': row[9],
'handy': row[10], 'handy': row[10],
'tele_firma': row[11], 'tele_firma': row[11],
'kontakt1': row[12], 'kontakt1': row[12],
'kontakt2': row[13], 'kontakt2': row[13],
'kontakt3': row[14] 'kontakt3': row[14]
} }
formatted_results.append(customer) formatted_results.append(customer)
conn.close() return jsonify(formatted_results)
return jsonify(formatted_results)
except Exception as e: except Exception as e:
logger.error(f"Fehler bei der Suche: {str(e)}") logger.error(f"Fehler bei der Suche: {str(e)}")
@@ -350,24 +350,22 @@ def search():
def get_fachrichtungen(): def get_fachrichtungen():
try: try:
search_term = request.args.get('q', '').lower() search_term = request.args.get('q', '').lower()
conn = get_db_connection() with get_db() as conn:
c = conn.cursor() c = conn.cursor()
# Hole alle eindeutigen Fachrichtungen, die mit dem Suchbegriff übereinstimmen # Hole alle eindeutigen Fachrichtungen, die mit dem Suchbegriff übereinstimmen
c.execute(''' c.execute('''
SELECT DISTINCT fachrichtung SELECT DISTINCT fachrichtung
FROM customers FROM customers
WHERE fachrichtung IS NOT NULL WHERE fachrichtung IS NOT NULL
AND fachrichtung != '' AND fachrichtung != ''
AND LOWER(fachrichtung) LIKE ? AND LOWER(fachrichtung) LIKE ?
ORDER BY fachrichtung ORDER BY fachrichtung
LIMIT 10 LIMIT 10
''', (f'%{search_term}%',)) ''', (f'%{search_term}%',))
fachrichtungen = [row[0] for row in c.fetchall()] fachrichtungen = [row[0] for row in c.fetchall()]
conn.close() return jsonify(fachrichtungen)
return jsonify(fachrichtungen)
except Exception as e: except Exception as e:
logger.error(f"Fehler beim Abrufen der Fachrichtungen: {str(e)}") logger.error(f"Fehler beim Abrufen der Fachrichtungen: {str(e)}")
return jsonify([]) return jsonify([])
@@ -376,24 +374,22 @@ def get_fachrichtungen():
def get_orte(): def get_orte():
try: try:
search_term = request.args.get('q', '').lower() search_term = request.args.get('q', '').lower()
conn = get_db_connection() with get_db() as conn:
c = conn.cursor() c = conn.cursor()
# Hole alle eindeutigen Orte, die mit dem Suchbegriff übereinstimmen # Hole alle eindeutigen Orte, die mit dem Suchbegriff übereinstimmen
c.execute(''' c.execute('''
SELECT DISTINCT ort SELECT DISTINCT ort
FROM customers FROM customers
WHERE ort IS NOT NULL WHERE ort IS NOT NULL
AND ort != '' AND ort != ''
AND LOWER(ort) LIKE ? AND LOWER(ort) LIKE ?
ORDER BY ort ORDER BY ort
LIMIT 10 LIMIT 10
''', (f'%{search_term}%',)) ''', (f'%{search_term}%',))
orte = [row[0] for row in c.fetchall()] orte = [row[0] for row in c.fetchall()]
conn.close() return jsonify(orte)
return jsonify(orte)
except Exception as e: except Exception as e:
logger.error(f"Fehler beim Abrufen der Orte: {str(e)}") logger.error(f"Fehler beim Abrufen der Orte: {str(e)}")
return jsonify([]) return jsonify([])
@@ -406,10 +402,10 @@ def init_app(app):
os.makedirs('data', exist_ok=True) os.makedirs('data', exist_ok=True)
# Lösche die alte Datenbank, falls sie existiert # Lösche die alte Datenbank, falls sie existiert
if os.path.exists(DB_FILE): if os.path.exists(app.config['DATABASE']):
try: try:
os.remove(DB_FILE) os.remove(app.config['DATABASE'])
logger.info(f"Alte Datenbank {DB_FILE} wurde gelöscht") logger.info(f"Alte Datenbank {app.config['DATABASE']} wurde gelöscht")
except Exception as e: except Exception as e:
logger.error(f"Fehler beim Löschen der alten Datenbank: {str(e)}") logger.error(f"Fehler beim Löschen der alten Datenbank: {str(e)}")

View File

@@ -151,6 +151,36 @@ body {
position: relative; position: relative;
} }
.reset-icon {
position: absolute;
right: 40px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #6c757d;
z-index: 10;
padding: 0.375rem;
display: none;
}
.input-group input:not(:placeholder-shown) + .reset-icon {
display: block;
}
.reset-icon:hover {
color: #dc3545;
}
.search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
z-index: 10;
padding: 0.375rem;
}
.result-counts { .result-counts {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -2,6 +2,15 @@ let searchTimeout;
let lastResults = []; let lastResults = [];
let fachrichtungTimeout; let fachrichtungTimeout;
let ortTimeout; let ortTimeout;
let currentPage = 1;
let totalPages = 1;
let currentResults = [];
let currentSearchQuery = '';
let currentFilters = {
fachrichtung: '',
plz: '',
ort: ''
};
function createPhoneLink(phone) { function createPhoneLink(phone) {
if (!phone) return ''; if (!phone) return '';
@@ -34,11 +43,23 @@ function createEmailLink(email) {
function highlightText(text, searchTerm) { function highlightText(text, searchTerm) {
if (!searchTerm || !text) return text; if (!searchTerm || !text) return text;
// Escapen von Sonderzeichen im Suchbegriff
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Teile den Suchbegriff in einzelne Wörter
// Erstelle einen regulären Ausdruck ohne Wortgrenzen const searchWords = searchTerm.split(/\s+/).filter(word => word.length > 0);
const regex = new RegExp(escapedSearchTerm, 'gi');
return text.replace(regex, '<mark>$&</mark>'); // Wenn keine Wörter gefunden wurden, gebe den ursprünglichen Text zurück
if (searchWords.length === 0) return text;
// Erstelle einen regulären Ausdruck für alle Suchwörter
const regexPattern = searchWords
.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('|');
// Erstelle den regulären Ausdruck
const regex = new RegExp(`(${regexPattern})`, 'gi');
// Ersetze alle Übereinstimmungen mit mark-Tags
return text.replace(regex, '<mark>$1</mark>');
} }
function createAddressLink(street, plz, city) { function createAddressLink(street, plz, city) {
@@ -119,14 +140,18 @@ async function copyCustomerLink(customerNumber) {
} }
function updateResultCounts() { function updateResultCounts() {
// Nur Gesamtzahl anzeigen const resultCount = document.getElementById('result-count');
const generalCount = lastResults.length; const exportButton = document.getElementById('exportButton');
document.getElementById('resultCount').textContent =
generalCount > 0 ? `${generalCount} Treffer gefunden` : '';
document.getElementById('resultCount').classList.toggle('visible', generalCount > 0);
// Export-Button anzeigen/verstecken if (lastResults && lastResults.length > 0) {
document.getElementById('exportButton').style.display = generalCount > 0 ? 'inline-block' : 'none'; resultCount.textContent = `${lastResults.length} Ergebnisse gefunden`;
resultCount.style.display = 'inline';
exportButton.style.display = 'inline-block';
} else {
resultCount.textContent = '';
resultCount.style.display = 'none';
exportButton.style.display = 'none';
}
} }
function exportToCSV() { function exportToCSV() {
@@ -198,16 +223,43 @@ function exportToCSV() {
document.body.removeChild(link); document.body.removeChild(link);
} }
function exportToVCF(customer) {
if (!customer) return;
const vcfData = [
'BEGIN:VCARD',
'VERSION:3.0',
`FN:${customer.name || ''}`,
`N:${customer.name || ''};;;`,
`TEL;TYPE=CELL:${customer.telefon || ''}`,
`TEL;TYPE=HOME:${customer.mobil || ''}`,
`EMAIL:${customer.email || ''}`,
`ADR;TYPE=HOME:;;${customer.strasse || ''};${customer.plz || ''};${customer.ort || ''};`,
`ORG:${customer.fachrichtung || ''}`,
'END:VCARD'
].join('\n');
const blob = new Blob([vcfData], { type: 'text/vcard;charset=utf-8' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `kontakt_${customer.name || ''}_${new Date().toISOString().split('T')[0]}.vcf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
function displayResults(results) { function displayResults(results) {
const resultsDiv = document.getElementById('results'); const resultsDiv = document.getElementById('results');
const resultCount = document.getElementById('resultCount'); const resultCount = document.getElementById('result-count');
const generalSearchTerm = document.getElementById('q').value; const generalSearchTerm = document.getElementById('q').value;
const nameSearchTerm = document.getElementById('nameInput').value; const nameSearchTerm = document.getElementById('nameInput').value;
const fachrichtungSearchTerm = document.getElementById('fachrichtungInput').value; const fachrichtungSearchTerm = document.getElementById('fachrichtungInput').value;
if (!results || results.length === 0) { if (!results || results.length === 0) {
resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>'; resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
resultCount.textContent = '0 Ergebnisse'; resultCount.textContent = '';
return; return;
} }
@@ -215,12 +267,6 @@ function displayResults(results) {
lastResults = results; lastResults = results;
const resultsHTML = results.map(customer => { const resultsHTML = results.map(customer => {
const highlightedName = highlightText(customer.name, nameSearchTerm);
const highlightedFachrichtung = highlightText(customer.fachrichtung, fachrichtungSearchTerm);
const highlightedGeneral = highlightText(customer.name, generalSearchTerm) ||
highlightText(customer.fachrichtung, generalSearchTerm) ||
highlightText(customer.ort, generalSearchTerm);
// Hilfsfunktion zum Erstellen von Feldern nur wenn sie Werte haben // Hilfsfunktion zum Erstellen von Feldern nur wenn sie Werte haben
const createFieldIfValue = (label, value, formatter = (v) => v) => { const createFieldIfValue = (label, value, formatter = (v) => v) => {
if (!value || value === 'N/A' || value === 'n/a' || value === 'N/a' || (typeof value === 'string' && value.trim() === '')) return ''; if (!value || value === 'N/A' || value === 'n/a' || value === 'N/a' || (typeof value === 'string' && value.trim() === '')) return '';
@@ -228,34 +274,52 @@ function displayResults(results) {
return `<p class="mb-1"><strong>${label}:</strong> ${formattedValue}</p>`; return `<p class="mb-1"><strong>${label}:</strong> ${formattedValue}</p>`;
}; };
// Highlighting für alle Felder
const highlightField = (value) => {
if (!value) return value;
let highlighted = value;
if (nameSearchTerm) {
highlighted = highlightText(highlighted, nameSearchTerm);
}
if (fachrichtungSearchTerm) {
highlighted = highlightText(highlighted, fachrichtungSearchTerm);
}
if (generalSearchTerm) {
highlighted = highlightText(highlighted, generalSearchTerm);
}
return highlighted;
};
return ` return `
<div class="customer-card"> <div class="customer-card">
<div class="customer-header"> <div class="customer-header">
<h3 class="customer-name">${highlightedName || highlightedGeneral}</h3> <h3 class="customer-name">${highlightField(customer.name)}</h3>
<div class="customer-actions"> <div class="customer-actions">
<span class="badge ${(customer.tag || 'medisoft') === 'medisoft' ? 'bg-primary' : 'bg-warning text-dark'}">${(customer.tag || 'medisoft').toUpperCase()}</span> <span class="badge ${(customer.tag || 'medisoft') === 'medisoft' ? 'bg-primary' : 'bg-warning text-dark'}">${(customer.tag || 'medisoft').toUpperCase()}</span>
<button class="btn btn-sm btn-outline-primary" onclick="copyCustomerLink('${customer.nummer}')"> <button class="btn btn-sm btn-outline-primary" onclick="copyCustomerLink('${customer.nummer}')" title="Link kopieren">
<i class="fas fa-link"></i> <i class="fas fa-link"></i>
</button> </button>
<button class="btn btn-sm btn-outline-primary" onclick='exportToVCF(${JSON.stringify(customer).replace(/'/g, "\\'")})' title="Als VCF exportieren">
<i class="bi bi-person-vcard"></i>
</button>
</div> </div>
</div> </div>
<div class="customer-details"> <div class="customer-details">
${createFieldIfValue('Nummer', highlightText(customer.nummer, generalSearchTerm), createCustomerLink)} ${createFieldIfValue('Nummer', highlightField(customer.nummer), createCustomerLink)}
${createFieldIfValue('Adresse', (customer.strasse && customer.plz && customer.ort) ? true : false, ${createFieldIfValue('Adresse', (customer.strasse && customer.plz && customer.ort) ? true : false,
() => createAddressLink( () => createAddressLink(
highlightText(customer.strasse, generalSearchTerm), customer.strasse,
highlightText(customer.plz, generalSearchTerm), highlightField(customer.plz),
highlightText(customer.ort, generalSearchTerm) highlightField(customer.ort)
))} ))}
${createFieldIfValue('Telefon', highlightText(customer.telefon, generalSearchTerm), createPhoneLink)} ${createFieldIfValue('Telefon', highlightField(customer.telefon), createPhoneLink)}
${createFieldIfValue('Mobil', highlightText(customer.mobil, generalSearchTerm), createPhoneLink)} ${createFieldIfValue('Mobil', highlightField(customer.mobil), createPhoneLink)}
${createFieldIfValue('Handy', highlightText(customer.handy, generalSearchTerm), createPhoneLink)} ${createFieldIfValue('Handy', highlightField(customer.handy), createPhoneLink)}
${createFieldIfValue('Telefon Firma', highlightText(customer.tele_firma, generalSearchTerm), createPhoneLink)} ${createFieldIfValue('E-Mail', highlightField(customer.email), createEmailLink)}
${createFieldIfValue('E-Mail', highlightText(customer.email, generalSearchTerm), createEmailLink)} ${createFieldIfValue('Fachrichtung', highlightField(customer.fachrichtung))}
${createFieldIfValue('Fachrichtung', highlightText(customer.fachrichtung, generalSearchTerm || fachrichtungSearchTerm))} ${createFieldIfValue('Kontakt 1', highlightField(customer.kontakt1), createPhoneLink)}
${createFieldIfValue('Kontakt 1', highlightText(customer.kontakt1, generalSearchTerm), createPhoneLink)} ${createFieldIfValue('Kontakt 2', highlightField(customer.kontakt2), createPhoneLink)}
${createFieldIfValue('Kontakt 2', highlightText(customer.kontakt2, generalSearchTerm), createPhoneLink)} ${createFieldIfValue('Kontakt 3', highlightField(customer.kontakt3), createPhoneLink)}
${createFieldIfValue('Kontakt 3', highlightText(customer.kontakt3, generalSearchTerm), createPhoneLink)}
${customer.tags && customer.tags.length > 0 ? ` ${customer.tags && customer.tags.length > 0 ? `
<p class="mb-0"><strong>Tags:</strong> <p class="mb-0"><strong>Tags:</strong>
${customer.tags.map(tag => `<span class="badge bg-primary me-1">${tag}</span>`).join('')} ${customer.tags.map(tag => `<span class="badge bg-primary me-1">${tag}</span>`).join('')}
@@ -272,10 +336,14 @@ function displayResults(results) {
function clearInput(inputId) { function clearInput(inputId) {
document.getElementById(inputId).value = ''; document.getElementById(inputId).value = '';
searchCustomers(); document.getElementById('results').innerHTML = '';
document.getElementById('result-count').textContent = '';
document.getElementById('exportButton').style.display = 'none';
lastResults = [];
} }
async function searchCustomers() { async function searchCustomers() {
let searchTimeout;
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
const results = document.getElementById('results'); const results = document.getElementById('results');
const generalSearch = document.getElementById('q').value; const generalSearch = document.getElementById('q').value;
@@ -285,6 +353,8 @@ async function searchCustomers() {
const plzSearch = document.getElementById('plzInput').value; const plzSearch = document.getElementById('plzInput').value;
const fachrichtungSearch = document.getElementById('fachrichtungInput').value; const fachrichtungSearch = document.getElementById('fachrichtungInput').value;
const tagFilter = document.getElementById('tagFilter').value; const tagFilter = document.getElementById('tagFilter').value;
currentSearchQuery = generalSearch;
currentPage = 1;
// Zeige Ladeanimation // Zeige Ladeanimation
loading.style.display = 'block'; loading.style.display = 'block';

View File

@@ -9,6 +9,7 @@
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
</head> </head>
<body> <body>
@@ -80,10 +81,10 @@
</div> </div>
</div> </div>
<div class="result-counts"> <div id="result-counts" class="mt-2">
<span id="resultCount" class="result-count"></span> <span id="result-count"></span>
<button id="exportButton" class="btn btn-sm btn-outline-success ms-2" onclick="exportToCSV()" style="display: none;"> <button id="exportButton" class="btn btn-sm btn-outline-primary ms-2" onclick="exportToCSV()" style="display: none;">
<i class="fas fa-file-csv"></i> CSV Export <i class="bi bi-file-earmark-spreadsheet"></i> Als CSV exportieren
</button> </button>
</div> </div>