12 Commits

5 changed files with 207 additions and 246 deletions

53
CHANGELOG.md Normal file
View File

@@ -0,0 +1,53 @@
# 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.2] - 2024-03-19
### Hinzugefügt
- Wetterinformationen für jeden Suchtreffer
- Integration der OpenWeather API
- Wetter-Icons und Temperaturanzeige
- Umgebungsvariablen für API-Keys
### Geändert
- Anpassung der API-Antwortstruktur
- Verbesserte Fehlerbehandlung für API-Anfragen
## [1.0.1] - 2024-03-19
### Hinzugefügt
- Neues Suchfeld für Telefonnummer
- Reset-Icons für alle Suchfelder
- Trefferzähler für Suchergebnisse
### Geändert
- Verbesserte Positionierung der UI-Elemente
- Optimierte Suchlogik im Backend
- CSV-Datei in data-Verzeichnis verschoben
- Allgemeine Suche um Telefonnummer erweitert
### Behoben
- Korrektur der Icon-Anzeige in Suchfeldern
- Verbesserte Fehlerbehandlung beim Laden der CSV-Datei
## [1.0.0] - 2024-03-18
### Hinzugefügt
- Grundlegende Suchfunktionalität
- Spezifische Suchfelder für:
- Name
- Ort
- Kundennummer
- Fachrichtung
- Allgemeine Suche über alle Felder
- Klickbare Links für:
- Telefonnummern
- E-Mail-Adressen
- Google Maps Integration
- Share-Funktion für Suchergebnisse
- Responsive Design mit Bootstrap
- Live-Suche während der Eingabe

230
README.md
View File

@@ -1,229 +1,107 @@
# medisoftware Kundensuche # medisoftware Kundensuche
Eine Flask-basierte Webanwendung zur Suche in Kundendaten aus einer CSV-Datei. Eine Webanwendung zur Suche in Kundendaten der medisoftware.
## Features ## Features
- Live-Suche während der Eingabe - Live-Suche in Kundendaten
- Spezifische Suchfelder für: - Spezifische Suchfelder für:
- Name
- Ort
- Kundennummer - Kundennummer
- Name (Vor- und Nachname)
- Fachrichtung - Fachrichtung
- Ort - Telefonnummer
- Allgemeine Suche über alle Felder - Allgemeine Suche über alle Felder
- Klickbare Links für: - Klickbare Telefonnummern
- Telefonnummern (tel:) - Klickbare E-Mail-Adressen
- E-Mail-Adressen (mailto:) - Google Maps Integration für Adressen
- Adressen (Google Maps) - Share-Funktion für Suchergebnisse
- Kundennummern (KKBefe-System) - Trefferzähler
- Teilen-Funktion für einzelne Suchergebnisse - Reset-Funktion für alle Suchfelder
- 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
## Installation ## Installation
### Lokale Entwicklung 1. Repository klonen:
1. Python 3.11 installieren
2. Virtuelle Umgebung erstellen und aktivieren:
```bash ```bash
python -m venv venv git clone https://gitea.elpatron.me/elpatron/medi-customers.git
source venv/bin/activate # Linux/Mac cd medi-customers
venv\Scripts\activate # Windows
``` ```
3. Abhängigkeiten installieren:
2. Python-Abhängigkeiten installieren:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
3. CSV-Datei in das `data`-Verzeichnis kopieren:
```bash
mkdir data
cp spezexpo.csv data/customers.csv
```
4. Anwendung starten: 4. Anwendung starten:
```bash ```bash
python app.py 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. Die Anwendung ist dann unter `http://localhost:5001` erreichbar.
## API-Endpunkte ## API-Beispiele
### GET / Die Such-API unterstützt folgende Parameter:
- Rendert die Hauptseite
### GET /search ### Spezifische Suche
- 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 ```bash
# Nach Name suchen
curl "http://localhost:5001/search?name=Schmidt" curl "http://localhost:5001/search?name=Schmidt"
```
2. Suche nach Ort: # Nach Ort suchen
```bash
curl "http://localhost:5001/search?ort=Berlin" curl "http://localhost:5001/search?ort=Berlin"
```
3. Suche nach Kundennummer: # Nach Kundennummer suchen
```bash
curl "http://localhost:5001/search?kundennummer=12345" curl "http://localhost:5001/search?kundennummer=12345"
```
4. Suche nach Fachrichtung: # Nach Fachrichtung suchen
```bash
curl "http://localhost:5001/search?fachrichtung=Allgemeinmedizin" curl "http://localhost:5001/search?fachrichtung=Allgemeinmedizin"
```
5. Kombinierte Suche: # Nach Telefonnummer suchen
```bash curl "http://localhost:5001/search?telefon=030"
# Kombinierte Suche
curl "http://localhost:5001/search?name=Schmidt&ort=Berlin&fachrichtung=Allgemeinmedizin" curl "http://localhost:5001/search?name=Schmidt&ort=Berlin&fachrichtung=Allgemeinmedizin"
``` ```
6. Allgemeine Suche: ### Allgemeine Suche
```bash ```bash
# Suche in allen Feldern
curl "http://localhost:5001/search?q=Schmidt" curl "http://localhost:5001/search?q=Schmidt"
``` ```
#### Beispiel-Response ### Beispiel-Response
```json ```json
[ [
{ {
"Nummer": "12345",
"Vorname": "Max", "Vorname": "Max",
"Nachname": "Schmidt", "Nachname": "Mustermann",
"Fachrichtung": "Allgemeinmedizin", "Nummer": "12345",
"Strasse": "Hauptstraße 1",
"PLZ": "10115",
"Ort": "Berlin", "Ort": "Berlin",
"Tel": "030-123456", "Fachrichtung": "Allgemeinmedizin",
"mail": "max.schmidt@example.com" "Tel": "030123456",
"Email": "max@example.com"
} }
] ]
``` ```
## Frontend-Funktionen ## Versionen
### Suchfunktion ### v1.0.1
- Live-Suche mit 300ms Debounce - Telefonnummer-Suchfeld hinzugefügt
- Spezifische Suchfelder für präzise Suche - Reset-Icons für alle Suchfelder
- Allgemeine Suche für breite Suche - Verbesserte Positionierung der UI-Elemente
- Kombinierbare Suchkriterien - Optimierte Suchlogik
- Trefferzähler für jedes Suchfeld - CSV-Datei in data-Verzeichnis verschoben
### Link-Generierung ### v1.0.0
- `createPhoneLink()`: Erstellt tel:-Links mit führender 0 - Erste Version
- `createEmailLink()`: Erstellt mailto:-Links - Grundlegende Suchfunktionalität
- `createAddressLink()`: Erstellt Google Maps-Links - Klickbare Links für Telefon, E-Mail und Adressen
- `createCustomerLink()`: Erstellt KKBefe-System-Links - Share-Funktion für Suchergebnisse
### 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:
```bash
python app.py
```
### Logging
- Backend-Logs werden mit Python's logging-Modul erstellt
- Log-Level: DEBUG
- Logs werden in der Konsole ausgegeben
## 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)

27
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.5"
# 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
@@ -95,10 +108,14 @@ def search():
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

@@ -2,3 +2,4 @@ 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

@@ -139,6 +139,18 @@
margin-left: 4px; margin-left: 4px;
font-size: 1.2em; font-size: 1.2em;
} }
.weather-info {
display: inline-flex;
align-items: center;
margin-left: 10px;
font-size: 0.9em;
color: #666;
}
.weather-info img {
width: 24px;
height: 24px;
margin-right: 4px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -300,52 +312,52 @@
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');
resultsContainer.innerHTML = '';
if (data.results && data.results.length > 0) {
data.results.forEach(customer => {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card result-card'; card.className = 'card mb-3';
card.innerHTML = ` card.innerHTML = `
<div class="card-body"> <div class="card-body">
<h5 class="card-title"> <h5 class="card-title">${customer.Vorname} ${customer.Nachname}</h5>
${customer.Vorname} ${customer.Nachname}
<span class="customer-number ms-2">(Kunde: ${createCustomerLink(customer.Nummer)})</span>
</h5>
<p class="card-text"> <p class="card-text">
<strong>Kundennummer:</strong> ${customer.Nummer}<br>
<strong>Fachrichtung:</strong> ${customer.Fachrichtung || 'N/A'}<br> <strong>Fachrichtung:</strong> ${customer.Fachrichtung || 'N/A'}<br>
<strong>Adresse:</strong> ${createAddressLink(customer.Strasse, customer.PLZ, customer.Ort)}<br> <strong>Adresse:</strong> ${createAddressLink(customer.Strasse, customer.PLZ, customer.Ort)}
${customer.weather ? `
<span class="weather-info">
<img src="http://openweathermap.org/img/wn/${customer.weather.icon}@2x.png"
alt="${customer.weather.description}"
title="${customer.weather.description}">
${customer.weather.temperature}°C
</span>
` : ''}
<br>
<strong>Telefon:</strong> ${createPhoneLink(customer.Tel)}<br> <strong>Telefon:</strong> ${createPhoneLink(customer.Tel)}<br>
<strong>E-Mail:</strong> ${createEmailLink(customer.mail)} <strong>E-Mail:</strong> ${createEmailLink(customer.mail)}
</p> </p>
<div class="card-actions">
<button class="btn btn-outline-primary share-button" onclick="copyCustomerLink('${customer.Nummer}')">
🔗 Teilen
</button>
</div>
</div> </div>
`; `;
resultsDiv.appendChild(card); resultsContainer.appendChild(card);
}); });
lastResults = data; // Zeige die Anzahl der Treffer an
updateResultCounts(); 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 => { .catch(error => {
console.error('Fehler:', error); console.error('Fehler:', error);