100 Commits

Author SHA1 Message Date
156078ff3a Version 1.2.13: Korrektur der SQL-Abfrage für die allgemeine Suche 2025-03-20 11:55:07 +01:00
5d56c7128e Remove: .bak-Dateien aus dem Repository entfernt und zur .gitignore hinzugefügt 2025-03-20 11:06:54 +01:00
98f887f51e Remove: CSV-Dateien aus dem Repository entfernt und zur .gitignore hinzugefügt 2025-03-20 10:23:45 +01:00
f46947b6d4 Fix: Performance-Optimierungen und verbessertes Highlighting in index.html 2025-03-20 10:14:54 +01:00
c1b55c3579 Version 1.2.12: Performance-Optimierungen und verbessertes Highlighting 2025-03-20 09:57:17 +01:00
6a2e290d54 Release v1.2.11: Filterung von intern-Einträgen und verbesserte SQL-Abfrage 2025-03-19 19:09:19 +01:00
7263464b89 Release v1.2.11: Filterung von intern-Einträgen und verbesserte SQL-Abfrage 2025-03-19 19:08:34 +01:00
7b88e62de0 Release v1.2.10: Version aus VERSION-Variable und verbesserte Zeilenabstände 2025-03-19 18:54:06 +01:00
d50ea60af9 Release v1.2.10: Version aus VERSION-Variable und verbesserte Zeilenabstände 2025-03-19 18:52:37 +01:00
ec559f5c72 Fix: Suchergebnisse werden wieder korrekt angezeigt 2025-03-19 18:39:29 +01:00
c454620ae1 Release v1.2.9: Verbesserte Adress-Links und Docker-Port-Mapping 2025-03-19 15:01:59 +01:00
94c381fbfc Aktualisierung der Dokumentation und Versionsangabe auf v1.2.8 2025-03-19 14:12:30 +01:00
06576b08d9 Verbesserte Tag-Anzeige: Badge neben dem Teilen-Button mit farblicher Hervorhebung 2025-03-19 14:07:58 +01:00
465a6f058a Merge-Konflikte gelöst und Feldreihenfolge korrigiert 2025-03-19 13:35:49 +01:00
daf7499b4e Korrektur der Feldreihenfolge in den Suchergebnissen und Verbesserung der Tag-Anzeige 2025-03-19 13:34:10 +01:00
fade9b8d62 Hervorhebung des Tag-Feldes in den Suchergebnissen: - Blauer Hintergrund für MEDISOFT-Tags - Oranger Hintergrund für MEDICONSULT-Tags - Verbesserte visuelle Darstellung der Tags 2025-03-19 13:18:22 +01:00
611a5dd906 Fix: Teilen-Button und Telefonnummern-Links wieder hinzugefügt, Zeilenabstand in Suchergebnissen optimiert 2025-03-19 09:56:06 +01:00
35645fc671 Fix: Login-Funktionalität wiederhergestellt und Passwort aktualisiert 2025-03-18 16:11:10 +01:00
fab869eb58 Logo mit Link zu medisoftware.de versehen 2025-03-18 15:47:56 +01:00
6cd8f199c4 Gitignore: docker-compose.yml hinzugefügt 2025-03-18 15:42:48 +01:00
948a17b739 Docker Compose: Beispiel-Konfiguration hinzugefügt und Original aus Repository entfernt 2025-03-18 15:41:23 +01:00
f2290cf77f Version 1.2.6: Verbesserte Suchfunktion und Highlighting - app.py aktualisiert 2025-03-18 15:33:01 +01:00
0627b6ff33 Version 1.2.6: Verbesserte Suchfunktion und Highlighting 2025-03-18 15:32:20 +01:00
24ba040537 Version 1.2.5: Dokumentation aktualisiert 2025-03-18 15:12:50 +01:00
997786be54 Feature: Suchfeld für Fachrichtung hinzugefügt 2025-03-18 15:11:15 +01:00
c4974787d4 Dokumentation: Korrigiere Versionsangabe am Ende der README.md 2025-03-18 14:08:04 +01:00
a42bdaa721 Dokumentation: Aktualisiere Versionsnummer in README.md auf v1.2.4 2025-03-18 14:06:31 +01:00
49938a1085 Version 1.2.4: Performance-Optimierung durch Datenbankindizes 2025-03-18 14:02:07 +01:00
d0a27fe095 Performance: Indizes für alle Suchfelder hinzugefügt 2025-03-18 14:01:08 +01:00
d388bce528 Footer Text geändert 2025-03-18 13:59:57 +01:00
aabb4540c9 Version 1.2.3: Performance-Optimierung durch Entfernung von Debug-Ausgaben 2025-03-18 13:57:32 +01:00
ffde078238 Git: Füge data/customers.db zur .gitignore hinzu 2025-03-18 13:55:17 +01:00
9e320c4eb2 Performance: Entferne alle console.log Anweisungen 2025-03-18 13:53:14 +01:00
58ed5fe867 Performance: Entferne Debug-Logging aus createPhoneLink Funktion 2025-03-18 13:50:32 +01:00
72676edc10 Git: Füge data/customers.db zur .gitignore hinzu 2025-03-18 13:48:55 +01:00
d5954eac89 Version 1.2.2: Verbesserte Telefonnummern-Formatierung und Dokumentation 2025-03-18 13:47:41 +01:00
68a2db28a1 Dokumentation: Aktualisierte README.md und CHANGELOG.md für Version 1.2.2 2025-03-18 13:46:33 +01:00
13709de515 Version 1.2.2: Verbesserte Telefonnummern-Formatierung 2025-03-18 13:43:07 +01:00
2c65d5f651 Dokumentation: Aktualisierung für Version 1.2.1 2025-03-18 12:56:15 +01:00
33ecb79e0b Version 1.2.1: Verbesserte CSV-Import-Funktionalität und Login-Fix 2025-03-18 12:54:31 +01:00
00bb197620 CSS in separate Datei ausgelagert 2025-03-18 12:34:14 +01:00
8e7d3da07f Release v1.2.0: IP-Subnetz-Prüfung und Debug-Logging 2025-03-18 12:32:06 +01:00
35e0d0a783 Beispiel-Umgebungsvariablen hinzugefügt 2025-03-18 12:28:46 +01:00
40196fa28f Debug-Logging für IP-Adressen hinzugefügt 2025-03-18 12:26:46 +01:00
7b5e90e3bd IP-Subnetz-Prüfung korrigiert und Debug-Logging hinzugefügt 2025-03-18 12:25:53 +01:00
3c48988e88 Routenplanung für Adressen hinzugefügt 2025-03-18 11:35:46 +01:00
869acdcb18 IP-Überprüfung für Telefonnummern-Links implementiert 2025-03-18 11:28:27 +01:00
91af1dfca0 README und CHANGELOG für v1.0.7 aktualisiert 2025-03-18 11:15:36 +01:00
c271bc1f60 IP-Überprüfung mit X-Forwarded-For-Header hinzugefügt 2025-03-18 11:10:24 +01:00
9c04bb973e Version im Footer hinzugefügt 2025-03-18 10:24:18 +01:00
6d7298548b Dokumentation für Release v1.2.0 aktualisiert 2025-03-18 10:19:21 +01:00
42a11abe61 Login 2025-03-18 10:10:17 +01:00
3c6d2f7c45 Titel von login.html auf 'medisoftware Kundensuche' geändert 2025-03-18 09:44:45 +01:00
d143d6c7b6 Logo hinzugefügt 2025-03-18 09:10:57 +01:00
e6638b737d Logo im oberen Bereich der App zentriert hinzugefügt 2025-03-18 09:08:53 +01:00
1f493e0a37 Korrektur: medisw: Link-Parameter wiederhergestellt 2025-03-18 08:58:12 +01:00
469ad0ce05 Kundennummer wird um 12000 reduziert in Anzeige und medisw: Link 2025-03-18 08:57:40 +01:00
8a8c13e407 Port 2025-03-18 00:45:45 +01:00
18974eb69b Footer: Nur das Wort 'medisoftware' ist jetzt verlinkt 2025-03-18 00:41:07 +01:00
9e406ed7a3 Footer: Nur das Wort 'medisoftware' ist jetzt verlinkt 2025-03-18 00:38:57 +01:00
8b82a44ad8 Footer-Text aktualisiert: Made with ❤️ and 🍺 by medisoftware 2025-03-18 00:37:39 +01:00
528baff7b5 Aktuelle Version in README.md auf v1.1.0 aktualisiert 2025-03-18 00:33:54 +01:00
e105dc4663 Dokumentation für Release v1.1.0 aktualisiert 2025-03-18 00:32:05 +01:00
3f69ba6198 Merge pull request 'sophisticated-search' (#1) from sophisticated-search into main
Reviewed-on: #1
2025-03-18 00:24:21 +01:00
7e812eb835 Verbesserte Darstellung der Telefonnummern: Separate Felder für Telefon, Firma und Mobilfunk, Faxnummer entfernt 2025-03-18 00:19:16 +01:00
ece7f984f7 UI-Verbesserungen: - Größere Abstände zwischen Suchergebnissen - Dünne Trennlinien zwischen den Karten - Verbesserte visuelle Hierarchie 2025-03-17 23:41:40 +01:00
6cfc2e0162 Version 1.0.6: Kombinierte Suche über mehrere Felder 2025-03-17 22:49:27 +01:00
bbcd04fd21 Dokumentation: Verlinkung der Version mit dem Changelog 2025-03-17 22:24:44 +01:00
89a5152ada Dokumentation: Aktualisierung der Version in der README 2025-03-17 22:23:34 +01:00
e6f43ca10a Dokumentation: Aktualisierung der README und des Changelogs für Version 1.0.5 2025-03-17 22:15:01 +01:00
da3ef358d2 Dokumentation: Aktualisierung der README und des Changelogs für Version 1.0.5 2025-03-17 22:10:53 +01:00
d77ee8ef04 Version 1.0.5: Verbesserte Suchfunktion - Leere Ergebnisliste bei leeren Suchfeldern 2025-03-17 22:05:22 +01:00
cbc95c0d29 Version 1.0.4: Verbesserte Adressanzeige mit Location-Icon 2025-03-17 22:01:27 +01:00
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
14 changed files with 1353 additions and 442 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Flask-Konfiguration
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=your-secret-key-here
# Datenbank-Konfiguration
DATABASE_URL=sqlite:///customers.db
# Authentifizierung
STATIC_PASSWORD=your-static-password-here
# IP-Bereiche für direkten Zugriff (ohne Login)
ALLOWED_IP_RANGES=192.168.0.0/24,192.168.177.0/24,213.178.68.218/29
# Logging-Konfiguration
LOG_LEVEL=INFO

22
.gitignore vendored
View File

@@ -17,6 +17,7 @@ pip-delete-this-directory.txt
.vscode/ .vscode/
*.swp *.swp
*.swo *.swo
*.bak
# Logs # Logs
*.log *.log
@@ -46,3 +47,24 @@ coverage.xml
# Daten # Daten
spezexpo.csv spezexpo.csv
# Database
*.db
data/customers.db
data/customers.csv
docker-compose.yml
/data/*.csv
# Virtual Environment
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
.installed.cfg
*.egg

190
CHANGELOG.md Normal file
View File

@@ -0,0 +1,190 @@
# 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.2.13] - 2024-03-20
### Fixed
- Korrektur der Parameteranzahl in der SQL-Abfrage für die allgemeine Suche
- Behebung des Fehlers bei der Suche in allen Datenbankfeldern
## [v1.2.12] - 2024-03-19
### Geändert
- Performance-Optimierung der Suchfunktion durch Reduzierung der Suchfelder
- Verbesserte Suchgeschwindigkeit durch LIMIT in SQL-Abfragen
- Optimiertes Debounce-Intervall für Live-Suche
- Verbessertes Highlighting für Teilstrings in Suchergebnissen
## [v1.2.11] - 2024-03-19
### Geändert
- Einträge mit der Fachrichtung "intern" werden aus den Suchergebnissen gefiltert
- Verbesserte SQL-Abfrage für die Suchergebnisse
## [v1.2.10] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Adress-Links mit Location- und Route-Icons
- Korrektur des Docker-Port-Mappings für bessere Erreichbarkeit
## [v1.2.9] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Adress-Links mit Location- und Route-Icons
- Korrektur des Docker-Port-Mappings für bessere Erreichbarkeit
## [v1.2.8] - 2024-03-19
### Geändert
- Verbesserte Tag-Anzeige: Badge neben dem Teilen-Button mit farblicher Hervorhebung (blau für MEDISOFT, orange für MEDICONSULT)
## [v1.2.7] - 2024-03-19
### Geändert
- Korrektur der Feldreihenfolge in den Suchergebnissen
- Verbesserung der Tag-Anzeige
## [v1.2.6] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Kundennummern in den Suchergebnissen
- Optimierte Anzeige der Kundennummern in der Detailansicht
- Anpassung der Kundennummernberechnung für die MEDISOFT-Integration
- Verbesserte Formatierung der Kundennummern in der Benutzeroberfläche
## [v1.2.5] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Kundennummern in den Suchergebnissen
- Optimierte Anzeige der Kundennummern in der Detailansicht
- Anpassung der Kundennummernberechnung für die MEDISOFT-Integration
- Verbesserte Formatierung der Kundennummern in der Benutzeroberfläche
## [v1.2.4] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Kundennummern in den Suchergebnissen
- Optimierte Anzeige der Kundennummern in der Detailansicht
- Anpassung der Kundennummernberechnung für die MEDISOFT-Integration
- Verbesserte Formatierung der Kundennummern in der Benutzeroberfläche
## [v1.2.3] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Kundennummern in den Suchergebnissen
- Optimierte Anzeige der Kundennummern in der Detailansicht
- Anpassung der Kundennummernberechnung für die MEDISOFT-Integration
- Verbesserte Formatierung der Kundennummern in der Benutzeroberfläche
## [v1.2.2] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Kundennummern in den Suchergebnissen
- Optimierte Anzeige der Kundennummern in der Detailansicht
- Anpassung der Kundennummernberechnung für die MEDISOFT-Integration
- Verbesserte Formatierung der Kundennummern in der Benutzeroberfläche
## [v1.2.1] - 2024-03-19
### Geändert
- Verbesserte Darstellung der Kundennummern in den Suchergebnissen
- Optimierte Anzeige der Kundennummern in der Detailansicht
- Anpassung der Kundennummernberechnung für die MEDISOFT-Integration
- Verbesserte Formatierung der Kundennummern in der Benutzeroberfläche
## [v1.2.0] - 2024-03-19
### Hinzugefügt
- Neue Spalte "Tag" in der Kundendatenbank
- Unterscheidung zwischen MEDISOFT und MEDICONSULT Kunden
- Filteroption für die Anzeige von MEDISOFT oder MEDICONSULT Kunden
- Verbesserte Darstellung der Kundennummern in den Suchergebnissen
- Optimierte Anzeige der Kundennummern in der Detailansicht
- Anpassung der Kundennummernberechnung für die MEDISOFT-Integration
- Verbesserte Formatierung der Kundennummern in der Benutzeroberfläche
## [v1.1.0] - 2024-03-19
### Hinzugefügt
- Neue Spalte "Tag" in der Kundendatenbank
- Unterscheidung zwischen MEDISOFT und MEDICONSULT Kunden
- Filteroption für die Anzeige von MEDISOFT oder MEDICONSULT Kunden
## [v1.0.0] - 2024-03-19
### Hinzugefügt
- Erste Version der Anwendung
- Grundlegende Suchfunktionalität
- Anzeige von Kundendetails
- Integration mit MEDISOFT
- Responsive Design
- IP-basierte Zugriffskontrolle
## [1.2.6] - 2024-03-19
### Geändert
- Verbesserte Suchfunktion: Keine Ergebnisse mehr bei leeren Suchfeldern
- Optimiertes Highlighting der Suchergebnisse für alle Suchfelder
- Fachrichtung wird jetzt in den Suchergebnissen hervorgehoben
## [1.2.5] - 2024-03-19
### Hinzugefügt
- Neues Suchfeld für Fachrichtung
- Index für das Fachrichtung-Feld in der Datenbank
- Fachrichtung in der allgemeinen Suche integriert
## [1.2.4] - 2024-03-19
### Geändert
- Performance-Optimierung: Indizes für alle Suchfelder hinzugefügt
- Verbesserte Suchgeschwindigkeit durch optimierte Datenbankindizes
- Zusammengesetzter Index für die häufigste Suchkombination (Name + Ort) hinzugefügt
## [1.2.3] - 2024-03-19
### Geändert
- Performance-Optimierung: Entfernung aller console.log Anweisungen
- Verbesserte Code-Qualität durch Entfernung von Debug-Ausgaben
## [1.2.2] - 2024-03-19
### Geändert
- Verbesserte Telefonnummern-Formatierung: Führende "0" wird immer hinzugefügt, wenn der Benutzer von einer erlaubten IP-Adresse zugreift
- Debug-Logging für Telefonnummern-Formatierung hinzugefügt
- Verbesserte Benutzerfreundlichkeit bei der Anzeige von Telefonnummern
### Behoben
- Problem mit fehlender führender "0" bei Telefonnummern für autorisierte Benutzer
## [1.2.1] - 2024-03-18
### Geändert
- Verbesserte CSV-Import-Funktionalität mit pandas
- Korrektur des Login-Prozesses
- Verbesserte Fehlerbehandlung und Logging
- Anpassung der Spaltennamen für den CSV-Import
### Behoben
- Login-Prozess funktioniert jetzt korrekt mit dem Passwort aus der .env Datei
- CSV-Import verarbeitet Anführungszeichen korrekt
- Verbesserte Fehlerbehandlung beim Datenbankimport
## [1.2.0] - 2024-03-18
### Geändert
- IP-Subnetz-Prüfung korrigiert (CIDR-Notation wird jetzt korrekt ausgewertet)
- Debug-Logging für IP-Adressen hinzugefügt
- Beispiel-Umgebungsvariablen (.env.example) hinzugefügt
### Hinzugefügt
- IP-basierte Zugriffssteuerung für medisoftware-Links
- IP-basierte Zugriffssteuerung für Telefonnummern-Links
- Google Maps Integration für Adressen
- Version im Footer angezeigt
### Verbessert
- Verbesserte Fehlerbehandlung bei der IP-Überprüfung
- Bessere Dokumentation der Konfigurationsmöglichkeiten
## [v1.1.0] - 2024-03-17
### Hinzugefügt
- Benutzer-Login für nicht-autorisierte IPs
- Verbesserte Darstellung der Telefonnummern
- Responsive Design für mobile Geräte
### Verbessert
- Optimierte Suchfunktion
- Verbesserte Benutzeroberfläche
## [v1.0.0] - 2024-03-17
### Hinzugefügt
- Erste Version mit grundlegenden Suchfunktionen
- Echtzeit-Suche über Kundendaten
- Hervorhebung von Suchbegriffen in den Ergebnissen
- Klickbare Links für Telefonnummern, E-Mail-Adressen und Adressen
- Docker-Container für einfache Installation und Deployment

View File

@@ -10,8 +10,12 @@ COPY requirements.txt .
# Installiere Abhängigkeiten # Installiere Abhängigkeiten
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Kopiere den Anwendungscode # Erstelle das data Verzeichnis und setze Berechtigungen
COPY . . RUN mkdir -p /app/data && \
chmod 755 /app/data
# Kopiere die Anwendungsdateien
COPY . /app/
# Exponiere Port 5000 # Exponiere Port 5000
EXPOSE 5000 EXPOSE 5000

227
README.md
View File

@@ -1,179 +1,112 @@
# medisoftware Kundensuche # Medi-Customers
Eine Flask-basierte Webanwendung zur Suche in Kundendaten aus einer CSV-Datei. Eine moderne Webanwendung zur Suche und Verwaltung von Kundendaten, die MEDISOFT und MEDICONSULT Daten kombiniert.
## Features ## Features
- Live-Suche während der Eingabe - 🔍 Echtzeit-Suche über alle Kundendaten
- Spezifische Suchfelder für: - 📱 Responsive Design für alle Geräte
- Kundennummer - 🔒 IP-basierte Zugriffskontrolle
- Name (Vor- und Nachname) - 🔗 Direkte Integration mit MEDISOFT
- Fachrichtung - 🏥 Unterscheidung zwischen MEDISOFT und MEDICONSULT Kunden
- Ort - 🎨 Farbliche Hervorhebung der Kundentypen (blau für MEDISOFT, orange für MEDICONSULT)
- Allgemeine Suche über alle Felder - 📍 Verbesserte Adress-Links mit Location- und Route-Icons
- Klickbare Links für:
- Telefonnummern (tel:)
- E-Mail-Adressen (mailto:)
- Adressen (Google Maps)
- Kundennummern (KKBefe-System)
- Teilen-Funktion für einzelne Suchergebnisse
- Responsive Design mit Bootstrap
- Docker-Container-Unterstützung
## Technische Details ## Version
### Technologie-Stack Aktuelle Version: 1.2.13
- **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. Klonen Sie das Repository:
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:
```bash
pip install -r requirements.txt
```
4. Anwendung starten:
```bash
python app.py
``` ```
### Docker-Container 2. Erstellen Sie die erforderlichen Verzeichnisse:
```bash
mkdir -p data
```
1. Docker installieren 3. Starten Sie die Anwendung mit Docker Compose:
2. Container mit Docker Compose starten:
```bash ```bash
docker-compose up --build docker-compose up --build
``` ```
Die Anwendung ist dann unter `http://localhost:5001` erreichbar.
## API-Endpunkte Die Anwendung ist dann unter `http://localhost:5000` erreichbar.
### GET /
- Rendert die Hauptseite
### GET /search
- 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 ## Entwicklung
### Debug-Modus Die Anwendung ist in Python mit Flask entwickelt und verwendet SQLite als Datenbank. Das Frontend wurde mit HTML, CSS und JavaScript implementiert.
Die Anwendung läuft standardmäßig im Debug-Modus:
## Lizenz
Alle Rechte vorbehalten. © 2024 medisoftware
## Konfiguration
Die Anwendung kann über folgende Umgebungsvariablen konfiguriert werden:
- `FLASK_APP`: Die Hauptanwendungsdatei (Standard: app.py)
- `FLASK_ENV`: Die Flask-Umgebung (development/production)
- `SECRET_KEY`: Der geheime Schlüssel für Flask-Sessions
- `DATABASE_URL`: Die URL zur SQLite-Datenbank
- `STATIC_PASSWORD`: Das Passwort für die Login-Seite
- `ALLOWED_IP_RANGES`: Komma-getrennte Liste von IP-Bereichen, die direkten Zugriff haben
- `LOG_LEVEL`: Das Logging-Level (INFO/DEBUG)
## IP-Bereiche
Die Anwendung unterstützt CIDR-Notation für IP-Bereiche. Beispiele:
- Einzelne IP: 192.168.1.1/32
- Subnetz: 192.168.1.0/24
- Größeres Netzwerk: 10.0.0.0/8
## API-Beispiele
### Suche nach Name
```bash ```bash
python app.py curl "http://localhost:5001/search?name=Mustermann"
``` ```
### Logging ### Suche nach Ort
- 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 curl "http://localhost:5001/search?ort=Berlin"
docker-compose down
# Container starten
docker-compose up
# Container im Hintergrund starten
docker-compose up -d
# Container-Logs anzeigen
docker-compose logs -f
``` ```
### Datenaktualisierung ### Suche nach Kundennummer
1. CSV-Datei aktualisieren
2. Container neu bauen und starten:
```bash ```bash
docker-compose down curl "http://localhost:5001/search?kundennummer=12345"
docker-compose up --build
``` ```
## Sicherheit ### Suche nach Fachrichtung
```bash
curl "http://localhost:5001/search?fachrichtung=Zahnarzt"
```
- Alle externen Links öffnen sich in neuen Tabs ### Suche nach Telefon
- Sicherheitsattribute für externe Links (noopener, noreferrer) ```bash
- Input-Validierung im Backend curl "http://localhost:5001/search?telefon=030"
- Fehlerbehandlung für ungültige Daten ```
## Browser-Kompatibilität ### Allgemeine Suche
```bash
curl "http://localhost:5001/search?q=Suchbegriff"
```
Die Anwendung wurde getestet mit: ### Kombinierte Suche
- Chrome (neueste Version) ```bash
- Firefox (neueste Version) # Suche nach Fachrichtung und Ort
- Edge (neueste Version) curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin"
- Safari (neueste Version)
# Suche nach Name und Telefon
curl "http://localhost:5001/search?name=Mustermann&telefon=030"
# Suche nach mehreren Kriterien
curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt"
```
## Version
Aktuelle Version: [v1.2.4](CHANGELOG.md#v124---2024-03-19)

406
app.py
View File

@@ -1,95 +1,379 @@
from flask import Flask, render_template, request, jsonify, url_for from flask import Flask, render_template, request, jsonify, url_for, redirect, session
import pandas as pd 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
import ipaddress
import csv
import sqlite3
from functools import wraps
app = Flask(__name__, static_folder='static') app = Flask(__name__, static_folder='static')
logging.basicConfig(level=logging.DEBUG) app.secret_key = os.getenv('SECRET_KEY', 'default-secret-key')
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Version der Anwendung
VERSION = "1.2.13"
# Pfad zur Datenbank
DB_FILE = 'data/customers.db'
# Lade Umgebungsvariablen
load_dotenv()
# Statisches Passwort aus der .env Datei
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password')
ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',')
def isIPInSubnet(ip, subnet):
"""Überprüft, ob eine IP-Adresse in einem Subnetz liegt."""
try:
# Teile die IP und das Subnetz in ihre Komponenten
subnet_ip, bits = subnet.split('/')
ip_parts = [int(x) for x in ip.split('.')]
subnet_parts = [int(x) for x in subnet_ip.split('.')]
# Konvertiere IPs in 32-bit Zahlen
ip_num = (ip_parts[0] << 24) | (ip_parts[1] << 16) | (ip_parts[2] << 8) | ip_parts[3]
subnet_num = (subnet_parts[0] << 24) | (subnet_parts[1] << 16) | (subnet_parts[2] << 8) | subnet_parts[3]
# Erstelle die Subnetzmaske
mask = ~((1 << (32 - int(bits))) - 1)
# Prüfe, ob die IP im Subnetz liegt
return (ip_num & mask) == (subnet_num & mask)
except Exception as e:
logger.error(f"Fehler bei der IP-Überprüfung: {str(e)}")
return False
def get_db_connection():
"""Erstellt eine neue Datenbankverbindung mit Timeout"""
conn = sqlite3.connect(DB_FILE, timeout=20)
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
conn = get_db_connection()
c = conn.cursor()
try:
# Erstelle die Kunden-Tabelle
c.execute('''
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nummer TEXT,
name TEXT,
strasse TEXT,
plz TEXT,
ort TEXT,
telefon TEXT,
mobil TEXT,
email TEXT,
bemerkung TEXT,
fachrichtung TEXT,
tag TEXT,
handy TEXT,
tele_firma TEXT,
kontakt1 TEXT,
kontakt2 TEXT,
kontakt3 TEXT
)
''')
# Erstelle Indizes für alle Suchfelder
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_nummer ON customers(nummer)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_strasse ON customers(strasse)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_plz ON customers(plz)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_ort ON customers(ort)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_telefon ON customers(telefon)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_mobil ON customers(mobil)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_fachrichtung ON customers(fachrichtung)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_tag ON customers(tag)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_handy ON customers(handy)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_tele_firma ON customers(tele_firma)')
# Erstelle einen zusammengesetzten Index für die häufigste Suchkombination
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name_ort ON customers(name, ort)')
conn.commit()
logger.info('Datenbank initialisiert')
except Exception as e:
logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}')
raise
finally:
conn.close()
def import_csv():
"""Importiert die CSV-Datei in die Datenbank"""
conn = None
try:
conn = get_db_connection()
c = conn.cursor()
# Lösche bestehende Daten
c.execute('DELETE FROM customers')
# Importiere MEDISOFT-Daten
if os.path.exists('data/customers.csv'):
logger.info("Importiere MEDISOFT-Daten...")
df = pd.read_csv('data/customers.csv', encoding='iso-8859-1')
df.columns = df.columns.str.strip().str.replace('"', '')
df = df.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x)
for _, row in df.iterrows():
c.execute('''
INSERT INTO customers (
name, nummer, strasse, plz, ort, telefon, mobil, email,
fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'medisoft',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
))
else:
logger.warning("MEDISOFT CSV-Datei nicht gefunden")
# Importiere MEDICONSULT-Daten
if os.path.exists('data/customers_snk.csv'):
logger.info("Importiere MEDICONSULT-Daten...")
df_snk = pd.read_csv('data/customers_snk.csv', encoding='iso-8859-1')
df_snk.columns = df_snk.columns.str.strip().str.replace('"', '')
df_snk = df_snk.apply(lambda x: x.str.strip().str.replace('"', '') if x.dtype == "object" else x)
for _, row in df_snk.iterrows():
c.execute('''
INSERT INTO customers (
name, nummer, strasse, plz, ort, telefon, mobil, email,
fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], 'mediconsult',
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
))
else:
logger.warning("MEDICONSULT CSV-Datei nicht gefunden")
conn.commit()
logger.info("CSV-Daten erfolgreich in die Datenbank importiert")
except Exception as e:
logger.error(f"Fehler beim Importieren der CSV-Datei: {str(e)}")
raise
finally:
if conn:
conn.close()
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})
# CSV-Datei laden @app.route('/login', methods=['GET', 'POST'])
def load_data(): def login():
try: # Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt
logger.info("Versuche CSV-Datei zu laden...") client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
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') # Überprüfe, ob die Client-IP in einem der erlaubten Bereichen liegt
df = clean_dataframe(df) is_allowed = any(isIPInSubnet(client_ip, range.strip()) for range in ALLOWED_IP_RANGES if range.strip())
logger.info(f"CSV-Datei erfolgreich geladen. {len(df)} Einträge gefunden.")
return df if is_allowed:
except Exception as e: logger.info(f"Client-IP {client_ip} ist in einem erlaubten Bereich, automatischer Login")
logger.error(f"Fehler beim Laden der CSV-Datei: {str(e)}") session['logged_in'] = True
return None return redirect(url_for('index'))
if request.method == 'POST':
password = request.form.get('password')
if password == STATIC_PASSWORD:
session['logged_in'] = True
logger.info("Erfolgreicher Login")
return redirect(url_for('index'))
else:
logger.warning("Falsches Passwort eingegeben")
return render_template('login.html', error="Falsches Passwort")
logger.info("Zeige Login-Seite")
return render_template('login.html')
@app.route('/') @app.route('/')
def index(): def index():
return render_template('index.html') logger.info(f"Index-Route aufgerufen. Session Status: {session}")
if not session.get('logged_in'):
logger.info("Benutzer nicht eingeloggt, Weiterleitung zum Login")
return redirect(url_for('login'))
@app.route('/search') client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
logger.info(f"Client-IP: {client_ip}")
logger.info(f"Erlaubte IP-Bereiche: {ALLOWED_IP_RANGES}")
return render_template('index.html', allowed_ip_ranges=','.join(ALLOWED_IP_RANGES), version=VERSION)
@app.route('/search', methods=['GET', 'POST'])
def search(): def search():
if not session.get('logged_in'):
return jsonify({'error': 'Nicht eingeloggt'}), 401
try: try:
# Spezifische Suchparameter if request.method == 'POST':
name = request.args.get('name', '').lower() data = request.get_json()
ort = request.args.get('ort', '').lower() search_query = data.get('query', '')
kundennummer = request.args.get('kundennummer', '').lower() tag = data.get('tag', 'medisoft')
fachrichtung = request.args.get('fachrichtung', '').lower() else:
search_query = request.args.get('q', '')
name = request.args.get('name', '')
ort = request.args.get('ort', '')
nummer = request.args.get('nummer', '')
plz = request.args.get('plz', '')
fachrichtung = request.args.get('fachrichtung', '')
tag = request.args.get('tag', 'medisoft')
# Allgemeine Suche (für die alte Funktionalität) conn = get_db_connection()
query = request.args.get('q', '').lower() c = conn.cursor()
logger.info(f"Suche nach - Name: {name}, Ort: {ort}, Kundennummer: {kundennummer}, Fachrichtung: {fachrichtung}, Query: {query}") # Baue die SQL-Abfrage
sql_query = '''
SELECT
nummer,
name,
strasse,
plz,
ort,
telefon,
mobil,
email,
fachrichtung,
tag,
handy,
tele_firma,
kontakt1,
kontakt2,
kontakt3
FROM customers
WHERE 1=1
'''
params = []
df = load_data() # Füge die Suchbedingungen hinzu
if df is None: if search_query:
return jsonify({"error": "Datenbank konnte nicht geladen werden"}), 500 # Optimierte Suche mit FTS (Full Text Search)
sql_query += """
# Basis-Mask (immer True) AND (
mask = pd.Series(True, index=df.index) name LIKE ? OR
nummer LIKE ? OR
# Spezifische Suchkriterien anwenden fachrichtung LIKE ? OR
if name: ort LIKE ? OR
name_mask = ( plz LIKE ? OR
df['Vorname'].str.lower().str.contains(name, na=False) | strasse LIKE ? OR
df['Nachname'].str.lower().str.contains(name, na=False) telefon LIKE ? OR
mobil LIKE ? OR
email LIKE ? OR
bemerkung LIKE ? OR
tag LIKE ? OR
handy LIKE ? OR
tele_firma LIKE ? OR
kontakt1 LIKE ? OR
kontakt2 LIKE ? OR
kontakt3 LIKE ?
) )
mask &= name_mask """
search_term = f"%{search_query}%"
params.extend([search_term] * 16) # 16 Felder für die allgemeine Suche
if name:
sql_query += " AND name LIKE ?"
params.append(f"%{name}%")
if ort: if ort:
ort_mask = df['Ort'].str.lower().str.contains(ort, na=False) sql_query += " AND ort LIKE ?"
mask &= ort_mask params.append(f"%{ort}%")
if kundennummer: if nummer:
kunden_mask = df['Nummer'].astype(str).str.contains(kundennummer, na=False) sql_query += " AND nummer LIKE ?"
mask &= kunden_mask params.append(f"%{nummer}%")
if plz:
sql_query += " AND plz LIKE ?"
params.append(f"%{plz}%")
if fachrichtung: if fachrichtung:
fach_mask = df['Fachrichtung'].str.lower().str.contains(fachrichtung, na=False) sql_query += " AND fachrichtung LIKE ?"
mask &= fach_mask params.append(f"%{fachrichtung}%")
# Wenn eine allgemeine Suche vorhanden ist, diese zusätzlich anwenden # Filter nach Tag
if query: if tag != 'all':
query_mask = ( sql_query += " AND tag = ?"
df['Vorname'].str.lower().str.contains(query, na=False) | params.append(tag)
df['Nachname'].str.lower().str.contains(query, na=False) |
df['Fachrichtung'].str.lower().str.contains(query, na=False) | # Füge LIMIT hinzu und optimiere die Sortierung
df['Ort'].str.lower().str.contains(query, na=False) | sql_query += " ORDER BY name LIMIT 100"
df['Nummer'].astype(str).str.contains(query, na=False)
) # Führe die Abfrage aus
mask &= query_mask cursor = conn.cursor()
cursor.execute(sql_query, params)
results = cursor.fetchall()
formatted_results = []
for row in results:
customer = {
'nummer': row[0],
'name': row[1],
'strasse': row[2],
'plz': row[3],
'ort': row[4],
'telefon': row[5],
'mobil': row[6],
'email': row[7],
'fachrichtung': row[8],
'tag': row[9],
'handy': row[10],
'tele_firma': row[11],
'kontakt1': row[12],
'kontakt2': row[13],
'kontakt3': row[14]
}
formatted_results.append(customer)
conn.close()
return jsonify(formatted_results)
results = df[mask].to_dict('records')
logger.info(f"{len(results)} Ergebnisse gefunden")
return jsonify(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
def init_app(app):
"""Initialisiert die Anwendung mit allen notwendigen Einstellungen."""
with app.app_context():
try:
# Stelle sicher, dass der data-Ordner existiert
os.makedirs('data', exist_ok=True)
# Lösche die alte Datenbank, falls sie existiert
if os.path.exists(DB_FILE):
try:
os.remove(DB_FILE)
logger.info(f"Alte Datenbank {DB_FILE} wurde gelöscht")
except Exception as e:
logger.error(f"Fehler beim Löschen der alten Datenbank: {str(e)}")
# Initialisiere die Datenbank
init_db()
# Importiere die CSV-Daten
import_csv()
logger.info("Anwendung erfolgreich initialisiert")
except Exception as e:
logger.error(f"Fehler bei der Initialisierung: {str(e)}")
raise
# Initialisiere die App
init_app(app)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True, port=5001)

View File

@@ -1,14 +0,0 @@
version: '3.8'
services:
web:
build: .
ports:
- "5001:5000"
volumes:
- .:/app
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
- FLASK_DEBUG=1
command: flask run --host=0.0.0.0

View File

@@ -0,0 +1,13 @@
services:
web:
build: .
ports:
- "5001:5000"
volumes:
- ./data:/app/data
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
- LOGIN_PASSWORD=changeme
- ALLOWED_IP_RANGES=213.178.68.218/29,192.168.0.0/24,192.168.177.0/24
command: flask run --host=0.0.0.0

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

24
static/css/style.css Normal file
View File

@@ -0,0 +1,24 @@
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.result-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
}
.tag-medisoft {
background-color: #e3f2fd;
color: #1976d2;
}
.tag-mediconsult {
background-color: #f3e5f5;
color: #7b1fa2;
}

293
static/css/styles.css Normal file
View File

@@ -0,0 +1,293 @@
body {
min-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
}
.main-content {
flex: 1 0 auto;
padding: 2rem 0;
margin-bottom: 4rem; /* Platz für die fixierte Fußzeile */
}
.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, .reset-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
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 {
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%;
position: fixed;
bottom: 0;
left: 0;
z-index: 100;
}
.share-feedback {
position: fixed;
bottom: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 8px 16px;
border-radius: 4px;
display: none;
animation: fadeOut 2s forwards;
z-index: 1000;
}
@keyframes fadeOut {
0% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; }
}
.card-actions {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
}
.share-button {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
background-color: #0d6efd;
color: white;
border: none;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.share-button:hover {
background-color: #0b5ed7;
transform: translateY(-1px);
}
.share-button i {
font-size: 1rem;
}
.search-fields {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.search-field {
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;
}
.customer-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border-bottom: 1px solid #e9ecef;
}
.customer-card:last-child {
border-bottom: none;
}
.customer-info {
margin-bottom: 1rem;
}
.footer-content {
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
width: 100%;
position: fixed;
bottom: 0;
left: 0;
z-index: 100;
}
.footer-link {
color: #0d6efd;
text-decoration: none;
}
.footer-link:hover {
text-decoration: underline;
}
.general-search {
max-width: 800px;
margin: 0 auto;
}
.general-search .input-group {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.general-search .form-control {
height: 3.5rem;
font-size: 1.2rem;
padding: 0.75rem 1rem;
}
.general-search .search-icon,
.general-search .reset-icon {
font-size: 1.2rem;
padding: 0 1rem;
}
.search-options {
font-size: 0.9em;
color: #666;
}
.search-options .form-check {
margin-right: 1rem;
}
.search-options .form-check-input {
cursor: pointer;
}
.search-options .form-check-label {
cursor: pointer;
user-select: none;
}
.result-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
color: white;
}
.tag-medisoft {
background-color: #1976d2;
}
.tag-mediconsult {
background-color: #ff9800;
}
/* Adress-Links */
.address-text {
margin-right: 5px;
}
.address-link, .route-link {
color: #666;
text-decoration: none;
margin-left: 5px;
transition: color 0.2s;
}
.address-link:hover, .route-link:hover {
color: #0d6efd;
}
.location-pin, .route-pin {
font-size: 1.1em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -6,127 +6,80 @@
<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">
<style> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
body { <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
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%;
}
.share-feedback {
position: fixed;
bottom: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 8px 16px;
border-radius: 4px;
display: none;
animation: fadeOut 2s forwards;
z-index: 1000;
}
@keyframes fadeOut {
0% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; }
}
.card-actions {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
}
.share-button {
padding: 5px 10px;
border-radius: 15px;
font-size: 0.9em;
}
.search-fields {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.search-field {
position: relative;
}
</style>
</head> </head>
<body> <body>
<div class="main-content"> <div class="main-content">
<div class="container search-container"> <div class="container">
<h1 class="text-center mb-4">medisoftware Kundensuche</h1> <div class="text-center mb-4">
<a href="https://medisoftware.de" target="_blank" rel="noopener noreferrer"><img src="{{ url_for('static', filename='medisoftware_logo_rb_200.png') }}" alt="medisoftware Logo" class="img-fluid" style="max-width: 200px;"></a>
</div>
<div class="search-container">
<h1 class="text-center mb-4">Kundensuche</h1>
<div class="general-search mb-4">
<div class="input-group">
<input type="text" id="q" class="form-control form-control-lg" placeholder="Allgemeine Suche" oninput="searchCustomers()">
<i class="fas fa-times reset-icon" onclick="clearInput('q')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</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" <div class="input-group">
placeholder="Name..."> <input type="text" id="nameInput" class="form-control" placeholder="Name" oninput="searchCustomers()">
</div> <i class="fas fa-times reset-icon" onclick="clearInput('nameInput')"></i>
<div class="search-field"> <i class="fas fa-search search-icon"></i>
<input type="text" id="ortInput" class="form-control"
placeholder="Ort...">
</div>
<div class="search-field">
<input type="text" id="kundennummerInput" class="form-control"
placeholder="Kundennummer...">
</div>
<div class="search-field">
<input type="text" id="fachrichtungInput" class="form-control"
placeholder="Fachrichtung...">
</div> </div>
</div> </div>
<div class="input-group mb-4 position-relative"> <div class="search-field">
<input type="text" id="searchInput" class="form-control form-control-lg" <div class="input-group">
placeholder="Allgemeine Suche..."> <input type="text" id="ortInput" class="form-control" placeholder="Ort" oninput="searchCustomers()">
<span class="search-icon">🔍</span> <i class="fas fa-times reset-icon" onclick="clearInput('ortInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div>
<div class="search-field">
<div class="input-group">
<input type="text" id="nummerInput" class="form-control" placeholder="Kundennummer" oninput="searchCustomers()">
<i class="fas fa-times reset-icon" onclick="clearInput('nummerInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div>
<div class="search-field">
<div class="input-group">
<input type="text" id="plzInput" class="form-control" placeholder="PLZ" oninput="searchCustomers()">
<i class="fas fa-times reset-icon" onclick="clearInput('plzInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div>
<div class="search-field">
<div class="input-group">
<input type="text" id="fachrichtungInput" class="form-control" placeholder="Fachrichtung" oninput="searchCustomers()">
<i class="fas fa-times reset-icon" onclick="clearInput('fachrichtungInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div>
<div class="search-field">
<div class="input-group">
<select id="tagFilter" class="form-select" onchange="searchCustomers()">
<option value="medisoft" selected>MEDISOFT</option>
<option value="mediconsult">MEDICONSULT</option>
<option value="all">Alle</option>
</select>
</div>
</div>
</div>
<div class="result-counts">
<span id="resultCount" class="result-count"></span>
</div> </div>
<div id="loading" class="loading"> <div id="loading" class="loading">
@@ -135,52 +88,118 @@
</div> </div>
</div> </div>
<div id="results" class="mt-4"> <div id="results"></div>
<!-- Hier werden die Suchergebnisse angezeigt -->
</div> </div>
</div> </div>
</div> </div>
<div id="shareFeedback" class="share-feedback"> <div id="shareFeedback" class="share-feedback">
Link kopiert! Link in die Zwischenablage kopiert!
</div> </div>
<footer class="footer"> <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> <div class="footer-content">
Proudly made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a>
<div style="font-size: 0.8em;">Version: {{ version }}</div>
</div>
</footer> </footer>
<script> <script>
let searchTimeout; let searchTimeout;
let lastResults = [];
function createPhoneLink(phone) { function createPhoneLink(phone) {
if (!phone) return 'N/A'; if (!phone) return '';
const cleaned = phone.replace(/\D/g, '');
const telLink = '0' + cleaned; const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
return `<a href="tel:${telLink}" class="phone-link">${phone}</a>`; const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
// Überprüfen, ob die Client-IP in einem der erlaubten Bereiche liegt
const isAllowed = allowedIPRanges.some(range => isIPInSubnet(clientIP, range.trim()));
// Entferne alle nicht-numerischen Zeichen
let cleanNumber = phone.replace(/\D/g, '');
// Formatiere die Nummer
let formattedNumber = cleanNumber;
if (cleanNumber.length === 11) {
formattedNumber = cleanNumber.replace(/(\d{4})(\d{7})/, '$1-$2');
} else if (cleanNumber.length === 10) {
formattedNumber = cleanNumber.replace(/(\d{3})(\d{7})/, '$1-$2');
}
// Erstelle den Link
return `<a href="tel:${cleanNumber}" class="phone-link">${formattedNumber}</a>`;
} }
function createEmailLink(email) { function createEmailLink(email) {
if (!email) return 'N/A'; if (!email) return '';
return `<a href="mailto:${email}" class="email-link">${email}</a>`; return `<a href="mailto:${email}" class="email-link">${email}</a>`;
} }
function highlightText(text, searchTerm) {
if (!searchTerm || !text) return text;
// Escapen von Sonderzeichen im Suchbegriff
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Erstelle einen regulären Ausdruck ohne Wortgrenzen
const regex = new RegExp(escapedSearchTerm, 'gi');
return text.replace(regex, '<mark>$&</mark>');
}
function createAddressLink(street, plz, city) { function createAddressLink(street, plz, city) {
if (!street || !plz || !city) return 'N/A'; if (!street || !plz || !city) return '';
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}" const routeQuery = encodeURIComponent(address);
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
return `<span class="address-text">${address}</span>
<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-dot location-pin"></i>
<span class="ms-1">🗺️</span> </a>
<a href="https://www.google.com/maps/dir/?api=1&destination=${routeQuery}"
class="route-link" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-route route-pin"></i>
</a>`; </a>`;
} }
function createCustomerLink(customerNumber) { function adjustCustomerNumber(number) {
if (!customerNumber) return 'N/A'; return number - 12000;
return `<a href="medisw:openkkbefe/P${customerNumber}?NetGrp=4" }
class="customer-link" target="_blank" rel="noopener noreferrer">
${customerNumber} function isIPInSubnet(ip, subnet) {
</a>`; // Teile die IP und das Subnetz in ihre Komponenten
const [subnetIP, bits] = subnet.split('/');
const ipParts = ip.split('.').map(Number);
const subnetParts = subnetIP.split('.').map(Number);
// Konvertiere IPs in 32-bit Zahlen
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
const subnetNum = (subnetParts[0] << 24) | (subnetParts[1] << 16) | (subnetParts[2] << 8) | subnetParts[3];
// Erstelle die Subnetzmaske
const mask = ~((1 << (32 - bits)) - 1);
// Prüfe, ob die IP im Subnetz liegt
return (ipNum & mask) === (subnetNum & mask);
}
function createCustomerLink(nummer) {
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
// Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt
const isAllowed = allowedIPRanges.some(range => {
const trimmedRange = range.trim();
return isIPInSubnet(clientIP, trimmedRange);
});
if (isAllowed) {
const adjustedNumber = adjustCustomerNumber(nummer);
return `<a href="medisw:openkkbefe/P${adjustedNumber}?NetGrp=4" class="customer-link">${nummer}</a>`;
} else {
return nummer;
}
} }
function showCopyFeedback() { function showCopyFeedback() {
@@ -201,101 +220,169 @@
await navigator.clipboard.writeText(url.toString()); await navigator.clipboard.writeText(url.toString());
showCopyFeedback(); showCopyFeedback();
} catch (err) { } catch (err) {
console.error('Fehler beim Kopieren:', err); // Fehlerbehandlung ohne console.log
} }
} }
function searchCustomers() { function updateResultCounts() {
const name = document.getElementById('nameInput').value; // Nur Gesamtzahl anzeigen
const ort = document.getElementById('ortInput').value; const generalCount = lastResults.length;
const kundennummer = document.getElementById('kundennummerInput').value; document.getElementById('resultCount').textContent =
const fachrichtung = document.getElementById('fachrichtungInput').value; generalCount > 0 ? `${generalCount} Treffer gefunden` : '';
const query = document.getElementById('searchInput').value; document.getElementById('resultCount').classList.toggle('visible', generalCount > 0);
}
// Prüfe, ob mindestens ein Suchfeld ausgefüllt ist function displayResults(results) {
if (!name && !ort && !kundennummer && !fachrichtung && !query) { const resultsDiv = document.getElementById('results');
document.getElementById('results').innerHTML = ''; const resultCount = document.getElementById('resultCount');
const generalSearchTerm = document.getElementById('q').value;
const nameSearchTerm = document.getElementById('nameInput').value;
const fachrichtungSearchTerm = document.getElementById('fachrichtungInput').value;
if (!results || results.length === 0) {
resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
resultCount.textContent = '0 Ergebnisse';
return; return;
} }
// Lade-Animation anzeigen resultCount.textContent = `${results.length} Ergebnisse`;
document.getElementById('loading').style.display = 'block';
document.getElementById('results').innerHTML = '';
// URL-Parameter erstellen const resultsList = results.map(customer => {
// Hilfsfunktion zum Erstellen von Feldern nur wenn sie Werte haben
const createFieldIfValue = (label, value, formatter = (v) => v) => {
if (!value || value === 'N/A' || value === 'n/a' || value === 'N/a' || (typeof value === 'string' && value.trim() === '')) return '';
const formattedValue = formatter(value);
return `<p class="mb-1"><strong>${label}:</strong> ${formattedValue}</p>`;
};
return `
<div class="card mb-1">
<div class="card-body py-1">
<div class="d-flex justify-content-between align-items-start">
<h5 class="card-title mb-1">${highlightText(customer.name, generalSearchTerm || nameSearchTerm)}</h5>
<div class="d-flex align-items-center gap-2">
<span class="badge ${(customer.tag || 'medisoft') === 'medisoft' ? 'bg-primary' : 'bg-warning text-dark'}">${(customer.tag || 'medisoft').toUpperCase()}</span>
<button class="btn btn-sm btn-outline-primary" onclick="copyCustomerLink('${customer.nummer}')">
<i class="fas fa-share-alt"></i> Teilen
</button>
</div>
</div>
<div class="card-text">
${createFieldIfValue('Nummer', customer.nummer, createCustomerLink)}
${createFieldIfValue('Adresse', (customer.strasse && customer.plz && customer.ort) ? true : false,
() => createAddressLink(customer.strasse, customer.plz, customer.ort))}
${createFieldIfValue('Telefon', customer.telefon, createPhoneLink)}
${createFieldIfValue('Mobil', customer.mobil, createPhoneLink)}
${createFieldIfValue('Handy', customer.handy, createPhoneLink)}
${createFieldIfValue('Telefon Firma', customer.tele_firma, createPhoneLink)}
${createFieldIfValue('E-Mail', customer.email, createEmailLink)}
${createFieldIfValue('Fachrichtung', highlightText(customer.fachrichtung, generalSearchTerm || fachrichtungSearchTerm))}
${createFieldIfValue('Kontakt 1', customer.kontakt1, createPhoneLink)}
${createFieldIfValue('Kontakt 2', customer.kontakt2, createPhoneLink)}
${createFieldIfValue('Kontakt 3', customer.kontakt3, createPhoneLink)}
${customer.tags && customer.tags.length > 0 ? `
<p class="mb-0"><strong>Tags:</strong>
${customer.tags.map(tag => `<span class="badge bg-primary me-1">${tag}</span>`).join('')}
</p>
` : ''}
</div>
</div>
</div>
`}).join('');
resultsDiv.innerHTML = resultsList;
}
function searchCustomers() {
const q = document.getElementById('q').value;
const name = document.getElementById('nameInput').value;
const ort = document.getElementById('ortInput').value;
const nummer = document.getElementById('nummerInput').value;
const plz = document.getElementById('plzInput').value;
const fachrichtung = document.getElementById('fachrichtungInput').value;
const selectedTag = document.getElementById('tagFilter').value;
// Prüfe, ob mindestens ein Suchfeld einen Wert hat
if (!q && !name && !ort && !nummer && !plz && !fachrichtung) {
document.getElementById('results').innerHTML = '';
document.getElementById('resultCount').textContent = '';
return;
}
// Zeige das Lade-Icon
document.getElementById('loading').style.display = 'block';
// Baue die Suchanfrage
const params = new URLSearchParams(); const params = new URLSearchParams();
if (q) params.append('q', q);
if (name) params.append('name', name); if (name) params.append('name', name);
if (ort) params.append('ort', ort); if (ort) params.append('ort', ort);
if (kundennummer) params.append('kundennummer', kundennummer); if (nummer) params.append('nummer', nummer);
if (plz) params.append('plz', plz);
if (fachrichtung) params.append('fachrichtung', fachrichtung); if (fachrichtung) params.append('fachrichtung', fachrichtung);
if (query) params.append('q', query); if (selectedTag) params.append('tag', selectedTag);
fetch(`/search?${params.toString()}`) // Führe die Suche durch
fetch('/search?' + params.toString())
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return response.json().then(data => { throw new Error('Netzwerk-Antwort war nicht ok');
throw new Error(data.error || 'Ein Fehler ist aufgetreten');
});
} }
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
const resultsDiv = document.getElementById('results'); // Verstecke das Lade-Icon
resultsDiv.innerHTML = ''; document.getElementById('loading').style.display = 'none';
if (data.length === 0) { if (data.error) {
resultsDiv.innerHTML = '<div class="alert alert-info">Keine Ergebnisse gefunden.</div>'; console.error('Fehler bei der Suche:', data.error);
return; return;
} }
data.forEach(customer => { lastResults = data;
const card = document.createElement('div'); updateResultCounts();
card.className = 'card result-card'; displayResults(data);
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 class="card-actions">
<button class="btn btn-outline-primary share-button" onclick="copyCustomerLink('${customer.Nummer}')">
🔗 Teilen
</button>
</div>
</div>
`;
resultsDiv.appendChild(card);
});
}) })
.catch(error => { .catch(error => {
console.error('Fehler:', error);
document.getElementById('results').innerHTML =
`<div class="alert alert-danger">${error.message}</div>`;
})
.finally(() => {
document.getElementById('loading').style.display = 'none'; document.getElementById('loading').style.display = 'none';
console.error('Fehler bei der Suche:', error);
document.getElementById('results').innerHTML = '<p>Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</p>';
}); });
} }
// Event-Listener für die Live-Suche // Event-Listener für die Live-Suche
const searchInputs = [ const searchInputs = [
document.getElementById('q'),
document.getElementById('nameInput'), document.getElementById('nameInput'),
document.getElementById('ortInput'), document.getElementById('ortInput'),
document.getElementById('kundennummerInput'), document.getElementById('nummerInput'),
document.getElementById('fachrichtungInput'), document.getElementById('plzInput'),
document.getElementById('searchInput') document.getElementById('fachrichtungInput')
]; ];
searchInputs.forEach(input => { const resetIcons = [
document.querySelector('.reset-icon[onclick="clearInput(\'q\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'nameInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'ortInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'nummerInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'plzInput\')"]'),
document.querySelector('.reset-icon[onclick="clearInput(\'fachrichtungInput\')"]')
];
searchInputs.forEach((input, index) => {
input.addEventListener('input', function() { input.addEventListener('input', function() {
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(searchCustomers, 300); // Erhöhe das Debounce-Intervall auf 500ms
searchTimeout = setTimeout(searchCustomers, 500);
// 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();
}); });
}); });
@@ -305,16 +392,14 @@
const name = urlParams.get('name'); const name = urlParams.get('name');
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 plz = urlParams.get('plz');
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('nummerInput').value = kundennummer;
if (fachrichtung) document.getElementById('fachrichtungInput').value = fachrichtung; if (plz) document.getElementById('plzInput').value = plz;
if (query) document.getElementById('searchInput').value = query;
if (name || ort || kundennummer || fachrichtung || query) { if (name || ort || kundennummer || plz) {
searchCustomers(); searchCustomers();
} }
}); });

60
templates/login.html Normal file
View File

@@ -0,0 +1,60 @@
<!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;
margin-bottom: 4rem; /* Platz für die fixierte Fußzeile */
}
.footer {
flex-shrink: 0;
text-align: center;
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
width: 100%;
position: fixed;
bottom: 0;
left: 0;
z-index: 100;
}
</style>
</head>
<body>
<div class="container mt-5">
<div class="text-center mb-4">
<img src="{{ url_for('static', filename='medisoftware_logo_rb_200.png') }}" alt="medisoftware Logo" class="img-fluid" style="max-width: 200px;">
</div>
<div class="row justify-content-center">
<div class="col-md-4">
<h2 class="text-center">Login</h2>
<form method="POST" action="/login">
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Einloggen</button>
</form>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-content">
Proudly made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a>
<div style="font-size: 0.8em;">Version: v1.2.3</div>
</div>
</footer>
</body>
</html>