27 Commits

Author SHA1 Message Date
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
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
6 changed files with 1534 additions and 245 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

214
README.md
View File

@@ -1,179 +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
## Frontend-Funktionen
### Suchfunktion
- Live-Suche mit 300ms Debounce
- Spezifische Suchfelder für präzise Suche
- Allgemeine Suche für breite Suche
- Kombinierbare Suchkriterien
### 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:
```bash ```bash
python app.py # Nach Name suchen
curl "http://localhost:5001/search?name=Schmidt"
# Nach Ort suchen
curl "http://localhost:5001/search?ort=Berlin"
# Nach Kundennummer suchen
curl "http://localhost:5001/search?kundennummer=12345"
# Nach Fachrichtung suchen
curl "http://localhost:5001/search?fachrichtung=Allgemeinmedizin"
# Nach Telefonnummer suchen
curl "http://localhost:5001/search?telefon=030"
# Kombinierte Suche
curl "http://localhost:5001/search?name=Schmidt&ort=Berlin&fachrichtung=Allgemeinmedizin"
``` ```
### Logging ### Allgemeine Suche
- Backend-Logs werden mit Python's logging-Modul erstellt
- Log-Level: DEBUG
- Logs werden in der Konsole ausgegeben
## Wartung
### Container-Verwaltung
```bash ```bash
# Container stoppen # Suche in allen Feldern
docker-compose down curl "http://localhost:5001/search?q=Schmidt"
# Container starten
docker-compose up
# Container im Hintergrund starten
docker-compose up -d
# Container-Logs anzeigen
docker-compose logs -f
``` ```
### Datenaktualisierung ### Beispiel-Response
1. CSV-Datei aktualisieren ```json
2. Container neu bauen und starten: [
```bash {
docker-compose down "Vorname": "Max",
docker-compose up --build "Nachname": "Mustermann",
"Nummer": "12345",
"Ort": "Berlin",
"Fachrichtung": "Allgemeinmedizin",
"Tel": "030123456",
"Email": "max@example.com"
}
]
``` ```
## Sicherheit ## Versionen
- Alle externen Links öffnen sich in neuen Tabs ### v1.0.1
- Sicherheitsattribute für externe Links (noopener, noreferrer) - Telefonnummer-Suchfeld hinzugefügt
- Input-Validierung im Backend - Reset-Icons für alle Suchfelder
- Fehlerbehandlung für ungültige Daten - Verbesserte Positionierung der UI-Elemente
- Optimierte Suchlogik
- CSV-Datei in data-Verzeichnis verschoben
## Browser-Kompatibilität ### v1.0.0
- Erste Version
Die Anwendung wurde getestet mit: - Grundlegende Suchfunktionalität
- Chrome (neueste Version) - Klickbare Links für Telefon, E-Mail und Adressen
- Firefox (neueste Version) - Share-Funktion für Suchergebnisse
- Edge (neueste Version)
- Safari (neueste Version)

96
app.py
View File

@@ -3,11 +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, 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 = "1.0.3"
# Pfad zur CSV-Datei
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})
@@ -16,11 +29,18 @@ 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') # 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
@@ -35,61 +55,67 @@ 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
name = request.args.get('name', '').strip()
ort = request.args.get('ort', '').strip()
kundennummer = request.args.get('kundennummer', '').strip()
fachrichtung = request.args.get('fachrichtung', '').strip()
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 = (
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)
)
else:
# Spezifische Suche
mask = pd.Series(True, index=df.index) mask = pd.Series(True, index=df.index)
# Spezifische Suchkriterien anwenden
if name: if name:
name_mask = ( name_mask = (
df['Vorname'].str.lower().str.contains(name, na=False) | df['Vorname'].str.contains(name, case=False, na=False) |
df['Nachname'].str.lower().str.contains(name, na=False) df['Nachname'].str.contains(name, case=False, na=False)
) )
mask &= name_mask mask &= name_mask
if ort: if ort:
ort_mask = df['Ort'].str.lower().str.contains(ort, na=False) ort_mask = df['Ort'].str.contains(ort, case=False, na=False)
mask &= ort_mask mask &= ort_mask
if kundennummer: if kundennummer:
kunden_mask = df['Nummer'].astype(str).str.contains(kundennummer, na=False) kundennummer_mask = df['Nummer'].astype(str).str.contains(kundennummer, case=False, na=False)
mask &= kunden_mask mask &= kundennummer_mask
if fachrichtung: if fachrichtung:
fach_mask = df['Fachrichtung'].str.lower().str.contains(fachrichtung, na=False) fachrichtung_mask = df['Fachrichtung'].str.contains(fachrichtung, case=False, na=False)
mask &= fach_mask mask &= fachrichtung_mask
# Wenn eine allgemeine Suche vorhanden ist, diese zusätzlich anwenden if telefon:
if query: telefon_mask = df['Tel'].astype(str).str.contains(telefon, case=False, na=False)
query_mask = ( mask &= telefon_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)
)
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")
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)

1172
data/customers.csv Normal file

File diff suppressed because it is too large Load Diff

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

@@ -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,45 @@
.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;
}
.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>
@@ -104,29 +158,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 +219,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 +237,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 +274,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,55 +308,63 @@
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()}`)
.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>';
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);
}); });
// 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 => { .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 +377,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 +413,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();
} }
}); });