13 Commits

9 changed files with 178 additions and 94 deletions

4
.gitignore vendored
View File

@@ -46,3 +46,7 @@ coverage.xml
# Daten # Daten
spezexpo.csv spezexpo.csv
# Database
*.db
data/customers.db

View File

@@ -5,9 +5,25 @@ 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/).
## [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 ## [1.2.2] - 2024-03-19
### Geändert ### Geändert
- Verbesserte Telefonnummern-Formatierung: Führende "0" wird immer hinzugefügt, wenn der Benutzer von einer erlaubten IP-Adresse zugreift - 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 ## [1.2.1] - 2024-03-18
### Geändert ### Geändert

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

@@ -51,7 +51,7 @@ Die Anwendung unterstützt CIDR-Notation für IP-Bereiche. Beispiele:
## Version ## Version
Aktuelle Version: 1.2.1 Aktuelle Version: 1.2.4
## 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)

108
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
query = "SELECT * FROM customers WHERE 1=1"
params = [] params = []
# Allgemeine Suche über alle Felder
if search_params.get('q'):
search_term = f"%{search_params['q']}%"
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)
# Spezifische Suche für einzelne Felder
if search_params.get('name'): if search_params.get('name'):
conditions.append('name LIKE ?') query += " AND name LIKE ?"
params.append(f'%{search_params["name"]}%') params.append(f"%{search_params['name']}%")
if search_params.get('ort'): if search_params.get('ort'):
conditions.append('ort LIKE ?') query += " AND ort LIKE ?"
params.append(f'%{search_params["ort"]}%') params.append(f"%{search_params['ort']}%")
if search_params.get('nummer'): if search_params.get('nummer'):
conditions.append('nummer LIKE ?') query += " AND nummer LIKE ?"
params.append(f'%{search_params["nummer"]}%') params.append(f"%{search_params['nummer']}%")
if search_params.get('plz'): if search_params.get('plz'):
conditions.append('plz LIKE ?') query += " AND plz LIKE ?"
params.append(f'%{search_params["plz"]}%') params.append(f"%{search_params['plz']}%")
# Erstelle die SQL-Abfrage
sql = 'SELECT * FROM customers'
if conditions:
sql += ' WHERE ' + ' AND '.join(conditions)
# Führe die Abfrage aus # Führe die Abfrage aus
c.execute(sql, params) c.execute(query, params)
results = c.fetchall() results = c.fetchall()
# Konvertiere die Ergebnisse in ein Dictionary # Formatiere die Ergebnisse
columns = ['id', 'nummer', 'name', 'strasse', 'plz', 'ort', 'telefon', 'mobil', 'email', 'bemerkung']
customers = [] customers = []
for row in results: for row in results:
customer = dict(zip(columns, row)) customer = {
'id': row[0],
'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) customers.append(customer)
conn.close()
return customers 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 {
@@ -205,3 +215,24 @@ 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

@@ -58,6 +58,11 @@
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
</div> </div>
</div> </div>
<div class="form-group">
<label for="fachrichtungInput">Fachrichtung</label>
<input type="text" class="form-control" id="fachrichtungInput" placeholder="Fachrichtung eingeben">
</div>
</div> </div>
<div class="result-counts"> <div class="result-counts">
@@ -81,7 +86,7 @@
<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.2</div> <div style="font-size: 0.8em;">Version: v1.2.2</div>
</div> </div>
</footer> </footer>
@@ -99,21 +104,12 @@
// Ü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 => isIPInSubnet(clientIP, range.trim())); const isAllowed = allowedIPRanges.some(range => isIPInSubnet(clientIP, range.trim()));
// Debug-Ausgabe für die IP-Bereiche
console.log('Client IP in createPhoneLink:', clientIP);
console.log('Allowed IP Ranges:', allowedIPRanges);
console.log('isAllowed in createPhoneLink:', isAllowed);
console.log('Original phone:', phone);
// Entferne alle nicht-numerischen Zeichen // Entferne alle nicht-numerischen Zeichen
let cleanNumber = phone.replace(/\D/g, ''); let cleanNumber = phone.replace(/\D/g, '');
console.log('Cleaned number:', cleanNumber);
// Füge eine führende 0 hinzu, wenn isAllowed true ist // Füge eine führende 0 hinzu, wenn isAllowed true ist
if (isAllowed) { if (isAllowed) {
console.log('Adding leading 0 to:', cleanNumber);
cleanNumber = '0' + cleanNumber; cleanNumber = '0' + cleanNumber;
console.log('Number after adding 0:', cleanNumber);
} }
// Formatiere die Nummer // Formatiere die Nummer
@@ -124,13 +120,8 @@
formattedNumber = cleanNumber.replace(/(\d{3})(\d{7})/, '$1-$2'); formattedNumber = cleanNumber.replace(/(\d{3})(\d{7})/, '$1-$2');
} }
console.log('Final formatted number:', formattedNumber);
console.log('Final clean number for tel link:', cleanNumber);
// Erstelle den Link // Erstelle den Link
const link = `<a href="tel:${cleanNumber}" class="phone-link">${formattedNumber}</a>`; return `<a href="tel:${cleanNumber}" class="phone-link">${formattedNumber}</a>`;
console.log('Final link:', link);
return link;
} }
function createEmailLink(email) { function createEmailLink(email) {
@@ -150,7 +141,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">
@@ -187,19 +177,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>`;
@@ -226,7 +209,7 @@
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
} }
} }
@@ -278,6 +261,7 @@
const ort = document.getElementById('ortInput').value; const ort = document.getElementById('ortInput').value;
const nummer = document.getElementById('nummerInput').value; const nummer = document.getElementById('nummerInput').value;
const plz = document.getElementById('plzInput').value; const plz = document.getElementById('plzInput').value;
const fachrichtung = document.getElementById('fachrichtungInput').value;
// Zeige das Lade-Icon // Zeige das Lade-Icon
document.getElementById('loading').style.display = 'block'; document.getElementById('loading').style.display = 'block';
@@ -289,6 +273,7 @@
if (ort) params.append('ort', ort); if (ort) params.append('ort', ort);
if (nummer) params.append('nummer', nummer); if (nummer) params.append('nummer', nummer);
if (plz) params.append('plz', plz); if (plz) params.append('plz', plz);
if (fachrichtung) params.append('fachrichtung', fachrichtung);
// Führe die Suche durch // Führe die Suche durch
fetch('/search?' + params.toString()) fetch('/search?' + params.toString())
@@ -298,7 +283,6 @@
document.getElementById('loading').style.display = 'none'; document.getElementById('loading').style.display = 'none';
if (data.error) { if (data.error) {
console.error('Fehler bei der Suche:', data.error);
return; return;
} }
@@ -336,7 +320,6 @@
}); });
}) })
.catch(error => { .catch(error => {
console.error('Fehler bei der Suche:', error);
document.getElementById('loading').style.display = 'none'; document.getElementById('loading').style.display = 'none';
}); });
} }
@@ -347,7 +330,8 @@
document.getElementById('nameInput'), document.getElementById('nameInput'),
document.getElementById('ortInput'), document.getElementById('ortInput'),
document.getElementById('nummerInput'), document.getElementById('nummerInput'),
document.getElementById('plzInput') document.getElementById('plzInput'),
document.getElementById('fachrichtungInput')
]; ];
const resetIcons = [ const resetIcons = [
@@ -355,7 +339,8 @@
document.querySelector('.reset-icon[onclick="clearInput(\'nameInput\')"]'), document.querySelector('.reset-icon[onclick="clearInput(\'nameInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'ortInput\')"]'), document.querySelector('.reset-icon[onclick="clearInput(\'ortInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'nummerInput\')"]'), document.querySelector('.reset-icon[onclick="clearInput(\'nummerInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'plzInput\')"]') document.querySelector('.reset-icon[onclick="clearInput(\'plzInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'fachrichtungInput\')"]')
]; ];
searchInputs.forEach((input, index) => { searchInputs.forEach((input, index) => {

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>