13 Commits

9 changed files with 178 additions and 94 deletions

6
.gitignore vendored
View File

@@ -45,4 +45,8 @@ coverage.xml
.docker/
# 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/),
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
### 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

View File

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

View File

@@ -51,7 +51,7 @@ Die Anwendung unterstützt CIDR-Notation für IP-Bereiche. Beispiele:
## Version
Aktuelle Version: 1.2.1
Aktuelle Version: 1.2.4
## Lizenz
@@ -103,4 +103,4 @@ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt
## 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"
# Pfad zur CSV-Datei
CSV_FILE = "data/customers.csv"
CSV_FILE = 'data/customers.csv'
# Pfad zur Datenbank
DB_FILE = 'data/customers.db'
# Lade Umgebungsvariablen
load_dotenv()
@@ -31,11 +34,11 @@ STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password')
ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',')
def init_db():
"""Initialisiert die SQLite-Datenbank und erstellt die notwendigen Tabellen."""
conn = sqlite3.connect('customers.db')
"""Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
# Erstelle die Kunden-Tabelle
# Erstelle die Tabelle mit Indizes
c.execute('''
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -47,16 +50,32 @@ def init_db():
telefon TEXT,
mobil 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.close()
logger.info('Datenbank initialisiert')
def import_csv():
"""Importiert die Daten aus der CSV-Datei in die SQLite-Datenbank."""
conn = sqlite3.connect('customers.db')
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
# Lösche bestehende Daten
@@ -64,7 +83,7 @@ def import_csv():
try:
# 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
df.columns = df.columns.str.strip('"')
@@ -80,8 +99,8 @@ def import_csv():
# Importiere die Daten
for _, row in df.iterrows():
c.execute('''
INSERT INTO customers (nummer, name, strasse, plz, ort, telefon, mobil, email, bemerkung)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO customers (nummer, name, strasse, plz, ort, telefon, mobil, email, bemerkung, fachrichtung)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
row['Nummer'],
row['name'],
@@ -91,7 +110,8 @@ def import_csv():
row['Tel'],
row['Handy'],
row['mail'],
f"Fachrichtung: {row['Fachrichtung']}"
f"Fachrichtung: {row['Fachrichtung']}",
row['Fachrichtung']
))
conn.commit()
@@ -103,48 +123,66 @@ def import_csv():
conn.close()
def search_customers(search_params):
"""Sucht Kunden in der Datenbank basierend auf den Suchparametern."""
conn = sqlite3.connect('customers.db')
"""Sucht nach Kunden basierend auf den Suchparametern."""
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
# Erstelle die WHERE-Bedingungen basierend auf den Suchparametern
conditions = []
params = []
if search_params.get('name'):
conditions.append('name LIKE ?')
params.append(f'%{search_params["name"]}%')
if search_params.get('ort'):
conditions.append('ort LIKE ?')
params.append(f'%{search_params["ort"]}%')
if search_params.get('nummer'):
conditions.append('nummer LIKE ?')
params.append(f'%{search_params["nummer"]}%')
if search_params.get('plz'):
conditions.append('plz LIKE ?')
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
c.execute(sql, params)
results = c.fetchall()
# Konvertiere die Ergebnisse in ein Dictionary
columns = ['id', 'nummer', 'name', 'strasse', 'plz', 'ort', 'telefon', 'mobil', 'email', 'bemerkung']
customers = []
for row in results:
customer = dict(zip(columns, row))
customers.append(customer)
conn.close()
return customers
try:
# Baue die SQL-Abfrage dynamisch auf
query = "SELECT * FROM customers WHERE 1=1"
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'):
query += " AND name LIKE ?"
params.append(f"%{search_params['name']}%")
if search_params.get('ort'):
query += " AND ort LIKE ?"
params.append(f"%{search_params['ort']}%")
if search_params.get('nummer'):
query += " AND nummer LIKE ?"
params.append(f"%{search_params['nummer']}%")
if search_params.get('plz'):
query += " AND plz LIKE ?"
params.append(f"%{search_params['plz']}%")
# Führe die Abfrage aus
c.execute(query, params)
results = c.fetchall()
# Formatiere die Ergebnisse
customers = []
for row in results:
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)
return customers
except Exception as e:
logger.error(f"Fehler bei der Kundensuche: {str(e)}")
raise
finally:
conn.close()
def clean_dataframe(df):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
@@ -233,7 +271,11 @@ def search():
'name': request.args.get('name', ''),
'ort': request.args.get('ort', ''),
'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

View File

@@ -4,9 +4,11 @@ services:
ports:
- "5001:5000"
volumes:
- .:/app
- ./data:/app/data
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
- FLASK_DEBUG=1
- FLASK_ENV=production
- 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

View File

@@ -109,21 +109,31 @@ body {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
}
.share-button {
padding: 5px 10px;
border-radius: 15px;
font-size: 0.9em;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
background-color: #0d6efd;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.2s;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.share-button:hover {
background-color: #0b5ed7;
transform: translateY(-1px);
}
.share-button i {
font-size: 1rem;
}
.search-fields {
@@ -204,4 +214,25 @@ body {
.footer-link:hover {
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>
</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 class="result-counts">
@@ -81,7 +86,7 @@
<footer class="footer">
<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>
</footer>
@@ -99,21 +104,12 @@
// Überprüfen, ob die Client-IP in einem der erlaubten Bereiche liegt
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
let cleanNumber = phone.replace(/\D/g, '');
console.log('Cleaned number:', cleanNumber);
// Füge eine führende 0 hinzu, wenn isAllowed true ist
if (isAllowed) {
console.log('Adding leading 0 to:', cleanNumber);
cleanNumber = '0' + cleanNumber;
console.log('Number after adding 0:', cleanNumber);
}
// Formatiere die Nummer
@@ -124,13 +120,8 @@
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
const link = `<a href="tel:${cleanNumber}" class="phone-link">${formattedNumber}</a>`;
console.log('Final link:', link);
return link;
return `<a href="tel:${cleanNumber}" class="phone-link">${formattedNumber}</a>`;
}
function createEmailLink(email) {
@@ -150,7 +141,6 @@
const searchQuery = encodeURIComponent(address);
const routeQuery = encodeURIComponent(address);
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>
<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}"
class="address-link" target="_blank" rel="noopener noreferrer">
@@ -187,19 +177,12 @@
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
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
const isAllowed = allowedIPRanges.some(range => {
const trimmedRange = range.trim();
console.log('Checking range:', trimmedRange);
return isIPInSubnet(clientIP, trimmedRange);
});
console.log('isAllowed in createCustomerLink:', isAllowed);
const adjustedNumber = adjustCustomerNumber(nummer);
if (isAllowed) {
return `<a href="medisw:openkkbefe/P${adjustedNumber}?NetGrp=4" class="customer-link">${nummer}</a>`;
@@ -226,7 +209,7 @@
await navigator.clipboard.writeText(url.toString());
showCopyFeedback();
} catch (err) {
console.error('Fehler beim Kopieren:', err);
// Fehlerbehandlung ohne console.log
}
}
@@ -278,6 +261,7 @@
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';
@@ -289,6 +273,7 @@
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())
@@ -298,7 +283,6 @@
document.getElementById('loading').style.display = 'none';
if (data.error) {
console.error('Fehler bei der Suche:', data.error);
return;
}
@@ -336,7 +320,6 @@
});
})
.catch(error => {
console.error('Fehler bei der Suche:', error);
document.getElementById('loading').style.display = 'none';
});
}
@@ -347,7 +330,8 @@
document.getElementById('nameInput'),
document.getElementById('ortInput'),
document.getElementById('nummerInput'),
document.getElementById('plzInput')
document.getElementById('plzInput'),
document.getElementById('fachrichtungInput')
];
const resetIcons = [
@@ -355,7 +339,8 @@
document.querySelector('.reset-icon[onclick="clearInput(\'nameInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'ortInput\')"]'),
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) => {

View File

@@ -52,8 +52,8 @@
</div>
<footer class="footer">
<div class="footer-content">
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>
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.3</div>
</div>
</footer>
</body>