17 Commits

Author SHA1 Message Date
163fb792a0 Reset-Icon-Anzeige korrigiert 2025-03-17 20:58:25 +01:00
733161dd46 Icon-Anzeige korrigiert 2025-03-17 20:57:33 +01:00
4e2703ba0a Reset-Icon in den Suchfeldern ganz rechts positioniert 2025-03-17 20:56:32 +01:00
02e72b8347 CSV-Datei wird jetzt in der search-Funktion geladen 2025-03-17 20:55:11 +01:00
0285f0a520 Suchfeld für Telefonnummer hinzugefügt 2025-03-17 20:54:15 +01:00
8fb10a35e8 Allgemeines Suchfeld über spezifische Suchfelder verschoben 2025-03-17 20:52:33 +01:00
6b017e641d CSV-Datei in data-Verzeichnis verschoben 2025-03-17 20:46:35 +01:00
4d110b6019 Version 1.0.1: Trefferzähler vereinfacht und Layout verbessert 2025-03-17 20:42:29 +01:00
71d811ea98 Trefferzähler auf Gesamtzahl vereinfacht 2025-03-17 20:41:16 +01:00
2de5c21442 Trefferzähler zwischen Suchfeldern und Suchergebnissen neu positioniert 2025-03-17 20:40:09 +01:00
c6f170e1fc Abstand zwischen Reset-Icon und Trefferzahl vergrößert 2025-03-17 20:38:24 +01:00
ff4f322ca4 Reset-Icons für alle Suchfelder hinzugefügt 2025-03-17 20:37:10 +01:00
5eaa79ed62 Location-Pin-Icon vergrößert 2025-03-17 20:35:31 +01:00
6657101cd6 Google Maps Link nur noch auf dem Pin-Icon 2025-03-17 20:34:15 +01:00
9f4c532d66 Google Maps Icon durch Font Awesome Pin ersetzt 2025-03-17 20:33:14 +01:00
83cedcc555 API-Beispiele und Response-Format zur Dokumentation hinzugefügt 2025-03-17 20:19:45 +01:00
2eddb3ad20 Trefferzähler für alle Suchfelder hinzugefügt 2025-03-17 20:18:17 +01:00
4 changed files with 1383 additions and 55 deletions

View File

@@ -100,6 +100,55 @@ Die Anwendung erwartet eine CSV-Datei (`spezexpo.csv`) mit folgenden Spalten:
- `q`: Allgemeine Suche über alle Felder - `q`: Allgemeine Suche über alle Felder
- Returns: JSON-Array mit gefundenen Kunden - 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 ## Frontend-Funktionen
### Suchfunktion ### Suchfunktion
@@ -107,6 +156,7 @@ Die Anwendung erwartet eine CSV-Datei (`spezexpo.csv`) mit folgenden Spalten:
- Spezifische Suchfelder für präzise Suche - Spezifische Suchfelder für präzise Suche
- Allgemeine Suche für breite Suche - Allgemeine Suche für breite Suche
- Kombinierbare Suchkriterien - Kombinierbare Suchkriterien
- Trefferzähler für jedes Suchfeld
### Link-Generierung ### Link-Generierung
- `createPhoneLink()`: Erstellt tel:-Links mit führender 0 - `createPhoneLink()`: Erstellt tel:-Links mit führender 0

95
app.py
View File

@@ -3,11 +3,18 @@ import pandas as pd
import os import os
import logging import logging
import numpy as np import numpy as np
from datetime import datetime
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 = "1.0.1"
# Pfad zur CSV-Datei
CSV_FILE = "data/customers.csv"
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})
@@ -16,11 +23,11 @@ def clean_dataframe(df):
def load_data(): def load_data():
try: try:
logger.info("Versuche CSV-Datei zu laden...") logger.info("Versuche CSV-Datei zu laden...")
if not os.path.exists('spezexpo.csv'): if not os.path.exists(CSV_FILE):
logger.error("CSV-Datei 'spezexpo.csv' nicht gefunden!") logger.error(f"CSV-Datei '{CSV_FILE}' nicht gefunden!")
return None return None
df = pd.read_csv('spezexpo.csv', encoding='utf-8') df = pd.read_csv(CSV_FILE, encoding='utf-8')
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
@@ -35,54 +42,56 @@ def index():
@app.route('/search') @app.route('/search')
def search(): def search():
try: try:
# Spezifische Suchparameter # CSV-Datei laden
name = request.args.get('name', '').lower()
ort = request.args.get('ort', '').lower()
kundennummer = request.args.get('kundennummer', '').lower()
fachrichtung = request.args.get('fachrichtung', '').lower()
# Allgemeine Suche (für die alte Funktionalität)
query = request.args.get('q', '').lower()
logger.info(f"Suche nach - Name: {name}, Ort: {ort}, Kundennummer: {kundennummer}, Fachrichtung: {fachrichtung}, Query: {query}")
df = load_data() df = load_data()
if df is None: if df is None:
return jsonify({"error": "Datenbank konnte nicht geladen werden"}), 500 return jsonify({"error": "Datenbank konnte nicht geladen werden"}), 500
# Basis-Mask (immer True) # Suchparameter aus der URL holen
mask = pd.Series(True, index=df.index) name = request.args.get('name', '').strip()
ort = request.args.get('ort', '').strip()
# Spezifische Suchkriterien anwenden kundennummer = request.args.get('kundennummer', '').strip()
if name: fachrichtung = request.args.get('fachrichtung', '').strip()
name_mask = ( telefon = request.args.get('telefon', '').strip()
df['Vorname'].str.lower().str.contains(name, na=False) | query = request.args.get('q', '').strip()
df['Nachname'].str.lower().str.contains(name, na=False)
# 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 = (
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) |
df['Nummer'].astype(str).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)
) )
mask &= name_mask else:
# Spezifische Suche
mask = pd.Series(True, index=df.index)
if ort: if name:
ort_mask = df['Ort'].str.lower().str.contains(ort, na=False) name_mask = (
mask &= ort_mask df['Vorname'].str.contains(name, case=False, na=False) |
df['Nachname'].str.contains(name, case=False, na=False)
)
mask &= name_mask
if kundennummer: if ort:
kunden_mask = df['Nummer'].astype(str).str.contains(kundennummer, na=False) ort_mask = df['Ort'].str.contains(ort, case=False, na=False)
mask &= kunden_mask mask &= ort_mask
if fachrichtung: if kundennummer:
fach_mask = df['Fachrichtung'].str.lower().str.contains(fachrichtung, na=False) kundennummer_mask = df['Nummer'].astype(str).str.contains(kundennummer, case=False, na=False)
mask &= fach_mask mask &= kundennummer_mask
# Wenn eine allgemeine Suche vorhanden ist, diese zusätzlich anwenden if fachrichtung:
if query: fachrichtung_mask = df['Fachrichtung'].str.contains(fachrichtung, case=False, na=False)
query_mask = ( mask &= fachrichtung_mask
df['Vorname'].str.lower().str.contains(query, na=False) |
df['Nachname'].str.lower().str.contains(query, na=False) | if telefon:
df['Fachrichtung'].str.lower().str.contains(query, na=False) | telefon_mask = df['Tel'].astype(str).str.contains(telefon, case=False, na=False)
df['Ort'].str.lower().str.contains(query, na=False) | mask &= telefon_mask
df['Nummer'].astype(str).str.contains(query, na=False)
)
mask &= query_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")

1172
data/customers.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
<title>medisoftware Kundensuche</title> <title>medisoftware Kundensuche</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
<style> <style>
body { body {
min-height: 100vh; min-height: 100vh;
@@ -42,12 +43,26 @@
.phone-link:hover, .email-link:hover, .address-link:hover, .customer-link:hover { .phone-link:hover, .email-link:hover, .address-link:hover, .customer-link:hover {
text-decoration: underline; text-decoration: underline;
} }
.search-icon { .search-icon, .reset-icon {
position: absolute; position: absolute;
right: 10px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: #6c757d; color: #6c757d;
cursor: pointer;
display: none;
z-index: 10;
}
.reset-icon {
right: 10px;
}
.search-icon {
right: 35px;
}
.reset-icon.visible {
display: block;
}
.search-icon.visible {
display: block;
} }
.customer-number { .customer-number {
color: #6c757d; color: #6c757d;
@@ -97,6 +112,33 @@
.search-field { .search-field {
position: relative; position: relative;
} }
.input-group {
position: relative;
}
.result-counts {
display: flex;
justify-content: center;
margin-bottom: 1rem;
padding: 0.5rem;
background-color: #f8f9fa;
border-radius: 4px;
}
.result-count {
background-color: #e9ecef;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.9em;
color: #6c757d;
display: none;
}
.result-count.visible {
display: inline-block;
}
.location-pin {
color: #dc3545;
margin-left: 4px;
font-size: 1.2em;
}
</style> </style>
</head> </head>
<body> <body>
@@ -104,29 +146,43 @@
<div class="container search-container"> <div class="container search-container">
<h1 class="text-center mb-4">medisoftware Kundensuche</h1> <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="Allgemeine Suche...">
<i class="fa-solid fa-xmark reset-icon" id="searchReset"></i>
<span class="search-icon">🔍</span>
</div>
<div class="search-fields"> <div class="search-fields">
<div class="search-field"> <div class="search-field">
<input type="text" id="nameInput" class="form-control" <input type="text" id="nameInput" class="form-control"
placeholder="Name..."> placeholder="Name...">
<i class="fa-solid fa-xmark reset-icon" id="nameReset"></i>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="ortInput" class="form-control" <input type="text" id="ortInput" class="form-control"
placeholder="Ort..."> placeholder="Ort...">
<i class="fa-solid fa-xmark reset-icon" id="ortReset"></i>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="kundennummerInput" class="form-control" <input type="text" id="kundennummerInput" class="form-control"
placeholder="Kundennummer..."> placeholder="Kundennummer...">
<i class="fa-solid fa-xmark reset-icon" id="kundennummerReset"></i>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="fachrichtungInput" class="form-control" <input type="text" id="fachrichtungInput" class="form-control"
placeholder="Fachrichtung..."> placeholder="Fachrichtung...">
<i class="fa-solid fa-xmark reset-icon" id="fachrichtungReset"></i>
</div>
<div class="search-field">
<input type="text" id="telefonInput" class="form-control"
placeholder="Telefon...">
<i class="fa-solid fa-xmark reset-icon" id="telefonReset"></i>
</div> </div>
</div> </div>
<div class="input-group mb-4 position-relative"> <div class="result-counts">
<input type="text" id="searchInput" class="form-control form-control-lg" <span id="generalCount" class="result-count"></span>
placeholder="Allgemeine Suche...">
<span class="search-icon">🔍</span>
</div> </div>
<div id="loading" class="loading"> <div id="loading" class="loading">
@@ -151,6 +207,7 @@
<script> <script>
let searchTimeout; let searchTimeout;
let lastResults = [];
function createPhoneLink(phone) { function createPhoneLink(phone) {
if (!phone) return 'N/A'; if (!phone) return 'N/A';
@@ -168,10 +225,10 @@
if (!street || !plz || !city) return 'N/A'; if (!street || !plz || !city) return 'N/A';
const address = `${street}, ${plz} ${city}`; const address = `${street}, ${plz} ${city}`;
const searchQuery = encodeURIComponent(address); const searchQuery = encodeURIComponent(address);
return `<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}" return `${address}
<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}"
class="address-link" target="_blank" rel="noopener noreferrer"> class="address-link" target="_blank" rel="noopener noreferrer">
${address} <i class="fa-solid fa-location-pin location-pin"></i>
<span class="ms-1">🗺️</span>
</a>`; </a>`;
} }
@@ -205,16 +262,27 @@
} }
} }
function updateResultCounts() {
// Nur Gesamtzahl anzeigen
const generalCount = lastResults.length;
document.getElementById('generalCount').textContent =
generalCount > 0 ? `${generalCount} Treffer gefunden` : '';
document.getElementById('generalCount').classList.toggle('visible', generalCount > 0);
}
function searchCustomers() { function searchCustomers() {
const name = document.getElementById('nameInput').value; const name = document.getElementById('nameInput').value;
const ort = document.getElementById('ortInput').value; const ort = document.getElementById('ortInput').value;
const kundennummer = document.getElementById('kundennummerInput').value; const kundennummer = document.getElementById('kundennummerInput').value;
const fachrichtung = document.getElementById('fachrichtungInput').value; const fachrichtung = document.getElementById('fachrichtungInput').value;
const telefon = document.getElementById('telefonInput').value;
const query = document.getElementById('searchInput').value; const query = document.getElementById('searchInput').value;
// Prüfe, ob mindestens ein Suchfeld ausgefüllt ist // Prüfe, ob mindestens ein Suchfeld ausgefüllt ist
if (!name && !ort && !kundennummer && !fachrichtung && !query) { if (!name && !ort && !kundennummer && !fachrichtung && !telefon && !query) {
document.getElementById('results').innerHTML = ''; document.getElementById('results').innerHTML = '';
lastResults = [];
updateResultCounts();
return; return;
} }
@@ -228,6 +296,7 @@
if (ort) params.append('ort', ort); if (ort) params.append('ort', ort);
if (kundennummer) params.append('kundennummer', kundennummer); if (kundennummer) params.append('kundennummer', kundennummer);
if (fachrichtung) params.append('fachrichtung', fachrichtung); if (fachrichtung) params.append('fachrichtung', fachrichtung);
if (telefon) params.append('telefon', telefon);
if (query) params.append('q', query); if (query) params.append('q', query);
fetch(`/search?${params.toString()}`) fetch(`/search?${params.toString()}`)
@@ -245,6 +314,8 @@
if (data.length === 0) { if (data.length === 0) {
resultsDiv.innerHTML = '<div class="alert alert-info">Keine Ergebnisse gefunden.</div>'; resultsDiv.innerHTML = '<div class="alert alert-info">Keine Ergebnisse gefunden.</div>';
lastResults = [];
updateResultCounts();
return; return;
} }
@@ -272,11 +343,16 @@
`; `;
resultsDiv.appendChild(card); resultsDiv.appendChild(card);
}); });
lastResults = data;
updateResultCounts();
}) })
.catch(error => { .catch(error => {
console.error('Fehler:', error); console.error('Fehler:', error);
document.getElementById('results').innerHTML = document.getElementById('results').innerHTML =
`<div class="alert alert-danger">${error.message}</div>`; `<div class="alert alert-danger">${error.message}</div>`;
lastResults = [];
updateResultCounts();
}) })
.finally(() => { .finally(() => {
document.getElementById('loading').style.display = 'none'; document.getElementById('loading').style.display = 'none';
@@ -289,13 +365,32 @@
document.getElementById('ortInput'), document.getElementById('ortInput'),
document.getElementById('kundennummerInput'), document.getElementById('kundennummerInput'),
document.getElementById('fachrichtungInput'), document.getElementById('fachrichtungInput'),
document.getElementById('telefonInput'),
document.getElementById('searchInput') document.getElementById('searchInput')
]; ];
searchInputs.forEach(input => { const resetIcons = [
document.getElementById('nameReset'),
document.getElementById('ortReset'),
document.getElementById('kundennummerReset'),
document.getElementById('fachrichtungReset'),
document.getElementById('telefonReset'),
document.getElementById('searchReset')
];
searchInputs.forEach((input, index) => {
input.addEventListener('input', function() { input.addEventListener('input', function() {
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(searchCustomers, 300); searchTimeout = setTimeout(searchCustomers, 300);
// Reset-Icon anzeigen/verstecken
resetIcons[index].classList.toggle('visible', this.value.length > 0);
});
// Reset-Funktionalität
resetIcons[index].addEventListener('click', function() {
searchInputs[index].value = '';
searchCustomers();
}); });
}); });
@@ -306,15 +401,17 @@
const ort = urlParams.get('ort'); const ort = urlParams.get('ort');
const kundennummer = urlParams.get('kundennummer'); const kundennummer = urlParams.get('kundennummer');
const fachrichtung = urlParams.get('fachrichtung'); const fachrichtung = urlParams.get('fachrichtung');
const telefon = urlParams.get('telefon');
const query = urlParams.get('q'); const query = urlParams.get('q');
if (name) document.getElementById('nameInput').value = name; if (name) document.getElementById('nameInput').value = name;
if (ort) document.getElementById('ortInput').value = ort; if (ort) document.getElementById('ortInput').value = ort;
if (kundennummer) document.getElementById('kundennummerInput').value = kundennummer; if (kundennummer) document.getElementById('kundennummerInput').value = kundennummer;
if (fachrichtung) document.getElementById('fachrichtungInput').value = fachrichtung; if (fachrichtung) document.getElementById('fachrichtungInput').value = fachrichtung;
if (telefon) document.getElementById('telefonInput').value = telefon;
if (query) document.getElementById('searchInput').value = query; if (query) document.getElementById('searchInput').value = query;
if (name || ort || kundennummer || fachrichtung || query) { if (name || ort || kundennummer || fachrichtung || telefon || query) {
searchCustomers(); searchCustomers();
} }
}); });