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

3
.gitignore vendored
View File

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

398
app.py
View File

@@ -20,9 +20,6 @@ logger = logging.getLogger(__name__)
# Version der Anwendung
VERSION = "1.2.1"
# Pfad zur CSV-Datei
CSV_FILE = 'data/customers.csv'
# Pfad zur Datenbank
DB_FILE = 'data/customers.db'
@@ -33,213 +30,220 @@ load_dotenv()
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password')
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():
"""Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
conn = sqlite3.connect(DB_FILE)
conn = get_db_connection()
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:
# Lese die CSV-Datei mit pandas
df = pd.read_csv(CSV_FILE, sep=',', encoding='utf-8', quotechar='"')
# 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
)
''')
# Entferne Anführungszeichen aus den Spaltennamen
df.columns = df.columns.str.strip('"')
# 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)')
# 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']
# 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']
))
# 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('CSV-Daten erfolgreich in die Datenbank importiert')
logger.info('Datenbank initialisiert')
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
finally:
conn.close()
def search_customers(search_params):
"""Sucht nach Kunden basierend auf den Suchparametern."""
# Prüfe, ob alle Suchfelder leer sind
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()
def import_csv():
"""Importiert die CSV-Datei in die Datenbank"""
conn = None
try:
# Baue die SQL-Abfrage dynamisch auf
query = "SELECT * FROM customers WHERE 1=1"
params = []
conn = get_db_connection()
c = conn.cursor()
# Allgemeine Suche über alle Felder
if search_params.get('q'):
search_term = f"%{search_params['q']}%"
operator = search_params.get('operator', 'or').upper()
# 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']
if operator == 'AND':
# Bei UND-Verknüpfung müssen alle Begriffe in mindestens einem Feld vorkommen
terms = search_params['q'].split()
conditions = []
for term in terms:
term = f"%{term}%"
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)
query += " AND " + " AND ".join(conditions)
else:
# Bei ODER-Verknüpfung (Standard) muss mindestens ein Begriff in einem Feld vorkommen
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 ?)"
params.extend([search_term] * 8)
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")
# Spezifische Suche für einzelne Felder
if search_params.get('name'):
query += " AND name LIKE ?"
params.append(f"%{search_params['name']}%")
# 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")
if search_params.get('ort'):
query += " AND ort LIKE ?"
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']}%")
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 = []
# Suchbedingungen
conditions = []
if q:
search_terms = q.split()
if operator == 'and':
for term in search_terms:
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:
term_conditions = []
for term in search_terms:
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) + ')')
if name:
conditions.append('c.name LIKE ?')
params.append(f'%{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 conditions:
query += ' AND ' + ' AND '.join(conditions)
if search_params.get('fachrichtung'):
query += " AND fachrichtung LIKE ?"
params.append(f"%{search_params['fachrichtung']}%")
# Führe die Abfrage aus
c.execute(query, params)
results = c.fetchall()
# Formatiere die Ergebnisse
customers = []
formatted_results = []
for row in results:
customer = {
'id': row[0],
'nummer': row[1],
'name': row[2],
'name': row[1],
'nummer': row[2],
'strasse': row[3],
'plz': row[4],
'ort': row[5],
'telefon': row[6],
'mobil': row[7],
'email': row[8],
'bemerkung': row[9],
'fachrichtung': row[10]
'fachrichtung': row[9],
'tag': row[10]
}
customers.append(customer)
return customers
except Exception as e:
logger.error(f"Fehler bei der Kundensuche: {str(e)}")
raise
finally:
formatted_results.append(customer)
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):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
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'])
def login():
@@ -296,38 +300,44 @@ def index():
@app.route('/search')
def search():
try:
# Hole die Suchparameter aus der Anfrage
search_params = {
'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
results = search_customers(search_params)
# Führe die Suche durch und hole die Ergebnisse
results = search_customers()
# Wenn results ein Response-Objekt ist, geben wir es direkt zurück
if isinstance(results, tuple):
return results
# 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:
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):
"""Initialisiert die Anwendung mit allen notwendigen Einstellungen."""
with app.app_context():
# Initialisiere die Datenbank
init_db()
# Importiere die CSV-Daten
import_csv()
logger.info("Anwendung erfolgreich initialisiert")
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
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
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

@@ -253,4 +253,21 @@ body {
.search-options .form-check-label {
cursor: pointer;
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
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
let formattedNumber = cleanNumber;
if (cleanNumber.length === 11) {
@@ -236,49 +231,42 @@
function displayResults(results) {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = '';
const resultCount = document.getElementById('resultCount');
if (results.length === 0) {
resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
resultCount.textContent = '0 Ergebnisse';
return;
}
// Hole alle Suchbegriffe
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
};
resultCount.textContent = `${results.length} Ergebnisse`;
results.forEach(customer => {
const card = document.createElement('div');
card.className = 'customer-card';
card.innerHTML = `
<div class="customer-info">
<h5 class="mb-1">${highlightText(customer.name, searchTerms.general || searchTerms.name)}</h5>
<p class="mb-1 customer-number">${createCustomerLink(customer.nummer)}</p>
<p class="mb-1">${createAddressLink(
customer.strasse,
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>` : ''}
const resultsList = results.map(customer => {
return `
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<h5 class="card-title">${customer.name}</h5>
<div class="d-flex align-items-center">
<span class="result-tag ${customer.tag === 'medisoft' ? 'tag-medisoft' : 'tag-mediconsult'} me-2">${customer.tag}</span>
<button class="btn btn-sm btn-outline-primary" onclick="copyCustomerLink('${customer.nummer}')">
<i class="fas fa-share-alt"></i> Teilen
</button>
</div>
</div>
<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 class="card-actions">
<button class="share-button" onclick="copyCustomerLink('${customer.nummer}')">
<i class="fas fa-share-alt"></i> Teilen
</button>
</div>
`;
resultsDiv.appendChild(card);
});
</div>
`}).join('');
resultsDiv.innerHTML = resultsList;
}
function searchCustomers() {