4 Commits

5 changed files with 402 additions and 308 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

182
app.py
View File

@@ -5,59 +5,48 @@ 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.16'
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
load_dotenv()
# Statisches Passwort aus der .env Datei
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password')
def isIPInSubnet(ip, subnet):
"""Überprüft, ob eine IP-Adresse in einem Subnetz liegt."""
try:
# Teile die IP und das Subnetz in ihre Komponenten
subnet_ip, bits = subnet.split('/')
ip_parts = [int(x) for x in ip.split('.')]
subnet_parts = [int(x) for x in subnet_ip.split('.')]
# Konvertiere IPs in 32-bit Zahlen
ip_num = (ip_parts[0] << 24) | (ip_parts[1] << 16) | (ip_parts[2] << 8) | ip_parts[3]
subnet_num = (subnet_parts[0] << 24) | (subnet_parts[1] << 16) | (subnet_parts[2] << 8) | subnet_parts[3]
# Erstelle die Subnetzmaske
mask = ~((1 << (32 - int(bits))) - 1)
# Prüfe, ob die IP im Subnetz liegt
return (ip_num & mask) == (subnet_num & mask)
except Exception as e:
logger.error(f"Fehler bei der IP-Überprüfung: {str(e)}")
return False
def get_db_connection(): def get_db_connection():
"""Erstellt eine neue Datenbankverbindung mit Timeout""" """Erstellt eine neue Datenbankverbindung für den aktuellen Thread"""
conn = sqlite3.connect(DB_FILE, timeout=20) if not hasattr(thread_local, "connection"):
conn.row_factory = sqlite3.Row thread_local.connection = sqlite3.connect(app.config['DATABASE'], timeout=app.config['DATABASE_TIMEOUT'])
return conn thread_local.connection.row_factory = sqlite3.Row
return thread_local.connection
@contextmanager
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(): def init_db():
"""Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle.""" """Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
conn = get_db_connection() with get_db() as conn:
c = conn.cursor() c = conn.cursor()
try: try:
@@ -84,36 +73,45 @@ def init_db():
) )
''') ''')
# Erstelle Indizes für alle Suchfelder # Optimierte Indizes für die häufigsten Suchanfragen
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_nummer ON customers(nummer)') 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 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_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_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_plz ON customers(plz)')
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 # Zusammengesetzter 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_search ON customers(name, ort, fachrichtung, tag)')
conn.commit()
logger.info('Datenbank initialisiert') logger.info('Datenbank initialisiert')
except Exception as e: except Exception as e:
logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}') logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}')
raise raise
finally:
conn.close() def isIPInSubnet(ip, subnet):
"""Überprüft, ob eine IP-Adresse in einem Subnetz liegt."""
try:
# Teile die IP und das Subnetz in ihre Komponenten
subnet_ip, bits = subnet.split('/')
ip_parts = [int(x) for x in ip.split('.')]
subnet_parts = [int(x) for x in subnet_ip.split('.')]
# Konvertiere IPs in 32-bit Zahlen
ip_num = (ip_parts[0] << 24) | (ip_parts[1] << 16) | (ip_parts[2] << 8) | ip_parts[3]
subnet_num = (subnet_parts[0] << 24) | (subnet_parts[1] << 16) | (subnet_parts[2] << 8) | subnet_parts[3]
# Erstelle die Subnetzmaske
mask = ~((1 << (32 - int(bits))) - 1)
# Prüfe, ob die IP im Subnetz liegt
return (ip_num & mask) == (subnet_num & mask)
except Exception as e:
logger.error(f"Fehler bei der IP-Überprüfung: {str(e)}")
return False
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
@@ -126,18 +124,24 @@ def import_csv():
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']
# Bereite die Daten für den Batch-Insert vor
data = [(
row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'medisoft',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
) for _, row in df.iterrows()]
# Führe Batch-Insert durch
c.executemany('''
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)
row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'medisoft',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
))
else: else:
logger.warning("MEDISOFT CSV-Datei nicht gefunden") logger.warning("MEDISOFT CSV-Datei nicht gefunden")
@@ -148,33 +152,31 @@ def import_csv():
df_snk.columns = df_snk.columns.str.strip().str.replace('"', '') 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 = df_snk.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x)
for _, row in df_snk.iterrows(): # Filtere Datensätze mit Fachrichtung "intern"
c.execute(''' 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['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'mediconsult',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
) for _, row in df_snk.iterrows()]
# Führe Batch-Insert durch
c.executemany('''
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)
row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'mediconsult',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
))
else: else:
logger.warning("MEDICONSULT CSV-Datei nicht gefunden") 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: 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():
@@ -233,7 +235,7 @@ 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
@@ -314,9 +316,8 @@ def search():
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:
@@ -339,7 +340,6 @@ def search():
} }
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:
@@ -350,7 +350,7 @@ 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
@@ -365,8 +365,6 @@ def get_fachrichtungen():
''', (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)}")
@@ -376,7 +374,7 @@ 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
@@ -391,8 +389,6 @@ def get_orte():
''', (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)}")
@@ -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('')}
@@ -276,6 +340,7 @@ function clearInput(inputId) {
} }
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 +350,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>