Initial commit
This commit is contained in:
14
.dockerignore
Normal file
14
.dockerignore
Normal 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
48
.gitignore
vendored
Normal 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
24
Dockerfile
Normal 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
169
README.md
Normal 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
62
app.py
Normal 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
4
requirements.txt
Normal 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
209
templates/index.html
Normal 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>
|
Reference in New Issue
Block a user