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.csv
docker-compose.yml
/data/*.csv

294
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,12 +30,19 @@ 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
try:
# Erstelle die Kunden-Tabelle
c.execute('''
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -51,7 +55,8 @@ def init_db():
mobil TEXT,
email 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_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)')
# 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')
except Exception as e:
logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}')
raise
finally:
conn.close()
def import_csv():
"""Importiert die Daten aus der CSV-Datei in die SQLite-Datenbank."""
conn = sqlite3.connect(DB_FILE)
"""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')
try:
# Lese die CSV-Datei mit pandas
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('"')
# Kombiniere Vorname und Nachname
# 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']
# Importiere die Daten
for _, row in df.iterrows():
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
row['Nummer'],
row['name'],
row['Strasse'],
row['PLZ'],
row['Ort'],
row['Tel'],
row['Handy'],
row['mail'],
f"Fachrichtung: {row['Fachrichtung']}",
row['Fachrichtung']
))
''', (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')
logger.info("CSV-Daten erfolgreich in die Datenbank importiert")
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
finally:
if conn:
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 []
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 = sqlite3.connect(DB_FILE)
conn = get_db_connection()
c = conn.cursor()
try:
# Baue die SQL-Abfrage dynamisch auf
query = "SELECT * FROM customers WHERE 1=1"
# 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 = []
# Allgemeine Suche über alle Felder
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()
# Suchbedingungen
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)
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:
# 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)
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) + ')')
# Spezifische Suche für einzelne Felder
if search_params.get('name'):
query += " AND name LIKE ?"
params.append(f"%{search_params['name']}%")
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 search_params.get('ort'):
query += " AND ort LIKE ?"
params.append(f"%{search_params['ort']}%")
if conditions:
query += ' AND ' + ' AND '.join(conditions)
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)
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)
formatted_results.append(customer)
return customers
except Exception as e:
logger.error(f"Fehler bei der Kundensuche: {str(e)}")
raise
finally:
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 durch und hole die Ergebnisse
results = search_customers()
# Führe die Suche in der Datenbank durch
results = search_customers(search_params)
# 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():
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

@@ -254,3 +254,20 @@ body {
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>` : ''}
</div>
<div class="card-actions">
<button class="share-button" onclick="copyCustomerLink('${customer.nummer}')">
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>
`;
resultsDiv.appendChild(card);
});
</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>
`}).join('');
resultsDiv.innerHTML = resultsList;
}
function searchCustomers() {