Initial commit

This commit is contained in:
2025-03-17 19:57:41 +01:00
commit 5a2806c1cd
7 changed files with 530 additions and 0 deletions

14
.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.env
*.log
.git
.gitignore
.pytest_cache/
.coverage
htmlcov/

48
.gitignore vendored Normal file
View File

@@ -0,0 +1,48 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
.env
.venv
pip-log.txt
pip-delete-this-directory.txt
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
# Distribution / packaging
dist/
build/
*.egg-info/
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Jupyter Notebook
.ipynb_checkpoints
# Docker
.docker/
# Daten
spezexpo.csv

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
# Python 3.11 als Basis-Image
FROM python:3.11-slim
# Arbeitsverzeichnis setzen
WORKDIR /app
# Kopiere requirements.txt
COPY requirements.txt .
# Installiere Abhängigkeiten
RUN pip install --no-cache-dir -r requirements.txt
# Kopiere den Anwendungscode
COPY . .
# Exponiere Port 5000
EXPOSE 5000
# Setze Umgebungsvariablen
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# Starte die Anwendung
CMD ["flask", "run", "--host=0.0.0.0"]

169
README.md Normal file
View File

@@ -0,0 +1,169 @@
# medisoftware Kundensuche
Eine Flask-basierte Webanwendung zur Suche in Kundendaten aus einer CSV-Datei.
## Features
- Live-Suche während der Eingabe
- Suche nach:
- Kundennummer
- Name (Vor- und Nachname)
- Fachrichtung
- Ort
- Klickbare Links für:
- Telefonnummern (tel:)
- E-Mail-Adressen (mailto:)
- Adressen (Google Maps)
- Kundennummern (KKBefe-System)
- 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
└── .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
### Lokale Entwicklung
1. Python 3.11 installieren
2. Virtuelle Umgebung erstellen und aktivieren:
```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 bauen:
```bash
docker build -t medi-customers .
```
3. Container starten:
```bash
docker run -d -p 5000:5000 --name medi-customers medi-customers
```
## API-Endpunkte
### GET /
- Rendert die Hauptseite
### GET /search
- Sucht nach Kunden basierend auf dem Query-Parameter
- Parameter: `q` (Suchbegriff)
- Returns: JSON-Array mit gefundenen Kunden
## Frontend-Funktionen
### Suchfunktion
- Live-Suche mit 300ms Debounce
- Minimale Suchlänge: 2 Zeichen
- Suche wird bei Enter-Taste sofort ausgeführt
### Link-Generierung
- `createPhoneLink()`: Erstellt tel:-Links mit führender 0
- `createEmailLink()`: Erstellt mailto:-Links
- `createAddressLink()`: Erstellt Google Maps-Links
- `createCustomerLink()`: Erstellt KKBefe-System-Links
## 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 stop medi-customers
# Container starten
docker start medi-customers
# Container-Logs anzeigen
docker logs medi-customers
# Container entfernen
docker rm medi-customers
```
### Datenaktualisierung
1. CSV-Datei aktualisieren
2. Container neu bauen und starten:
```bash
docker stop medi-customers
docker build -t medi-customers .
docker run -d -p 5000:5000 --name medi-customers medi-customers
```
## 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)

62
app.py Normal file
View File

@@ -0,0 +1,62 @@
from flask import Flask, render_template, request, jsonify
import pandas as pd
import os
import logging
import numpy as np
app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def clean_dataframe(df):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
return df.replace({np.nan: None})
# CSV-Datei laden
def load_data():
try:
logger.info("Versuche CSV-Datei zu laden...")
if not os.path.exists('spezexpo.csv'):
logger.error("CSV-Datei 'spezexpo.csv' nicht gefunden!")
return None
df = pd.read_csv('spezexpo.csv', encoding='utf-8')
df = clean_dataframe(df)
logger.info(f"CSV-Datei erfolgreich geladen. {len(df)} Einträge gefunden.")
return df
except Exception as e:
logger.error(f"Fehler beim Laden der CSV-Datei: {str(e)}")
return None
@app.route('/')
def index():
return render_template('index.html')
@app.route('/search')
def search():
try:
query = request.args.get('q', '').lower()
logger.info(f"Suche nach: {query}")
df = load_data()
if df is None:
return jsonify({"error": "Datenbank konnte nicht geladen werden"}), 500
# Suche über verschiedene Felder
mask = (
df['Vorname'].str.lower().str.contains(query, na=False) |
df['Nachname'].str.lower().str.contains(query, na=False) |
df['Fachrichtung'].str.lower().str.contains(query, na=False) |
df['Ort'].str.lower().str.contains(query, na=False) |
df['Nummer'].astype(str).str.contains(query, na=False) # Suche nach Kundennummer
)
results = df[mask].to_dict('records')
logger.info(f"{len(results)} Ergebnisse gefunden")
return jsonify(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)

4
requirements.txt Normal file
View File

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

209
templates/index.html Normal file
View File

@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>medisoftware Kundensuche</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
min-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
}
.main-content {
flex: 1 0 auto;
padding: 2rem 0;
}
.search-container {
max-width: 800px;
margin: 0 auto;
}
.result-card {
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.loading {
display: none;
text-align: center;
margin: 2rem 0;
}
.phone-link, .email-link, .address-link, .customer-link {
text-decoration: none;
color: #0d6efd;
}
.phone-link:hover, .email-link:hover, .address-link:hover, .customer-link:hover {
text-decoration: underline;
}
.search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
}
.customer-number {
color: #6c757d;
font-size: 0.9em;
}
.footer {
flex-shrink: 0;
text-align: center;
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
width: 100%;
}
</style>
</head>
<body>
<div class="main-content">
<div class="container search-container">
<h1 class="text-center mb-4">medisoftware Kundensuche</h1>
<div class="input-group mb-4 position-relative">
<input type="text" id="searchInput" class="form-control form-control-lg"
placeholder="Suchen Sie nach Name, Fachrichtung, Ort oder Kundennummer...">
<span class="search-icon">🔍</span>
</div>
<div id="loading" class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
<div id="results" class="mt-4">
<!-- Hier werden die Suchergebnisse angezeigt -->
</div>
</div>
</div>
<footer class="footer">
<p class="mb-0">(c) 2025 <a href="https://medisoftware.de" target="_blank" rel="noopener noreferrer" class="text-decoration-none">medisoftware</a></p>
</footer>
<script>
let searchTimeout;
function createPhoneLink(phone) {
if (!phone) return 'N/A';
// Entferne alle nicht-numerischen Zeichen für den tel: Link
const cleaned = phone.replace(/\D/g, '');
// Füge immer eine führende 0 hinzu
const telLink = '0' + cleaned;
// Zeige die ursprüngliche Nummer an
return `<a href="tel:${telLink}" class="phone-link">${phone}</a>`;
}
function createEmailLink(email) {
if (!email) return 'N/A';
return `<a href="mailto:${email}" class="email-link">${email}</a>`;
}
function createAddressLink(street, plz, city) {
if (!street || !plz || !city) return 'N/A';
const address = `${street}, ${plz} ${city}`;
const searchQuery = encodeURIComponent(address);
return `<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}"
class="address-link" target="_blank" rel="noopener noreferrer">
${address}
<span class="ms-1">🗺️</span>
</a>`;
}
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>`;
}
function searchCustomers() {
const query = document.getElementById('searchInput').value;
if (query.length < 2) {
document.getElementById('results').innerHTML = '';
return;
}
// Lade-Animation anzeigen
document.getElementById('loading').style.display = 'block';
document.getElementById('results').innerHTML = '';
fetch(`/search?q=${encodeURIComponent(query)}`)
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Ein Fehler ist aufgetreten');
});
}
return 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>';
return;
}
data.forEach(customer => {
const card = document.createElement('div');
card.className = 'card result-card';
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>
<p class="card-text">
<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>
`;
resultsDiv.appendChild(card);
});
})
.catch(error => {
console.error('Fehler:', error);
document.getElementById('results').innerHTML =
`<div class="alert alert-danger">${error.message}</div>`;
})
.finally(() => {
// Lade-Animation ausblenden
document.getElementById('loading').style.display = 'none';
});
}
// Event-Listener für die Live-Suche
document.getElementById('searchInput').addEventListener('input', function(e) {
// Clear previous timeout
clearTimeout(searchTimeout);
// Set new timeout
searchTimeout = setTimeout(() => {
searchCustomers();
}, 300); // 300ms Verzögerung
});
// Suche bei Enter-Taste
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
clearTimeout(searchTimeout);
searchCustomers();
}
});
</script>
</body>
</html>