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

252
README.md
View File

@@ -1,229 +1,99 @@
# medisoftware Kundensuche
Eine Flask-basierte Webanwendung zur Suche in Kundendaten aus einer CSV-Datei.
Eine webbasierte Kundensuche für medisoftware mit erweiterten Suchfunktionen.
## Features
- Live-Suche während der Eingabe
- Spezifische Suchfelder für:
- Kundennummer
- Schnelle und präzise Kundensuche
- Mehrere Suchfelder für gezielte Suche:
- Name (Vor- und Nachname)
- Fachrichtung
- Ort
- Kundennummer
- Fachrichtung
- Telefon
- Allgemeine Suche über alle Felder
- Klickbare Links für:
- Telefonnummern (tel:)
- E-Mail-Adressen (mailto:)
- Adressen (Google Maps)
- Kundennummern (KKBefe-System)
- Teilen-Funktion für einzelne Suchergebnisse
- Responsive Design mit Bootstrap
- Docker-Container-Unterstützung
## Technische Details
### 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
- Kombinierte Suche über mehrere Felder
- Hervorhebung der Suchbegriffe in den Ergebnissen
- Direkte Links zu:
- medisoftware Kundenkartei (Kundennummer)
- Google Maps (Adresse)
- Telefon (Klick zum Anrufen)
- E-Mail (Klick zum Mailen)
- Responsive Design für alle Geräte
- Automatische Aktualisierung der Ergebnisse
- Leere Ergebnisliste bei leeren Suchfeldern
## Installation
### Lokale Entwicklung
1. Python 3.11 installieren
2. Virtuelle Umgebung erstellen und aktivieren:
1. Repository klonen:
```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
git clone https://gitea.elpatron.me/elpatron/medi-customers.git
cd medi-customers
```
### Docker-Container
1. Docker installieren
2. Container mit Docker Compose starten:
2. Docker Container 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"
docker-compose up -d
```
2. Suche nach Ort:
3. Die Anwendung ist unter `http://localhost:5001` erreichbar.
## Entwicklung
- Python 3.11
- Flask
- Docker
- Bootstrap 5
- Font Awesome
## Lizenz
Alle Rechte vorbehalten. © 2025 medisoftware
## API-Beispiele
### Suche nach Name
```bash
curl "http://localhost:5001/search?name=Mustermann"
```
### Suche nach Ort
```bash
curl "http://localhost:5001/search?ort=Berlin"
```
3. Suche nach Kundennummer:
### Suche nach Kundennummer
```bash
curl "http://localhost:5001/search?kundennummer=12345"
```
4. Suche nach Fachrichtung:
### Suche nach Fachrichtung
```bash
curl "http://localhost:5001/search?fachrichtung=Allgemeinmedizin"
curl "http://localhost:5001/search?fachrichtung=Zahnarzt"
```
5. Kombinierte Suche:
### Suche nach Telefon
```bash
curl "http://localhost:5001/search?name=Schmidt&ort=Berlin&fachrichtung=Allgemeinmedizin"
curl "http://localhost:5001/search?telefon=030"
```
6. Allgemeine Suche:
### Allgemeine Suche
```bash
curl "http://localhost:5001/search?q=Schmidt"
curl "http://localhost:5001/search?q=Suchbegriff"
```
#### 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
### Suchfunktion
- 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
### Debug-Modus
Die Anwendung läuft standardmäßig im Debug-Modus:
### Kombinierte Suche
```bash
python app.py
# Suche nach Fachrichtung und Ort
curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin"
# 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"
```
### Logging
- Backend-Logs werden mit Python's logging-Modul erstellt
- Log-Level: DEBUG
- Logs werden in der Konsole ausgegeben
## Version
## Wartung
### Container-Verwaltung
```bash
# Container stoppen
docker-compose down
# Container starten
docker-compose up
# Container im Hintergrund starten
docker-compose up -d
# Container-Logs anzeigen
docker-compose logs -f
```
### Datenaktualisierung
1. CSV-Datei aktualisieren
2. Container neu bauen und starten:
```bash
docker-compose down
docker-compose up --build
```
## Sicherheit
- Alle externen Links öffnen sich in neuen Tabs
- Sicherheitsattribute für externe Links (noopener, noreferrer)
- Input-Validierung im Backend
- Fehlerbehandlung für ungültige Daten
## Browser-Kompatibilität
Die Anwendung wurde getestet mit:
- Chrome (neueste Version)
- Firefox (neueste Version)
- Edge (neueste Version)
- Safari (neueste Version)
Aktuelle Version: [1.0.5](CHANGELOG.md#105---2024-03-17)

42
app.py
View File

@@ -3,18 +3,24 @@ import pandas as pd
import os
import logging
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')
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Version der Anwendung
VERSION = "1.0.1"
VERSION = "1.0.6"
# Pfad zur CSV-Datei
CSV_FILE = "data/customers.csv"
# Lade Umgebungsvariablen
load_dotenv()
def clean_dataframe(df):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
return df.replace({np.nan: None})
@@ -27,7 +33,14 @@ def load_data():
logger.error(f"CSV-Datei '{CSV_FILE}' nicht gefunden!")
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)
logger.info(f"CSV-Datei erfolgreich geladen. {len(df)} Einträge gefunden.")
return df
@@ -55,10 +68,12 @@ def search():
telefon = request.args.get('telefon', '').strip()
query = request.args.get('q', '').strip()
# Wenn keine spezifischen Suchkriterien angegeben sind, aber eine allgemeine Suche
if not any([name, ort, kundennummer, fachrichtung, telefon]) and query:
# Suche in allen relevanten Feldern
mask = (
# Initialisiere die Maske für die Filterung
mask = pd.Series(True, index=df.index)
# Wenn eine allgemeine Suche angegeben ist
if query:
query_mask = (
df['Vorname'].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) |
@@ -66,10 +81,9 @@ def search():
df['Fachrichtung'].str.contains(query, case=False, na=False) |
df['Tel'].astype(str).str.contains(query, case=False, na=False)
)
else:
# Spezifische Suche
mask = pd.Series(True, index=df.index)
mask &= query_mask
# Spezifische Suchkriterien anwenden
if name:
name_mask = (
df['Vorname'].str.contains(name, case=False, na=False) |
@@ -95,10 +109,14 @@ def search():
results = df[mask].to_dict('records')
logger.info(f"{len(results)} Ergebnisse gefunden")
return jsonify(results)
return jsonify({
'results': results,
'total': len(results)
})
except Exception as e:
logger.error(f"Fehler bei der Suche: {str(e)}")
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True)
app.run(debug=True, port=5001)

View File

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

View File

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