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

294
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,12 +30,19 @@ 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 try:
# Erstelle die Kunden-Tabelle
c.execute(''' c.execute('''
CREATE TABLE IF NOT EXISTS customers ( CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -51,7 +55,8 @@ def init_db():
mobil TEXT, mobil TEXT,
email TEXT, email TEXT,
bemerkung TEXT, bemerkung TEXT,
fachrichtung TEXT fachrichtung TEXT,
tag TEXT
) )
''') ''')
@@ -65,181 +70,180 @@ def init_db():
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_mobil ON customers(mobil)') 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_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_fachrichtung ON customers(fachrichtung)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_tag ON customers(tag)')
# Erstelle einen zusammengesetzten Index für die häufigste Suchkombination # 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)') c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name_ort ON customers(name, ort)')
conn.commit() conn.commit()
conn.close()
logger.info('Datenbank initialisiert') 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 Daten aus der CSV-Datei in die SQLite-Datenbank.""" """Importiert die CSV-Datei in die Datenbank"""
conn = sqlite3.connect(DB_FILE) conn = None
try:
conn = get_db_connection()
c = conn.cursor() c = conn.cursor()
# Lösche bestehende Daten # Lösche bestehende Daten
c.execute('DELETE FROM customers') c.execute('DELETE FROM customers')
try: # Importiere MEDISOFT-Daten
# Lese die CSV-Datei mit pandas if os.path.exists('data/customers.csv'):
df = pd.read_csv(CSV_FILE, sep=',', encoding='utf-8', quotechar='"') logger.info("Importiere MEDISOFT-Daten...")
df = pd.read_csv('data/customers.csv', encoding='utf-8')
# Entferne Anführungszeichen aus den Spaltennamen df.columns = df.columns.str.strip().str.replace('"', '')
df.columns = df.columns.str.strip('"') df = df.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x)
# Entferne Anführungszeichen aus den Werten
for col in df.columns:
if df[col].dtype == 'object':
df[col] = df[col].str.strip('"')
# Kombiniere Vorname und Nachname
df['name'] = df['Vorname'] + ' ' + df['Nachname'] df['name'] = df['Vorname'] + ' ' + df['Nachname']
# Importiere die Daten
for _, row in df.iterrows(): for _, row in df.iterrows():
c.execute(''' c.execute('''
INSERT INTO customers (nummer, name, strasse, plz, ort, telefon, mobil, email, bemerkung, fachrichtung) INSERT INTO customers (name, nummer, strasse, plz, ort, telefon, mobil, email, fachrichtung, tag)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (row['name'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Nummer'], row['Tel'], row['Handy'], row['mail'], row['Fachrichtung'], 'medisoft'))
row['name'], else:
row['Strasse'], logger.warning("MEDISOFT CSV-Datei nicht gefunden")
row['PLZ'],
row['Ort'], # Importiere MEDICONSULT-Daten
row['Tel'], if os.path.exists('data/customers_snk.csv'):
row['Handy'], logger.info("Importiere MEDICONSULT-Daten...")
row['mail'], df_snk = pd.read_csv('data/customers_snk.csv', encoding='utf-8')
f"Fachrichtung: {row['Fachrichtung']}", df_snk.columns = df_snk.columns.str.strip().str.replace('"', '')
row['Fachrichtung'] 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() conn.commit()
logger.info('CSV-Daten erfolgreich in die Datenbank importiert') logger.info("CSV-Daten erfolgreich in die Datenbank importiert")
except Exception as e: except Exception as e:
logger.error(f'Fehler beim Import der CSV-Daten: {str(e)}') logger.error(f"Fehler beim Importieren der CSV-Datei: {str(e)}")
raise raise
finally: finally:
if conn:
conn.close() conn.close()
def search_customers(search_params): def search_customers():
"""Sucht nach Kunden basierend auf den Suchparametern.""" try:
# Prüfe, ob alle Suchfelder leer sind q = request.args.get('q', '')
if not any([ name = request.args.get('name', '')
search_params.get('q'), ort = request.args.get('ort', '')
search_params.get('name'), nummer = request.args.get('nummer', '')
search_params.get('ort'), plz = request.args.get('plz', '')
search_params.get('nummer'), fachrichtung = request.args.get('fachrichtung', '')
search_params.get('plz'), operator = request.args.get('operator', 'or')
search_params.get('telefon'),
search_params.get('email'),
search_params.get('fachrichtung')
]):
return []
conn = sqlite3.connect(DB_FILE) conn = get_db_connection()
c = conn.cursor() c = conn.cursor()
try: # Basis-SQL-Query
# Baue die SQL-Abfrage dynamisch auf query = '''
query = "SELECT * FROM customers WHERE 1=1" 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'):
search_term = f"%{search_params['q']}%"
operator = search_params.get('operator', 'or').upper()
if operator == 'AND':
# Bei UND-Verknüpfung müssen alle Begriffe in mindestens einem Feld vorkommen
terms = search_params['q'].split()
conditions = [] conditions = []
for term in terms: if q:
term = f"%{term}%" search_terms = q.split()
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 ?)") if operator == 'and':
params.extend([term] * 8) for term in search_terms:
query += " AND " + " AND ".join(conditions) 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)
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():
try:
# Stelle sicher, dass der data-Ordner existiert
os.makedirs('data', exist_ok=True)
# 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 # Initialisiere die Datenbank
init_db() init_db()
# Importiere die CSV-Daten # Importiere die CSV-Daten
import_csv() import_csv()
logger.info("Anwendung erfolgreich initialisiert") 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),
highlightText(customer.ort, searchTerms.general || searchTerms.ort)
)}</p>
<p class="mb-1">Tel: ${createPhoneLink(customer.telefon)}</p>
${customer.mobil ? `<p class="mb-1">Mobil: ${createPhoneLink(customer.mobil)}</p>` : ''}
${customer.email ? `<p class="mb-1">E-Mail: ${createEmailLink(customer.email)}</p>` : ''}
${customer.bemerkung ? `<p class="mb-1">Bemerkung: ${customer.bemerkung}</p>` : ''}
${customer.fachrichtung ? `<p class="mb-1">Fachrichtung: ${highlightText(customer.fachrichtung, searchTerms.general || searchTerms.fachrichtung)}</p>` : ''}
</div>
<div class="card-actions">
<button class="share-button" onclick="copyCustomerLink('${customer.nummer}')">
<i class="fas fa-share-alt"></i> Teilen <i class="fas fa-share-alt"></i> Teilen
</button> </button>
</div> </div>
`; </div>
resultsDiv.appendChild(card); <div class="card-text">
}); <p><strong>Nummer:</strong> ${createCustomerLink(customer.nummer)}</p>
<p><strong>Adresse:</strong> ${createAddressLink(customer.strasse, customer.plz, customer.ort)}</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>
`}).join('');
resultsDiv.innerHTML = resultsList;
} }
function searchCustomers() { function searchCustomers() {