Hervorhebung des Tag-Feldes in den Suchergebnissen: - Blauer Hintergrund für MEDISOFT-Tags - Oranger Hintergrund für MEDICONSULT-Tags - Verbesserte visuelle Darstellung der Tags

This commit is contained in:
2025-03-19 13:18:22 +01:00
parent 35645fc671
commit fade9b8d62
6 changed files with 275 additions and 1407 deletions

1
.gitignore vendored
View File

@@ -52,3 +52,4 @@ spezexpo.csv
data/customers.db data/customers.db
data/customers.csv data/customers.csv
docker-compose.yml docker-compose.yml
/data/*.csv

390
app.py
View File

@@ -20,9 +20,6 @@ logger = logging.getLogger(__name__)
# Version der Anwendung # Version der Anwendung
VERSION = "1.2.1" VERSION = "1.2.1"
# Pfad zur CSV-Datei
CSV_FILE = 'data/customers.csv'
# Pfad zur Datenbank # Pfad zur Datenbank
DB_FILE = 'data/customers.db' DB_FILE = 'data/customers.db'
@@ -33,213 +30,220 @@ load_dotenv()
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password') STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password')
ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',') ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',')
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(): def init_db():
"""Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle.""" """Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
conn = sqlite3.connect(DB_FILE) conn = get_db_connection()
c = conn.cursor() c = conn.cursor()
# Erstelle die 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
)
''')
# 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)')
# 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()
conn.close()
logger.info('Datenbank initialisiert')
def import_csv():
"""Importiert die Daten aus der CSV-Datei in die SQLite-Datenbank."""
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
# Lösche bestehende Daten
c.execute('DELETE FROM customers')
try: try:
# Lese die CSV-Datei mit pandas # Erstelle die Kunden-Tabelle
df = pd.read_csv(CSV_FILE, sep=',', encoding='utf-8', quotechar='"') 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
)
''')
# Entferne Anführungszeichen aus den Spaltennamen # Erstelle Indizes für alle Suchfelder
df.columns = df.columns.str.strip('"') 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)')
# Entferne Anführungszeichen aus den Werten # Erstelle einen zusammengesetzten Index für die häufigste Suchkombination
for col in df.columns: c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name_ort ON customers(name, ort)')
if df[col].dtype == 'object':
df[col] = df[col].str.strip('"')
# Kombiniere Vorname und Nachname
df['name'] = df['Vorname'] + ' ' + df['Nachname']
# Importiere die Daten
for _, row in df.iterrows():
c.execute('''
INSERT INTO customers (nummer, name, strasse, plz, ort, telefon, mobil, email, bemerkung, fachrichtung)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
row['Nummer'],
row['name'],
row['Strasse'],
row['PLZ'],
row['Ort'],
row['Tel'],
row['Handy'],
row['mail'],
f"Fachrichtung: {row['Fachrichtung']}",
row['Fachrichtung']
))
conn.commit() conn.commit()
logger.info('CSV-Daten erfolgreich in die Datenbank importiert') logger.info('Datenbank initialisiert')
except Exception as e: except Exception as e:
logger.error(f'Fehler beim Import der CSV-Daten: {str(e)}') logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}')
raise raise
finally: finally:
conn.close() conn.close()
def search_customers(search_params): def import_csv():
"""Sucht nach Kunden basierend auf den Suchparametern.""" """Importiert die CSV-Datei in die Datenbank"""
# Prüfe, ob alle Suchfelder leer sind conn = None
if not any([
search_params.get('q'),
search_params.get('name'),
search_params.get('ort'),
search_params.get('nummer'),
search_params.get('plz'),
search_params.get('telefon'),
search_params.get('email'),
search_params.get('fachrichtung')
]):
return []
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
try: try:
# Baue die SQL-Abfrage dynamisch auf conn = get_db_connection()
query = "SELECT * FROM customers WHERE 1=1" 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='utf-8')
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['name'] = df['Vorname'] + ' ' + df['Nachname']
for _, row in df.iterrows():
c.execute('''
INSERT INTO customers (name, nummer, strasse, plz, ort, telefon, mobil, email, fachrichtung, tag)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (row['name'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Handy'], row['mail'], row['Fachrichtung'], 'medisoft'))
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='utf-8')
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)
df_snk['name'] = df_snk['Vorname'] + ' ' + df_snk['Nachname']
for _, row in df_snk.iterrows():
c.execute('''
INSERT INTO customers (name, nummer, strasse, plz, ort, telefon, mobil, email, fachrichtung, tag)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (row['name'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Handy'], row['mail'], row['Fachrichtung'], 'mediconsult'))
else:
logger.warning("MEDICONSULT CSV-Datei nicht gefunden")
conn.commit()
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 search_customers():
try:
q = request.args.get('q', '')
name = request.args.get('name', '')
ort = request.args.get('ort', '')
nummer = request.args.get('nummer', '')
plz = request.args.get('plz', '')
fachrichtung = request.args.get('fachrichtung', '')
operator = request.args.get('operator', 'or')
conn = get_db_connection()
c = conn.cursor()
# Basis-SQL-Query
query = '''
SELECT DISTINCT
c.id,
c.name,
c.nummer,
c.strasse,
c.plz,
c.ort,
c.telefon,
c.mobil,
c.email,
c.fachrichtung,
c.tag
FROM customers c
WHERE 1=1
'''
params = [] params = []
# Allgemeine Suche über alle Felder # Suchbedingungen
if search_params.get('q'): conditions = []
search_term = f"%{search_params['q']}%" if q:
operator = search_params.get('operator', 'or').upper() search_terms = q.split()
if operator == 'and':
if operator == 'AND': for term in search_terms:
# Bei UND-Verknüpfung müssen alle Begriffe in mindestens einem Feld vorkommen conditions.append('''
terms = search_params['q'].split() (c.name LIKE ? OR c.nummer LIKE ? OR c.strasse LIKE ?
conditions = [] OR c.plz LIKE ? OR c.ort LIKE ? OR c.telefon LIKE ?
for term in terms: OR c.mobil LIKE ? OR c.email LIKE ? OR c.fachrichtung LIKE ?
term = f"%{term}%" OR c.tag LIKE ?)
conditions.append("(name LIKE ? OR ort LIKE ? OR nummer LIKE ? OR telefon LIKE ? OR mobil LIKE ? OR email LIKE ? OR bemerkung LIKE ? OR fachrichtung LIKE ?)") ''')
params.extend([term] * 8) params.extend([f'%{term}%'] * 10)
query += " AND " + " AND ".join(conditions)
else: else:
# Bei ODER-Verknüpfung (Standard) muss mindestens ein Begriff in einem Feld vorkommen term_conditions = []
query += " AND (name LIKE ? OR ort LIKE ? OR nummer LIKE ? OR telefon LIKE ? OR mobil LIKE ? OR email LIKE ? OR bemerkung LIKE ? OR fachrichtung LIKE ?)" for term in search_terms:
params.extend([search_term] * 8) term_conditions.append('''
(c.name LIKE ? OR c.nummer LIKE ? OR c.strasse LIKE ?
OR c.plz LIKE ? OR c.ort LIKE ? OR c.telefon LIKE ?
OR c.mobil LIKE ? OR c.email LIKE ? OR c.fachrichtung LIKE ?
OR c.tag LIKE ?)
''')
params.extend([f'%{term}%'] * 10)
conditions.append('(' + ' OR '.join(term_conditions) + ')')
# Spezifische Suche für einzelne Felder if name:
if search_params.get('name'): conditions.append('c.name LIKE ?')
query += " AND name LIKE ?" params.append(f'%{name}%')
params.append(f"%{search_params['name']}%") if ort:
conditions.append('c.ort LIKE ?')
params.append(f'%{ort}%')
if nummer:
conditions.append('c.nummer LIKE ?')
params.append(f'%{nummer}%')
if plz:
conditions.append('c.plz LIKE ?')
params.append(f'%{plz}%')
if fachrichtung:
conditions.append('c.fachrichtung LIKE ?')
params.append(f'%{fachrichtung}%')
if search_params.get('ort'): if conditions:
query += " AND ort LIKE ?" query += ' AND ' + ' AND '.join(conditions)
params.append(f"%{search_params['ort']}%")
if search_params.get('nummer'):
query += " AND nummer LIKE ?"
params.append(f"%{search_params['nummer']}%")
if search_params.get('plz'):
query += " AND plz LIKE ?"
params.append(f"%{search_params['plz']}%")
if search_params.get('fachrichtung'):
query += " AND fachrichtung LIKE ?"
params.append(f"%{search_params['fachrichtung']}%")
# Führe die Abfrage aus
c.execute(query, params) c.execute(query, params)
results = c.fetchall() results = c.fetchall()
# Formatiere die Ergebnisse # Formatiere die Ergebnisse
customers = [] formatted_results = []
for row in results: for row in results:
customer = { customer = {
'id': row[0], 'id': row[0],
'nummer': row[1], 'name': row[1],
'name': row[2], 'nummer': row[2],
'strasse': row[3], 'strasse': row[3],
'plz': row[4], 'plz': row[4],
'ort': row[5], 'ort': row[5],
'telefon': row[6], 'telefon': row[6],
'mobil': row[7], 'mobil': row[7],
'email': row[8], 'email': row[8],
'bemerkung': row[9], 'fachrichtung': row[9],
'fachrichtung': row[10] 'tag': row[10]
} }
customers.append(customer) formatted_results.append(customer)
return customers
except Exception as e:
logger.error(f"Fehler bei der Kundensuche: {str(e)}")
raise
finally:
conn.close() 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
def clean_dataframe(df): def clean_dataframe(df):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität""" """Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
return df.replace({np.nan: None}) return df.replace({np.nan: None})
# CSV-Datei laden
def load_data():
try:
logger.info("Versuche CSV-Datei zu laden...")
if not os.path.exists(CSV_FILE):
logger.error(f"CSV-Datei '{CSV_FILE}' nicht gefunden!")
return None
# Lade CSV mit Komma als Trennzeichen
df = pd.read_csv(CSV_FILE, sep=',', encoding='utf-8', quotechar='"')
# Entferne Anführungszeichen aus den Spaltennamen
df.columns = df.columns.str.strip('"')
# Entferne Anführungszeichen aus den Werten
for col in df.columns:
if df[col].dtype == 'object':
df[col] = df[col].str.strip('"')
df = clean_dataframe(df)
logger.info(f"CSV-Datei erfolgreich geladen. {len(df)} Einträge gefunden.")
return df
except Exception as e:
logger.error(f"Fehler beim Laden der CSV-Datei: {str(e)}")
return None
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
@@ -296,38 +300,44 @@ def index():
@app.route('/search') @app.route('/search')
def search(): def search():
try: try:
# Hole die Suchparameter aus der Anfrage # Führe die Suche durch und hole die Ergebnisse
search_params = { results = search_customers()
'name': request.args.get('name', ''),
'ort': request.args.get('ort', ''),
'nummer': request.args.get('nummer', ''),
'plz': request.args.get('plz', ''),
'telefon': request.args.get('telefon', ''),
'email': request.args.get('email', ''),
'q': request.args.get('q', ''),
'fachrichtung': request.args.get('fachrichtung', ''),
'operator': request.args.get('operator', 'or')
}
# Führe die Suche in der Datenbank durch # Wenn results ein Response-Objekt ist, geben wir es direkt zurück
results = search_customers(search_params) if isinstance(results, tuple):
return results
# Protokolliere die Anzahl der gefundenen Ergebnisse # Protokolliere die Anzahl der gefundenen Ergebnisse
logger.info(f'Suchergebnisse gefunden: {len(results)}') logger.info(f'Suchergebnisse gefunden: {len(results.get_json())}')
return jsonify(results) return 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)}')
return jsonify({"error": str(e)}), 500 return jsonify({'error': str(e)}), 500
def init_app(app): def init_app(app):
"""Initialisiert die Anwendung mit allen notwendigen Einstellungen.""" """Initialisiert die Anwendung mit allen notwendigen Einstellungen."""
with app.app_context(): with app.app_context():
# Initialisiere die Datenbank try:
init_db() # Stelle sicher, dass der data-Ordner existiert
# Importiere die CSV-Daten os.makedirs('data', exist_ok=True)
import_csv()
logger.info("Anwendung erfolgreich initialisiert") # Lösche die alte Datenbank, falls sie existiert
if os.path.exists(DB_FILE):
try:
os.remove(DB_FILE)
logger.info(f"Alte Datenbank {DB_FILE} wurde gelöscht")
except Exception as e:
logger.error(f"Fehler beim Löschen der alten Datenbank: {str(e)}")
# Initialisiere die Datenbank
init_db()
# Importiere die CSV-Daten
import_csv()
logger.info("Anwendung erfolgreich initialisiert")
except Exception as e:
logger.error(f"Fehler bei der Initialisierung: {str(e)}")
raise
# Initialisiere die App # Initialisiere die App
init_app(app) init_app(app)

File diff suppressed because it is too large Load Diff

24
static/css/style.css Normal file
View File

@@ -0,0 +1,24 @@
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.result-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
}
.tag-medisoft {
background-color: #e3f2fd;
color: #1976d2;
}
.tag-mediconsult {
background-color: #f3e5f5;
color: #7b1fa2;
}

View File

@@ -254,3 +254,20 @@ body {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.result-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
color: white;
}
.tag-medisoft {
background-color: #1976d2;
}
.tag-mediconsult {
background-color: #ff9800;
}

View File

@@ -120,11 +120,6 @@
// Entferne alle nicht-numerischen Zeichen // Entferne alle nicht-numerischen Zeichen
let cleanNumber = phone.replace(/\D/g, ''); let cleanNumber = phone.replace(/\D/g, '');
// Füge eine führende 0 hinzu, wenn isAllowed true ist
if (isAllowed) {
cleanNumber = '0' + cleanNumber;
}
// Formatiere die Nummer // Formatiere die Nummer
let formattedNumber = cleanNumber; let formattedNumber = cleanNumber;
if (cleanNumber.length === 11) { if (cleanNumber.length === 11) {
@@ -236,49 +231,42 @@
function displayResults(results) { function displayResults(results) {
const resultsDiv = document.getElementById('results'); const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = ''; const resultCount = document.getElementById('resultCount');
if (results.length === 0) { if (results.length === 0) {
resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>'; resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
resultCount.textContent = '0 Ergebnisse';
return; return;
} }
// Hole alle Suchbegriffe resultCount.textContent = `${results.length} Ergebnisse`;
const searchTerms = {
general: document.getElementById('q').value,
name: document.getElementById('nameInput').value,
ort: document.getElementById('ortInput').value,
nummer: document.getElementById('nummerInput').value,
plz: document.getElementById('plzInput').value,
fachrichtung: document.getElementById('fachrichtungInput').value
};
results.forEach(customer => { const resultsList = results.map(customer => {
const card = document.createElement('div'); return `
card.className = 'customer-card'; <div class="card mb-3">
card.innerHTML = ` <div class="card-body">
<div class="customer-info"> <div class="d-flex justify-content-between align-items-start">
<h5 class="mb-1">${highlightText(customer.name, searchTerms.general || searchTerms.name)}</h5> <h5 class="card-title">${customer.name}</h5>
<p class="mb-1 customer-number">${createCustomerLink(customer.nummer)}</p> <div class="d-flex align-items-center">
<p class="mb-1">${createAddressLink( <span class="result-tag ${customer.tag === 'medisoft' ? 'tag-medisoft' : 'tag-mediconsult'} me-2">${customer.tag}</span>
customer.strasse, <button class="btn btn-sm btn-outline-primary" onclick="copyCustomerLink('${customer.nummer}')">
highlightText(customer.plz, searchTerms.general || searchTerms.plz), <i class="fas fa-share-alt"></i> Teilen
highlightText(customer.ort, searchTerms.general || searchTerms.ort) </button>
)}</p> </div>
<p class="mb-1">Tel: ${createPhoneLink(customer.telefon)}</p> </div>
${customer.mobil ? `<p class="mb-1">Mobil: ${createPhoneLink(customer.mobil)}</p>` : ''} <div class="card-text">
${customer.email ? `<p class="mb-1">E-Mail: ${createEmailLink(customer.email)}</p>` : ''} <p><strong>Nummer:</strong> ${createCustomerLink(customer.nummer)}</p>
${customer.bemerkung ? `<p class="mb-1">Bemerkung: ${customer.bemerkung}</p>` : ''} <p><strong>Adresse:</strong> ${createAddressLink(customer.strasse, customer.plz, customer.ort)}</p>
${customer.fachrichtung ? `<p class="mb-1">Fachrichtung: ${highlightText(customer.fachrichtung, searchTerms.general || searchTerms.fachrichtung)}</p>` : ''} <p><strong>Telefon:</strong> ${createPhoneLink(customer.telefon)}</p>
<p><strong>Mobil:</strong> ${createPhoneLink(customer.mobil)}</p>
<p><strong>E-Mail:</strong> ${createEmailLink(customer.email)}</p>
<p><strong>Fachrichtung:</strong> ${customer.fachrichtung}</p>
</div>
</div> </div>
<div class="card-actions"> </div>
<button class="share-button" onclick="copyCustomerLink('${customer.nummer}')"> `}).join('');
<i class="fas fa-share-alt"></i> Teilen
</button> resultsDiv.innerHTML = resultsList;
</div>
`;
resultsDiv.appendChild(card);
});
} }
function searchCustomers() { function searchCustomers() {