17 Commits

Author SHA1 Message Date
6cfc2e0162 Version 1.0.6: Kombinierte Suche über mehrere Felder 2025-03-17 22:49:27 +01:00
bbcd04fd21 Dokumentation: Verlinkung der Version mit dem Changelog 2025-03-17 22:24:44 +01:00
89a5152ada Dokumentation: Aktualisierung der Version in der README 2025-03-17 22:23:34 +01:00
e6f43ca10a Dokumentation: Aktualisierung der README und des Changelogs für Version 1.0.5 2025-03-17 22:15:01 +01:00
da3ef358d2 Dokumentation: Aktualisierung der README und des Changelogs für Version 1.0.5 2025-03-17 22:10:53 +01:00
d77ee8ef04 Version 1.0.5: Verbesserte Suchfunktion - Leere Ergebnisliste bei leeren Suchfeldern 2025-03-17 22:05:22 +01:00
cbc95c0d29 Version 1.0.4: Verbesserte Adressanzeige mit Location-Icon 2025-03-17 22:01:27 +01:00
3639344a13 Version 1.0.3: Verbesserte Adressanzeige mit Location-Icon 2025-03-17 22:00:58 +01:00
84ba72ab72 Frontend-Logik an neue API-Antwortstruktur angepasst 2025-03-17 21:29:37 +01:00
2f4671cbc4 CSV-Datei-Verarbeitung korrigiert: Komma als Trennzeichen 2025-03-17 21:27:50 +01:00
df87868ab5 CSV-Datei-Verarbeitung verbessert: Anführungszeichen entfernt 2025-03-17 21:26:06 +01:00
d1c4f6a1d0 Spaltennamen an CSV-Datei angepasst 2025-03-17 21:24:38 +01:00
88d33b1a30 CSV-Dateipfad korrigiert 2025-03-17 21:22:41 +01:00
0e9a1156e2 requests Modul hinzugefügt 2025-03-17 21:20:32 +01:00
cdf0bc31d9 Wetter-API Integration hinzugefügt 2025-03-17 21:18:55 +01:00
f1c2e9227e CHANGELOG.md hinzugefügt 2025-03-17 21:01:21 +01:00
a5383ccce8 README.md aktualisiert: Neue Features und Version 1.0.1 dokumentiert 2025-03-17 21:00:28 +01:00
5 changed files with 204 additions and 283 deletions

41
CHANGELOG.md Normal file
View File

@@ -0,0 +1,41 @@
# Changelog
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.0.6] - 2024-03-17
### Geändert
- Verbesserte Suchfunktion: Kombinierte Suche über mehrere Felder möglich
- Dokumentation: Beispiele für kombinierte Suche hinzugefügt
## [1.0.5] - 2024-03-17
### Geändert
- Verbesserte Suchfunktion: Ergebnisliste wird gelöscht, wenn alle Suchfelder leer sind
## [1.0.4] - 2024-03-17
### Geändert
- Verbesserte Adressanzeige: Location-Icon neben der Adresse
- Entfernung des Google Maps Links aus dem Adresstext
## [1.0.3] - 2024-03-17
### Geändert
- Hervorhebung der Suchbegriffe in den Ergebnissen
- Verbesserte Benutzeroberfläche
## [1.0.2] - 2024-03-17
### Geändert
- Entfernung der Wetterinformationen
- Optimierung der Suchfunktion
## [1.0.1] - 2024-03-17
### Hinzugefügt
- Wetterinformationen für Kundensitz
- Caching für Wetterdaten
## [1.0.0] - 2024-03-17
### Hinzugefügt
- Erste Version der Kundensuche
- Grundlegende Suchfunktionen
- Responsive Design

274
README.md
View File

@@ -1,229 +1,99 @@
# medisoftware Kundensuche # medisoftware Kundensuche
Eine Flask-basierte Webanwendung zur Suche in Kundendaten aus einer CSV-Datei. Eine webbasierte Kundensuche für medisoftware mit erweiterten Suchfunktionen.
## Features ## Features
- Live-Suche während der Eingabe - Schnelle und präzise Kundensuche
- Spezifische Suchfelder für: - Mehrere Suchfelder für gezielte Suche:
- Kundennummer
- Name (Vor- und Nachname) - Name (Vor- und Nachname)
- Fachrichtung
- Ort - Ort
- Kundennummer
- Fachrichtung
- Telefon
- Allgemeine Suche über alle Felder - Allgemeine Suche über alle Felder
- Klickbare Links für: - Kombinierte Suche über mehrere Felder
- Telefonnummern (tel:) - Hervorhebung der Suchbegriffe in den Ergebnissen
- E-Mail-Adressen (mailto:) - Direkte Links zu:
- Adressen (Google Maps) - medisoftware Kundenkartei (Kundennummer)
- Kundennummern (KKBefe-System) - Google Maps (Adresse)
- Teilen-Funktion für einzelne Suchergebnisse - Telefon (Klick zum Anrufen)
- Responsive Design mit Bootstrap - E-Mail (Klick zum Mailen)
- Docker-Container-Unterstützung - Responsive Design für alle Geräte
- Automatische Aktualisierung der Ergebnisse
## Technische Details - Leere Ergebnisliste bei leeren Suchfeldern
### Technologie-Stack
- **Backend**: Python 3.11 mit Flask
- **Frontend**: HTML, CSS, JavaScript, Bootstrap 5
- **Datenverarbeitung**: pandas, numpy
- **Container**: Docker
### Projektstruktur
```
medi-customers/
├── app.py # Flask-Anwendung
├── templates/ # HTML-Templates
│ └── index.html # Hauptseite
├── spezexpo.csv # Kundendaten
├── requirements.txt # Python-Abhängigkeiten
├── Dockerfile # Docker-Konfiguration
├── docker-compose.yml # Docker Compose Konfiguration
└── .dockerignore # Docker-Ignore-Datei
```
### Datenformat
Die Anwendung erwartet eine CSV-Datei (`spezexpo.csv`) mit folgenden Spalten:
- Nummer (Kundennummer)
- Vorname
- Nachname
- Fachrichtung
- Strasse
- PLZ
- Ort
- Tel
- mail
## Installation ## Installation
### Lokale Entwicklung 1. Repository klonen:
```bash
1. Python 3.11 installieren git clone https://gitea.elpatron.me/elpatron/medi-customers.git
2. Virtuelle Umgebung erstellen und aktivieren: cd medi-customers
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
```
3. Abhängigkeiten installieren:
```bash
pip install -r requirements.txt
```
4. Anwendung starten:
```bash
python app.py
```
### Docker-Container
1. Docker installieren
2. Container mit Docker Compose starten:
```bash
docker-compose up --build
```
Die Anwendung ist dann unter `http://localhost:5001` erreichbar.
## API-Endpunkte
### GET /
- Rendert die Hauptseite
### GET /search
- Sucht nach Kunden basierend auf verschiedenen Parametern
- Parameter:
- `name`: Suche nach Vor- oder Nachname
- `ort`: Suche nach Ort
- `kundennummer`: Suche nach Kundennummer
- `fachrichtung`: Suche nach Fachrichtung
- `q`: Allgemeine Suche über alle Felder
- Returns: JSON-Array mit gefundenen Kunden
#### API-Beispiele
1. Suche nach Namen:
```bash
curl "http://localhost:5001/search?name=Schmidt"
```
2. Suche nach Ort:
```bash
curl "http://localhost:5001/search?ort=Berlin"
```
3. Suche nach Kundennummer:
```bash
curl "http://localhost:5001/search?kundennummer=12345"
```
4. Suche nach Fachrichtung:
```bash
curl "http://localhost:5001/search?fachrichtung=Allgemeinmedizin"
```
5. Kombinierte Suche:
```bash
curl "http://localhost:5001/search?name=Schmidt&ort=Berlin&fachrichtung=Allgemeinmedizin"
```
6. Allgemeine Suche:
```bash
curl "http://localhost:5001/search?q=Schmidt"
```
#### Beispiel-Response
```json
[
{
"Nummer": "12345",
"Vorname": "Max",
"Nachname": "Schmidt",
"Fachrichtung": "Allgemeinmedizin",
"Strasse": "Hauptstraße 1",
"PLZ": "10115",
"Ort": "Berlin",
"Tel": "030-123456",
"mail": "max.schmidt@example.com"
}
]
``` ```
## Frontend-Funktionen 2. Docker Container starten:
```bash
docker-compose up -d
```
### Suchfunktion 3. Die Anwendung ist unter `http://localhost:5001` erreichbar.
- Live-Suche mit 300ms Debounce
- Spezifische Suchfelder für präzise Suche
- Allgemeine Suche für breite Suche
- Kombinierbare Suchkriterien
- Trefferzähler für jedes Suchfeld
### Link-Generierung
- `createPhoneLink()`: Erstellt tel:-Links mit führender 0
- `createEmailLink()`: Erstellt mailto:-Links
- `createAddressLink()`: Erstellt Google Maps-Links
- `createCustomerLink()`: Erstellt KKBefe-System-Links
### Teilen-Funktion
- Individueller Teilen-Button für jedes Suchergebnis
- Kopiert einen direkten Link zum spezifischen Kunden
- Visuelles Feedback beim Kopieren
## Fehlerbehandlung
- Logging für Backend-Fehler
- Benutzerfreundliche Fehlermeldungen im Frontend
- Graceful Degradation bei fehlenden Daten
## Entwicklung ## Entwicklung
### Debug-Modus - Python 3.11
Die Anwendung läuft standardmäßig im Debug-Modus: - Flask
- Docker
- Bootstrap 5
- Font Awesome
## Lizenz
Alle Rechte vorbehalten. © 2025 medisoftware
## API-Beispiele
### Suche nach Name
```bash ```bash
python app.py curl "http://localhost:5001/search?name=Mustermann"
``` ```
### Logging ### Suche nach Ort
- Backend-Logs werden mit Python's logging-Modul erstellt
- Log-Level: DEBUG
- Logs werden in der Konsole ausgegeben
## Wartung
### Container-Verwaltung
```bash ```bash
# Container stoppen curl "http://localhost:5001/search?ort=Berlin"
docker-compose down
# Container starten
docker-compose up
# Container im Hintergrund starten
docker-compose up -d
# Container-Logs anzeigen
docker-compose logs -f
``` ```
### Datenaktualisierung ### Suche nach Kundennummer
1. CSV-Datei aktualisieren ```bash
2. Container neu bauen und starten: curl "http://localhost:5001/search?kundennummer=12345"
```bash ```
docker-compose down
docker-compose up --build
```
## Sicherheit ### Suche nach Fachrichtung
```bash
curl "http://localhost:5001/search?fachrichtung=Zahnarzt"
```
- Alle externen Links öffnen sich in neuen Tabs ### Suche nach Telefon
- Sicherheitsattribute für externe Links (noopener, noreferrer) ```bash
- Input-Validierung im Backend curl "http://localhost:5001/search?telefon=030"
- Fehlerbehandlung für ungültige Daten ```
## Browser-Kompatibilität ### Allgemeine Suche
```bash
curl "http://localhost:5001/search?q=Suchbegriff"
```
Die Anwendung wurde getestet mit: ### Kombinierte Suche
- Chrome (neueste Version) ```bash
- Firefox (neueste Version) # Suche nach Fachrichtung und Ort
- Edge (neueste Version) curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin"
- Safari (neueste Version)
# Suche nach Name und Telefon
curl "http://localhost:5001/search?name=Mustermann&telefon=030"
# Suche nach mehreren Kriterien
curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt"
```
## Version
Aktuelle Version: [1.0.5](CHANGELOG.md#105---2024-03-17)

86
app.py
View File

@@ -3,18 +3,24 @@ import pandas as pd
import os import os
import logging import logging
import numpy as np import numpy as np
from datetime import datetime from datetime import datetime, timedelta
from dotenv import load_dotenv
import requests
from collections import defaultdict
app = Flask(__name__, static_folder='static') app = Flask(__name__, static_folder='static')
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Version der Anwendung # Version der Anwendung
VERSION = "1.0.1" VERSION = "1.0.6"
# Pfad zur CSV-Datei # Pfad zur CSV-Datei
CSV_FILE = "data/customers.csv" CSV_FILE = "data/customers.csv"
# Lade Umgebungsvariablen
load_dotenv()
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"""
return df.replace({np.nan: None}) return df.replace({np.nan: None})
@@ -27,7 +33,14 @@ def load_data():
logger.error(f"CSV-Datei '{CSV_FILE}' nicht gefunden!") logger.error(f"CSV-Datei '{CSV_FILE}' nicht gefunden!")
return None return None
df = pd.read_csv(CSV_FILE, encoding='utf-8') # Lade CSV mit Komma als Trennzeichen
df = pd.read_csv(CSV_FILE, sep=',', encoding='utf-8', quotechar='"')
# Entferne Anführungszeichen aus den Spaltennamen
df.columns = df.columns.str.strip('"')
# Entferne Anführungszeichen aus den Werten
for col in df.columns:
if df[col].dtype == 'object':
df[col] = df[col].str.strip('"')
df = clean_dataframe(df) df = clean_dataframe(df)
logger.info(f"CSV-Datei erfolgreich geladen. {len(df)} Einträge gefunden.") logger.info(f"CSV-Datei erfolgreich geladen. {len(df)} Einträge gefunden.")
return df return df
@@ -55,10 +68,12 @@ def search():
telefon = request.args.get('telefon', '').strip() telefon = request.args.get('telefon', '').strip()
query = request.args.get('q', '').strip() query = request.args.get('q', '').strip()
# Wenn keine spezifischen Suchkriterien angegeben sind, aber eine allgemeine Suche # Initialisiere die Maske für die Filterung
if not any([name, ort, kundennummer, fachrichtung, telefon]) and query: mask = pd.Series(True, index=df.index)
# Suche in allen relevanten Feldern
mask = ( # Wenn eine allgemeine Suche angegeben ist
if query:
query_mask = (
df['Vorname'].str.contains(query, case=False, na=False) | df['Vorname'].str.contains(query, case=False, na=False) |
df['Nachname'].str.contains(query, case=False, na=False) | df['Nachname'].str.contains(query, case=False, na=False) |
df['Ort'].str.contains(query, case=False, na=False) | df['Ort'].str.contains(query, case=False, na=False) |
@@ -66,39 +81,42 @@ def search():
df['Fachrichtung'].str.contains(query, case=False, na=False) | df['Fachrichtung'].str.contains(query, case=False, na=False) |
df['Tel'].astype(str).str.contains(query, case=False, na=False) df['Tel'].astype(str).str.contains(query, case=False, na=False)
) )
else: mask &= query_mask
# Spezifische Suche
mask = pd.Series(True, index=df.index) # Spezifische Suchkriterien anwenden
if name:
if name: name_mask = (
name_mask = ( df['Vorname'].str.contains(name, case=False, na=False) |
df['Vorname'].str.contains(name, case=False, na=False) | df['Nachname'].str.contains(name, case=False, na=False)
df['Nachname'].str.contains(name, case=False, na=False) )
) mask &= name_mask
mask &= name_mask
if ort:
if ort: ort_mask = df['Ort'].str.contains(ort, case=False, na=False)
ort_mask = df['Ort'].str.contains(ort, case=False, na=False) mask &= ort_mask
mask &= ort_mask
if kundennummer:
if kundennummer: kundennummer_mask = df['Nummer'].astype(str).str.contains(kundennummer, case=False, na=False)
kundennummer_mask = df['Nummer'].astype(str).str.contains(kundennummer, case=False, na=False) mask &= kundennummer_mask
mask &= kundennummer_mask
if fachrichtung:
if fachrichtung: fachrichtung_mask = df['Fachrichtung'].str.contains(fachrichtung, case=False, na=False)
fachrichtung_mask = df['Fachrichtung'].str.contains(fachrichtung, case=False, na=False) mask &= fachrichtung_mask
mask &= fachrichtung_mask
if telefon: if telefon:
telefon_mask = df['Tel'].astype(str).str.contains(telefon, case=False, na=False) telefon_mask = df['Tel'].astype(str).str.contains(telefon, case=False, na=False)
mask &= telefon_mask mask &= telefon_mask
results = df[mask].to_dict('records') results = df[mask].to_dict('records')
logger.info(f"{len(results)} Ergebnisse gefunden") logger.info(f"{len(results)} Ergebnisse gefunden")
return jsonify(results)
return jsonify({
'results': results,
'total': len(results)
})
except Exception as e: except Exception as e:
logger.error(f"Fehler bei der Suche: {str(e)}") logger.error(f"Fehler bei der Suche: {str(e)}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True, port=5001)

View File

@@ -1,4 +1,5 @@
flask==3.0.2 flask==3.0.2
pandas==2.2.1 pandas==2.2.1
numpy==1.26.4 numpy==1.26.4
python-dotenv==1.0.1 python-dotenv==1.0.1
requests==2.32.3

View File

@@ -234,10 +234,7 @@
function createCustomerLink(customerNumber) { function createCustomerLink(customerNumber) {
if (!customerNumber) return 'N/A'; if (!customerNumber) return 'N/A';
return `<a href="medisw:openkkbefe/P${customerNumber}?NetGrp=4" return `<a href="medisw:openkkbefe/P${customerNumber}?NetGrp=4" class="customer-link">${customerNumber}</a>`;
class="customer-link" target="_blank" rel="noopener noreferrer">
${customerNumber}
</a>`;
} }
function showCopyFeedback() { function showCopyFeedback() {
@@ -300,52 +297,46 @@
if (query) params.append('q', query); if (query) params.append('q', query);
fetch(`/search?${params.toString()}`) fetch(`/search?${params.toString()}`)
.then(response => { .then(response => response.json())
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Ein Fehler ist aufgetreten');
});
}
return response.json();
})
.then(data => { .then(data => {
const resultsDiv = document.getElementById('results'); if (data.error) {
resultsDiv.innerHTML = ''; showError(data.error);
if (data.length === 0) {
resultsDiv.innerHTML = '<div class="alert alert-info">Keine Ergebnisse gefunden.</div>';
lastResults = [];
updateResultCounts();
return; return;
} }
data.forEach(customer => { const resultsContainer = document.getElementById('results');
const card = document.createElement('div'); resultsContainer.innerHTML = '';
card.className = 'card result-card';
card.innerHTML = ` if (data.results && data.results.length > 0) {
<div class="card-body"> data.results.forEach(customer => {
<h5 class="card-title"> const card = document.createElement('div');
${customer.Vorname} ${customer.Nachname} card.className = 'card mb-3';
<span class="customer-number ms-2">(Kunde: ${createCustomerLink(customer.Nummer)})</span> const customerLink = createCustomerLink(customer.Nummer);
</h5> console.log('Customer:', customer); // Debug-Ausgabe
<p class="card-text"> console.log('Customer link:', customerLink); // Debug-Ausgabe
<strong>Fachrichtung:</strong> ${customer.Fachrichtung || 'N/A'}<br> card.innerHTML = `
<strong>Adresse:</strong> ${createAddressLink(customer.Strasse, customer.PLZ, customer.Ort)}<br> <div class="card-body">
<strong>Telefon:</strong> ${createPhoneLink(customer.Tel)}<br> <h5 class="card-title">${customer.Vorname} ${customer.Nachname}</h5>
<strong>E-Mail:</strong> ${createEmailLink(customer.mail)} <p class="card-text">
</p> <strong>Kundennummer:</strong> ${customerLink}<br>
<div class="card-actions"> <strong>Fachrichtung:</strong> ${customer.Fachrichtung || 'N/A'}<br>
<button class="btn btn-outline-primary share-button" onclick="copyCustomerLink('${customer.Nummer}')"> <strong>Adresse:</strong> ${createAddressLink(customer.Strasse, customer.PLZ, customer.Ort)}<br>
🔗 Teilen <strong>Telefon:</strong> ${createPhoneLink(customer.Tel)}<br>
</button> <strong>E-Mail:</strong> ${createEmailLink(customer.mail)}
</p>
</div> </div>
</div> `;
`; resultsContainer.appendChild(card);
resultsDiv.appendChild(card); });
});
// Zeige die Anzahl der Treffer an
lastResults = data; const totalResults = document.getElementById('total-results');
updateResultCounts(); if (totalResults) {
totalResults.textContent = `${data.total} Treffer gefunden`;
}
} else {
resultsContainer.innerHTML = '<div class="alert alert-info">Keine Ergebnisse gefunden.</div>';
}
}) })
.catch(error => { .catch(error => {
console.error('Fehler:', error); console.error('Fehler:', error);