17 Commits

Author SHA1 Message Date
0627b6ff33 Version 1.2.6: Verbesserte Suchfunktion und Highlighting 2025-03-18 15:32:20 +01:00
24ba040537 Version 1.2.5: Dokumentation aktualisiert 2025-03-18 15:12:50 +01:00
997786be54 Feature: Suchfeld für Fachrichtung hinzugefügt 2025-03-18 15:11:15 +01:00
c4974787d4 Dokumentation: Korrigiere Versionsangabe am Ende der README.md 2025-03-18 14:08:04 +01:00
a42bdaa721 Dokumentation: Aktualisiere Versionsnummer in README.md auf v1.2.4 2025-03-18 14:06:31 +01:00
49938a1085 Version 1.2.4: Performance-Optimierung durch Datenbankindizes 2025-03-18 14:02:07 +01:00
d0a27fe095 Performance: Indizes für alle Suchfelder hinzugefügt 2025-03-18 14:01:08 +01:00
d388bce528 Footer Text geändert 2025-03-18 13:59:57 +01:00
aabb4540c9 Version 1.2.3: Performance-Optimierung durch Entfernung von Debug-Ausgaben 2025-03-18 13:57:32 +01:00
ffde078238 Git: Füge data/customers.db zur .gitignore hinzu 2025-03-18 13:55:17 +01:00
9e320c4eb2 Performance: Entferne alle console.log Anweisungen 2025-03-18 13:53:14 +01:00
58ed5fe867 Performance: Entferne Debug-Logging aus createPhoneLink Funktion 2025-03-18 13:50:32 +01:00
72676edc10 Git: Füge data/customers.db zur .gitignore hinzu 2025-03-18 13:48:55 +01:00
d5954eac89 Version 1.2.2: Verbesserte Telefonnummern-Formatierung und Dokumentation 2025-03-18 13:47:41 +01:00
68a2db28a1 Dokumentation: Aktualisierte README.md und CHANGELOG.md für Version 1.2.2 2025-03-18 13:46:33 +01:00
13709de515 Version 1.2.2: Verbesserte Telefonnummern-Formatierung 2025-03-18 13:43:07 +01:00
2c65d5f651 Dokumentation: Aktualisierung für Version 1.2.1 2025-03-18 12:56:15 +01:00
9 changed files with 351 additions and 284 deletions

6
.gitignore vendored
View File

@@ -45,4 +45,8 @@ coverage.xml
.docker/ .docker/
# Daten # Daten
spezexpo.csv spezexpo.csv
# Database
*.db
data/customers.db

View File

@@ -5,7 +5,51 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/), Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
und dieses Projekt adhäriert zu [Semantic Versioning](https://semver.org/lang/de/). und dieses Projekt adhäriert zu [Semantic Versioning](https://semver.org/lang/de/).
## [v1.2.0] - 2024-03-18 ## [1.2.6] - 2024-03-19
### Geändert
- Verbesserte Suchfunktion: Keine Ergebnisse mehr bei leeren Suchfeldern
- Optimiertes Highlighting der Suchergebnisse für alle Suchfelder
- Fachrichtung wird jetzt in den Suchergebnissen hervorgehoben
## [1.2.5] - 2024-03-19
### Hinzugefügt
- Neues Suchfeld für Fachrichtung
- Index für das Fachrichtung-Feld in der Datenbank
- Fachrichtung in der allgemeinen Suche integriert
## [1.2.4] - 2024-03-19
### Geändert
- Performance-Optimierung: Indizes für alle Suchfelder hinzugefügt
- Verbesserte Suchgeschwindigkeit durch optimierte Datenbankindizes
- Zusammengesetzter Index für die häufigste Suchkombination (Name + Ort) hinzugefügt
## [1.2.3] - 2024-03-19
### Geändert
- Performance-Optimierung: Entfernung aller console.log Anweisungen
- Verbesserte Code-Qualität durch Entfernung von Debug-Ausgaben
## [1.2.2] - 2024-03-19
### Geändert
- Verbesserte Telefonnummern-Formatierung: Führende "0" wird immer hinzugefügt, wenn der Benutzer von einer erlaubten IP-Adresse zugreift
- Debug-Logging für Telefonnummern-Formatierung hinzugefügt
- Verbesserte Benutzerfreundlichkeit bei der Anzeige von Telefonnummern
### Behoben
- Problem mit fehlender führender "0" bei Telefonnummern für autorisierte Benutzer
## [1.2.1] - 2024-03-18
### Geändert
- Verbesserte CSV-Import-Funktionalität mit pandas
- Korrektur des Login-Prozesses
- Verbesserte Fehlerbehandlung und Logging
- Anpassung der Spaltennamen für den CSV-Import
### Behoben
- Login-Prozess funktioniert jetzt korrekt mit dem Passwort aus der .env Datei
- CSV-Import verarbeitet Anführungszeichen korrekt
- Verbesserte Fehlerbehandlung beim Datenbankimport
## [1.2.0] - 2024-03-18
### Geändert ### Geändert
- IP-Subnetz-Prüfung korrigiert (CIDR-Notation wird jetzt korrekt ausgewertet) - IP-Subnetz-Prüfung korrigiert (CIDR-Notation wird jetzt korrekt ausgewertet)

View File

@@ -10,8 +10,12 @@ COPY requirements.txt .
# Installiere Abhängigkeiten # Installiere Abhängigkeiten
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Kopiere den Anwendungscode # Erstelle das data Verzeichnis und setze Berechtigungen
COPY . . RUN mkdir -p /app/data && \
chmod 755 /app/data
# Kopiere die Anwendungsdateien
COPY . /app/
# Exponiere Port 5000 # Exponiere Port 5000
EXPOSE 5000 EXPOSE 5000

View File

@@ -1,6 +1,6 @@
# medisoftware Kundensuche # Medi-Customers
Eine einfache Webanwendung zur Suche nach medisoftware Kunden mit IP-basierter Zugriffssteuerung. Eine Flask-basierte Webanwendung zur Verwaltung von Kundenkontakten für medizinische Einrichtungen.
## Features ## Features
@@ -51,7 +51,7 @@ Die Anwendung unterstützt CIDR-Notation für IP-Bereiche. Beispiele:
## Version ## Version
Aktuelle Version: v1.2.0 Aktuelle Version: v1.2.6
## Lizenz ## Lizenz
@@ -103,4 +103,4 @@ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt
## Version ## Version
Aktuelle Version: [v1.2.0](CHANGELOG.md#v120---2024-03-17) Aktuelle Version: [v1.2.4](CHANGELOG.md#v124---2024-03-19)

144
app.py
View File

@@ -21,7 +21,10 @@ logger = logging.getLogger(__name__)
VERSION = "1.2.1" VERSION = "1.2.1"
# Pfad zur CSV-Datei # Pfad zur CSV-Datei
CSV_FILE = "data/customers.csv" CSV_FILE = 'data/customers.csv'
# Pfad zur Datenbank
DB_FILE = 'data/customers.db'
# Lade Umgebungsvariablen # Lade Umgebungsvariablen
load_dotenv() load_dotenv()
@@ -31,11 +34,11 @@ 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 init_db(): def init_db():
"""Initialisiert die SQLite-Datenbank und erstellt die notwendigen Tabellen.""" """Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
conn = sqlite3.connect('customers.db') conn = sqlite3.connect(DB_FILE)
c = conn.cursor() c = conn.cursor()
# Erstelle die Kunden-Tabelle # Erstelle die Tabelle mit Indizes
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,
@@ -47,16 +50,32 @@ def init_db():
telefon TEXT, telefon TEXT,
mobil TEXT, mobil TEXT,
email TEXT, email TEXT,
bemerkung 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.commit()
conn.close() conn.close()
logger.info('Datenbank initialisiert')
def import_csv(): def import_csv():
"""Importiert die Daten aus der CSV-Datei in die SQLite-Datenbank.""" """Importiert die Daten aus der CSV-Datei in die SQLite-Datenbank."""
conn = sqlite3.connect('customers.db') conn = sqlite3.connect(DB_FILE)
c = conn.cursor() c = conn.cursor()
# Lösche bestehende Daten # Lösche bestehende Daten
@@ -64,7 +83,7 @@ def import_csv():
try: try:
# Lese die CSV-Datei mit pandas # Lese die CSV-Datei mit pandas
df = pd.read_csv('data/customers.csv', sep=',', encoding='utf-8', quotechar='"') df = pd.read_csv(CSV_FILE, sep=',', encoding='utf-8', quotechar='"')
# Entferne Anführungszeichen aus den Spaltennamen # Entferne Anführungszeichen aus den Spaltennamen
df.columns = df.columns.str.strip('"') df.columns = df.columns.str.strip('"')
@@ -80,8 +99,8 @@ def import_csv():
# Importiere die Daten # 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) INSERT INTO customers (nummer, name, strasse, plz, ort, telefon, mobil, email, bemerkung, fachrichtung)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
row['Nummer'], row['Nummer'],
row['name'], row['name'],
@@ -91,7 +110,8 @@ def import_csv():
row['Tel'], row['Tel'],
row['Handy'], row['Handy'],
row['mail'], row['mail'],
f"Fachrichtung: {row['Fachrichtung']}" f"Fachrichtung: {row['Fachrichtung']}",
row['Fachrichtung']
)) ))
conn.commit() conn.commit()
@@ -103,48 +123,66 @@ def import_csv():
conn.close() conn.close()
def search_customers(search_params): def search_customers(search_params):
"""Sucht Kunden in der Datenbank basierend auf den Suchparametern.""" """Sucht nach Kunden basierend auf den Suchparametern."""
conn = sqlite3.connect('customers.db') conn = sqlite3.connect(DB_FILE)
c = conn.cursor() c = conn.cursor()
# Erstelle die WHERE-Bedingungen basierend auf den Suchparametern try:
conditions = [] # Baue die SQL-Abfrage dynamisch auf
params = [] query = "SELECT * FROM customers WHERE 1=1"
params = []
if search_params.get('name'):
conditions.append('name LIKE ?') # Allgemeine Suche über alle Felder
params.append(f'%{search_params["name"]}%') if search_params.get('q'):
search_term = f"%{search_params['q']}%"
if search_params.get('ort'): 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 ?)"
conditions.append('ort LIKE ?') params.extend([search_term] * 8)
params.append(f'%{search_params["ort"]}%')
# Spezifische Suche für einzelne Felder
if search_params.get('nummer'): if search_params.get('name'):
conditions.append('nummer LIKE ?') query += " AND name LIKE ?"
params.append(f'%{search_params["nummer"]}%') params.append(f"%{search_params['name']}%")
if search_params.get('plz'): if search_params.get('ort'):
conditions.append('plz LIKE ?') query += " AND ort LIKE ?"
params.append(f'%{search_params["plz"]}%') params.append(f"%{search_params['ort']}%")
# Erstelle die SQL-Abfrage if search_params.get('nummer'):
sql = 'SELECT * FROM customers' query += " AND nummer LIKE ?"
if conditions: params.append(f"%{search_params['nummer']}%")
sql += ' WHERE ' + ' AND '.join(conditions)
if search_params.get('plz'):
# Führe die Abfrage aus query += " AND plz LIKE ?"
c.execute(sql, params) params.append(f"%{search_params['plz']}%")
results = c.fetchall()
# Führe die Abfrage aus
# Konvertiere die Ergebnisse in ein Dictionary c.execute(query, params)
columns = ['id', 'nummer', 'name', 'strasse', 'plz', 'ort', 'telefon', 'mobil', 'email', 'bemerkung'] results = c.fetchall()
customers = []
for row in results: # Formatiere die Ergebnisse
customer = dict(zip(columns, row)) customers = []
customers.append(customer) for row in results:
customer = {
conn.close() 'id': row[0],
return customers 'nummer': row[1],
'name': 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]
}
customers.append(customer)
return customers
except Exception as e:
logger.error(f"Fehler bei der Kundensuche: {str(e)}")
raise
finally:
conn.close()
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"""
@@ -233,7 +271,11 @@ def search():
'name': request.args.get('name', ''), 'name': request.args.get('name', ''),
'ort': request.args.get('ort', ''), 'ort': request.args.get('ort', ''),
'nummer': request.args.get('nummer', ''), 'nummer': request.args.get('nummer', ''),
'plz': request.args.get('plz', '') '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', '')
} }
# Führe die Suche in der Datenbank durch # Führe die Suche in der Datenbank durch

View File

@@ -4,9 +4,11 @@ services:
ports: ports:
- "5001:5000" - "5001:5000"
volumes: volumes:
- .:/app - ./data:/app/data
environment: environment:
- FLASK_APP=app.py - FLASK_APP=app.py
- FLASK_ENV=development - FLASK_ENV=production
- FLASK_DEBUG=1 - SECRET_KEY=your-super-secret-key-here
- LOGIN_PASSWORD=changeme
- ALLOWED_IP_RANGES=213.178.68.218/29,192.168.0.0/24,192.168.177.0/24
command: flask run --host=0.0.0.0 command: flask run --host=0.0.0.0

View File

@@ -109,21 +109,31 @@ body {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 1rem; margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
} }
.share-button { .share-button {
padding: 5px 10px; padding: 0.5rem 1rem;
border-radius: 15px; border-radius: 20px;
font-size: 0.9em; font-size: 0.9rem;
background-color: #0d6efd; background-color: #0d6efd;
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.share-button:hover { .share-button:hover {
background-color: #0b5ed7; background-color: #0b5ed7;
transform: translateY(-1px);
}
.share-button i {
font-size: 1rem;
} }
.search-fields { .search-fields {
@@ -204,4 +214,25 @@ body {
.footer-link:hover { .footer-link:hover {
text-decoration: underline; text-decoration: underline;
}
.general-search {
max-width: 800px;
margin: 0 auto;
}
.general-search .input-group {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.general-search .form-control {
height: 3.5rem;
font-size: 1.2rem;
padding: 0.75rem 1rem;
}
.general-search .search-icon,
.general-search .reset-icon {
font-size: 1.2rem;
padding: 0 1rem;
} }

View File

@@ -16,45 +16,60 @@
<img src="{{ url_for('static', filename='medisoftware_logo_rb_200.png') }}" alt="medisoftware Logo" class="img-fluid" style="max-width: 200px;"> <img src="{{ url_for('static', filename='medisoftware_logo_rb_200.png') }}" alt="medisoftware Logo" class="img-fluid" style="max-width: 200px;">
</div> </div>
<div class="search-container"> <div class="search-container">
<h1 class="text-center mb-4">medisoftware Kundensuche</h1> <h1 class="text-center mb-4">Kundensuche</h1>
<div class="input-group mb-4 position-relative"> <div class="general-search mb-4">
<input type="text" id="searchInput" class="form-control form-control-lg" <div class="input-group">
placeholder="Allgemeine Suche..."> <input type="text" id="q" class="form-control form-control-lg" placeholder="Allgemeine Suche" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="searchReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('q')"></i>
<span class="search-icon">🔍</span> <i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-fields"> <div class="search-fields">
<div class="search-field"> <div class="search-field">
<input type="text" id="nameInput" class="form-control" <div class="input-group">
placeholder="Name..."> <input type="text" id="nameInput" class="form-control" placeholder="Name" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="nameReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('nameInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="ortInput" class="form-control" <div class="input-group">
placeholder="Ort..."> <input type="text" id="ortInput" class="form-control" placeholder="Ort" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="ortReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('ortInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="kundennummerInput" class="form-control" <div class="input-group">
placeholder="Kundennummer..."> <input type="text" id="nummerInput" class="form-control" placeholder="Kundennummer" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="kundennummerReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('nummerInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="fachrichtungInput" class="form-control" <div class="input-group">
placeholder="Fachrichtung..."> <input type="text" id="plzInput" class="form-control" placeholder="PLZ" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="fachrichtungReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('plzInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="telefonInput" class="form-control" <div class="input-group">
placeholder="Telefon..."> <input type="text" id="fachrichtungInput" class="form-control" placeholder="Fachrichtung" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="telefonReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('fachrichtungInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
</div> </div>
<div class="result-counts"> <div class="result-counts">
<span id="generalCount" class="result-count"></span> <span id="resultCount" class="result-count"></span>
</div> </div>
<div id="loading" class="loading"> <div id="loading" class="loading">
@@ -63,21 +78,19 @@
</div> </div>
</div> </div>
<div id="results" class="mt-4"> <div id="results"></div>
<!-- Hier werden die Suchergebnisse angezeigt -->
</div>
</div> </div>
</div> </div>
</div> </div>
<div id="shareFeedback" class="share-feedback"> <div id="shareFeedback" class="share-feedback">
Link kopiert! Link in die Zwischenablage kopiert!
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="footer-content"> <div class="footer-content">
Made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a> Proudly made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a>
<div style="font-size: 0.8em;">Version: v1.2.0</div> <div style="font-size: 0.8em;">Version: v1.2.6</div>
</div> </div>
</footer> </footer>
@@ -87,15 +100,31 @@
function createPhoneLink(phone) { function createPhoneLink(phone) {
if (!phone) return 'N/A'; if (!phone) return 'N/A';
const cleaned = phone.replace(/[^\d+\s]/g, '');
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}'; const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(','); const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
// Überprüfen, ob die Client-IP in einem der erlaubten Bereiche liegt // Überprüfen, ob die Client-IP in einem der erlaubten Bereiche liegt
const isAllowed = allowedIPRanges.some(range => clientIP.startsWith(range.trim())); const isAllowed = allowedIPRanges.some(range => isIPInSubnet(clientIP, range.trim()));
const telLink = cleaned.startsWith('+') ? cleaned : (isAllowed ? '0' + cleaned.replace(/\s/g, '') : cleaned.replace(/\s/g, '')); // Entferne alle nicht-numerischen Zeichen
return `<a href="tel:${telLink}" class="phone-link">${phone}</a>`; 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) {
formattedNumber = cleanNumber.replace(/(\d{4})(\d{7})/, '$1-$2');
} else if (cleanNumber.length === 10) {
formattedNumber = cleanNumber.replace(/(\d{3})(\d{7})/, '$1-$2');
}
// Erstelle den Link
return `<a href="tel:${cleanNumber}" class="phone-link">${formattedNumber}</a>`;
} }
function createEmailLink(email) { function createEmailLink(email) {
@@ -103,48 +132,10 @@
return `<a href="mailto:${email}" class="email-link">${email}</a>`; return `<a href="mailto:${email}" class="email-link">${email}</a>`;
} }
function highlightText(text, searchTerms) { function highlightText(text, searchTerm) {
// Konvertiere text zu String und prüfe auf null/undefined if (!searchTerm) return text;
const textStr = String(text || ''); const regex = new RegExp(`(${searchTerm})`, 'gi');
if (!textStr || !searchTerms || searchTerms.length === 0) return textStr; return text.replace(regex, '<mark>$1</mark>');
// Escapen der Suchbegriffe für reguläre Ausdrücke
const escapedTerms = searchTerms.map(term =>
String(term || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
).filter(term => term.length > 0);
if (escapedTerms.length === 0) return textStr;
// Erstelle einen temporären div-Element
const tempDiv = document.createElement('div');
tempDiv.innerHTML = textStr;
// Funktion zum Hervorheben von Text
function highlightNode(node) {
if (node.nodeType === 3) { // Text node
const text = node.textContent;
let newText = text;
escapedTerms.forEach(term => {
const regex = new RegExp(`(${term})`, 'gi');
newText = newText.replace(regex, '<mark>$1</mark>');
});
if (newText !== text) {
const span = document.createElement('span');
span.innerHTML = newText;
node.parentNode.replaceChild(span, node);
}
} else if (node.nodeType === 1) { // Element node
// Überspringe mark-Tags und Links
if (node.tagName !== 'MARK' && node.tagName !== 'A') {
Array.from(node.childNodes).forEach(highlightNode);
}
}
}
highlightNode(tempDiv);
return tempDiv.innerHTML;
} }
function createAddressLink(street, plz, city) { function createAddressLink(street, plz, city) {
@@ -153,7 +144,6 @@
const searchQuery = encodeURIComponent(address); const searchQuery = encodeURIComponent(address);
const routeQuery = encodeURIComponent(address); const routeQuery = encodeURIComponent(address);
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}'; const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
console.log('Client IP in createAddressLink:', clientIP);
return `<span class="address-text">${address}</span> return `<span class="address-text">${address}</span>
<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}" <a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}"
class="address-link" target="_blank" rel="noopener noreferrer"> class="address-link" target="_blank" rel="noopener noreferrer">
@@ -190,19 +180,12 @@
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}'; const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(','); const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
// Debug-Ausgabe für die IP-Bereiche
console.log('Client IP in createCustomerLink:', clientIP);
console.log('Allowed IP Ranges:', allowedIPRanges);
// Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt // Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt
const isAllowed = allowedIPRanges.some(range => { const isAllowed = allowedIPRanges.some(range => {
const trimmedRange = range.trim(); const trimmedRange = range.trim();
console.log('Checking range:', trimmedRange);
return isIPInSubnet(clientIP, trimmedRange); return isIPInSubnet(clientIP, trimmedRange);
}); });
console.log('isAllowed in createCustomerLink:', isAllowed);
const adjustedNumber = adjustCustomerNumber(nummer); const adjustedNumber = adjustCustomerNumber(nummer);
if (isAllowed) { if (isAllowed) {
return `<a href="medisw:openkkbefe/P${adjustedNumber}?NetGrp=4" class="customer-link">${nummer}</a>`; return `<a href="medisw:openkkbefe/P${adjustedNumber}?NetGrp=4" class="customer-link">${nummer}</a>`;
@@ -229,161 +212,122 @@
await navigator.clipboard.writeText(url.toString()); await navigator.clipboard.writeText(url.toString());
showCopyFeedback(); showCopyFeedback();
} catch (err) { } catch (err) {
console.error('Fehler beim Kopieren:', err); // Fehlerbehandlung ohne console.log
} }
} }
function updateResultCounts() { function updateResultCounts() {
// Nur Gesamtzahl anzeigen // Nur Gesamtzahl anzeigen
const generalCount = lastResults.length; const generalCount = lastResults.length;
document.getElementById('generalCount').textContent = document.getElementById('resultCount').textContent =
generalCount > 0 ? `${generalCount} Treffer gefunden` : ''; generalCount > 0 ? `${generalCount} Treffer gefunden` : '';
document.getElementById('generalCount').classList.toggle('visible', generalCount > 0); document.getElementById('resultCount').classList.toggle('visible', generalCount > 0);
} }
function searchCustomers() { function displayResults(results) {
const query = document.getElementById('searchInput').value.trim(); const resultsDiv = document.getElementById('results');
const fachrichtung = document.getElementById('fachrichtungInput').value.trim(); resultsDiv.innerHTML = '';
const ort = document.getElementById('ortInput').value.trim();
const name = document.getElementById('nameInput').value.trim();
const telefon = document.getElementById('telefonInput').value.trim();
const kundennummer = document.getElementById('kundennummerInput')?.value.trim() || '';
// Sammle alle nicht-leeren Suchbegriffe if (results.length === 0) {
const searchTerms = [query, fachrichtung, ort, name, telefon, kundennummer] resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
.filter(term => term && term.length > 0);
// Prüfe, ob alle Suchfelder leer sind
if (searchTerms.length === 0) {
const resultsDiv = document.getElementById('results');
const generalCount = document.getElementById('generalCount');
resultsDiv.innerHTML = '';
generalCount.textContent = '';
generalCount.classList.remove('visible');
return; return;
} }
const resultsDiv = document.getElementById('results'); // Hole alle Suchbegriffe
const loadingDiv = document.getElementById('loading'); const searchTerms = {
loadingDiv.style.display = 'block'; general: document.getElementById('q').value,
resultsDiv.innerHTML = ''; 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
};
const searchParams = new URLSearchParams(); results.forEach(customer => {
if (query) searchParams.append('q', query); const card = document.createElement('div');
if (fachrichtung) searchParams.append('fachrichtung', fachrichtung); card.className = 'customer-card';
if (ort) searchParams.append('ort', ort); card.innerHTML = `
if (name) searchParams.append('name', name); <div class="customer-info">
if (telefon) searchParams.append('telefon', telefon); <h5 class="mb-1">${highlightText(customer.name, searchTerms.general || searchTerms.name)}</h5>
if (kundennummer) searchParams.append('kundennummer', kundennummer); <p class="mb-1 customer-number">${createCustomerLink(customer.nummer)}</p>
<p class="mb-1">${createAddressLink(
fetch(`/search?${searchParams.toString()}`) 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}')">
<i class="fas fa-share-alt"></i> Teilen
</button>
</div>
`;
resultsDiv.appendChild(card);
});
}
function searchCustomers() {
const q = document.getElementById('q').value;
const name = document.getElementById('nameInput').value;
const ort = document.getElementById('ortInput').value;
const nummer = document.getElementById('nummerInput').value;
const plz = document.getElementById('plzInput').value;
const fachrichtung = document.getElementById('fachrichtungInput').value;
// Zeige das Lade-Icon
document.getElementById('loading').style.display = 'block';
// Baue die Suchanfrage
const params = new URLSearchParams();
if (q) params.append('q', q);
if (name) params.append('name', name);
if (ort) params.append('ort', ort);
if (nummer) params.append('nummer', nummer);
if (plz) params.append('plz', plz);
if (fachrichtung) params.append('fachrichtung', fachrichtung);
// Führe die Suche durch
fetch('/search?' + params.toString())
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
resultsDiv.innerHTML = ''; // Verstecke das Lade-Icon
document.getElementById('loading').style.display = 'none';
// Prüfe, ob data ein Objekt mit results-Array ist if (data.error) {
if (!data || !data.results || !Array.isArray(data.results)) {
console.error('Unerwartetes Datenformat:', data);
resultsDiv.innerHTML = '<div class="error">Unerwartetes Datenformat vom Server</div>';
return; return;
} }
const results = data.results; lastResults = data;
if (results.length === 0) { updateResultCounts();
resultsDiv.innerHTML = '<div class="no-results">Keine Ergebnisse gefunden</div>'; displayResults(data);
} else {
results.forEach(customer => {
const card = document.createElement('div');
card.className = 'customer-card';
// Debug-Ausgabe für die Kundendaten
console.log('Kundendaten:', customer);
console.log('Alle verfügbaren Felder:', Object.keys(customer));
console.log('Telefon-bezogene Felder:', {
Telefon: customer.Telefon,
Telefonnummer: customer.Telefonnummer,
telefon: customer.telefon,
telefonnummer: customer.telefonnummer,
phone: customer.phone,
'phone.number': customer.phone?.number
});
// Erstelle die Adresse mit Hervorhebung
const address = `${customer.Strasse || ''}, ${customer.PLZ || ''} ${customer.Ort || ''}`;
const addressLink = createAddressLink(customer.Strasse, customer.PLZ, customer.Ort);
const highlightedAddress = highlightText(addressLink, searchTerms);
// Erstelle die Kundennummer mit Hervorhebung
const highlightedNumber = highlightText(customer.Nummer, searchTerms);
const customerLink = createCustomerLink(customer.Nummer);
// Erstelle die Telefonnummern mit Hervorhebung
let phoneNumber = '';
let companyPhone = '';
let mobilePhone = '';
if (typeof customer === 'object') {
phoneNumber = customer.Tel || '';
companyPhone = customer['Tele Firma'] || '';
mobilePhone = customer.Handy || '';
}
const phoneLink = createPhoneLink(phoneNumber);
const companyPhoneLink = createPhoneLink(companyPhone);
const mobilePhoneLink = createPhoneLink(mobilePhone);
const highlightedPhone = highlightText(phoneLink, searchTerms);
const highlightedCompanyPhone = highlightText(companyPhoneLink, searchTerms);
const highlightedMobilePhone = highlightText(mobilePhoneLink, searchTerms);
card.innerHTML = `
<div class="customer-info">
<strong>Kundennummer:</strong> ${customerLink}<br>
<strong>Name:</strong> ${highlightText(`${customer.Vorname || ''} ${customer.Nachname || ''}`, searchTerms)}<br>
<strong>Fachrichtung:</strong> ${highlightText(customer.Fachrichtung || '', searchTerms)}<br>
<strong>Adresse:</strong> ${highlightedAddress}<br>
<strong>Telefon:</strong> ${highlightedPhone}<br>
<strong>Firma:</strong> ${highlightedCompanyPhone}<br>
<strong>Mobil:</strong> ${highlightedMobilePhone}
</div>
<button class="share-button" onclick="copyCustomerLink('${adjustCustomerNumber(customer.Nummer)}')">
<i class="fas fa-share-alt"></i> Teilen
</button>
`;
resultsDiv.appendChild(card);
});
}
// Aktualisiere die Anzahl der Treffer
const generalCount = document.getElementById('generalCount');
generalCount.textContent = results.length > 0 ? `${results.length} Treffer gefunden` : '';
generalCount.classList.toggle('visible', results.length > 0);
}) })
.catch(error => { .catch(error => {
console.error('Fehler bei der Suche:', error); document.getElementById('loading').style.display = 'none';
resultsDiv.innerHTML = '<div class="error">Ein Fehler ist aufgetreten</div>';
})
.finally(() => {
loadingDiv.style.display = 'none';
}); });
} }
// Event-Listener für die Live-Suche // Event-Listener für die Live-Suche
const searchInputs = [ const searchInputs = [
document.getElementById('q'),
document.getElementById('nameInput'), document.getElementById('nameInput'),
document.getElementById('ortInput'), document.getElementById('ortInput'),
document.getElementById('kundennummerInput'), document.getElementById('nummerInput'),
document.getElementById('fachrichtungInput'), document.getElementById('plzInput'),
document.getElementById('telefonInput'), document.getElementById('fachrichtungInput')
document.getElementById('searchInput')
]; ];
const resetIcons = [ const resetIcons = [
document.getElementById('nameReset'), document.querySelector('.reset-icon[onclick="clearInput(\'q\')"]'),
document.getElementById('ortReset'), document.querySelector('.reset-icon[onclick="clearInput(\'nameInput\')"]'),
document.getElementById('kundennummerReset'), document.querySelector('.reset-icon[onclick="clearInput(\'ortInput\')"]'),
document.getElementById('fachrichtungReset'), document.querySelector('.reset-icon[onclick="clearInput(\'nummerInput\')"]'),
document.getElementById('telefonReset'), document.querySelector('.reset-icon[onclick="clearInput(\'plzInput\')"]'),
document.getElementById('searchReset') document.querySelector('.reset-icon[onclick="clearInput(\'fachrichtungInput\')"]')
]; ];
searchInputs.forEach((input, index) => { searchInputs.forEach((input, index) => {
@@ -408,18 +352,14 @@
const name = urlParams.get('name'); const name = urlParams.get('name');
const ort = urlParams.get('ort'); const ort = urlParams.get('ort');
const kundennummer = urlParams.get('kundennummer'); const kundennummer = urlParams.get('kundennummer');
const fachrichtung = urlParams.get('fachrichtung'); const plz = urlParams.get('plz');
const telefon = urlParams.get('telefon');
const query = urlParams.get('q');
if (name) document.getElementById('nameInput').value = name; if (name) document.getElementById('nameInput').value = name;
if (ort) document.getElementById('ortInput').value = ort; if (ort) document.getElementById('ortInput').value = ort;
if (kundennummer) document.getElementById('kundennummerInput').value = kundennummer; if (kundennummer) document.getElementById('nummerInput').value = kundennummer;
if (fachrichtung) document.getElementById('fachrichtungInput').value = fachrichtung; if (plz) document.getElementById('plzInput').value = plz;
if (telefon) document.getElementById('telefonInput').value = telefon;
if (query) document.getElementById('searchInput').value = query;
if (name || ort || kundennummer || fachrichtung || telefon || query) { if (name || ort || kundennummer || plz) {
searchCustomers(); searchCustomers();
} }
}); });

View File

@@ -52,8 +52,8 @@
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="footer-content"> <div class="footer-content">
Made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a> Proudly made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a>
<div style="font-size: 0.8em;">Version: v1.2.0</div> <div style="font-size: 0.8em;">Version: v1.2.3</div>
</div> </div>
</footer> </footer>
</body> </body>